"use client"; import { useCallback, useEffect, useState } from "react"; import { FileUploadField } from "@/components/admin/FileUploadField"; // ── styles (match ProjectScenes) ────────────────────────────────────────────── const inp = "rounded-lg border border-[#262b40] bg-[#0c0e1a] px-2.5 py-1.5 text-sm text-gray-100 outline-none focus:border-indigo-500"; const btn = "rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; const ghost = "rounded-lg border border-[#262b40] px-2.5 py-1 text-xs text-gray-300 hover:bg-[#161a2e] disabled:opacity-50"; const lbl = "mb-1 block text-xs text-gray-400"; const del = "rounded-lg border border-red-500/30 px-2.5 py-1 text-xs text-red-300 hover:bg-red-500/10"; // ── types (snake_case — matches content-svc JSON) ───────────────────────────── interface PresetScene { id?: string; scene_id: string; scene_key?: string | null; scene_title?: string | null; sort: number; default_duration_sec?: number | null } interface StorySummary { id: string; project_id: string; name: string; description?: string | null; demo?: string | null; sort: number; is_published: boolean; scene_count: number } interface StoryFull extends StorySummary { music_id?: string | null; scenes_spa?: string | null; scenes: PresetScene[] } interface Scene { id: string; key: string; title: string; default_duration_sec?: number | null } type Draft = { name: string; description: string; demo: string; is_published: boolean; sort: number; scenes_spa: string; scenes: PresetScene[]; }; function emptyDraft(sort: number): Draft { return { name: "", description: "", demo: "", is_published: true, sort, scenes_spa: "", scenes: [] }; } function num(v: number | null | undefined) { return v === null || v === undefined ? "" : String(v); } function toNum(v: string): number | null { return v.trim() === "" ? null : Number(v); } export function ProjectPresetStories({ projectId }: { projectId: string }) { const [rows, setRows] = useState([]); const [scenes, setScenes] = useState([]); const [loading, setLoading] = useState(true); const [draft, setDraft] = useState(null); const [editId, setEditId] = useState(null); const [saving, setSaving] = useState(false); const [err, setErr] = useState(null); const base = "/api/admin/resource/preset-stories"; const load = useCallback(async () => { setLoading(true); const [r, sc] = await Promise.all([ fetch(`${base}?project_id=${projectId}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null), fetch(`/api/admin/resource/scenes?project_id=${projectId}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null), ]); setRows(Array.isArray(r) ? r : r?.data ?? []); setScenes(Array.isArray(sc) ? sc : sc?.data ?? []); setLoading(false); }, [projectId]); useEffect(() => { load(); }, [load]); const openNew = () => { setEditId(null); setErr(null); setDraft(emptyDraft(rows.length)); }; const openEdit = async (s: StorySummary) => { setErr(null); const full: StoryFull | null = await fetch(`${base}/${s.id}`, { cache: "no-store" }).then((x) => x.json()).catch(() => null); if (!full) { setErr("بارگذاری ویدیوی نمونه ناموفق بود"); return; } setEditId(s.id); setDraft({ name: full.name, description: full.description ?? "", demo: full.demo ?? "", is_published: full.is_published, sort: full.sort, scenes_spa: full.scenes_spa ?? "", scenes: (full.scenes ?? []).map((p) => ({ ...p })), }); }; const set = (p: Partial) => setDraft((d) => (d ? { ...d, ...p } : d)); const save = async () => { if (!draft) return; if (!draft.name.trim()) { setErr("نام ویدیوی نمونه الزامی است"); return; } setSaving(true); setErr(null); const body = { project_id: projectId, name: draft.name.trim(), description: draft.description || null, demo: draft.demo || null, is_published: draft.is_published, sort: draft.sort ?? 0, scenes_spa: draft.scenes_spa || null, scenes: draft.scenes.map((s, i) => ({ scene_id: s.scene_id, sort: i, default_duration_sec: s.default_duration_sec ?? null })), }; const res = await fetch(editId ? `${base}/${editId}` : base, { method: editId ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const d = await res.json().catch(() => null); if (res.ok) { setDraft(null); setEditId(null); load(); } else setErr(d?.message ?? d?.error?.message ?? "ذخیرهٔ ویدیوی نمونه ناموفق بود"); setSaving(false); }; const remove = async (s: StorySummary) => { if (!confirm(`ویدیوی نمونهٔ «${s.name}» حذف شود؟`)) return; await fetch(`${base}/${s.id}`, { method: "DELETE" }); load(); }; // ── scene-list editing ────────────────────────────────────────────────────── const addScene = () => { if (!draft) return; const first = scenes[0]; set({ scenes: [...draft.scenes, { scene_id: first?.id ?? "", scene_key: first?.key, scene_title: first?.title, sort: draft.scenes.length, default_duration_sec: first?.default_duration_sec ?? null }] }); }; const setSceneAt = (i: number, p: Partial) => { if (!draft) return; const a = [...draft.scenes]; a[i] = { ...a[i], ...p }; set({ scenes: a }); }; const removeSceneAt = (i: number) => { if (!draft) return; set({ scenes: draft.scenes.filter((_, j) => j !== i) }); }; if (draft) { return (

{editId ? "ویرایش ویدیوی نمونه" : "ویدیوی نمونهٔ جدید"}

{err &&

{err}

}
set({ name: e.target.value })} />
set({ sort: Number(e.target.value) || 0 })} />
set({ description: e.target.value })} />
set({ demo: u })} accept="video/*,image/*" />
{/* Scene list */}
صحنه‌های این ویدیو (ترتیب + مدت)
{scenes.length === 0 &&

این پروژه هنوز صحنه‌ای ندارد. ابتدا از «صحنه‌ها» صحنه تعریف کنید.

} {draft.scenes.length === 0 ? (

هنوز صحنه‌ای انتخاب نشده.

) : draft.scenes.map((ps, i) => (
#{i} setSceneAt(i, { default_duration_sec: toNum(e.target.value) })} />
))}
مقادیر آماده (JSON پیشرفته) — اختیاری