176 lines
6.2 KiB
TypeScript
176 lines
6.2 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|