// 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 ? (

{/* 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 (
);
}
return (
{/* ── Header ── */}
Edit Asset
{asset.original_name}
{unsavedChangesDialog}
);
}