profile-sheet.tsx
1 'use client' 2 3 import { useState, useEffect } from 'react' 4 import { useAppStore } from '@/stores/use-app-store' 5 import { BottomSheet } from '@/components/shared/bottom-sheet' 6 import { AgentAvatar } from '@/components/agents/agent-avatar' 7 import { api } from '@/lib/app/api-client' 8 import { toast } from 'sonner' 9 10 interface Props { 11 open: boolean 12 onClose: () => void 13 } 14 15 export function ProfileSheet({ open, onClose }: Props) { 16 const appSettings = useAppStore((s) => s.appSettings) 17 const loadSettings = useAppStore((s) => s.loadSettings) 18 const setUser = useAppStore((s) => s.setUser) 19 const currentUser = useAppStore((s) => s.currentUser) 20 21 const [name, setName] = useState('') 22 const [avatarSeed, setAvatarSeed] = useState('') 23 const [saving, setSaving] = useState(false) 24 25 useEffect(() => { 26 if (open) { 27 setName(appSettings.userName || currentUser || '') 28 setAvatarSeed(appSettings.userAvatarSeed || '') 29 } 30 }, [open, appSettings.userName, appSettings.userAvatarSeed, currentUser]) 31 32 const handleSave = async () => { 33 const trimmed = name.trim() 34 if (!trimmed || saving) return 35 setSaving(true) 36 try { 37 await api('PUT', '/settings', { 38 userName: trimmed.toLowerCase(), 39 userAvatarSeed: avatarSeed.trim() || undefined, 40 }) 41 setUser(trimmed.toLowerCase()) 42 await loadSettings() 43 toast.success('Profile updated') 44 onClose() 45 } catch (err: unknown) { 46 toast.error(err instanceof Error ? err.message : 'Failed to update profile') 47 } finally { 48 setSaving(false) 49 } 50 } 51 52 const handleSignOut = () => { 53 setUser(null) 54 onClose() 55 } 56 57 return ( 58 <BottomSheet open={open} onClose={onClose}> 59 <div className="p-6 max-w-[400px] mx-auto"> 60 <h2 className="font-display text-[18px] font-700 text-text mb-6 text-center">Profile</h2> 61 62 {/* Avatar preview */} 63 <div className="flex justify-center mb-6"> 64 <AgentAvatar seed={avatarSeed || null} name={name || '?'} size={72} /> 65 </div> 66 67 {/* Avatar seed */} 68 <div className="mb-4"> 69 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Avatar</label> 70 <div className="flex items-center gap-2"> 71 <input 72 type="text" 73 value={avatarSeed} 74 onChange={(e) => setAvatarSeed(e.target.value)} 75 placeholder="Avatar seed (any text)" 76 className="flex-1 px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40" 77 /> 78 <button 79 type="button" 80 onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))} 81 className="px-3 py-2 rounded-[8px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] shrink-0" 82 > 83 Randomize 84 </button> 85 </div> 86 </div> 87 88 {/* Name */} 89 <div className="mb-6"> 90 <label className="block text-[12px] font-600 text-text-2 mb-1.5">Name</label> 91 <input 92 type="text" 93 value={name} 94 onChange={(e) => setName(e.target.value)} 95 placeholder="Your name" 96 className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40" 97 /> 98 </div> 99 100 {/* Save */} 101 <button 102 onClick={handleSave} 103 disabled={!name.trim() || saving} 104 className="w-full py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer mb-4" 105 > 106 {saving ? 'Saving...' : 'Save'} 107 </button> 108 109 {/* Sign out */} 110 <button 111 onClick={handleSignOut} 112 className="w-full text-center text-[12px] text-text-3 hover:text-text-2 transition-all cursor-pointer bg-transparent border-none" 113 > 114 Sign in as different user 115 </button> 116 </div> 117 </BottomSheet> 118 ) 119 }