137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { Camera } from 'lucide-react';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useForm } from 'hooks/useForm';
|
|
import { useAuth } from 'hooks/useAuth';
|
|
|
|
type ProfileFormState = {
|
|
name?: string;
|
|
};
|
|
|
|
const MAX_AVATAR_SIZE = 384_000; // ~384KB to stay under 512KB varchar after base64 overhead
|
|
|
|
const readFileAsBase64 = (file: File): Promise<string> => {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result as string);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
};
|
|
|
|
export const UserData = () => {
|
|
const { user, updateUser } = useAuth();
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
const profileForm = useForm<ProfileFormState>({ name: user?.name ?? '' });
|
|
|
|
const handleAvatarChange = async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = ev.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error('Please select an image file');
|
|
return;
|
|
}
|
|
|
|
if (file.size > MAX_AVATAR_SIZE) {
|
|
toast.error('Image must be smaller than 384KB');
|
|
return;
|
|
}
|
|
|
|
const base64 = await readFileAsBase64(file);
|
|
setAvatarPreview(base64);
|
|
};
|
|
|
|
const handleSubmit = async (ev: React.FormEvent) => {
|
|
ev.preventDefault();
|
|
if (isUpdating) return;
|
|
|
|
setIsUpdating(true);
|
|
try {
|
|
await updateUser({
|
|
name: profileForm.state.name ?? '',
|
|
avatar: avatarPreview ?? user?.avatar ?? '',
|
|
});
|
|
toast.success('Profile updated');
|
|
setAvatarPreview(null);
|
|
} catch (ex) {
|
|
const error = ex as { message?: string };
|
|
toast.error(error.message || 'Failed to update profile');
|
|
} finally {
|
|
setIsUpdating(false);
|
|
}
|
|
};
|
|
|
|
const displayAvatar = avatarPreview ?? user?.avatar ?? undefined;
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex justify-center mb-6">
|
|
<button
|
|
type="button"
|
|
className="relative group cursor-pointer rounded-full"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
>
|
|
<Avatar className="h-20 w-20 rounded-full">
|
|
<AvatarImage src={displayAvatar} />
|
|
<AvatarFallback className="bg-duck-teal text-duck-yellow text-2xl font-bold rounded-full">
|
|
{user?.name?.charAt(0).toUpperCase() ?? '?'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="absolute inset-0 rounded-full bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
<Camera className="h-6 w-6 text-white" />
|
|
</div>
|
|
</button>
|
|
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleAvatarChange} />
|
|
</div>
|
|
|
|
<form ref={profileForm.formRef} onSubmit={handleSubmit} className="grid gap-4">
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">Email</span>
|
|
<Input
|
|
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
|
type="email"
|
|
value={user?.email ?? ''}
|
|
disabled
|
|
/>
|
|
</Label>
|
|
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">Username</span>
|
|
<Input
|
|
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
|
type="text"
|
|
value={user?.username ?? ''}
|
|
disabled
|
|
/>
|
|
</Label>
|
|
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">Name</span>
|
|
<Input
|
|
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
|
type="text"
|
|
name="name"
|
|
placeholder="Your name"
|
|
autoComplete="name"
|
|
/>
|
|
</Label>
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isUpdating}
|
|
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
|
|
>
|
|
{isUpdating ? 'Saving...' : 'Save'}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|