'use client'; import Image from 'next/image'; import { type ChangeEvent, useRef, useState } from 'react'; import { type Preloaded, usePreloadedQuery, useMutation, useQuery, } from 'convex/react'; import { api } from '~/convex/_generated/api'; import { BasedAvatar, Button, CardContent, ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset, Input, } from '@/components/ui'; import { toast } from 'sonner'; import { Loader2, Pencil, Upload, XIcon } from 'lucide-react'; import { type Id } from '~/convex/_generated/dataModel'; type AvatarUploadProps = { preloadedUser: Preloaded; }; const dataUrlToBlob = async ( dataUrl: string, ): Promise<{ blob: Blob; type: string }> => { const re = /^data:([^;,]+)[;,]/; const m = re.exec(dataUrl); const type = m?.[1] ?? 'image/png'; const res = await fetch(dataUrl); const blob = await res.blob(); return { blob, type }; }; export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => { const user = usePreloadedQuery(preloadedUser); const [isUploading, setIsUploading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [croppedImage, setCroppedImage] = useState(null); const inputRef = useRef(null); const generateUploadUrl = useMutation(api.files.generateUploadUrl); const updateUserImage = useMutation(api.auth.updateUserImage); const currentImageUrl = useQuery( api.files.getImageUrl, user?.image ? { storageId: user.image } : 'skip', ); const handleFileChange = (event: ChangeEvent) => { const file = event.target.files?.[0] ?? null; if (!file) return; if (!file.type.startsWith('image/')) { toast.error('Please select an image file.'); if (inputRef.current) inputRef.current.value = ''; return; } setSelectedFile(file); setCroppedImage(null); }; const handleReset = () => { setSelectedFile(null); setCroppedImage(null); if (inputRef.current) inputRef.current.value = ''; }; const handleSave = async () => { if (!croppedImage) { toast.error('Please apply a crop first.'); return; } setIsUploading(true); try { const { blob, type } = await dataUrlToBlob(croppedImage); const postUrl = await generateUploadUrl(); const result = await fetch(postUrl, { method: 'POST', headers: { 'Content-Type': type }, body: blob, }); if (!result.ok) { const msg = await result.text().catch(() => 'Upload failed.'); throw new Error(msg); } const uploadResponse = (await result.json()) as { storageId: Id<'_storage'>; }; await updateUserImage({ storageId: uploadResponse.storageId }); toast.success('Profile picture updated.'); handleReset(); } catch (error) { console.error('Upload failed:', error); toast.error('Upload failed. Please try again.'); } finally { setIsUploading(false); } }; return (
{/* Current avatar + trigger (hidden when cropping) */} {!selectedFile && (
inputRef.current?.click()} >
)} {/* File input (hidden) */} {/* Crop UI */} {selectedFile && !croppedImage && (
)} {/* Cropped preview + actions */} {croppedImage && (
Cropped preview
)} {/* Uploading indicator */} {isUploading && !croppedImage && (
Uploading...
)}
); };