// 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 { ArrowLeft, ArrowRight, UploadCloud, X, FileVideo, FileText, Image, FileAudio } from "lucide-react";
import { useAssets } from "@/contexts/AdminAssetsContext";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
import { Progress } from "@/components/ui/progress";
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({
display_name: z.string().min(1, "Display name is required."),
description: z.string().optional(),
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 { uploadAsset, loading } = useAssets(); const { user } = useAuth(); const fileRef = useRef(null); const thumbnailRef = useRef(null); const [thumbKey, setThumbKey] = useState(0); const [progress, setProgress] = useState(null); // { phase: 'uploading'|'processing'|'done'|'error', pct } | null const { register, control, handleSubmit, setValue, watch, setError, clearErrors, formState: { errors, isDirty }, } = useForm({ resolver: zodResolver(schema), defaultValues: { display_name: "", description: "", 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); if (!watch("display_name")) setValue("display_name", f.name); clearErrors("_file"); }; const setThumbnail = (f) => { thumbnailRef.current = f; setValue("_thumbnail", f); clearErrors("_thumbnail"); }; const onSubmit = async (data) => { let hasFileError = false; if (!fileRef.current) { setError("_file", { message: "A file is required." }); hasFileError = true; } if (hasFileError) return; setProgress({ phase: "uploading", pct: 0 }); const result = await uploadAsset({ file: fileRef.current, thumbnail: thumbnailRef.current ?? undefined, display_name: data.display_name, description: data.description ?? "", file_type: fileType, is_public: data.is_public === "true", storage_provider: data.storage_provider, createdBy: user?.user_id, onProgress: setProgress, }); setProgress(null); if (result) { bypassOnce(); navigate("/admin/assets"); } }; const progressLabel = { uploading: "Uploading…", processing: "Processing…", done: "Done.", error: "Upload failed.", }[progress?.phase]; return (Upload a new file to the asset library.