"use client"; import { useCallback, useState } from "react"; import { apiFetch, ApiError, toFormData } from "@/lib/api"; import { useList } from "@/lib/useResource"; import { useToast } from "@/components/toast"; import { DataTable, type Column } from "@/components/DataTable"; import { Button, ConfirmDialog, Field, Input, Modal, PageHeader, Select, Textarea, } from "@/components/ui"; import { EditIcon, EyeIcon, PlusIcon, TrashIcon } from "@/components/icons"; import { MediaPreview } from "@/components/MediaPreview"; import { ImagePicker } from "@/components/ImagePicker"; import { pickUrl, SOUND_KEYS, VIDEO_KEYS, IMAGE_KEYS } from "@/lib/media"; import { formatMinutes, fileNameToTitle } from "@/lib/utils"; interface Cat { id: number; name?: string; } interface Media { id: number; title?: string; caption?: string; type?: string; duration?: number; visibility?: string; is_deeplink_only?: boolean; is_premium?: boolean; external_url?: string | null; deep_link?: string | null; image_id?: number | null; image?: unknown; detail_image_id?: number | null; detail_image?: unknown; categories?: Cat[]; sub_categories?: Cat[]; subcategories?: Cat[]; subCategories?: Cat[]; } function DeepLink({ deep_link }: { deep_link?: string | null }) { return (
{deep_link ? ( {deep_link} ) : ( — )}
); } export default function MediaDeepLinksPage() { const { data, loading, error, reload: reloadHidden } = useList( "/media", { deeplink_only: 1 }, ); // The "use an existing media" picker. /media only lists visible media, so it // must be refetched whenever a media moves in or out of deep-link mode — // otherwise a just-unhidden media is missing from the picker until a reload. const { data: allMedia, loading: allLoading, reload: reloadAll, } = useList("/media", { count: 500 }); const toast = useToast(); // Both lists change together on every toggle. const reload = useCallback(() => { reloadHidden(); reloadAll(); }, [reloadHidden, reloadAll]); // Non-null while the modal edits an existing row (null = create). const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); const [saving, setSaving] = useState(false); // Source: 'new' (upload / external url) or 'existing' (pick a media record). const [source, setSource] = useState<"new" | "existing">("new"); const [existingId, setExistingId] = useState(""); // Fields for creating a new hidden media. const [title, setTitle] = useState(""); const [caption, setCaption] = useState(""); const [type, setType] = useState("audio"); const [duration, setDuration] = useState(""); const [file, setFile] = useState(null); const [externalUrl, setExternalUrl] = useState(""); const [imageId, setImageId] = useState(null); const [imageFile, setImageFile] = useState(null); const [deleting, setDeleting] = useState(null); const [removing, setRemoving] = useState(false); function resetForm() { setSource("new"); setExistingId(""); setTitle(""); setCaption(""); setType("audio"); setDuration(""); setFile(null); setExternalUrl(""); setImageId(null); setImageFile(null); } function openCreate() { setEditing(null); resetForm(); setOpen(true); } function openEdit(row: Media) { setEditing(row); setSource("new"); // the edit form always shows the media fields setExistingId(""); setTitle(row.title ?? ""); setCaption(row.caption ?? ""); setType(row.type ?? "audio"); setDuration(row.duration != null ? String(row.duration) : ""); setFile(null); setExternalUrl(row.external_url ?? ""); setImageId(row.image_id ?? null); setImageFile(null); setOpen(true); } // Take a media back out of deep-link-only mode (it returns to the lists). async function unhide(row: Media) { try { await apiFetch(`/media/${row.id}`, { method: "POST", body: toFormData({ is_deeplink_only: 0 }), }); toast.success("از لینک مستقیم خارج شد."); reload(); } catch (err) { toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); } } async function save(e: React.FormEvent) { e.preventDefault(); setSaving(true); try { if (editing) { // Edit an existing deep-link media. It stays deeplink-only. const body = toFormData({ title, caption, type, duration, file, external_url: externalUrl, image_id: imageId, image: imageFile, }); await apiFetch(`/media/${editing.id}`, { method: "POST", body }); toast.success("لینک مستقیم بهروزرسانی شد."); } else if (source === "existing") { // Toggle the chosen existing media to deeplink-only. if (!existingId) { toast.error("رسانهای را انتخاب کنید."); return; } await apiFetch(`/media/${existingId}`, { method: "POST", body: toFormData({ is_deeplink_only: 1 }), }); toast.success("رسانه به لینک مستقیم تبدیل شد."); } else { // Create a brand new hidden media (upload / external url). const body = toFormData({ title, caption, type, duration, file, external_url: externalUrl, image_id: imageId, image: imageFile, is_deeplink_only: true, }); await apiFetch("/media", { method: "POST", body }); toast.success("لینک مستقیم جدید افزوده شد."); } setOpen(false); reload(); } catch (err) { toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); } finally { setSaving(false); } } async function confirmDelete() { if (!deleting) return; setRemoving(true); try { await apiFetch(`/media/${deleting.id}`, { method: "DELETE" }); toast.success("رسانه حذف شد."); setDeleting(null); reload(); } catch (err) { toast.error(err instanceof ApiError ? err.message : "خطا در حذف"); } finally { setRemoving(false); } } const columns: Column[] = [ { key: "play", header: "پخش", className: "w-20", render: (r) => r.type === "video" ? ( ) : ( ), }, { key: "thumbnail", header: "تصویر", className: "w-20", render: (r) => ( ), }, { key: "title", header: "عنوان", render: (r) => r.title ?? "—" }, { key: "deep_link", header: "لینک مستقیم", render: (r) => , }, { key: "duration", header: "مدت", render: (r) => formatMinutes(r.duration), }, ]; return (
} onClick={openCreate}> لینک مستقیم جدید } /> } onClick={openCreate}> لینک مستقیم جدید } actions={(row) => ( <> )} /> setOpen(false)} title={editing ? "ویرایش لینک مستقیم رسانه" : "لینک مستقیم رسانه جدید"} footer={ <> } >