|
import { useState, useCallback } from 'react'; |
|
import { useStore } from '@nanostores/react'; |
|
import { classNames } from '~/utils/classNames'; |
|
import { profileStore, updateProfile } from '~/lib/stores/profile'; |
|
import { toast } from 'react-toastify'; |
|
import { debounce } from '~/utils/debounce'; |
|
|
|
export default function ProfileTab() { |
|
const profile = useStore(profileStore); |
|
const [isUploading, setIsUploading] = useState(false); |
|
|
|
|
|
const debouncedUpdate = useCallback( |
|
debounce((field: 'username' | 'bio', value: string) => { |
|
updateProfile({ [field]: value }); |
|
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`); |
|
}, 1000), |
|
[], |
|
); |
|
|
|
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { |
|
const file = e.target.files?.[0]; |
|
|
|
if (!file) { |
|
return; |
|
} |
|
|
|
try { |
|
setIsUploading(true); |
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
reader.onloadend = () => { |
|
const base64String = reader.result as string; |
|
updateProfile({ avatar: base64String }); |
|
setIsUploading(false); |
|
toast.success('Profile picture updated'); |
|
}; |
|
|
|
reader.onerror = () => { |
|
console.error('Error reading file:', reader.error); |
|
setIsUploading(false); |
|
toast.error('Failed to update profile picture'); |
|
}; |
|
reader.readAsDataURL(file); |
|
} catch (error) { |
|
console.error('Error uploading avatar:', error); |
|
setIsUploading(false); |
|
toast.error('Failed to update profile picture'); |
|
} |
|
}; |
|
|
|
const handleProfileUpdate = (field: 'username' | 'bio', value: string) => { |
|
|
|
updateProfile({ [field]: value }); |
|
|
|
|
|
debouncedUpdate(field, value); |
|
}; |
|
|
|
return ( |
|
<div className="max-w-2xl mx-auto"> |
|
<div className="space-y-6"> |
|
{/* Personal Information Section */} |
|
<div> |
|
{/* Avatar Upload */} |
|
<div className="flex items-start gap-6 mb-8"> |
|
<div |
|
className={classNames( |
|
'w-24 h-24 rounded-full overflow-hidden', |
|
'bg-gray-100 dark:bg-gray-800/50', |
|
'flex items-center justify-center', |
|
'ring-1 ring-gray-200 dark:ring-gray-700', |
|
'relative group', |
|
'transition-all duration-300 ease-out', |
|
'hover:ring-purple-500/30 dark:hover:ring-purple-500/30', |
|
'hover:shadow-lg hover:shadow-purple-500/10', |
|
)} |
|
> |
|
{profile.avatar ? ( |
|
<img |
|
src={profile.avatar} |
|
alt="Profile" |
|
className={classNames( |
|
'w-full h-full object-cover', |
|
'transition-all duration-300 ease-out', |
|
'group-hover:scale-105 group-hover:brightness-90', |
|
)} |
|
/> |
|
) : ( |
|
<div className="i-ph:robot-fill w-16 h-16 text-gray-400 dark:text-gray-500 transition-colors group-hover:text-purple-500/70 transform -translate-y-1" /> |
|
)} |
|
|
|
<label |
|
className={classNames( |
|
'absolute inset-0', |
|
'flex items-center justify-center', |
|
'bg-black/0 group-hover:bg-black/40', |
|
'cursor-pointer transition-all duration-300 ease-out', |
|
isUploading ? 'cursor-wait' : '', |
|
)} |
|
> |
|
<input |
|
type="file" |
|
accept="image/*" |
|
className="hidden" |
|
onChange={handleAvatarUpload} |
|
disabled={isUploading} |
|
/> |
|
{isUploading ? ( |
|
<div className="i-ph:spinner-gap w-6 h-6 text-white animate-spin" /> |
|
) : ( |
|
<div className="i-ph:camera-plus w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-all duration-300 ease-out transform group-hover:scale-110" /> |
|
)} |
|
</label> |
|
</div> |
|
|
|
<div className="flex-1 pt-1"> |
|
<label className="block text-base font-medium text-gray-900 dark:text-gray-100 mb-1"> |
|
Profile Picture |
|
</label> |
|
<p className="text-sm text-gray-500 dark:text-gray-400">Upload a profile picture or avatar</p> |
|
</div> |
|
</div> |
|
|
|
{/* Username Input */} |
|
<div className="mb-6"> |
|
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Username</label> |
|
<div className="relative group"> |
|
<div className="absolute left-3.5 top-1/2 -translate-y-1/2"> |
|
<div className="i-ph:user-circle-fill w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" /> |
|
</div> |
|
<input |
|
type="text" |
|
value={profile.username} |
|
onChange={(e) => handleProfileUpdate('username', e.target.value)} |
|
className={classNames( |
|
'w-full pl-11 pr-4 py-2.5 rounded-xl', |
|
'bg-white dark:bg-gray-800/50', |
|
'border border-gray-200 dark:border-gray-700/50', |
|
'text-gray-900 dark:text-white', |
|
'placeholder-gray-400 dark:placeholder-gray-500', |
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50', |
|
'transition-all duration-300 ease-out', |
|
)} |
|
placeholder="Enter your username" |
|
/> |
|
</div> |
|
</div> |
|
|
|
{/* Bio Input */} |
|
<div className="mb-8"> |
|
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Bio</label> |
|
<div className="relative group"> |
|
<div className="absolute left-3.5 top-3"> |
|
<div className="i-ph:text-aa w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" /> |
|
</div> |
|
<textarea |
|
value={profile.bio} |
|
onChange={(e) => handleProfileUpdate('bio', e.target.value)} |
|
className={classNames( |
|
'w-full pl-11 pr-4 py-2.5 rounded-xl', |
|
'bg-white dark:bg-gray-800/50', |
|
'border border-gray-200 dark:border-gray-700/50', |
|
'text-gray-900 dark:text-white', |
|
'placeholder-gray-400 dark:placeholder-gray-500', |
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50', |
|
'transition-all duration-300 ease-out', |
|
'resize-none', |
|
'h-32', |
|
)} |
|
placeholder="Tell us about yourself" |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|