pelagia-portal/App/components/ui/table-controls.tsx
Hardik 9758dcd8ab feat(admin): add client-side search, sort, and filter chips to all admin tables
Adds a reusable useTableControls hook and TableControls/SortableTh
components, then wires them into all six admin table pages (users,
vendors, vessels, sites, accounts, products). Each page now supports
a global search bar, clickable sortable column headers with ↑/↓/⇅
indicators, and role/status filter chips — all purely client-side with
no URL params or server round-trips. Server pages continue to fetch the
full list and pass it as props to a new *-table.tsx Client Component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 02:46:52 +05:30

141 lines
4.3 KiB
TypeScript

"use client";
import { cn } from "@/lib/utils";
import type { SortDir } from "./use-table-controls";
// ─── Search bar ──────────────────────────────────────────────────────────────
interface TableSearchProps {
value: string;
onChange: (v: string) => void;
placeholder?: string;
}
export function TableSearch({ value, onChange, placeholder = "Search…" }: TableSearchProps) {
return (
<div className="relative">
<svg
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607z"
/>
</svg>
<input
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="h-9 w-64 rounded-lg border border-neutral-300 bg-white pl-9 pr-3 py-2 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
/>
</div>
);
}
// ─── Filter chips ────────────────────────────────────────────────────────────
interface FilterChipsProps {
chips: string[];
active: string[];
onToggle: (chip: string) => void;
}
export function FilterChips({ chips, active, onToggle }: FilterChipsProps) {
return (
<div className="flex flex-wrap gap-1.5">
{chips.map((chip) => {
const isActive = active.includes(chip);
return (
<button
key={chip}
type="button"
onClick={() => onToggle(chip)}
className={cn(
"inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-colors",
isActive
? "border-primary-200 bg-primary-50 text-primary-700"
: "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50"
)}
>
{chip}
</button>
);
})}
</div>
);
}
// ─── TableControls wrapper ────────────────────────────────────────────────────
interface TableControlsProps {
search: string;
onSearch: (v: string) => void;
searchPlaceholder?: string;
chips: string[];
activeFilters: string[];
onToggleFilter: (chip: string) => void;
}
export function TableControls({
search,
onSearch,
searchPlaceholder,
chips,
activeFilters,
onToggleFilter,
}: TableControlsProps) {
return (
<div className="mb-3 flex flex-wrap items-center gap-3">
<TableSearch value={search} onChange={onSearch} placeholder={searchPlaceholder} />
{chips.length > 0 && (
<FilterChips chips={chips} active={activeFilters} onToggle={onToggleFilter} />
)}
</div>
);
}
// ─── Sortable column header ───────────────────────────────────────────────────
interface SortableThProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
sortKey: string;
activeSortKey: string | null;
sortDir: SortDir;
onSort: (key: string) => void;
}
export function SortableTh({
sortKey,
activeSortKey,
sortDir,
onSort,
children,
className,
...rest
}: SortableThProps) {
const isActive = activeSortKey === sortKey;
return (
<th
{...rest}
className={cn(
"px-4 py-3 text-left font-medium text-neutral-600 cursor-pointer select-none hover:bg-neutral-50",
className
)}
onClick={() => onSort(sortKey)}
>
<span className="inline-flex items-center gap-1">
{children}
<span className="text-neutral-400 text-xs">
{isActive ? (sortDir === "asc" ? "↑" : "↓") : "⇅"}
</span>
</span>
</th>
);
}