import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { getAuth } from '../lib/auth'; import { updateProfileResource, uploadAvatarResource, useProfile } from '../lib/resources'; interface Props { locale: string; dashboard: { brandName: string; navHome: string; navStore: string; navDivination: string; navManual: string; navAuto: string; navHistory: string; navLanguage: string; navSettings: string; logout: string }; profile: { avatarTitle: string; avatarHint: string; uploadBtn: string; formTitle: string; emailLabel: string; displayNameLabel: string; displayNamePlaceholder: string; bioLabel: string; bioPlaceholder: string; saveBtn: string; cancelBtn: string }; } // Compress image before upload async function compressImage(file: File, maxWidth = 512, maxHeight = 512, quality = 0.8): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { // Calculate new dimensions let width = img.width; let height = img.height; if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height); width = Math.round(width * ratio); height = Math.round(height * ratio); } // Create canvas and draw const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Canvas context not available')); return; } ctx.drawImage(img, 0, 0, width, height); // Convert to blob canvas.toBlob( (blob) => { if (!blob) { reject(new Error('Failed to compress image')); return; } const compressedFile = new File([blob], file.name.replace(/\.[^.]+$/, '.jpg'), { type: 'image/jpeg', }); resolve(compressedFile); }, 'image/jpeg', quality ); }; img.onerror = () => reject(new Error('Failed to load image')); img.src = URL.createObjectURL(file); }); } export default function ProfileDetailPage({ locale, profile: p }: Props) { const navigate = useNavigate(); const profileState = useProfile(); const profile = profileState.data ?? null; const [displayName, setDisplayName] = useState(''); const [bio, setBio] = useState(''); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const fileInputRef = useRef(null); useEffect(() => { if (!profileState.data) return; setDisplayName(profileState.data.display_name || ''); setBio(profileState.data.bio || ''); }, [profileState.data]); useEffect(() => { if (profileState.error instanceof Error) setError(profileState.error.message || 'Failed to load profile'); }, [profileState.error]); // Clear messages after 3 seconds useEffect(() => { if (success) { const timer = setTimeout(() => setSuccess(null), 3000); return () => clearTimeout(timer); } }, [success]); const handleSave = async () => { setSaving(true); setError(null); try { await updateProfileResource({ display_name: displayName || undefined, bio: bio || undefined, }); // Navigate back to settings on success navigate(-1); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save'); } finally { setSaving(false); } }; const handleCancel = () => { navigate(-1); }; const handleAvatarClick = () => { fileInputRef.current?.click(); }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // Validate file type if (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type)) { setError(locale === 'en' ? 'Only PNG, JPG, WEBP allowed' : '仅支持 PNG、JPG、WEBP'); return; } setUploading(true); setError(null); try { // Compress image before upload const compressedFile = await compressImage(file, 512, 512, 0.8); // Check compressed size (max 2MB after compression) if (compressedFile.size > 2 * 1024 * 1024) { throw new Error(locale === 'en' ? 'Image too large, please choose a smaller one' : '图片太大,请选择更小的图片'); } await uploadAvatarResource(compressedFile); setSuccess(locale === 'en' ? 'Avatar updated' : '头像已更新'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to upload'); } finally { setUploading(false); // Reset file input if (fileInputRef.current) fileInputRef.current.value = ''; } }; if (profileState.loading) { return (
{locale === 'en' ? 'Loading...' : '加载中...'}
); } const email = getAuth()?.user?.email || ''; return (
{/* Avatar edit */}
{/* Avatar preview */} {profile?.avatar_url ? ( {displayName} ) : (
{displayName ? displayName[0].toUpperCase() : '?'}
)}

{p.avatarTitle}

{p.avatarHint}

{/* Form */}

{p.formTitle}

{/* Error message */} {error && (
{error}
)} {/* Success message */} {success && (
{success}
)} {/* Email readonly */}
email

{p.emailLabel}

{email || '-'}

{/* Display name */}
setDisplayName(e.target.value)} placeholder={p.displayNamePlaceholder} maxLength={30} className="w-full h-11 px-4 rounded-lg bg-slate-50 border border-slate-200 text-sm focus:outline-none focus:border-violet-400 focus:ring-1 focus:ring-violet-400" />
{/* Bio */}