feat(data-table): Introduce comprehensive DataTable component with full feature set

This commit is contained in:
ghost2023
2026-05-19 16:50:01 +03:00
parent 4085e856e3
commit 634747640a
7 changed files with 399 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import { Table as TanstackTable } from "@tanstack/react-table";
import { TableCell, TableRow } from "#components/table";
import { Button } from "#components/button.tsx";
export function DataTableError({
table,
message,
description,
onRetry,
}: {
table: TanstackTable<any>;
message?: string;
description?: string;
onRetry?: () => void;
}) {
return (
<TableRow>
<TableCell colSpan={table.getVisibleFlatColumns().length}>
<div className="flex items-center my-6 justify-center">
<div className="text-center">
<div className="my-4">
<h2 className="text-xl font-semibold ">
{message ?? "No results"}
</h2>
<p className="mt-2 text-sm text-secondary-foreground">
{description ?? "No results found"}
</p>
</div>
{onRetry && (
<Button variant={"outline"} onClick={onRetry}>
Retry
</Button>
)}
</div>
</div>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,119 @@
import { Button } from "../button";
import { DataTableFooterProps } from "./types";
export interface DataTableFooterOptions {
pageSizeOptions?: number[];
showPageSizeSelector?: boolean;
showRowCount?: boolean;
showPagination?: boolean;
labels?: {
rowsPerPage?: string;
page?: string;
of?: string;
showing?: string;
ofLabel?: string;
items?: string;
previous?: string;
next?: string;
};
}
interface DataTableFooterComponentProps<
TData,
> extends DataTableFooterProps<TData> {
options?: DataTableFooterOptions;
}
const defaultOptions: DataTableFooterOptions = {
pageSizeOptions: [5, 10, 25, 50],
showPageSizeSelector: true,
showRowCount: true,
showPagination: true,
labels: {
rowsPerPage: "Rows per page",
page: "Page",
of: "of",
showing: "Showing",
ofLabel: "of",
items: "items",
previous: "Previous",
next: "Next",
},
};
export function DataTableFooter<TData>({
table,
pagination,
options = {},
}: DataTableFooterComponentProps<TData>) {
const opts = { ...defaultOptions, ...options };
const labels = { ...defaultOptions.labels, ...options.labels };
const pageIndex = pagination.pageIndex ?? 0;
const pageSize = pagination.pageSize ?? 10;
const totalCount = pagination.totalCount ?? 0;
const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
const handlePageSizeChange = (newPageSize: number) => {
table?.setPageSize(newPageSize);
};
return (
<div className="flex flex-wrap items-center justify-between gap-4 p-4 max-sm:flex-col max-sm:items-start">
{(opts.showPageSizeSelector || opts.showRowCount) && (
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
{opts.showPageSizeSelector && (
<>
<label htmlFor="page-size" className="font-medium text-slate-700">
{labels.rowsPerPage}
</label>
<select
id="page-size"
value={pageSize}
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
>
{opts.pageSizeOptions?.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</>
)}
{opts.showRowCount && (
<span>
{labels.showing} {start}{end} {labels.ofLabel} {totalCount}{" "}
{labels.items}
</span>
)}
</div>
)}
{opts.showPagination && (
<div className="flex items-center justify-end space-x-2">
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table?.previousPage()}
disabled={!table?.getCanPreviousPage()}
>
{labels.previous}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table?.nextPage()}
disabled={!table?.getCanNextPage()}
>
{labels.next}
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { PaginationState } from "@tanstack/react-table";
import { useState } from "react";
export const usePagination = ({
pageSize,
pageIndex,
}: {
pageSize?: number;
pageIndex?: number;
} = {}) => {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: pageIndex ?? 0,
pageSize: pageSize ?? 10,
});
const setPage = (pageIndex: number) => {
setPagination((prev) => ({ ...prev, pageIndex }));
};
return {
pagination,
setPagination,
setPage,
};
};

View File

@@ -0,0 +1,12 @@
export { DataTableError } from "./error";
export { DataTableSkeleton } from "./skeleton";
export { DataTableFooter, type DataTableFooterOptions } from "./footer";
export type {
DataTableProps,
DataTablePagination,
DataTableFooterProps,
DataTableFooterComponent,
} from "./types";
export { usePagination } from "./hooks";
export { DataTable } from "./table";
export * from "@tanstack/react-table";

View File

@@ -0,0 +1,16 @@
import { Table as TanstackTable } from "@tanstack/react-table";
import { Skeleton } from "../skeleton";
import { TableCell, TableRow } from "#components/table";
export function DataTableSkeleton({ table }: { table: TanstackTable<any> }) {
return Array.from({ length: 10 }).map((_, i) => (
<TableRow key={i}>
{table.getAllColumns().map((column) => (
<TableCell key={column.id}>
<Skeleton className="h-8" />
</TableCell>
))}
</TableRow>
));
}

View File

@@ -0,0 +1,149 @@
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "#components/table";
import { DataTableProps } from "./types";
import { DataTableSkeleton } from "./skeleton";
import { DataTableError } from "./error";
import { DataTableFooter } from "./footer";
export function DataTable<TData, TValue>({
columns,
data,
status,
onRowClick,
tableOptions,
pagination,
footer,
footerClassName,
containerClassName,
error,
emptyMessage,
}: DataTableProps<TData, TValue>) {
const { state, ...otherOptions } = tableOptions ?? {};
const baseState = state ?? {};
if (pagination) {
baseState.pagination = {
pageIndex: pagination.pageIndex ?? 0,
pageSize: pagination.pageSize ?? 1,
};
}
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
...(pagination && {
getPaginationRowModel: getPaginationRowModel(),
pageCount: pagination.pageCount,
}),
state: baseState,
...otherOptions,
});
return (
<>
<div className={containerClassName}>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className={
(header.column.columnDef.meta as Record<string, any>)
?.headerClassName ?? ""
}
style={{ width: `${header.getSize()}px` }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{status === "loading" && <DataTableSkeleton table={table} />}
{status === "error" && (
<DataTableError
table={table}
message={error?.message ?? "Something went wrong"}
description={error?.description ?? "Please try again"}
onRetry={error?.onRetry}
/>
)}
{status === "success" &&
(table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={() => onRowClick?.(row.original)}
role={onRowClick ? "button" : ""}
className={
onRowClick
? "cursor-pointer hover:bg-accent hover:text-foreground "
: ""
}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={
(cell.column.columnDef.meta as Record<string, any>)
?.cellClassName ?? ""
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getVisibleFlatColumns().length}
className="h-24 text-center"
>
{emptyMessage ?? "No data"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{pagination && (
<div className={footerClassName}>
{footer ? (
footer({ table, pagination })
) : (
<DataTableFooter table={table} pagination={pagination} />
)}
</div>
)}
</>
);
}

View File

@@ -0,0 +1,38 @@
import type { Table as TanstackTable, TableOptions, ColumnDef } from "@tanstack/react-table";
export interface DataTablePagination {
pageSize?: number;
pageIndex?: number;
pageCount?: number;
totalCount?: number;
}
export interface DataTableFooterProps<TData> {
table: TanstackTable<TData>;
pagination: DataTablePagination;
}
export type DataTableFooterComponent<TData> = (
props: DataTableFooterProps<TData>
) => React.ReactNode;
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
status?: "loading" | "error" | "success";
onRowClick?: (row: TData) => void;
tableOptions?: Omit<
TableOptions<TData>,
"data" | "columns" | "getCoreRowModel"
>;
pagination?: DataTablePagination;
footer?: DataTableFooterComponent<TData>;
footerClassName?: string;
containerClassName?: string;
emptyMessage?: string;
error?: {
message: string;
description?: string;
onRetry?: () => void;
};
}