Files
lti-web-client/src/components/Table.tsx
T
2025-10-04 12:08:08 +07:00

264 lines
7.6 KiB
TypeScript

'use client';
import { ReactNode, useCallback, useEffect, useState } from 'react';
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
TableOptions,
useReactTable,
ColumnDef,
FilterFn,
SortingState,
OnChangeFn,
} 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;
bodyColumnClassName?: string;
paginationClassName?: string;
}
export interface TableProps<TData extends object> {
data: TData[];
columns: ColumnDef<TData, unknown>[];
pageSize?: number;
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<SortingState>;
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
const emptyContentDefaultValue = (
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Tidak ada data yang dapat ditampilkan...
</span>
</div>
);
const Table = <TData extends object>({
data = [],
columns = [],
pageSize = 10,
totalItems,
page,
onPageChange,
isLoading = false,
fuzzySearchValue,
onFuzzySearchValueChange,
className = {
containerClassName: '',
tableWrapperClassName: '',
tableClassName: '',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: '',
tableBodyClassName: '',
bodyRowClassName: '',
bodyColumnClassName: '',
paginationClassName: '',
},
emptyContent = emptyContentDefaultValue,
sorting,
setSorting,
}: TableProps<TData>) => {
const isServerSideTable =
totalItems !== undefined &&
page !== undefined &&
onPageChange !== undefined;
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: pageSize,
});
const fuzzyFilter: FilterFn<TData> = useCallback(
(row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
},
[]
);
const tableOptions: TableOptions<TData> = {
columns,
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
onSortingChange: setSorting,
state: {
pagination,
globalFilter: fuzzySearchValue,
sorting,
},
filterFns: {
fuzzy: fuzzyFilter,
},
globalFilterFn: fuzzyFilter,
};
if (fuzzySearchValue !== null) {
tableOptions.onGlobalFilterChange = onFuzzySearchValueChange;
tableOptions.getFilteredRowModel = getFilteredRowModel();
}
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 (
<div className={className.containerClassName}>
<div className={className.tableWrapperClassName}>
<table className={className.tableClassName}>
<thead className={className.tableHeaderClassName}>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={className.headerRowClassName}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
className.headerColumnClassName
)}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<div className='flex items-center'>
<Icon
icon='lucide:arrow-up'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='lucide:arrow-down'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className={className.tableBodyClassName}>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.bodyRowClassName}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className={className.bodyColumnClassName}>
{!isLoading &&
flexRender(cell.column.columnDef.cell, cell.getContext())}
{isLoading && <div className='skeleton w-full h-4' />}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
!isLoading &&
emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', className.paginationClassName)}>
<Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize}
currentPage={
isServerSideTable
? page
: table.getState().pagination.pageIndex + 1
}
onPrevPage={prevPageClickHandler}
onNextPage={nextPageClickHandler}
onPageChange={pageChangeHandler}
/>
</div>
)}
</div>
);
};
export default Table;