import { Box, Button, Flex, FormControl, FormLabel, Input, Popover, PopoverArrow, PopoverBody, PopoverCloseButton, PopoverContent, PopoverTrigger, Spacer, Table, TableCaption, TableContainer, TableRowProps, Tbody, Td, Th, Thead, Tr, useDisclosure, } from "@chakra-ui/react"; import { Cell, ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table"; import { Filter } from "lucide-react"; import { useTranslation } from "next-i18next"; import { ChangeEvent, ReactNode } from "react"; import { useDebouncedCallback } from "use-debounce"; export type DataTableColumnDef = ColumnDef & { filterable?: boolean; span?: number | ((cell: Cell) => number | undefined); }; // TODO: stricter type export type FilterItem = { id: string; value: string; }; export type DataTableRowPropsCallback = (row: Row) => TableRowProps; export type DataTableProps = { data: T[]; columns: DataTableColumnDef[]; caption?: string; filterValues?: FilterItem[]; onNextClick?: () => void; onPreviousClick?: () => void; onFilterChange?: (items: FilterItem[]) => void; disableNext?: boolean; disablePrevious?: boolean; disablePagination?: boolean; rowProps?: TableRowProps | DataTableRowPropsCallback; }; export const DataTable = ({ data, columns, caption, filterValues = [], onNextClick, onPreviousClick, onFilterChange, disableNext, disablePrevious, disablePagination, rowProps, }: DataTableProps) => { const { t } = useTranslation("leaderboard"); const { getHeaderGroups, getRowModel } = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }); const handleFilterChange = (value: FilterItem) => { const idx = filterValues.findIndex((oldValue) => oldValue.id === value.id); let newValues: FilterItem[] = []; if (idx === -1) { newValues = [...filterValues, value]; } else { newValues = filterValues.map((oldValue) => (oldValue.id === value.id ? value : oldValue)); } onFilterChange && onFilterChange(newValues); }; return ( <> {!disablePagination && ( )} {caption} {getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {getRowModel().rows.map((row) => { const props = typeof rowProps === "function" ? rowProps(row) : rowProps; return ( ); })}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {(header.column.columnDef as DataTableColumnDef).filterable && ( value.id === header.id)?.value ?? ""} onChange={(value) => handleFilterChange({ id: header.id, value })} label={flexRender(header.column.columnDef.header, header.getContext())} > )}
); }; type WithSpanCell = Cell & { span?: number }; const DataTableRow = ({ row }: { row: Row }) => { const cells: WithSpanCell[] = row.getVisibleCells(); const renderCells: WithSpanCell[] = []; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const span = (cell.column.columnDef as DataTableColumnDef).span; const spanValue = typeof span === "function" ? span(cell) : span; if (spanValue && spanValue > 1) { i += spanValue - 1; // skip next `spanValue - 1` cell } cell.span = spanValue; renderCells.push(cell); } return ( <> {renderCells.map((cell) => { return ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ); })} ); }; const FilterModal = ({ label, onChange, value, }: { label: ReactNode; onChange: (val: string) => void; value: string; }) => { const { isOpen, onOpen, onClose } = useDisclosure(); const handleInputChange = useDebouncedCallback((e: ChangeEvent) => { onChange(e.target.value); }, 500); return ( {label} ); };