import { useState, useCallback, useRef } from "react"; import { Upload, FileArchive, X, AlertCircle, Package } from "lucide-react"; interface ZipUploaderProps { onFilesReady: (files: File[], zipName: string) => void; disabled?: boolean; } export function ZipUploader({ onFilesReady, disabled }: ZipUploaderProps) { const [dragActive, setDragActive] = useState(false); const [error, setError] = useState(null); const [zipInfo, setZipInfo] = useState<{ name: string; size: string } | null>(null); const fileInputRef = useRef(null); const processZip = useCallback( async (file: File) => { if (!file.name.toLowerCase().endsWith(".zip")) { setError("Please upload a .zip file containing car images."); return; } setError(null); setZipInfo({ name: file.name, size: `${(file.size / 1024 / 1024).toFixed(2)} MB`, }); const JSZip = (await import("jszip")).default; const zip = await JSZip.loadAsync(file); const imageFiles: File[] = []; const IMAGE_EXTS = /\.(jpe?g|png|webp|gif|bmp)$/i; const entries = Object.entries(zip.files).filter( ([name, entry]) => !entry.dir && IMAGE_EXTS.test(name) ); if (entries.length === 0) { setError("No image files found inside the ZIP archive."); setZipInfo(null); return; } for (const [name, entry] of entries) { const blob = await entry.async("blob"); const basename = name.split("/").pop() ?? name; const ext = basename.split(".").pop()?.toLowerCase() ?? "jpg"; const mimeMap: Record = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", bmp: "image/bmp", }; const imgFile = new File([blob], basename, { type: mimeMap[ext] ?? "image/jpeg", }); imageFiles.push(imgFile); } onFilesReady(imageFiles, file.name); }, [onFilesReady] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setDragActive(false); const file = e.dataTransfer.files[0]; if (file) processZip(file); }, [processZip] ); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) processZip(file); e.target.value = ""; }, [processZip] ); const clearSelection = () => { setZipInfo(null); setError(null); }; return (
{/* Drop Zone */}
!disabled && fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); if (!disabled) setDragActive(true); }} onDragLeave={() => setDragActive(false)} onDrop={disabled ? undefined : handleDrop} className={[ "relative cursor-pointer rounded-2xl border-2 border-dashed transition-all duration-300 overflow-hidden", "group select-none", disabled ? "opacity-50 cursor-not-allowed border-surface-500" : dragActive ? "border-brand-500 bg-brand-500/10 scale-[1.01] shadow-[0_0_40px_rgba(26,107,255,0.2)]" : zipInfo ? "border-emerald-500/60 bg-emerald-500/5 hover:border-emerald-400" : "border-surface-500 hover:border-brand-500/60 hover:bg-brand-500/5", ].join(" ")} > {/* Animated background grid */}
{zipInfo ? ( <>

{zipInfo.name}

{zipInfo.size} · Ready to classify

Click to replace with a different ZIP

) : ( <>
{dragActive ? ( ) : ( )}

{dragActive ? "Drop your ZIP here" : "Upload Car Photos ZIP"}

Drag & drop a .zip archive containing car photos, or click to browse

JPG, PNG, WEBP supported
Multiple images in one ZIP
Detects 8 car angles
)}
{/* Error */} {error && (
{error}
)}
); }