diff --git a/app/dashboard/deep-links/categories/page.tsx b/app/dashboard/deep-links/categories/page.tsx new file mode 100644 index 0000000..1bd027b --- /dev/null +++ b/app/dashboard/deep-links/categories/page.tsx @@ -0,0 +1,366 @@ +"use client"; + +import { 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, PlusIcon, TrashIcon } from "@/components/icons"; +import { MediaPreview } from "@/components/MediaPreview"; +import { pickUrl } from "@/lib/media"; +import { toFa } from "@/lib/utils"; + +interface Category { + id: number; + name: string; + type: string; + order?: number; + description?: string | null; + icon?: string | null; + icon_url?: string | null; + deep_link?: string | null; + is_deeplink_only?: boolean; + subcategories_count?: number; +} + +const ICON_KEYS = ["icon", "icon_url"]; + +export default function CategoryDeepLinksPage() { + const { data, loading, error, reload } = useList("/categories", { + deeplink_only: 1, + }); + const { data: allCategories, loading: allLoading } = + useList("/categories"); + const toast = useToast(); + + const [editing, setEditing] = useState(null); + const [open, setOpen] = useState(false); + const [saving, setSaving] = useState(false); + + // Source: 'new' (create a hidden category) or 'existing' (pick existing to hide). + const [source, setSource] = useState<"new" | "existing">("new"); + const [existingId, setExistingId] = useState(""); + + // Fields for creating a new hidden category. + const [name, setName] = useState(""); + const [type, setType] = useState("media"); + const [order, setOrder] = useState("0"); + const [description, setDescription] = useState(""); + const [icon, setIcon] = useState(null); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + function resetForm() { + setSource("new"); + setExistingId(""); + setName(""); + setType("media"); + setOrder("0"); + setDescription(""); + setIcon(null); + } + + function openCreate() { + setEditing(null); + resetForm(); + setOpen(true); + } + + async function toggleHidden(row: Category) { + try { + await apiFetch(`/categories/${row.id}`, { + method: "POST", + body: toFormData({ + is_deeplink_only: !row.is_deeplink_only ? 1 : 0, + }), + }); + toast.success( + row.is_deeplink_only + ? "از لینک مستقیم خارج شد." + : "به لینک مستقیم تبدیل شد.", + ); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); + } + } + + async function save(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + if (editing) { + setOpen(false); + reload(); + toast.success("لینک مستقیم بهروزرسانی شد."); + return; + } + + if (source === "existing") { + if (!existingId) { + toast.error("دسته‌بندی را انتخاب کنید."); + return; + } + await apiFetch(`/categories/${existingId}`, { + method: "POST", + body: toFormData({ is_deeplink_only: 1 }), + }); + toast.success("دسته‌بندی به لینک مستقیم تبدیل شد."); + } else { + const body = toFormData({ + name, + type, + order, + description, + icon, + is_deeplink_only: true, + }); + await apiFetch("/categories", { 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(`/categories/${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: "icon", + header: "آیکن", + className: "w-20", + render: (r) => ( + + ), + }, + { key: "name", header: "نام دسته‌بندی" }, + { + key: "type", + header: "نوع", + render: (r) => + r.type === "media" + ? "رسانه" + : r.type === "playlist" + ? "پلی‌لیست" + : r.type === "breathing_template" + ? "قالب تنفس" + : r.type, + className: "w-28", + }, + { + key: "deep_link", + header: "لینک مستقیم", + render: (r) => + r.deep_link ? ( + + {r.deep_link} + + ) : ( + — + ), + }, + { + key: "subcategories_count", + header: "زیر‌دسته‌ها", + render: (r) => toFa(r.subcategories_count ?? 0), + className: "w-28", + }, + ]; + + return ( +
+ } onClick={openCreate}> + لینک مستقیم جدید + + } + /> + + } onClick={openCreate}> + لینک مستقیم جدید + + } + actions={(row) => ( + <> + + + + )} + /> + + setOpen(false)} + title="لینک مستقیم دسته‌بندی جدید" + footer={ + <> + + + + } + > +