'use client'; import { Fragment, ReactNode, useCallback, useEffect, useState } from 'react'; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getSortedRowModel, TableOptions, useReactTable, ColumnDef, FilterFn, SortingState, OnChangeFn, Row, HeaderContext, ExpandedState, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; import Pagination from '@/components/Pagination'; import { cn } from '@/lib/helper'; interface TableClassNames { containerClassName?: string; tableWrapperClassName?: string; tableClassName?: string; tableHeaderClassName?: string; headerRowClassName?: string; headerColumnClassName?: string; tableBodyClassName?: string; bodyRowClassName?: string; selectedBodyRowClassName?: string; bodyColumnClassName?: string; bodySubRowClassName?: (depth: number) => string; selectedBodySubRowClassName?: (depth: number) => string; bodySubRowColumnClassName?: (depth: number) => string; tableFooterClassName?: string; footerRowClassName?: string; footerColumnClassName?: string; paginationClassName?: string; skeletonCellClassName?: string; } export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; isLoading?: boolean; fuzzySearchValue?: string | null; onFuzzySearchValueChange?: (value: string) => void; className?: TableClassNames; emptyContent?: ReactNode; sorting?: SortingState; setSorting?: OnChangeFn; manualSorting?: boolean; rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; withCheckbox?: boolean; withPagination?: boolean; rowOptions?: number[]; /** * Custom row renderer. Should return a complete element or null. * This gives full control over the row structure including colspan. * Return null to render the default row. */ renderCustomRow?: (row: Row) => ReactNode | null; getRowCanExpand?: (row: Row) => boolean; renderSubComponent?: (props: { row: Row }) => React.ReactElement; expanded?: ExpandedState; getSubRows?: (originalRow: TData, index: number) => TData[] | undefined; } const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ id: index, })); const emptyContentDefaultValue = (
Tidak ada data yang dapat ditampilkan...
); export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', headerColumnClassName: 'px-4 py-3 border-base-content/10 text-base-content/50 text-sm font-medium', tableBodyClassName: '', bodyRowClassName: 'transition-all duration-200 border-t border-base-content/10 bg-transparent', selectedBodyRowClassName: 'bg-primary/5', bodyColumnClassName: 'px-4 py-3 text-base-content font-medium', bodySubRowClassName: (depth: number) => 'transition-all duration-200 border-t border-base-content/10 bg-transparent', selectedBodySubRowClassName: (depth: number) => 'bg-primary/5', bodySubRowColumnClassName: (depth: number) => 'px-4 py-3 text-base-content font-medium', paginationClassName: 'px-3', tableFooterClassName: 'font-semibold border-base-content/10', footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ data = [], columns = [], pageSize = 10, onPageSizeChange, totalItems, page, onPageChange, isLoading = false, fuzzySearchValue, onFuzzySearchValueChange, className = TABLE_DEFAULT_STYLING, emptyContent = emptyContentDefaultValue, sorting, setSorting, manualSorting = false, rowSelection, setRowSelection, enableRowSelection, renderFooter = false, withCheckbox = false, withPagination = true, rowOptions = [10, 20, 50, 100], renderCustomRow, getRowCanExpand, renderSubComponent, expanded = {}, getSubRows, }: TableProps) => { const isServerSideTable = totalItems !== undefined && page !== undefined && onPageChange !== undefined; const tableClassNames = { ...TABLE_DEFAULT_STYLING, ...className, }; const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: pageSize, }); const fuzzyFilter: FilterFn = useCallback( (row, columnId, value, addMeta) => { const itemRank = rankItem(row.getValue(columnId), value); addMeta({ itemRank }); return itemRank.passed; }, [] ); const tableOptions: TableOptions = { columns, data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, getExpandedRowModel: getExpandedRowModel(), getRowCanExpand: getRowCanExpand ?? (getSubRows ? undefined : () => false), getSubRows, manualSorting, state: { pagination, globalFilter: fuzzySearchValue, expanded, }, filterFns: { fuzzy: fuzzyFilter, }, globalFilterFn: fuzzyFilter, }; if (fuzzySearchValue !== null) { tableOptions.onGlobalFilterChange = onFuzzySearchValueChange; tableOptions.getFilteredRowModel = getFilteredRowModel(); } if (sorting && setSorting) { tableOptions.onSortingChange = setSorting; tableOptions.state = { ...tableOptions.state, sorting, }; } if (rowSelection && setRowSelection) { tableOptions.onRowSelectionChange = setRowSelection; tableOptions.state = { ...tableOptions.state, rowSelection, }; tableOptions.getRowId = (row) => (row as { id: string }).id; } if (enableRowSelection !== undefined) { tableOptions.enableRowSelection = enableRowSelection; } const table = useReactTable(tableOptions); const { setPageSize } = table; const prevPageClickHandler = () => { table.previousPage(); if (isServerSideTable) { onPageChange(page - 1); } }; const nextPageClickHandler = () => { table.nextPage(); if (isServerSideTable) { onPageChange(page + 1); } }; const pageChangeHandler = (pageNumber: number) => { const currentPage = pageNumber - 1; table.setPageIndex(pageNumber ? currentPage : 0); if (isServerSideTable) { onPageChange(pageNumber); } }; useEffect(() => { setPageSize(pageSize); }, [pageSize, setPageSize]); return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const columnRelativeDepth = header.depth - header.column.depth; if ( !header.isPlaceholder && columnRelativeDepth > 1 && header.id === header.column.id ) { return null; } let rowSpan = 1; if (header.isPlaceholder) { const leafs = header.getLeafHeaders(); rowSpan = leafs[leafs.length - 1].depth - header.depth; } return ( ); })} ))} {table.getRowModel().rows.map((row) => { const customRowContent = renderCustomRow?.(row); if (customRowContent) { return renderCustomRow?.(row); } return ( 0 ? tableClassNames.bodySubRowClassName(row.depth) : tableClassNames.bodyRowClassName, { [tableClassNames.selectedBodyRowClassName!]: row.getIsSelected() && row.depth === 0, [tableClassNames.selectedBodySubRowClassName( row.depth )!]: row.getIsSelected() && row.depth > 0, } )} > {row.getVisibleCells().map((cell) => ( ))} {row.getIsExpanded() && ( <> {renderSubComponent && ( )} )} ); })} {(data.length === 0 || table.getRowModel().rows.length === 0) && !isLoading && ( )} {renderFooter && ( {table.getAllLeafColumns().map((column) => ( ))} )}
1, }, TABLE_DEFAULT_STYLING.headerColumnClassName, tableClassNames.headerColumnClassName )} >
1, })} > {flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getCanSort() && (
)}
0 ? tableClassNames.bodySubRowColumnClassName( row.depth ) : tableClassNames.bodyColumnClassName )} > {!isLoading && flexRender( cell.column.columnDef.cell, cell.getContext() )} {isLoading && (
)}
{renderSubComponent({ row })}
{emptyContent}
{column.columnDef.footer && flexRender(column.columnDef.footer, { column, header: column.columnDef, table, } as HeaderContext)}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && withPagination && (
)}
); }; export default Table;