jayvatliq's picture
Upload folder using huggingface_hub
16533c4 verified
Raw
History Blame Contribute Delete
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>
);
}