feat: add chat topic

This commit is contained in:
2026-06-03 17:08:59 +03:30
parent 4d67af0bfc
commit 07cbeec816
9 changed files with 359 additions and 17 deletions
+67 -9
View File
@@ -10,17 +10,26 @@ import {
ConfirmDialog,
Field,
Input,
Textarea,
Modal,
PageHeader,
} 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;
description?: string | null;
icon?: string | null;
subcategories_count?: number;
}
// The icon is a direct image-file field on the category (not an image_id ref).
const ICON_KEYS = ["icon", "icon_url"];
export default function MediaCategoriesPage() {
const { data, loading, error, reload } = useList<Category>("/categories");
const toast = useToast();
@@ -28,6 +37,8 @@ export default function MediaCategoriesPage() {
const [editing, setEditing] = useState<Category | null>(null);
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [icon, setIcon] = useState<File | null>(null);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState<Category | null>(null);
@@ -36,11 +47,15 @@ export default function MediaCategoriesPage() {
function openCreate() {
setEditing(null);
setName("");
setDescription("");
setIcon(null);
setOpen(true);
}
function openEdit(row: Category) {
setEditing(row);
setName(row.name);
setName(row.name ?? "");
setDescription(row.description ?? "");
setIcon(null);
setOpen(true);
}
@@ -49,17 +64,19 @@ export default function MediaCategoriesPage() {
setSaving(true);
try {
if (editing) {
// Update is JSON on /categories/:id
await apiFetch(`/categories/${editing.id}`, {
method: "PUT",
body: { name },
// Icon is a file, so update goes through POST + Laravel method spoofing.
const body = toFormData({
_method: "PUT",
name,
description,
icon,
});
await apiFetch(`/categories/${editing.id}`, { method: "POST", body });
toast.success("دسته‌بندی ویرایش شد.");
} else {
// Create is multipart on /categories
await apiFetch("/categories", {
method: "POST",
body: toFormData({ name }),
body: toFormData({ name, description, icon }),
});
toast.success("دسته‌بندی افزوده شد.");
}
@@ -88,8 +105,26 @@ export default function MediaCategoriesPage() {
}
const columns: Column<Category>[] = [
{ key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-24" },
{
key: "icon",
header: "آیکن",
className: "w-20",
render: (r) => (
<MediaPreview kind="image" src={pickUrl(r, ICON_KEYS)} label={r.name} />
),
},
{ key: "name", header: "نام دسته‌بندی" },
{
key: "description",
header: "توضیحات",
render: (r) => r.description || "—",
},
{
key: "subcategories_count",
header: "زیر‌دسته‌ها",
render: (r) => toFa(r.subcategories_count ?? 0),
className: "w-28",
},
];
return (
@@ -148,7 +183,7 @@ export default function MediaCategoriesPage() {
</>
}
>
<form id="category-form" onSubmit={save}>
<form id="category-form" onSubmit={save} className="flex flex-col gap-4">
<Field label="نام دسته‌بندی" required>
<Input
value={name}
@@ -158,6 +193,29 @@ export default function MediaCategoriesPage() {
autoFocus
/>
</Field>
<Field label="توضیحات">
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="توضیح کوتاه درباره این دسته‌بندی"
/>
</Field>
<Field
label="آیکن"
hint={
editing
? "در صورت عدم انتخاب، آیکن فعلی حفظ می‌شود."
: "یک تصویر برای نمایش دسته‌بندی انتخاب کنید."
}
>
<Input
type="file"
accept="image/*"
onChange={(e) => setIcon(e.target.files?.[0] ?? null)}
/>
</Field>
</form>
</Modal>