Download src/components/ZipUploader.tsx from jayvatliq/car-view-classifier-frontend: direct link, hf CLI and curl.
- Browser
- Download file 7.54 kB
-
https://huggingface.co/spaces/jayvatliq/car-view-classifier-frontend/resolve/main/src/components/ZipUploader.tsx
- Command line
-
hf download hf://spaces/jayvatliq/car-view-classifier-frontend/src/components/ZipUploader.tsx
-
curl -L -o ZipUploader.tsx https://huggingface.co/spaces/jayvatliq/car-view-classifier-frontend/resolve/main/src/components/ZipUploader.tsx
7.54 kB
| 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<string | null>(null); | |
| const [zipInfo, setZipInfo] = useState<{ name: string; size: string } | null>(null); | |
| const fileInputRef = useRef<HTMLInputElement>(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<string, string> = { | |
| 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<HTMLInputElement>) => { | |
| const file = e.target.files?.[0]; | |
| if (file) processZip(file); | |
| e.target.value = ""; | |
| }, | |
| [processZip] | |
| ); | |
| const clearSelection = () => { | |
| setZipInfo(null); | |
| setError(null); | |
| }; | |
| return ( | |
| <div className="w-full"> | |
| {/* Drop Zone */} | |
| <div | |
| onClick={() => !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 */} | |
| <div className="absolute inset-0 bg-grid-pattern bg-[size:32px_32px] opacity-20 pointer-events-none" /> | |
| <div className="relative z-10 flex flex-col items-center justify-center py-16 px-8 text-center gap-6"> | |
| {zipInfo ? ( | |
| <> | |
| <div className="relative"> | |
| <div className="w-20 h-20 rounded-2xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center"> | |
| <FileArchive className="w-10 h-10 text-emerald-400" /> | |
| </div> | |
| <div className="absolute -top-2 -right-2 w-6 h-6 rounded-full bg-emerald-500 flex items-center justify-center"> | |
| <span className="text-white text-xs font-bold">✓</span> | |
| </div> | |
| </div> | |
| <div> | |
| <p className="text-white font-semibold text-lg">{zipInfo.name}</p> | |
| <p className="text-slate-400 text-sm mt-1">{zipInfo.size} · Ready to classify</p> | |
| </div> | |
| <p className="text-emerald-400 text-sm font-medium"> | |
| Click to replace with a different ZIP | |
| </p> | |
| </> | |
| ) : ( | |
| <> | |
| <div | |
| className={[ | |
| "w-24 h-24 rounded-2xl flex items-center justify-center transition-all duration-300", | |
| "bg-surface-600 border border-surface-500", | |
| "group-hover:bg-brand-500/20 group-hover:border-brand-500/40", | |
| dragActive ? "bg-brand-500/20 border-brand-500/40 scale-110" : "", | |
| ].join(" ")} | |
| > | |
| {dragActive ? ( | |
| <Package className="w-12 h-12 text-brand-400 animate-bounce-subtle" /> | |
| ) : ( | |
| <Upload className="w-12 h-12 text-slate-400 group-hover:text-brand-400 transition-colors" /> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <h3 className="text-2xl font-bold text-white"> | |
| {dragActive ? "Drop your ZIP here" : "Upload Car Photos ZIP"} | |
| </h3> | |
| <p className="text-slate-400 text-base max-w-md"> | |
| Drag & drop a <span className="text-brand-400 font-semibold">.zip archive</span> containing | |
| car photos, or <span className="text-brand-400 font-semibold">click to browse</span> | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-6 text-sm text-slate-500"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-1.5 h-1.5 rounded-full bg-brand-500" /> | |
| <span>JPG, PNG, WEBP supported</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-1.5 h-1.5 rounded-full bg-brand-500" /> | |
| <span>Multiple images in one ZIP</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-1.5 h-1.5 rounded-full bg-brand-500" /> | |
| <span>Detects 8 car angles</span> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".zip" | |
| onChange={handleFileSelect} | |
| className="hidden" | |
| disabled={disabled} | |
| /> | |
| </div> | |
| {/* Error */} | |
| {error && ( | |
| <div className="mt-4 flex items-center gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 animate-fade-in"> | |
| <AlertCircle className="w-5 h-5 flex-shrink-0" /> | |
| <span className="text-sm">{error}</span> | |
| <button | |
| onClick={clearSelection} | |
| className="ml-auto hover:text-red-300 transition-colors" | |
| > | |
| <X className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |