From e248943f4c50198ea72274815ae04b98dfa1c1ed Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Wed, 2 Sep 2026 11:56:19 +0330 Subject: [PATCH] fix --- app/dashboard/categories/page.tsx | 87 ++++++++++++- app/dashboard/deep-links/categories/page.tsx | 129 +++++++++++++------ app/dashboard/deep-links/media/page.tsx | 126 ++++++++++++------ app/dashboard/media/page.tsx | 90 +++++++++++-- 4 files changed, 340 insertions(+), 92 deletions(-) diff --git a/app/dashboard/categories/page.tsx b/app/dashboard/categories/page.tsx index ef5dc64..d629ae5 100644 --- a/app/dashboard/categories/page.tsx +++ b/app/dashboard/categories/page.tsx @@ -14,8 +14,9 @@ import { Textarea, Modal, PageHeader, + Switch, } from "@/components/ui"; -import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { EditIcon, EyeIcon, PlusIcon, TrashIcon } from "@/components/icons"; import { MediaPreview } from "@/components/MediaPreview"; import { pickUrl } from "@/lib/media"; import { toFa } from "@/lib/utils"; @@ -29,6 +30,8 @@ interface Category { order?: number; description?: string | null; icon?: string | null; + is_deeplink_only?: boolean; + deep_link?: string | null; subcategories_count?: number; } @@ -48,9 +51,16 @@ const ICON_KEYS = ["icon", "icon_url"]; export default function CategoriesPage() { const [typeFilter, setTypeFilter] = useState("media"); - const { data, loading, error, reload } = useList("/categories", { - type: typeFilter, - }); + // Deep-link-only categories are hidden from the normal listing, so they need + // their own view here — otherwise a category sent to deep links disappears + // from this page and can only be brought back from the deep-links screen. + const [showDeepLinks, setShowDeepLinks] = useState(false); + const { data, loading, error, reload } = useList( + "/categories", + // `deeplink_only` returns every hidden category regardless of type, so the + // type filter is not sent alongside it. + showDeepLinks ? { deeplink_only: 1 } : { type: typeFilter }, + ); const toast = useToast(); const [editing, setEditing] = useState(null); @@ -128,6 +138,25 @@ export default function CategoriesPage() { } } + // Move a category into or out of deep-link-only mode, both directions. + // Playlist categories live in music_categories, which has no such column. + async function toggleDeepLink(row: Category) { + try { + await apiFetch(`/categories/${row.id}`, { + method: "POST", + body: toFormData({ is_deeplink_only: row.is_deeplink_only ? 0 : 1 }), + }); + toast.success( + row.is_deeplink_only + ? "به فهرست عادی بازگشت." + : "به لینک مستقیم تبدیل شد.", + ); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); + } + } + async function confirmDelete() { if (!deleting) return; setRemoving(true); @@ -174,6 +203,25 @@ export default function CategoriesPage() { header: "توضیحات", render: (r) => r.description || "—", }, + { + key: "deep_link", + header: "لینک مستقیم", + className: "w-56", + render: (r) => + r.is_deeplink_only ? ( + + {r.deep_link ?? "—"} + + ) : ( + — + ), + }, { key: "subcategories_count", header: "زیر‌دسته‌ها", @@ -189,11 +237,17 @@ export default function CategoriesPage() { subtitle="مدیریت دسته‌بندی‌های عمومی رسانه، پلی‌لیست و قالب تنفس" action={
+ setSource(e.target.value as "new" | "existing")} - > - - - - + {!editing && ( + + + + )} {source === "existing" ? ( @@ -309,13 +345,14 @@ export default function CategoryDeepLinksPage() { /> + {/* No playlist option: playlist categories live in the + music_categories table, which has no deep-link column. */} @@ -337,7 +374,7 @@ export default function CategoryDeepLinksPage() { /> - + - پس از ذخیره، این دسته‌بندی در فهرست نمایش داده نمیشود و فقط با لینک - مستقیم قابل دسترسی است. + {editing ? ( + <> + این دسته‌بندی در فهرست نمایش داده نمیشود و فقط با لینک مستقیم قابل + دسترسی است. برای بازگرداندن آن به فهرست، از دکمه «نمایش در فهرست» + استفاده کنید. + + ) : ( + <> + پس از ذخیره، این دسته‌بندی در فهرست نمایش داده نمیشود و فقط با لینک + مستقیم قابل دسترسی است. + + )}

diff --git a/app/dashboard/deep-links/media/page.tsx b/app/dashboard/deep-links/media/page.tsx index bea87ee..cb7269c 100644 --- a/app/dashboard/deep-links/media/page.tsx +++ b/app/dashboard/deep-links/media/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { apiFetch, ApiError, toFormData } from "@/lib/api"; import { useList } from "@/lib/useResource"; import { useToast } from "@/components/toast"; @@ -14,10 +14,8 @@ import { PageHeader, Select, Textarea, - Badge, - Switch, } from "@/components/ui"; -import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +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"; @@ -70,14 +68,27 @@ function DeepLink({ deep_link }: { deep_link?: string | null }) { } export default function MediaDeepLinksPage() { - const { data, loading, error, reload } = useList("/media", { - deeplink_only: 1, - }); - const { data: allMedia, loading: allLoading } = useList("/media", { - count: 500, - }); + 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); @@ -118,17 +129,29 @@ export default function MediaDeepLinksPage() { setOpen(true); } - async function toggleHidden(row: Media) { + 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: !row.is_deeplink_only ? 1 : 0, - }), + body: toFormData({ is_deeplink_only: 0 }), }); - toast.success( - row.is_deeplink_only ? "از لینک مستقیم خارج شد." : "به لینک مستقیم تبدیل شد.", - ); + toast.success("از لینک مستقیم خارج شد."); reload(); } catch (err) { toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); @@ -140,13 +163,20 @@ export default function MediaDeepLinksPage() { setSaving(true); try { if (editing) { - setOpen(false); - reload(); + // 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("لینک مستقیم بهروزرسانی شد."); - return; - } - - if (source === "existing") { + } else if (source === "existing") { // Toggle the chosen existing media to deeplink-only. if (!existingId) { toast.error("رسانهای را انتخاب کنید."); @@ -256,12 +286,16 @@ export default function MediaDeepLinksPage() { } actions={(row) => ( <> + +