152 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
};
|