Files
next-template/src/components/default/auth/forms/client/profile/avatar-upload.tsx
2025-07-09 11:54:01 -05:00

152 lines
4.5 KiB
TypeScript

'use client';
import { useFileUpload } from '@/lib/hooks';
import { useAuth } from '@/lib/hooks/context';
import { useSupabaseClient } from '@/utils/supabase';
import {
BasedAvatar,
Card,
CardContent,
} from '@/components/ui';
import { Loader2, Pencil, Upload } from 'lucide-react';
import type { ComponentProps, ChangeEvent } from 'react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise<void>;
cardProps?: ComponentProps<typeof Card>;
cardContentProps?: ComponentProps<typeof CardContent>;
containerProps?: ComponentProps<'div'>;
basedAvatarProps?: ComponentProps<typeof BasedAvatar>;
iconProps?: ComponentProps<typeof Upload>;
};
export const AvatarUpload = ({
onAvatarUploaded,
cardProps,
cardContentProps,
containerProps,
basedAvatarProps,
iconProps = {
size: 32,
},
}: AvatarUploadProps) => {
const { profile, isAuthenticated } = useAuth();
const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload();
const client = useSupabaseClient();
const handleAvatarClick = () => {
if (!isAuthenticated) {
toast.error('You must be logged in to upload an avatar!');
return;
}
fileInputRef.current?.click();
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
try {
const file = e.target.files?.[0];
if (!file) throw new Error('No file selected!');
if (!client) throw new Error('Supabase client not found!');
if (!isAuthenticated) throw new Error('User is not authenticated!');
if (!file.type.startsWith('image/')) throw new Error('File is not an image!');
if (file.size > 8 * 1024 * 1024) throw new Error('File is too large!');
const avatarPath = profile?.avatar_url ??
`${profile?.id}.${file.name.split('.').pop()}`;
const avatarUrl = await uploadAvatarMutation.mutateAsync({
client,
file,
bucket: 'avatars',
resize: {
maxWidth: 500,
maxHeight: 500,
quality: 0.8,
},
replace: avatarPath,
});
if (avatarUrl) await onAvatarUploaded(avatarUrl);
} catch (error) {
toast.error(`Error: ${error as string}`);
}
};
return (
<Card
{...cardProps}
className={cn('', cardProps?.className)}
>
<CardContent
{...cardContentProps}
className={cn('flex flex-col items-center', cardContentProps?.className)}
>
<div
{...containerProps}
className={cn(
'relative group cursor-pointer mb-4',
containerProps?.className
)}
>
<BasedAvatar
{...basedAvatarProps}
src={profile?.avatar_url}
fullName={profile?.full_name}
className={cn('h-32, w-32', basedAvatarProps?.className)}
fallbackProps={{ className: 'text-4xl font-semibold' }}
userIconProps={{ size: 100 }}
/>
<div
className={cn(
'absoloute inset-0 rounded-full bg-black/0\
group-hover:bg-black/50 transition-all flex\
items-center justify-center'
)}
>
<Upload
{...iconProps}
className={cn('text-white opacity-0 group-hover:opacity-100\
transition-opacity', iconProps?.className
)}
/>
</div>
<div
className={cn(
'absolute inset-1 transition-all flex\
items-end justify-end',
)}
>
<Pencil
{...iconProps}
className={cn(
'text-white opacity-100 group-hover:opacity-0\
transition-opacity', iconProps?.className
)}
/>
</div>
<input
ref={fileInputRef}
type='file'
accept='image/*'
className={cn('hidden')}
onChange={handleFileChange}
disabled={isUploading}
/>
{isUploading && (
<div className={cn('flex items-center text-sm text-gray-500 mt-2')}>
<Loader2 className={cn('h-4 w-4 mr-2 animate-spin')} />
Uploading...
</div>
)}
{!isAuthenticated && (
<p className={cn('text-sm text-muted-foreground mt-2')}>
Sign in to upload an avatar.
</p>
)}
</div>
</CardContent>
</Card>
);
};