// modules/admin/pages/assets/EditAsset.jsx import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useForm, Controller } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowLeft, UploadCloud, X, FileVideo, FileText, Image, RefreshCw } from "lucide-react"; import { useAssets } from "@/contexts/AdminAssetsContext"; import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc"; import { useAuth } from "@/contexts/AuthContext"; 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 { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; // ─── 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"]), }); // ─── Helpers ────────────────────────────────────────────────────────────────── function FileTypeIcon({ mimeType = "" }) { if (mimeType?.startsWith("video/")) return ; if (mimeType?.startsWith("image/")) return ; return ; } function FieldError({ message }) { if (!message) return null; return

{message}

; } function formatBytes(bytes) { if (!bytes) return "—"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } // ─── Thumbnail Drop Zone ────────────────────────────────────────────────────── function ThumbnailDropZone({ currentUrl, newFile, onFile, onClear, error }) { const inputRef = useRef(null); const preview = newFile ? URL.createObjectURL(newFile) : currentUrl ?? null; return (
e.preventDefault()} onDrop={(e) => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) onFile(f); }} onClick={() => !newFile && inputRef.current?.click()} className={[ "relative rounded-lg border-2 border-dashed transition-colors overflow-hidden", error ? "border-destructive/60 bg-destructive/5" : "", newFile ? "border-border cursor-default" : "", !newFile && !error ? "border-muted-foreground/30 hover:border-primary/50 hover:bg-muted/20 cursor-pointer" : "", ].join(" ")} > { if (e.target.files[0]) onFile(e.target.files[0]); }} /> {preview ? (
Thumbnail preview {/* overlay actions */}
{newFile && ( )}
{newFile && ( New )}
) : (

Drag & drop or browse to upload
JPEG, PNG

)}
); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function EditAsset() { const navigate = useNavigate(); const { assetId } = useParams(); const { fetchAsset, updateAsset, loading } = useAssets(); const { user } = useAuth(); const thumbnailRef = useRef(null); const [thumbKey, setThumbKey] = useState(0); const [initializing, setInitializing] = useState(true); const [asset, setAsset] = useState(null); // ← local, always fresh const { register, control, handleSubmit, reset, formState: { errors, isDirty }, } = useForm({ resolver: zodResolver(schema), defaultValues: { display_name: "", description: "", is_public: "false", }, }); useEffect(() => { (async () => { const res = await fetchAsset(assetId); const fetched = res?.data?.data ?? null; if (!fetched) return; setAsset(fetched); reset({ display_name: fetched.display_name ?? "", description: fetched.description ?? "", is_public: fetched.is_public ? "true" : "false", }); setInitializing(false); })(); }, [assetId]); const isVideo = asset?.file_type === "video"; const hasThumbnailChange = !!thumbnailRef.current; // asset.file_url is redacted to null for S3-stored assets (see // redactS3Url in assets.controller.js) — resolve the real preview src // the same way ViewImageAsset.jsx does instead of reading it raw. const { src: previewSrc } = useAssetPreviewSrc(asset, { scope: "admin" }); const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty || hasThumbnailChange); const onSubmit = async (data) => { const result = await updateAsset( assetId, { display_name: data.display_name, description: data.description ?? "", is_public: data.is_public === "true", updatedBy: user?.user_id, ...(isVideo && hasThumbnailChange ? { is_thumbnail: true } : {}), }, hasThumbnailChange ? thumbnailRef.current : null, ); if (!result) return; bypassOnce(); navigate(-1); }; if (initializing) { return (
); } if (!asset) { return (

Asset not found.

); } return (
{/* ── Header ── */}

Edit Asset

{asset.original_name}

{/* ── Current file info (read-only) ── */}

File Info

{asset.original_name}

{asset.file_type} {asset.extension?.toUpperCase()} {formatBytes(asset.file_size)} {asset.resolution && ( {asset.resolution} )}
{/* ── Editable fields ── */}
{/* ── Thumbnail (video or image) ── */} {(isVideo || asset.file_type === "image" || asset.file_type === "avatar") && (
{ thumbnailRef.current = f; setThumbKey((k) => k + 1); }} onClear={() => { thumbnailRef.current = null; setThumbKey((k) => k + 1); }} error={null} />
)} {/* ── Display Name ── */}
{/* ── Description ── */}