feat(admin): search + pagination on all data-driven tables

- AdminResource: client-side search box (matches across all fields) + 25/page
  pagination with prev/next and a filtered-count footer
- bumped pageSize on server-paged configs (users/blogs/comments/discounts/music/
  fonts/tags) so search/paginate covers the full set

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 06:48:58 +03:30
parent 7f2f65dd8a
commit db167062e6
2 changed files with 43 additions and 5 deletions
+33 -2
View File
@@ -43,6 +43,8 @@ const btn = "rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white
const btnGhost = "rounded-lg border border-[#262b40] px-3 py-1.5 text-xs text-gray-300 hover:bg-[#161a2e]";
const inputCls = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500";
const PAGE_SIZE = 25;
export function AdminResource({ config }: { config: ResourceConfig }) {
const idKey = config.idKey ?? "id";
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
@@ -51,6 +53,8 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
const [editing, setEditing] = useState<Record<string, unknown> | null>(null);
const [creating, setCreating] = useState(false);
const [form, setForm] = useState<Record<string, unknown>>({});
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [saving, setSaving] = useState(false);
const url = (suffix = "") => `/api/admin/resource/${config.basePath}${suffix}`;
@@ -129,6 +133,13 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
}
};
// Client-side search (across all fields) + pagination.
const q = query.trim().toLowerCase();
const filtered = q ? rows.filter((r) => JSON.stringify(r).toLowerCase().includes(q)) : rows;
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const safePage = Math.min(page, totalPages);
const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -136,10 +147,17 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
<h1 className="text-xl font-semibold text-white">{config.title}</h1>
{config.description && <p className="mt-1 text-sm text-gray-400">{config.description}</p>}
</div>
<div className="flex items-center gap-2">
<input
className="w-52 rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
placeholder="جستجو…" value={query}
onChange={(e) => { setQuery(e.target.value); setPage(1); }}
/>
{config.canCreate && config.fields && (
<button className={btn} onClick={openCreate}>+ مورد جدید</button>
)}
</div>
</div>
{error && <p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">{error}</p>}
@@ -158,10 +176,10 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
<tbody>
{loading ? (
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>در حال بارگذاری</td></tr>
) : rows.length === 0 ? (
) : paged.length === 0 ? (
<tr><td className="px-4 py-8 text-center text-gray-500" colSpan={99}>رکوردی یافت نشد.</td></tr>
) : (
rows.map((row, i) => (
paged.map((row, i) => (
<tr key={String(row[idKey] ?? i)} className="border-b border-[#161a2e] hover:bg-[#12152a]">
{config.columns.map((c) => (
<td key={c.key} className="px-4 py-3 text-gray-200">
@@ -193,6 +211,19 @@ export function AdminResource({ config }: { config: ResourceConfig }) {
</table>
</div>
{!loading && filtered.length > 0 && (
<div className="flex items-center justify-between text-xs text-gray-500" dir="rtl">
<span>{filtered.length.toLocaleString("fa-IR")} مورد{q ? " (فیلترشده)" : ""}</span>
{totalPages > 1 && (
<div className="flex items-center gap-2">
<button className={btnGhost} disabled={safePage <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>قبلی</button>
<span>صفحهٔ {safePage.toLocaleString("fa-IR")} از {totalPages.toLocaleString("fa-IR")}</span>
<button className={btnGhost} disabled={safePage >= totalPages} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>بعدی</button>
</div>
)}
</div>
)}
{(creating || editing) && config.fields && (
<div className="fixed inset-0 z-50 flex items-stretch justify-center bg-black/70 p-2 sm:p-6" onClick={closeForm}>
<div className={`${card} flex max-h-full w-full max-w-5xl flex-col`} onClick={(e) => e.stopPropagation()}>
+7
View File
@@ -70,6 +70,7 @@ export const tagsConfig: ResourceConfig = {
title: "برچسب‌ها",
description: "برچسب‌های کلیدواژه برای قالب‌ها و محتوا.",
basePath: "tags",
listQuery: "pageSize=500&page_size=500",
listKey: "items",
canCreate: true,
canEdit: true,
@@ -91,6 +92,7 @@ export const fontsConfig: ResourceConfig = {
title: "فونت‌ها",
description: "فونت‌های در دسترس در ویرایشگرهای استودیو.",
basePath: "fonts",
listQuery: "pageSize=500&page_size=500",
listKey: "items",
canCreate: true,
canEdit: true,
@@ -115,6 +117,7 @@ export const musicConfig: ResourceConfig = {
title: "موسیقی",
description: "ترک‌های صوتی موجود در کتابخانهٔ موسیقی استودیو.",
basePath: "music",
listQuery: "pageSize=500&page_size=500",
listKey: "items",
canCreate: true,
canEdit: false,
@@ -143,6 +146,7 @@ export const blogsConfig: ResourceConfig = {
title: "مقالات بلاگ",
description: "مقالات سیستم مدیریت محتوا (تولیدشده توسط هوش مصنوعی نیز).",
basePath: "blogs",
listQuery: "pageSize=500&page_size=500",
listKey: "items",
canCreate: true,
canEdit: true,
@@ -213,6 +217,7 @@ export const commentsConfig: ResourceConfig = {
title: "نظرات",
description: "مدیریت نظرات کاربران روی مقالات و قالب‌ها.",
basePath: "comments",
listQuery: "pageSize=500&page_size=500",
listKey: "data",
canCreate: false,
canEdit: false,
@@ -264,6 +269,7 @@ export const usersConfig: ResourceConfig = {
title: "کاربران",
description: "حساب‌های این مجموعه. مسدودسازی یا مدیریت در زیر.",
basePath: "users",
listQuery: "pageSize=500&page_size=500",
listKey: "data",
columns: [
{ key: "email", label: "ایمیل" },
@@ -298,6 +304,7 @@ export const discountsConfig: ResourceConfig = {
title: "تخفیف‌ها",
description: "کدهای تخفیف / کوپن. (کدها اینجا ساخته می‌شوند؛ هنوز API ویرایش/حذف وجود ندارد.)",
basePath: "discounts",
listQuery: "pageSize=500&page_size=500",
listKey: "data",
canCreate: true,
columns: [