"use client" import { useState, useRef, useEffect, useCallback } from "react" import { motion, AnimatePresence } from "motion/react" import { Camera, Trash2, Loader2, Upload } from "lucide-react" interface ProfilePhotoUploadProps { currentImage: string | null userName: string userId: string } const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"] const MAX_SIZE_MB = 5 const MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024 function isR2Avatar(url: string): boolean { return url.includes(".r2.dev") } function getInitials(name: string): string { return name.charAt(0).toUpperCase() } export function ProfilePhotoUpload({ currentImage, userName, userId }: ProfilePhotoUploadProps) { const [previewUrl, setPreviewUrl] = useState(currentImage) const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success" | "error">("idle") const [errorMessage, setErrorMessage] = useState(null) const [isDragging, setIsDragging] = useState(false) const fileInputRef = useRef(null) const tempUrlRef = useRef(null) const cleanupTempUrl = () => { if (tempUrlRef.current) { URL.revokeObjectURL(tempUrlRef.current) tempUrlRef.current = null } } useEffect(() => { return cleanupTempUrl }, []) useEffect(() => { setPreviewUrl(currentImage) }, [currentImage]) const validateFile = (file: File): string | null => { if (!ALLOWED_TYPES.includes(file.type)) { return "Only JPEG, PNG, and WebP images are allowed." } if (file.size > MAX_SIZE_BYTES) { return "File must be under 5MB." } return null } const handleFile = async (file: File) => { const validationError = validateFile(file) if (validationError) { setErrorMessage(validationError) setUploadState("error") return } setErrorMessage(null) setUploadState("uploading") cleanupTempUrl() const objectUrl = URL.createObjectURL(file) tempUrlRef.current = objectUrl setPreviewUrl(objectUrl) const formData = new FormData() formData.append("file", file) try { const res = await fetch("/api/user/profile-photo", { method: "POST", body: formData, }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || "Upload failed") } const data = await res.json() if (data.imageUrl) { setPreviewUrl(data.imageUrl) cleanupTempUrl() } setUploadState("success") } catch (err) { setErrorMessage(err instanceof Error ? err.message : "Upload failed") setUploadState("error") setPreviewUrl(currentImage) cleanupTempUrl() } } const handleInputChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) handleFile(file) e.target.value = "" } const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const file = e.dataTransfer.files?.[0] if (file) handleFile(file) }, [currentImage]) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(true) }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) }, []) const handleDelete = async () => { if (!previewUrl || !isR2Avatar(previewUrl)) return setUploadState("uploading") try { const res = await fetch("/api/user/profile-photo", { method: "DELETE", }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || "Delete failed") } setPreviewUrl(null) setUploadState("idle") setErrorMessage(null) } catch (err) { setErrorMessage(err instanceof Error ? err.message : "Delete failed") setUploadState("error") } } const triggerFileInput = () => { fileInputRef.current?.click() } const showDelete = previewUrl ? isR2Avatar(previewUrl) : false return (

Profile Photo

{/* Avatar area */}
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault() triggerFileInput() } }} > {previewUrl ? ( {`${userName}'s ) : (
{getInitials(userName)}
)} {/* Upload overlay */}
{uploadState === "uploading" && (
)}
{/* Controls */}
{showDelete && ( )}

JPEG, PNG, or WebP. Max 5MB.

{errorMessage && ( {errorMessage} )} {uploadState === "success" && !errorMessage && ( Photo updated successfully. )}
{/* Hidden file input */}
) }