feat: initial
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"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 {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Field,
|
||||
Input,
|
||||
Select,
|
||||
PageHeader,
|
||||
Card,
|
||||
} from "@/components/ui";
|
||||
import { TrashIcon } from "@/components/icons";
|
||||
import { toFa } from "@/lib/utils";
|
||||
|
||||
interface Comment {
|
||||
id?: number;
|
||||
content?: string;
|
||||
text?: string;
|
||||
body?: string;
|
||||
created_at?: string;
|
||||
user?: {
|
||||
name?: string;
|
||||
};
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function CommentsPage() {
|
||||
const toast = useToast();
|
||||
|
||||
// Form (uncommitted) state
|
||||
const [type, setType] = useState("media");
|
||||
const [id, setId] = useState("");
|
||||
|
||||
// Committed query state used to build the request path
|
||||
const [query, setQuery] = useState<{ type: string; id: string } | null>(null);
|
||||
|
||||
const path =
|
||||
query && query.type && query.id
|
||||
? `/comments/${query.type}/${query.id}`
|
||||
: null;
|
||||
|
||||
const { data, loading, error, reload } = useList<Comment>(path);
|
||||
|
||||
const [deleting, setDeleting] = useState<Comment | null>(null);
|
||||
const [removing, setRemoving] = useState(false);
|
||||
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!type || !id.trim()) {
|
||||
toast.error("نوع و شناسه را وارد کنید.");
|
||||
return;
|
||||
}
|
||||
setQuery({ type, id: id.trim() });
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!deleting || !query) return;
|
||||
setRemoving(true);
|
||||
try {
|
||||
await apiFetch(`/comments/${query.type}/${query.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
toast.success("نظر حذف شد.");
|
||||
setDeleting(null);
|
||||
reload();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : "خطا در حذف");
|
||||
} finally {
|
||||
setRemoving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Column<Comment>[] = [
|
||||
{
|
||||
key: "user",
|
||||
header: "کاربر",
|
||||
render: (r) => r.user?.name ?? r.name ?? "—",
|
||||
},
|
||||
{
|
||||
key: "content",
|
||||
header: "متن",
|
||||
render: (r) => r.content ?? r.text ?? r.body ?? "—",
|
||||
},
|
||||
{
|
||||
key: "created_at",
|
||||
header: "تاریخ",
|
||||
render: (r) =>
|
||||
r.created_at
|
||||
? toFa(new Date(r.created_at).toLocaleDateString("fa-IR"))
|
||||
: "—",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="نظرات"
|
||||
subtitle="مشاهده و مدیریت نظرات بر اساس نوع و شناسه محتوا"
|
||||
/>
|
||||
|
||||
<Card className="mb-4">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="flex flex-wrap items-end gap-4"
|
||||
>
|
||||
<div className="min-w-40">
|
||||
<Field label="نوع">
|
||||
<Select value={type} onChange={(e) => setType(e.target.value)}>
|
||||
<option value="media">رسانه</option>
|
||||
<option value="music">موسیقی</option>
|
||||
<option value="playlist">پلیلیست</option>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="min-w-40">
|
||||
<Field label="شناسه">
|
||||
<Input
|
||||
value={id}
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
placeholder="شناسه محتوا"
|
||||
dir="ltr"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Button type="submit">نمایش</Button>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{path ? (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
rows={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={reload}
|
||||
emptyMessage={
|
||||
path
|
||||
? "نظری برای این مورد ثبت نشده است."
|
||||
: "برای مشاهده نظرات، نوع و شناسه را انتخاب و روی «نمایش» بزنید."
|
||||
}
|
||||
actions={(row) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setDeleting(row)}
|
||||
aria-label="حذف"
|
||||
className="text-danger"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<p className="text-sm text-muted">
|
||||
برای مشاهده نظرات، نوع و شناسه را انتخاب و دکمه نمایش را بزنید.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleting}
|
||||
message="آیا از حذف این نظر مطمئن هستید؟"
|
||||
loading={removing}
|
||||
onConfirm={confirmDelete}
|
||||
onClose={() => setDeleting(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user