'use client' import { useState, useRef, useCallback } from 'react' import Cropper from 'react-easy-crop' import type { Area } from 'react-easy-crop' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Slider } from '@/components/ui/slider' import { Upload, Loader2, Trash2, ZoomIn, ImageIcon } from 'lucide-react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' type ProjectLogoUploadProps = { projectId: string currentLogoUrl?: string | null onUploadComplete?: () => void children?: React.ReactNode } const MAX_SIZE_MB = 5 const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise { const image = new Image() image.crossOrigin = 'anonymous' await new Promise((resolve, reject) => { image.onload = () => resolve() image.onerror = reject image.src = imageSrc }) const canvas = document.createElement('canvas') canvas.width = pixelCrop.width canvas.height = pixelCrop.height const ctx = canvas.getContext('2d')! ctx.drawImage( image, pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, 0, 0, pixelCrop.width, pixelCrop.height ) return new Promise((resolve, reject) => { canvas.toBlob( (blob) => { if (blob) resolve(blob) else reject(new Error('Canvas toBlob failed')) }, 'image/png', 0.9 ) }) } export function ProjectLogoUpload({ projectId, currentLogoUrl, onUploadComplete, children, }: ProjectLogoUploadProps) { const [open, setOpen] = useState(false) const [imageSrc, setImageSrc] = useState(null) const [crop, setCrop] = useState({ x: 0, y: 0 }) const [zoom, setZoom] = useState(1) const [croppedAreaPixels, setCroppedAreaPixels] = useState(null) const [isUploading, setIsUploading] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const fileInputRef = useRef(null) const getUploadUrl = trpc.applicant.getProjectLogoUploadUrl.useMutation() const confirmUpload = trpc.applicant.confirmProjectLogo.useMutation() const deleteLogo = trpc.applicant.deleteProjectLogo.useMutation() const onCropComplete = useCallback((_croppedArea: Area, croppedPixels: Area) => { setCroppedAreaPixels(croppedPixels) }, []) const handleFileSelect = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return if (!ALLOWED_TYPES.includes(file.type)) { toast.error('Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.') return } if (file.size > MAX_SIZE_MB * 1024 * 1024) { toast.error(`File too large. Maximum size is ${MAX_SIZE_MB}MB.`) return } const reader = new FileReader() reader.onload = (ev) => { setImageSrc(ev.target?.result as string) setCrop({ x: 0, y: 0 }) setZoom(1) } reader.readAsDataURL(file) }, []) const handleUpload = async () => { if (!imageSrc || !croppedAreaPixels) return setIsUploading(true) try { const croppedBlob = await getCroppedImg(imageSrc, croppedAreaPixels) const { uploadUrl, key, providerType } = await getUploadUrl.mutateAsync({ projectId, fileName: 'logo.png', contentType: 'image/png', }) const uploadResponse = await fetch(uploadUrl, { method: 'PUT', body: croppedBlob, headers: { 'Content-Type': 'image/png' }, }) if (!uploadResponse.ok) { throw new Error('Failed to upload file') } await confirmUpload.mutateAsync({ projectId, key, providerType }) toast.success('Project logo updated') setOpen(false) resetState() onUploadComplete?.() } catch (error) { console.error('Upload error:', error) toast.error('Failed to upload logo. Please try again.') } finally { setIsUploading(false) } } const handleDelete = async () => { setIsDeleting(true) try { await deleteLogo.mutateAsync({ projectId }) toast.success('Logo removed') setOpen(false) onUploadComplete?.() } catch (error) { console.error('Delete error:', error) toast.error('Failed to remove logo') } finally { setIsDeleting(false) } } const resetState = () => { setImageSrc(null) setCrop({ x: 0, y: 0 }) setZoom(1) setCroppedAreaPixels(null) if (fileInputRef.current) fileInputRef.current.value = '' } const handleCancel = () => { resetState() setOpen(false) } return ( {children || ( )} Project Logo {imageSrc ? 'Drag to reposition and use the slider to zoom. The logo will be cropped to a square.' : `Upload a logo for your project. Allowed formats: JPEG, PNG, GIF, WebP. Max size: ${MAX_SIZE_MB}MB.`}
{imageSrc ? ( <> {/* Cropper */}
{/* Zoom slider */}
setZoom(val)} className="flex-1" />
{/* Change image button */} ) : ( <> {/* Current logo preview */} {currentLogoUrl && (
Current logo
)} {/* File input */}
)}
{currentLogoUrl && !imageSrc && ( )}
{imageSrc && ( )}
) }