From 012d92812048e508694e2b1dceed5f44287f8fae Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Wed, 17 Jun 2026 16:18:28 +0330 Subject: [PATCH] feat: add faq --- app/dashboard/faq/categories/page.tsx | 206 ++++++++++++++++++++ app/dashboard/faq/page.tsx | 261 ++++++++++++++++++++++++++ lib/nav.ts | 8 + 3 files changed, 475 insertions(+) create mode 100644 app/dashboard/faq/categories/page.tsx create mode 100644 app/dashboard/faq/page.tsx diff --git a/app/dashboard/faq/categories/page.tsx b/app/dashboard/faq/categories/page.tsx new file mode 100644 index 0000000..1cbcc4e --- /dev/null +++ b/app/dashboard/faq/categories/page.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { useState } from "react"; +import { apiFetch, ApiError } from "@/lib/api"; +import { useList } from "@/lib/useResource"; +import { useToast } from "@/components/toast"; +import { DataTable, type Column } from "@/components/DataTable"; +import { + Badge, + Button, + ConfirmDialog, + Field, + Input, + Modal, + PageHeader, + Switch, +} from "@/components/ui"; +import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { toFa } from "@/lib/utils"; + +interface FaqCategory { + id: number; + name?: string; + order?: number; + is_active?: boolean; + faqs_count?: number; +} + +export default function FaqCategoriesPage() { + const { data, loading, error, reload } = useList( + "/faq-categories?include_inactive=1", + ); + const toast = useToast(); + + const [editing, setEditing] = useState(null); + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [order, setOrder] = useState(""); + const [isActive, setIsActive] = useState(true); + const [saving, setSaving] = useState(false); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + function openCreate() { + setEditing(null); + setName(""); + setOrder(""); + setIsActive(true); + setOpen(true); + } + function openEdit(row: FaqCategory) { + setEditing(row); + setName(row.name ?? ""); + setOrder(row.order != null ? String(row.order) : ""); + setIsActive(row.is_active ?? true); + setOpen(true); + } + + async function save(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + const body = { + name, + order: order === "" ? undefined : Number(order), + is_active: isActive, + }; + if (editing) { + await apiFetch(`/faq-categories/${editing.id}`, { method: "PUT", body }); + toast.success("دسته‌بندی ویرایش شد."); + } else { + await apiFetch("/faq-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(`/faq-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: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" }, + { key: "name", header: "نام" }, + { + key: "faqs_count", + header: "تعداد پرسش", + render: (r) => toFa(r.faqs_count ?? 0), + className: "w-28", + }, + { key: "order", header: "ترتیب", render: (r) => toFa(r.order ?? 0), className: "w-20" }, + { + key: "is_active", + header: "وضعیت", + className: "w-24", + render: (r) => + r.is_active ? فعال : غیرفعال, + }, + ]; + + return ( +
+ } onClick={openCreate}> + دسته‌بندی جدید + + } + /> + + } onClick={openCreate}> + دسته‌بندی جدید + + } + actions={(row) => ( + <> + + + + )} + /> + + setOpen(false)} + title={editing ? "ویرایش دسته‌بندی" : "دسته‌بندی جدید"} + footer={ + <> + + + + } + > +
+ + setName(e.target.value)} + placeholder="مثلاً حساب کاربری" + required + autoFocus + /> + + + setOrder(e.target.value)} + dir="ltr" + /> + + + +
+ + setDeleting(null)} + /> +
+ ); +} diff --git a/app/dashboard/faq/page.tsx b/app/dashboard/faq/page.tsx new file mode 100644 index 0000000..e475031 --- /dev/null +++ b/app/dashboard/faq/page.tsx @@ -0,0 +1,261 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { apiFetch, ApiError } from "@/lib/api"; +import { useList } from "@/lib/useResource"; +import { useToast } from "@/components/toast"; +import { DataTable, type Column } from "@/components/DataTable"; +import { + Badge, + Button, + ConfirmDialog, + Field, + Input, + Textarea, + Select, + Modal, + PageHeader, + Switch, +} from "@/components/ui"; +import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { toFa } from "@/lib/utils"; + +interface Faq { + id: number; + faq_category_id?: number; + question?: string; + answer?: string; + order?: number; + is_active?: boolean; + category_name?: string; +} + +interface FaqCategory { + id: number; + name?: string; + faqs?: Faq[]; +} + +export default function FaqsPage() { + // /faqs returns categories each with their faqs; flatten for the table. + const { data: grouped, loading, error, reload } = useList( + "/faqs?include_inactive=1", + ); + const { data: categories } = useList("/faq-categories?include_inactive=1"); + const toast = useToast(); + + const rows = useMemo( + () => + grouped.flatMap((c) => + (c.faqs ?? []).map((f) => ({ ...f, category_name: c.name })), + ), + [grouped], + ); + + const [editing, setEditing] = useState(null); + const [open, setOpen] = useState(false); + const [categoryId, setCategoryId] = useState(""); + const [question, setQuestion] = useState(""); + const [answer, setAnswer] = useState(""); + const [order, setOrder] = useState(""); + const [isActive, setIsActive] = useState(true); + const [saving, setSaving] = useState(false); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + function openCreate() { + setEditing(null); + setCategoryId(categories[0] ? String(categories[0].id) : ""); + setQuestion(""); + setAnswer(""); + setOrder(""); + setIsActive(true); + setOpen(true); + } + function openEdit(row: Faq) { + setEditing(row); + setCategoryId(row.faq_category_id != null ? String(row.faq_category_id) : ""); + setQuestion(row.question ?? ""); + setAnswer(row.answer ?? ""); + setOrder(row.order != null ? String(row.order) : ""); + setIsActive(row.is_active ?? true); + setOpen(true); + } + + async function save(e: React.FormEvent) { + e.preventDefault(); + if (!categoryId) { + toast.error("دسته‌بندی را انتخاب کنید."); + return; + } + setSaving(true); + try { + const body = { + faq_category_id: Number(categoryId), + question, + answer, + order: order === "" ? undefined : Number(order), + is_active: isActive, + }; + if (editing) { + await apiFetch(`/faqs/${editing.id}`, { method: "PUT", body }); + toast.success("پرسش ویرایش شد."); + } else { + await apiFetch("/faqs", { 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(`/faqs/${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: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" }, + { key: "category", header: "دسته‌بندی", render: (r) => r.category_name ?? "—" }, + { key: "question", header: "پرسش", render: (r) => r.question ?? "—" }, + { + key: "answer", + header: "پاسخ", + render: (r) => + r.answer ? ( + {r.answer} + ) : ( + "—" + ), + }, + { key: "order", header: "ترتیب", render: (r) => toFa(r.order ?? 0), className: "w-20" }, + { + key: "is_active", + header: "وضعیت", + className: "w-24", + render: (r) => + r.is_active ? فعال : غیرفعال, + }, + ]; + + return ( +
+ } onClick={openCreate}> + پرسش جدید + + } + /> + + } onClick={openCreate}> + پرسش جدید + + } + actions={(row) => ( + <> + + + + )} + /> + + setOpen(false)} + title={editing ? "ویرایش پرسش" : "پرسش جدید"} + footer={ + <> + + + + } + > +
+ + + + + setQuestion(e.target.value)} + placeholder="مثلاً چطور رمز عبورم را تغییر دهم؟" + required + autoFocus + /> + + +