// modules/admin/pages/assets/AddAsset.jsx
import { useState, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useForm, Controller } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { ArrowLeft, ArrowRight, UploadCloud, X, FileVideo, FileText, Image, FileAudio } from "lucide-react";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useUploadQueue } from "@/contexts/UploadQueueContext";
import { useAuth } from "@/contexts/AuthContext";
import { MAX_ASSET_FILE_SIZE_SINGLE, MAX_ASSET_FILE_SIZE_SINGLE_LABEL } from "@/utils/assetUpload.util";
import { formatFileSize } from "@/utils/format.util";
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// ─── Derive file_type from MIME type ──────────────────────────────────────────
function resolveFileType(mimeType = "") {
if (mimeType.startsWith("video/")) return "video";
if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("audio/")) return "audio";
if (mimeType.startsWith("application/") || mimeType.startsWith("text/")) return "document";
return "document";
}
// ─── Schema ───────────────────────────────────────────────────────────────────
const schema = z.object({
is_public: z.enum(["true", "false"]),
storage_provider: z.enum(["chibisafe", "local", "s3"]),
});
// ─── File type icon ───────────────────────────────────────────────────────────
function FileTypeIcon({ mimeType = "" }) {
if (mimeType.startsWith("video/")) return
{file.name}
{formatFileSize(file.size)} · {file.type}
Drag & drop or browse to upload
{label}
{message}
; } // ─── Page ───────────────────────────────────────────────────────────────────── export default function AddAsset() { const navigate = useNavigate(); const { fetchAssets } = useAssets(); const { addBatch } = useUploadQueue(); const { user } = useAuth(); const fileRef = useRef(null); const thumbnailRef = useRef(null); const [thumbKey, setThumbKey] = useState(0); const { control, handleSubmit, setValue, watch, setError, clearErrors, formState: { errors, isDirty }, } = useForm({ resolver: zodResolver(schema), defaultValues: { is_public: "false", storage_provider: "s3", // ← changed from "chibisafe" }, }); const file = watch("_file"); const isVideo = file?.type?.startsWith("video/"); const isAudio = file?.type?.startsWith("audio/"); // setValue("_file", ...) doesn't mark isDirty (no shouldDirty), so a // picked-but-unsubmitted file wouldn't otherwise be caught by the guard. const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty || !!file); // ── Auto-derive file_type from MIME ─────────────────────────────────────── const fileType = file ? resolveFileType(file.type) : null; const setFile = (f) => { if (f.size > MAX_ASSET_FILE_SIZE_SINGLE) { setError("_file", { message: `File exceeds the ${MAX_ASSET_FILE_SIZE_SINGLE_LABEL} size limit.` }); return; } fileRef.current = f; setValue("_file", f); clearErrors("_file"); }; const setThumbnail = (f) => { thumbnailRef.current = f; setValue("_thumbnail", f); clearErrors("_thumbnail"); }; // Fire-and-forget: the job goes on the same global UploadQueueContext the // Bulk flow uses, so it keeps uploading in the background no matter where // you navigate to next — a single sonner toast.promise() tracks it // through loading -> success/error instead of a dedicated widget. const onSubmit = (data) => { if (!fileRef.current) { setError("_file", { message: "A file is required." }); return; } const fileName = fileRef.current.name; const uploadPromise = new Promise((resolve, reject) => { addBatch( [{ file: fileRef.current, thumbnail: thumbnailRef.current ?? undefined }], { is_public: data.is_public === "true", storage_provider: data.storage_provider, createdBy: user?.user_id, }, { source: "single", onSettled: ([result]) => { fetchAssets({ force: true }); if (result?.status === "uploaded") resolve(result); else reject(new Error(result?.error || "Upload failed.")); }, } ); }); toast.promise(uploadPromise, { loading: `Uploading ${fileName}…`, success: (result) => `${result.name} uploaded successfully.`, error: (err) => err.message || "Upload failed.", }); bypassOnce(); navigate("/admin/assets"); }; return (Upload a new file to the file library.