/** * UploadZone — Drag-and-drop zone with click-to-browse, camera capture, * and full mobile responsiveness. * * On mobile/tablet the component shows three distinct action buttons: * 1. Browse Files — standard file-picker (all images in gallery) * 2. Take Photo — opens camera directly via `capture="environment"` * 3. (desktop) — full drag-and-drop surface as usual * * Expected input: 4 required orthographic views (Front, Back, Left, Right) * plus up to 2 optional views (Top, Bottom). */ import { useCallback, useRef, useState } from 'react'; import { motion } from 'framer-motion'; interface UploadZoneProps { onFilesSelected: (files: File[]) => void; disabled?: boolean; fileCount: number; } const MAX_FILES = 6; const ACCEPTED = 'image/jpeg,image/png'; /** Detect a coarse-grained touch/mobile environment */ function isMobileDevice(): boolean { return typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches; } export default function UploadZone({ onFilesSelected, disabled, fileCount }: UploadZoneProps) { const [isDragging, setIsDragging] = useState(false); const browseRef = useRef(null); const cameraRef = useRef(null); const isFull = fileCount >= MAX_FILES; const isBlocked = disabled || isFull; /* ── file helpers ── */ const filterAndEmit = useCallback( (rawFiles: FileList | null) => { if (!rawFiles || isBlocked) return; const valid = Array.from(rawFiles).filter( (f) => f.type === 'image/jpeg' || f.type === 'image/png', ); if (valid.length > 0) onFilesSelected(valid); }, [isBlocked, onFilesSelected], ); /* ── drag-and-drop handlers ── */ const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); filterAndEmit(e.dataTransfer.files); }, [filterAndEmit], ); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (!isBlocked) setIsDragging(true); }; const handleDragLeave = () => setIsDragging(false); /* ── click handlers ── */ const openBrowse = () => { if (!isBlocked) browseRef.current?.click(); }; const openCamera = () => { if (!isBlocked) cameraRef.current?.click(); }; const onBrowseChange = (e: React.ChangeEvent) => { filterAndEmit(e.target.files); e.target.value = ''; }; const onCameraChange = (e: React.ChangeEvent) => { filterAndEmit(e.target.files); e.target.value = ''; }; /* ── derived UI state ── */ const mobile = isMobileDevice(); return ( {/* Hidden file inputs */} {/* Camera input — capture="environment" opens rear camera on mobile */} {/* ── Main drop surface (desktop-first) ── */}
{/* Icon */}
{/* Primary label */}

{isFull ? 'Maximum images reached' : isDragging ? 'Drop images here!' : mobile ? 'Add photos of your object' : 'Drag & drop images here'}

{!isBlocked && (

{fileCount === 0 ? 'Need Front, Back, Left, Right views (4 required, up to 6 total)' : `${fileCount} / ${MAX_FILES} images added — need ${Math.max(0, 4 - fileCount)} more required views`}

)}
{/* Format badges */}
{['JPG', 'PNG', 'Max 10 MB each'].map((label) => ( {label} ))}
{/* ── Action buttons ── */} {!isBlocked && (
{/* Browse files */} {/* Take photo — prominent on mobile */}
)}
{/* Mobile tip */} {!isBlocked && mobile && fileCount === 0 && ( Tip: Capture orthographic views in order - Front, Back, Left, Right. )}
); }