satupeta-main/shared/components/data-table.tsx

176 lines
6.2 KiB
TypeScript
Raw Normal View History

2026-01-27 02:31:12 +00:00
"use client";
import React, { useState } from "react";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, getSortedRowModel, SortingState, getFilteredRowModel, ColumnFiltersState, Column } from "@tanstack/react-table";
import { Input } from "@/shared/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table";
import { Button } from "@/shared/components/ui/button";
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ChevronsUpDown } from "lucide-react";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchColumn?: string;
searchPlaceholder?: string;
pageSize?: number;
}
export function createSortableHeader<T>(label: string) {
const SortableHeader = ({ column }: { column: Column<T, unknown> }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} className="flex items-center gap-1 font-medium">
{label}
<ChevronsUpDown className="ml-2 h-4 w-4" />
</Button>
);
};
SortableHeader.displayName = `SortableHeader(${label})`;
return SortableHeader;
}
export function DataTable<TData, TValue>({ columns, data, searchColumn, searchPlaceholder = "Search...", pageSize = 10 }: Readonly<DataTableProps<TData, TValue>>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [searchValue, setSearchValue] = useState("");
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
state: {
sorting,
columnFilters,
},
initialState: {
pagination: {
pageSize,
},
},
});
const handleSearch = (value: string) => {
setSearchValue(value);
if (searchColumn) {
table.getColumn(searchColumn)?.setFilterValue(value);
}
};
const renderPageNumbers = () => {
const currentPage = table.getState().pagination.pageIndex + 1;
const pageCount = table.getPageCount();
const pagesToShow = 5;
const halfPagesToShow = Math.floor(pagesToShow / 2);
let startPage = Math.max(1, currentPage - halfPagesToShow);
let endPage = Math.min(pageCount, currentPage + halfPagesToShow);
if (currentPage <= halfPagesToShow) {
endPage = Math.min(pagesToShow, pageCount);
}
if (currentPage >= pageCount - halfPagesToShow) {
startPage = Math.max(1, pageCount - pagesToShow + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(
<Button key={i} variant={i === currentPage ? "default" : "outline"} size="sm" onClick={() => table.setPageIndex(i - 1)}>
{i}
</Button>
);
}
return (
<div className="flex items-center space-x-1">
{currentPage > halfPagesToShow + 1 && pageCount > pagesToShow && (
<>
<Button variant="outline" size="sm" onClick={() => table.setPageIndex(0)}>
<ChevronsLeft className="h-4 w-4" />
</Button>
<span className="px-2">...</span>
</>
)}
{pages}
{currentPage < pageCount - halfPagesToShow && pageCount > pagesToShow && (
<>
<span className="px-2">...</span>
<Button variant="outline" size="sm" onClick={() => table.setPageIndex(pageCount - 1)}>
<ChevronsRight className="h-4 w-4" />
</Button>
</>
)}
</div>
);
};
return (
<div className="space-y-4">
{/* Search Input */}
{searchColumn && (
<div className="flex items-center w-full max-w-sm">
<Input placeholder={searchPlaceholder} value={searchValue} onChange={(e) => handleSearch(e.target.value)} className="max-w-sm" />
</div>
)}
{/* Table */}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="text-zinc-500 font-normal">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* Pagination */}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500">
Showing <strong>{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}</strong> to{" "}
<strong>{Math.min((table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length)}</strong> of <strong>{table.getFilteredRowModel().rows.length}</strong> results
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
<ChevronLeft className="h-4 w-4" />
</Button>
{renderPageNumbers()}
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}