Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { PageHeader, DataTable, EmptyState, Input } from "@/components/ds";
|
|
import { Search } from "lucide-react";
|
|
import { SportsCenterBreadcrumbs } from "@/modules/sports-center/components/SportsCenterBreadcrumbs";
|
|
|
|
export type SportsCenterTableColumn = {
|
|
key: string;
|
|
header: string;
|
|
className?: string;
|
|
render?: (row: Record<string, unknown>) => React.ReactNode;
|
|
searchable?: boolean;
|
|
};
|
|
|
|
export function SportsCenterTablePage({
|
|
title,
|
|
description,
|
|
breadcrumb,
|
|
columns,
|
|
rows,
|
|
empty,
|
|
actions,
|
|
searchPlaceholder = "جستجو…",
|
|
filterFn,
|
|
toolbar,
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
breadcrumb?: string;
|
|
columns: SportsCenterTableColumn[];
|
|
rows: Record<string, unknown>[];
|
|
empty?: React.ReactNode;
|
|
actions?: React.ReactNode;
|
|
searchPlaceholder?: string;
|
|
filterFn?: (row: Record<string, unknown>, query: string) => boolean;
|
|
toolbar?: React.ReactNode;
|
|
}) {
|
|
const [query, setQuery] = useState("");
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = query.trim().toLowerCase();
|
|
if (!q) return rows;
|
|
if (filterFn) return rows.filter((r) => filterFn(r, q));
|
|
return rows.filter((r) =>
|
|
columns.some((c) => {
|
|
if (c.searchable === false) return false;
|
|
const val = r[c.key];
|
|
return val != null && String(val).toLowerCase().includes(q);
|
|
})
|
|
);
|
|
}, [rows, query, columns, filterFn]);
|
|
|
|
return (
|
|
<div>
|
|
{breadcrumb ? <SportsCenterBreadcrumbs current={breadcrumb} /> : null}
|
|
<PageHeader title={title} description={description} actions={actions} />
|
|
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
<div className="relative max-w-md flex-1">
|
|
<Search className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--muted)]" />
|
|
<Input
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder={searchPlaceholder}
|
|
className="pr-10"
|
|
aria-label="جستجو"
|
|
/>
|
|
</div>
|
|
{toolbar}
|
|
</div>
|
|
<DataTable
|
|
columns={columns}
|
|
rows={filtered}
|
|
empty={empty ?? <EmptyState title="موردی یافت نشد" />}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|