"use client"; import * as React from "react"; import { Search } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; type ListLike = { loading: boolean; error: string | null; total: number; page: number; totalPages: number; setPage: (p: number) => void; }; /** * Shared chrome for every admin list page: a title with a live total, an * optional debounced search box, an optional toolbar (filters/tabs), the * table (children), and a pagination footer. Keeps the resource pages tiny. */ export function ListShell({ title, total, searchPlaceholder, onQueryChange, toolbar, list, children, }: { title: string; total: number; searchPlaceholder?: string; onQueryChange?: (q: string) => void; toolbar?: React.ReactNode; list: ListLike; children: React.ReactNode; }) { const [raw, setRaw] = React.useState(""); // Debounce search input → parent query (300ms). React.useEffect(() => { if (!onQueryChange) return; const t = setTimeout(() => onQueryChange(raw.trim()), 300); return () => clearTimeout(t); }, [raw, onQueryChange]); return (

{title} {total.toLocaleString()}

{toolbar}
{onQueryChange ? (
setRaw(e.target.value)} placeholder={searchPlaceholder ?? "Search…"} className="pl-9" />
) : null} {list.error ? (

{list.error}

) : null}
{children}
Page {list.page} of {list.totalPages} {list.loading ? " · loading…" : ""}
); } /** Centered empty-state row spanning the table. */ export function EmptyRow({ cols, label }: { cols: number; label: string }) { return ( {label} ); }