173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
'use client';
|
|
|
|
import { FormError } from '@/components/shared/form-error';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { authClient } from '@/lib/auth-client';
|
|
import { cn } from '@/lib/utils';
|
|
import { uploadFileFromBrowser } from '@/storage/client';
|
|
import { User2Icon } from 'lucide-react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useEffect, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
|
|
interface UpdateAvatarCardProps {
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Update the user's avatar
|
|
*/
|
|
export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) {
|
|
const t = useTranslations('Dashboard.settings.profile');
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [error, setError] = useState<string | undefined>('');
|
|
const { data: session, refetch } = authClient.useSession();
|
|
const [avatarUrl, setAvatarUrl] = useState('');
|
|
const [tempAvatarUrl, setTempAvatarUrl] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (session?.user?.image) {
|
|
setAvatarUrl(session.user.image);
|
|
}
|
|
}, [session]);
|
|
|
|
const user = session?.user;
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
const handleUploadClick = () => {
|
|
// Create a hidden file input and trigger it
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'image/png, image/jpeg, image/webp';
|
|
input.onchange = (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
if (file) {
|
|
handleFileUpload(file);
|
|
}
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
const handleFileUpload = async (file: File) => {
|
|
setIsUploading(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Create a temporary URL for preview and store the original URL
|
|
const tempUrl = URL.createObjectURL(file);
|
|
setTempAvatarUrl(tempUrl);
|
|
// Show temporary avatar immediately for better UX
|
|
setAvatarUrl(tempUrl);
|
|
|
|
// Upload the file to storage
|
|
const result = await uploadFileFromBrowser(file, 'avatars');
|
|
// console.log('uploadFileFromBrowser, result', result);
|
|
const { url } = result;
|
|
console.log('uploadFileFromBrowser, url', url);
|
|
|
|
// Update the user's avatar using authClient
|
|
await authClient.updateUser(
|
|
{
|
|
image: url,
|
|
},
|
|
{
|
|
onRequest: () => {
|
|
// console.log('update avatar, request:', ctx.url);
|
|
},
|
|
onResponse: () => {
|
|
// console.log('update avatar, response:', ctx.response);
|
|
},
|
|
onSuccess: () => {
|
|
// console.log('update avatar, success:', ctx.data);
|
|
// Set the permanent avatar URL on success
|
|
setAvatarUrl(url);
|
|
toast.success(t('avatar.success'));
|
|
// Refetch the session to get the latest data
|
|
refetch();
|
|
},
|
|
onError: (ctx) => {
|
|
console.error('update avatar error:', ctx.error);
|
|
setError(`${ctx.error.status}: ${ctx.error.message}`);
|
|
// Restore the previous avatar on error
|
|
if (session?.user?.image) {
|
|
setAvatarUrl(session.user.image);
|
|
}
|
|
toast.error(t('avatar.fail'));
|
|
},
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error('update avatar error:', error);
|
|
setError(error instanceof Error ? error.message : t('avatar.fail'));
|
|
// Restore the previous avatar if there was an error
|
|
if (session?.user?.image) {
|
|
setAvatarUrl(session.user.image);
|
|
}
|
|
toast.error(t('avatar.fail'));
|
|
} finally {
|
|
setIsUploading(false);
|
|
// Clean up temporary URL
|
|
if (tempAvatarUrl) {
|
|
URL.revokeObjectURL(tempAvatarUrl);
|
|
setTempAvatarUrl('');
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
className={cn(
|
|
'w-full overflow-hidden py-0 pt-6 flex flex-col',
|
|
className
|
|
)}
|
|
>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-semibold">
|
|
{t('avatar.title')}
|
|
</CardTitle>
|
|
<CardDescription>{t('avatar.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 flex-1">
|
|
<div className="flex flex-col items-center sm:flex-row gap-4 sm:gap-8">
|
|
{/* avatar */}
|
|
<Avatar className="h-16 w-16 border">
|
|
<AvatarImage src={avatarUrl ?? ''} alt={user.name} />
|
|
<AvatarFallback>
|
|
<User2Icon className="h-8 w-8 text-muted-foreground" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
{/* upload button */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleUploadClick}
|
|
disabled={isUploading}
|
|
className="cursor-pointer"
|
|
>
|
|
{isUploading ? t('avatar.uploading') : t('avatar.uploadAvatar')}
|
|
</Button>
|
|
</div>
|
|
|
|
<FormError message={error} />
|
|
</CardContent>
|
|
<CardFooter className="mt-auto px-6 py-4 flex justify-between items-center bg-background rounded-none">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('avatar.recommendation')}
|
|
</p>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|