"use client" import { type ReactNode, useMemo, useState } from "react" import { Search } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" export interface DataTableColumn { key: string header: string cell: (row: T) => ReactNode headerClassName?: string cellClassName?: string } export interface DataTableFilter { key: string label: string options: string[] accessor: (row: T) => string } export interface DataTableProps { data: readonly T[] columns: DataTableColumn[] getRowId: (row: T) => string | number searchAccessor?: (row: T) => string searchPlaceholder?: string filters?: DataTableFilter[] actions?: (row: T) => ReactNode pageSize?: number emptyMessage?: string } const ALL_VALUE = "All" export function DataTable({ data, columns, getRowId, searchAccessor, searchPlaceholder = "Search...", filters = [], actions, pageSize = 5, emptyMessage = "No results found.", }: DataTableProps) { const [query, setQuery] = useState("") const [activeFilters, setActiveFilters] = useState>({}) const [page, setPage] = useState(1) const filteredData = useMemo(() => { let rows = data if (query.trim() && searchAccessor) { const q = query.trim().toLowerCase() rows = rows.filter((row) => searchAccessor(row).toLowerCase().includes(q)) } for (const filter of filters) { const active = activeFilters[filter.key] if (active && active !== ALL_VALUE) { rows = rows.filter((row) => filter.accessor(row) === active) } } return rows }, [data, query, searchAccessor, filters, activeFilters]) const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize)) const currentPage = Math.min(page, totalPages) const start = (currentPage - 1) * pageSize const visibleRows = filteredData.slice(start, start + pageSize) const goTo = (target: number) => setPage(Math.min(Math.max(target, 1), totalPages)) const setFilter = (key: string, value: string) => { setActiveFilters((prev) => ({ ...prev, [key]: value })) setPage(1) } return (
{(searchAccessor || filters.length > 0) && (
{searchAccessor && (
{ setQuery(e.target.value) setPage(1) }} placeholder={searchPlaceholder} className="h-10 rounded-full bg-slate-50 pl-9" />
)} {filters.map((filter) => { const active = activeFilters[filter.key] ?? ALL_VALUE return (
{[ALL_VALUE, ...filter.options].map((option) => ( ))}
) })}
)} {columns.map((column, index) => ( {column.header} ))} {actions && ( Actions )} {visibleRows.length === 0 && ( {emptyMessage} )} {visibleRows.map((row) => ( {columns.map((column, index) => ( {column.cell(row)} ))} {actions && ( {actions(row)} )} ))}
{totalPages > 1 && ( { e.preventDefault() goTo(currentPage - 1) }} aria-disabled={currentPage === 1} className={currentPage === 1 ? "pointer-events-none opacity-50" : undefined} /> {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => ( { e.preventDefault() goTo(pageNumber) }} > {pageNumber} ))} { e.preventDefault() goTo(currentPage + 1) }} aria-disabled={currentPage === totalPages} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : undefined } /> )}
) }