import { useState, useEffect, useRef } from 'react'; import { Pencil, Check, ImagePlus, ChevronDown } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import { Card } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/hooks/use-i18n'; import { toast } from 'sonner'; import { useUserProfileStore, AVATAR_OPTIONS } from '@/lib/store/user-profile'; /** Check whether avatar is a custom upload (data-URL) */ function isCustomAvatar(avatar: string) { return avatar.startsWith('data:'); } /** Max uploaded image size before we reject */ const MAX_AVATAR_SIZE = 5 * 1024 * 1024; // 5 MB const FILE_INPUT_ID = 'user-avatar-upload'; export function UserProfileCard() { const { t } = useI18n(); const avatar = useUserProfileStore((s) => s.avatar); const nickname = useUserProfileStore((s) => s.nickname); const bio = useUserProfileStore((s) => s.bio); const setAvatar = useUserProfileStore((s) => s.setAvatar); const setNickname = useUserProfileStore((s) => s.setNickname); const setBio = useUserProfileStore((s) => s.setBio); const [editingName, setEditingName] = useState(false); const [nameDraft, setNameDraft] = useState(''); const [avatarPickerOpen, setAvatarPickerOpen] = useState(false); const [hydrated, setHydrated] = useState(false); const nameInputRef = useRef(null); useEffect(() => { setHydrated(true); // eslint-disable-line react-hooks/set-state-in-effect -- Store hydration on mount }, []); useEffect(() => { if (editingName) nameInputRef.current?.focus(); }, [editingName]); const displayName = nickname || t('profile.defaultNickname'); const startEditName = () => { setNameDraft(nickname); setEditingName(true); }; const commitName = () => { setNickname(nameDraft.trim()); setEditingName(false); }; const handleAvatarUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > MAX_AVATAR_SIZE) { toast.error(t('profile.fileTooLarge')); return; } if (!file.type.startsWith('image/')) { toast.error(t('profile.invalidFileType')); return; } const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = 128; canvas.height = 128; const ctx = canvas.getContext('2d')!; const scale = Math.max(128 / img.width, 128 / img.height); const w = img.width * scale; const h = img.height * scale; ctx.drawImage(img, (128 - w) / 2, (128 - h) / 2, w, h); setAvatar(canvas.toDataURL('image/jpeg', 0.85)); }; img.src = reader.result as string; }; reader.readAsDataURL(file); e.target.value = ''; }; if (!hydrated) { return (
); } return ( {/* File input — sr-only keeps it in the flow but invisible; label triggers it */} {/* Row 1: Avatar + Name */}
{/* Avatar — click to toggle picker */} {/* Name */}
{editingName ? (
setNameDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') commitName(); if (e.key === 'Escape') setEditingName(false); }} onBlur={commitName} maxLength={20} placeholder={t('profile.defaultNickname')} className="flex-1 min-w-0 h-7 bg-transparent border-b-2 border-violet-400 dark:border-violet-500 text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground/40" />
) : ( )}

{t('profile.avatarHint')}

{/* Avatar picker — collapsible */} {avatarPickerOpen && ( {/* p-1 gives breathing room so ring-offset / hover-scale aren't clipped */}
{AVATAR_OPTIONS.map((url) => ( ))} {/* Upload — uses
)}
{/* Bio input */}