import React, { useState, useEffect, useRef } from 'react'; import { X, Camera, Image as ImageIcon, Upload, Loader2 } from 'lucide-react'; import { useAuth } from '../context/AuthContext'; import { usersApi, UserProfile } from '../services/api'; interface EditProfileModalProps { isOpen: boolean; onClose: () => void; onSaved?: () => void; } export const EditProfileModal: React.FC = ({ isOpen, onClose, onSaved }) => { const { user, token, refreshUser, updateUsername } = useAuth(); const [loading, setLoading] = useState(true); const [profile, setProfile] = useState(null); const [editUsername, setEditUsername] = useState(''); const [editBio, setEditBio] = useState(''); const [editAvatarUrl, setEditAvatarUrl] = useState(''); const [editBannerUrl, setEditBannerUrl] = useState(''); const [usernameError, setUsernameError] = useState(''); const [isSaving, setIsSaving] = useState(false); const [avatarFile, setAvatarFile] = useState(null); const [bannerFile, setBannerFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(null); const [bannerPreview, setBannerPreview] = useState(null); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [uploadingBanner, setUploadingBanner] = useState(false); const avatarInputRef = useRef(null); const bannerInputRef = useRef(null); useEffect(() => { if (isOpen && user && token) { loadProfile(); } }, [isOpen, user, token]); const loadProfile = async () => { if (!user || !token) return; setLoading(true); try { const res = await usersApi.getProfile(user.username, token); setProfile(res.user); setEditUsername(res.user.username || ''); setEditBio(res.user.bio || ''); setEditAvatarUrl(res.user.avatar_url || ''); setEditBannerUrl(res.user.banner_url || ''); setUsernameError(''); } catch (error) { console.error('Failed to load profile:', error); } finally { setLoading(false); } }; const handleAvatarChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setAvatarFile(file); const reader = new FileReader(); reader.onload = (ev) => setAvatarPreview(ev.target?.result as string); reader.readAsDataURL(file); } }; const handleBannerChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setBannerFile(file); const reader = new FileReader(); reader.onload = (ev) => setBannerPreview(ev.target?.result as string); reader.readAsDataURL(file); } }; const handleSaveProfile = async () => { if (!token || !profile) return; setIsSaving(true); setUsernameError(''); try { // Update username if changed if (editUsername && editUsername !== profile.username) { const sanitized = editUsername.trim().replace(/[^a-zA-Z0-9_-]/g, ''); if (sanitized.length < 2) { setUsernameError('Username must be at least 2 characters'); setIsSaving(false); return; } try { await updateUsername(sanitized); } catch (err: unknown) { const error = err as Error & { message?: string }; if (error.message?.includes('taken')) { setUsernameError('Username is already taken'); } else { setUsernameError('Failed to update username'); } setIsSaving(false); return; } } if (avatarFile) { setUploadingAvatar(true); const avatarRes = await usersApi.uploadAvatar(avatarFile, token); setEditAvatarUrl(avatarRes.url); setUploadingAvatar(false); } if (bannerFile) { setUploadingBanner(true); const bannerRes = await usersApi.uploadBanner(bannerFile, token); setEditBannerUrl(bannerRes.url); setUploadingBanner(false); } const updates: Record = { bio: editBio }; if (!avatarFile && editAvatarUrl !== profile.avatar_url) { updates.avatarUrl = editAvatarUrl; } if (!bannerFile && editBannerUrl !== profile.banner_url) { updates.bannerUrl = editBannerUrl; } if (Object.keys(updates).length > 0) { await usersApi.updateProfile(updates, token); } await refreshUser(); handleClose(); onSaved?.(); } catch (error) { console.error('Failed to update profile:', error); alert('Failed to update profile'); } finally { setIsSaving(false); setUploadingAvatar(false); setUploadingBanner(false); } }; const handleClose = () => { setAvatarFile(null); setBannerFile(null); setAvatarPreview(null); setBannerPreview(null); setUsernameError(''); onClose(); }; if (!isOpen) return null; return (

Edit Profile

{loading ? (
) : ( <>
{/* Username Input */}
@ { setEditUsername(e.target.value); setUsernameError(''); }} placeholder="username" maxLength={50} className="flex-1 bg-black border border-zinc-800 rounded-lg px-3 py-2 text-white placeholder-zinc-600 focus:outline-none focus:border-indigo-500 transition-colors" />
{usernameError && (

{usernameError}

)}

Letters, numbers, underscores, and hyphens only

{/* Avatar Upload */}
{(avatarPreview || editAvatarUrl) ? ( (e.currentTarget.style.display = 'none')} /> ) : (
)} {uploadingAvatar && (
)}

JPG, PNG, WebP, GIF - Max 5MB

{/* Banner Upload */}
bannerInputRef.current?.click()} className="relative w-full h-32 rounded-lg bg-zinc-800 border-2 border-zinc-700 border-dashed overflow-hidden cursor-pointer hover:border-zinc-600 transition-colors" > {(bannerPreview || editBannerUrl) ? ( (e.currentTarget.style.display = 'none')} /> ) : (
Click to upload banner
)} {uploadingBanner && (
)}

Recommended: 1500x500px - JPG, PNG, WebP, GIF - Max 5MB

{/* Bio Input */}