// modules/admin/pages/assets/AddAssetsBulk.jsx import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { ArrowLeft, UploadCloud, X, RotateCcw, FileVideo, FileText, Image, FileAudio, CheckCircle2, XCircle, AlertTriangle, Loader2, Trash2, } from "lucide-react"; import { useAssets } from "@/contexts/AdminAssetsContext"; import { useUploadQueue } from "@/contexts/UploadQueueContext"; import { useAuth } from "@/contexts/AuthContext"; import { formatFileSize } from "@/utils/format.util"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; // ─── File type icon ─────────────────────────────────────────────────────────── function FileTypeIcon({ mime = "" }) { if (mime.startsWith("video/")) return ; if (mime.startsWith("image/")) return ; if (mime.startsWith("audio/")) return ; return ; } // ─── Drop zone ──────────────────────────────────────────────────────────────── function DropZone({ onFiles }) { const inputRef = useRef(null); const [dragOver, setDragOver] = useState(false); return (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer.files?.length) onFiles([...e.dataTransfer.files]); }} onClick={() => inputRef.current?.click()} className={[ "rounded-lg border-2 border-dashed cursor-pointer transition-colors", dragOver ? "border-primary/60 bg-primary/5" : "border-muted-foreground/30 hover:border-primary/50 hover:bg-muted/20", ].join(" ")} > { if (e.target.files?.length) onFiles([...e.target.files]); e.target.value = ""; }} />

Drag & drop or browse to upload
Images, videos, audio, documents — multiple files allowed

); } // ─── File row ───────────────────────────────────────────────────────────────── function FileRow({ job, onRetry, onRemove }) { const isUploading = job.status === "uploading" || job.status === "queued"; const isFailed = job.status === "failed" || job.status === "invalid"; const isUploaded = job.status === "uploaded"; const barClass = isUploaded ? "[&>div]:bg-green-500" : isFailed ? "[&>div]:bg-destructive" : ""; return (

{job.name}

{formatFileSize(job.size)}
{isUploading && <>{job.status === "queued" ? "Queued" : "Uploading…"}} {isUploaded && <>Uploaded} {job.status === "failed" && <>Upload failed} {job.status === "invalid" && <>Rejected}
{isFailed && job.error &&

{job.error}

}
{job.status === "failed" && ( )}
); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function AddAssetsBulk() { const navigate = useNavigate(); const { fetchAssets } = useAssets(); const { jobs: allJobs, addBatch, retryJob, removeJob, clearFinished } = useUploadQueue(); const { user } = useAuth(); // Add File (single) shares this same queue so its upload survives // navigation too — but it's a separate flow the user never asked to // batch, so it shouldn't appear here looking like part of a bulk run. const jobs = allJobs.filter((j) => j.source === "bulk"); const [isPublic, setIsPublic] = useState("false"); // TEMPORARY: locked to S3 (see zrok tunnel note; revert after VPS migration) const storageProvider = "s3"; const total = jobs.length; const done = jobs.filter((j) => j.status === "uploaded").length; const failed = jobs.filter((j) => j.status === "failed" || j.status === "invalid").length; const handleFiles = (files) => { addBatch(files, { is_public: isPublic === "true", storage_provider: storageProvider, createdBy: user?.user_id, }, { onSettled: () => fetchAssets({ force: true }), source: "bulk", }); }; return (
{/* ── Header ── */}

Uploading Files

{total ? `${total} file${total === 1 ? "" : "s"} · ${done} done · ${failed} failed` : "Upload one or more files to the file library."}

{/* ── Batch settings ── */}
{/* TEMPORARY: locked to S3 (see zrok tunnel note; revert after VPS migration) */}

Applies to files added below. Video thumbnails and per-asset details can be edited afterward.

{/* ── Drop zone ── */} {/* ── File list ── */} {total > 0 && (
Files {(done > 0 || failed > 0) && ( )}
{jobs.map((job) => ( ))}
)} {/* ── Actions ── */}
); }