feat: add chat topic
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ConfirmDialog,
|
||||
Field,
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
Modal,
|
||||
PageHeader,
|
||||
@@ -25,6 +26,7 @@ interface Category {
|
||||
interface SubCategory {
|
||||
id: number;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
category_id?: number;
|
||||
category?: { id?: number; name?: string };
|
||||
}
|
||||
@@ -39,6 +41,7 @@ export default function SubCategoriesPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [categoryId, setCategoryId] = useState("");
|
||||
|
||||
const [deleting, setDeleting] = useState<SubCategory | null>(null);
|
||||
@@ -47,6 +50,7 @@ export default function SubCategoriesPage() {
|
||||
function openCreate() {
|
||||
setEditing(null);
|
||||
setName("");
|
||||
setDescription("");
|
||||
setCategoryId("");
|
||||
setOpen(true);
|
||||
}
|
||||
@@ -54,6 +58,7 @@ export default function SubCategoriesPage() {
|
||||
function openEdit(row: SubCategory) {
|
||||
setEditing(row);
|
||||
setName(row.name ?? "");
|
||||
setDescription(row.description ?? "");
|
||||
setCategoryId(
|
||||
row.category_id != null
|
||||
? String(row.category_id)
|
||||
@@ -72,14 +77,14 @@ export default function SubCategoriesPage() {
|
||||
// Update is JSON on /sub-categories/:id
|
||||
await apiFetch(`/sub-categories/${editing.id}`, {
|
||||
method: "PUT",
|
||||
body: { name, category_id: categoryId },
|
||||
body: { name, category_id: categoryId, description },
|
||||
});
|
||||
toast.success("زیردسته ویرایش شد.");
|
||||
} else {
|
||||
// Create is multipart on /sub-categories
|
||||
await apiFetch("/sub-categories", {
|
||||
method: "POST",
|
||||
body: toFormData({ category_id: categoryId, name }),
|
||||
body: toFormData({ category_id: categoryId, name, description }),
|
||||
});
|
||||
toast.success("زیردسته افزوده شد.");
|
||||
}
|
||||
@@ -115,6 +120,11 @@ export default function SubCategoriesPage() {
|
||||
header: "دستهبندی والد",
|
||||
render: (r) => r.category?.name ?? "—",
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
header: "توضیحات",
|
||||
render: (r) => r.description || "—",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -202,6 +212,14 @@ export default function SubCategoriesPage() {
|
||||
autoFocus
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="توضیحات">
|
||||
<Textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="توضیح کوتاه درباره این زیردسته"
|
||||
/>
|
||||
</Field>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user