Files
meditation-admin/app/dashboard/questions/page.tsx
T
2026-06-03 17:08:59 +03:30

267 lines
7.7 KiB
TypeScript

"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,
Badge,
} from "@/components/ui";
import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
import { toFa } from "@/lib/utils";
interface Question {
id: number;
title?: string;
// Submitted as a string (name); returned by the API as a related object.
category?: { id?: number; name?: string } | string | null;
tags?: string[] | string;
}
function categoryName(c: Question["category"]): string {
if (!c) return "";
if (typeof c === "string") return c;
return c.name ?? "";
}
function asTags(value: Question["tags"]): string[] {
if (Array.isArray(value)) return value.map((t) => String(t));
if (typeof value === "string" && value.trim() !== "") {
return value
.split(",")
.map((t) => t.trim())
.filter(Boolean);
}
return [];
}
function parseTagsInput(input: string): string[] {
// Accept both Latin "," and Persian "،" separators.
return input
.split(/[,،]/)
.map((t) => t.trim())
.filter(Boolean);
}
export default function QuestionsPage() {
const [filterTag, setFilterTag] = useState("");
const [filterCategory, setFilterCategory] = useState("");
const { data, loading, error, reload } = useList<Question>("/questions", {
tag: filterTag || undefined,
category: filterCategory || undefined,
});
const toast = useToast();
const [editing, setEditing] = useState<Question | null>(null);
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [category, setCategory] = useState("");
const [tagsInput, setTagsInput] = useState("");
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState<Question | null>(null);
const [removing, setRemoving] = useState(false);
function openCreate() {
setEditing(null);
setTitle("");
setCategory("");
setTagsInput("");
setOpen(true);
}
function openEdit(row: Question) {
setEditing(row);
setTitle(row.title ?? "");
setCategory(categoryName(row.category));
setTagsInput(asTags(row.tags).join("، "));
setOpen(true);
}
async function save(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
try {
const tags = parseTagsInput(tagsInput);
if (editing) {
// Update is JSON on /questions/:id
await apiFetch(`/questions/${editing.id}`, {
method: "PUT",
body: { title, category, tags },
});
toast.success("پرسش ویرایش شد.");
} else {
// Create is multipart on /questions (toFormData expands tags -> tags[])
await apiFetch("/questions", {
method: "POST",
body: toFormData({ title, category, tags }),
});
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(`/questions/${deleting.id}`, { method: "DELETE" });
toast.success("پرسش حذف شد.");
setDeleting(null);
reload();
} catch (err) {
toast.error(err instanceof ApiError ? err.message : "خطا در حذف");
} finally {
setRemoving(false);
}
}
const columns: Column<Question>[] = [
{ key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" },
{ key: "title", header: "عنوان", render: (r) => r.title ?? "—" },
{ key: "category", header: "دسته", render: (r) => categoryName(r.category) || "—" },
{
key: "tags",
header: "برچسب‌ها",
render: (r) => {
const tags = asTags(r.tags);
return tags.length ? (
<div className="flex flex-wrap gap-1">
{tags.map((t, i) => (
<Badge key={`${t}-${i}`}>{t}</Badge>
))}
</div>
) : (
<span className="text-muted"></span>
);
},
},
];
return (
<div>
<PageHeader
title="بانک پرسش‌ها"
subtitle="مدیریت پرسش‌ها و دسته‌بندی آن‌ها"
action={
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
پرسش جدید
</Button>
}
/>
<div className="mb-4 flex flex-wrap items-end gap-3">
<Field label="فیلتر برچسب">
<Input
value={filterTag}
onChange={(e) => setFilterTag(e.target.value)}
placeholder="مثلاً انگیزشی"
/>
</Field>
<Field label="فیلتر دسته">
<Input
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
placeholder="مثلاً صبحگاهی"
/>
</Field>
</div>
<DataTable
columns={columns}
rows={data}
loading={loading}
error={error}
onRetry={reload}
emptyMessage="هنوز پرسشی ثبت نشده است."
emptyAction={
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
پرسش جدید
</Button>
}
actions={(row) => (
<>
<Button variant="ghost" onClick={() => openEdit(row)} aria-label="ویرایش">
<EditIcon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={() => setDeleting(row)}
aria-label="حذف"
className="text-danger"
>
<TrashIcon className="h-4 w-4" />
</Button>
</>
)}
/>
<Modal
open={open}
onClose={() => setOpen(false)}
title={editing ? "ویرایش پرسش" : "پرسش جدید"}
footer={
<>
<Button variant="secondary" onClick={() => setOpen(false)}>
انصراف
</Button>
<Button form="question-form" type="submit" loading={saving}>
ذخیره
</Button>
</>
}
>
<form id="question-form" onSubmit={save} className="flex flex-col gap-4">
<Field label="عنوان" required>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="متن پرسش"
required
autoFocus
/>
</Field>
<Field label="دسته">
<Input
value={category}
onChange={(e) => setCategory(e.target.value)}
placeholder="مثلاً صبحگاهی"
/>
</Field>
<Field
label="برچسب‌ها"
hint="برچسب‌ها را با کاما (،) از هم جدا کنید"
>
<Input
value={tagsInput}
onChange={(e) => setTagsInput(e.target.value)}
placeholder="انگیزشی، آرامش، تمرکز"
/>
</Field>
</form>
</Modal>
<ConfirmDialog
open={!!deleting}
message={`آیا از حذف «${deleting?.title ?? deleting?.id}» مطمئن هستید؟`}
loading={removing}
onConfirm={confirmDelete}
onClose={() => setDeleting(null)}
/>
</div>
);
}