144 lines
4.8 KiB
JavaScript
144 lines
4.8 KiB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import { fetchIslandByUsername, uploadProfileAvatar } from '../../api/client';
|
||
import { useAuth } from '../../auth/AuthContext';
|
||
import '../../styles/profile.css';
|
||
|
||
/**
|
||
* UserProfile: Modal showing user info with "Visit Island" button
|
||
*
|
||
* Props:
|
||
* - username: string (required)
|
||
* - onClose: function
|
||
* - onVisitIsland: function(island) - called when "Visit Island" is clicked
|
||
*/
|
||
export default function UserProfile({ username, onClose, onVisitIsland }) {
|
||
const { authenticated, username: authedUser, setAvatarUrl } = useAuth();
|
||
const [island, setIsland] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [uploading, setUploading] = useState(false);
|
||
const fileRef = useRef(null);
|
||
|
||
const isSelf = authenticated && authedUser && authedUser.toLowerCase() === username.toLowerCase();
|
||
|
||
useEffect(() => {
|
||
if (!username) return;
|
||
|
||
fetchIslandByUsername(username)
|
||
.then(data => {
|
||
setIsland(data);
|
||
setLoading(false);
|
||
})
|
||
.catch(err => {
|
||
console.error('[UserProfile] Error fetching island:', err);
|
||
setLoading(false);
|
||
});
|
||
}, [username]);
|
||
|
||
const handleVisitIsland = () => {
|
||
if (island && onVisitIsland) {
|
||
onVisitIsland(island);
|
||
}
|
||
};
|
||
|
||
const handleChooseAvatar = () => {
|
||
if (fileRef.current) fileRef.current.click();
|
||
};
|
||
|
||
const handleAvatarChange = async (e) => {
|
||
const file = e.target.files && e.target.files[0];
|
||
if (!file) return;
|
||
setUploading(true);
|
||
const res = await uploadProfileAvatar(file);
|
||
if (res && res.ok && res.avatar_url) {
|
||
setIsland(prev => (prev ? { ...prev, avatar_url: res.avatar_url } : prev));
|
||
if (setAvatarUrl) setAvatarUrl(res.avatar_url);
|
||
}
|
||
setUploading(false);
|
||
};
|
||
|
||
return (
|
||
<div className="user-profile-modal" onClick={onClose}>
|
||
<div className="user-profile-container" onClick={(e) => e.stopPropagation()}>
|
||
<button className="user-profile-close" onClick={onClose}>×</button>
|
||
|
||
<div className="user-profile-header">
|
||
<div className="user-profile-avatar">
|
||
{island?.avatar_url ? (
|
||
<img src={island.avatar_url} alt={username} />
|
||
) : (
|
||
<div className="user-profile-avatar-letter">
|
||
{username[0].toUpperCase()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<h2 className="user-profile-username">@{username}</h2>
|
||
{island?.display_name && island.display_name !== `${username}'s Island` && (
|
||
<p className="user-profile-display-name">{island.display_name}</p>
|
||
)}
|
||
{isSelf ? (
|
||
<div className="user-profile-avatar-actions">
|
||
<button
|
||
className="user-profile-btn user-profile-btn-ghost"
|
||
type="button"
|
||
disabled={uploading}
|
||
onClick={handleChooseAvatar}
|
||
>
|
||
{uploading ? 'Uploading...' : 'Change photo'}
|
||
</button>
|
||
<input
|
||
ref={fileRef}
|
||
type="file"
|
||
accept="image/*"
|
||
style={{ display: 'none' }}
|
||
onChange={handleAvatarChange}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="user-profile-loading">Loading profile...</div>
|
||
) : island ? (
|
||
<>
|
||
<div className="user-profile-bio">
|
||
<p>{island.bio || 'No bio yet.'}</p>
|
||
</div>
|
||
|
||
<div className="user-profile-stats">
|
||
<div className="user-profile-stat">
|
||
<span className="user-profile-stat-value">{island.content_count || 0}</span>
|
||
<span className="user-profile-stat-label">Posts</span>
|
||
</div>
|
||
<div className="user-profile-stat">
|
||
<span className="user-profile-stat-value">{island.visitors || 0}</span>
|
||
<span className="user-profile-stat-label">Visitors</span>
|
||
</div>
|
||
<div className="user-profile-stat">
|
||
<span className="user-profile-stat-value">{island.terrain_type || 'tropical'}</span>
|
||
<span className="user-profile-stat-label">Terrain</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="user-profile-actions">
|
||
<button
|
||
className="user-profile-btn user-profile-btn-primary"
|
||
onClick={handleVisitIsland}
|
||
>
|
||
🏝️ Visit Island
|
||
</button>
|
||
</div>
|
||
|
||
<div className="user-profile-footer">
|
||
<p className="user-profile-seed">Seed: {island.seed}</p>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="user-profile-no-island">
|
||
<p>This user doesn't have an island yet.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|