// 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 ── */}
);
}