"use client"; import { useState, useMemo } from "react"; export type SortDir = "asc" | "desc"; export interface TableControlsResult { search: string; setSearch: (v: string) => void; sortKey: keyof T | null; sortDir: SortDir; toggleSort: (key: keyof T) => void; activeFilters: string[]; toggleFilter: (chip: string) => void; filtered: T[]; } export interface UseTableControlsOptions { /** All rows from the server. */ rows: T[]; /** * Return a flat string of all searchable text for a row. * Concatenate every visible column value, lower-cased. */ searchText: (row: T) => string; /** * Return true if the row matches the given active chip label. * Called once per active chip; row must match ALL active chips. */ chipMatch: (row: T, chip: string) => boolean; /** * Return a comparable primitive for a sort key so the hook can sort. * Strings are compared case-insensitively; numbers/booleans natively. */ sortValue?: (row: T, key: keyof T) => string | number | boolean | null | undefined; /** Default sort column. Pass null to start unsorted. */ defaultSortKey?: keyof T | null; /** Default sort direction. */ defaultSortDir?: SortDir; } export function useTableControls({ rows, searchText, chipMatch, sortValue, defaultSortKey = null, defaultSortDir = "asc", }: UseTableControlsOptions): TableControlsResult { const [search, setSearch] = useState(""); const [sortKey, setSortKey] = useState(defaultSortKey); const [sortDir, setSortDir] = useState(defaultSortDir); const [activeFilters, setActiveFilters] = useState([]); const toggleSort = (key: keyof T) => { setSortKey((prev) => { if (prev === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); return key; } setSortDir("asc"); return key; }); }; const toggleFilter = (chip: string) => { setActiveFilters((prev) => prev.includes(chip) ? prev.filter((c) => c !== chip) : [...prev, chip] ); }; const filtered = useMemo(() => { let result = rows; // 1. Search const q = search.trim().toLowerCase(); if (q) { result = result.filter((row) => searchText(row).toLowerCase().includes(q)); } // 2. Chip filters — row must satisfy every active chip if (activeFilters.length > 0) { result = result.filter((row) => activeFilters.every((chip) => chipMatch(row, chip))); } // 3. Sort if (sortKey !== null && sortValue) { result = [...result].sort((a, b) => { const av = sortValue(a, sortKey) ?? ""; const bv = sortValue(b, sortKey) ?? ""; let cmp = 0; if (typeof av === "string" && typeof bv === "string") { cmp = av.toLowerCase().localeCompare(bv.toLowerCase()); } else { cmp = av < bv ? -1 : av > bv ? 1 : 0; } return sortDir === "asc" ? cmp : -cmp; }); } return result; }, [rows, search, activeFilters, sortKey, sortDir, searchText, chipMatch, sortValue]); return { search, setSearch, sortKey, sortDir, toggleSort, activeFilters, toggleFilter, filtered, }; }