refactor: redesign table module

This commit is contained in:
2026-03-18 01:03:58 +05:30
parent caf527f584
commit 344f37719e
24 changed files with 991 additions and 815 deletions

View File

@@ -1,20 +1,19 @@
"use client";
import React from "react";
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
getPaginationRowModel,
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
getPaginationRowModel,
} from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Edit2, Trash2 } from "lucide-react";
import { Edit3, MoreHorizontal, Trash2 } from "lucide-react";
import TopHeader from "./Header";
import TableHeader from "./TableHeader";
@@ -22,165 +21,169 @@ import { TableFooter } from "./Footer";
import { cn } from "@/lib/utils";
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
title?: string;
onAddNew?: () => void;
addButtonText?: string;
isLoading?: boolean;
onEdit?: (item: TData) => void;
onDelete?: (item: TData) => void;
pagination?: {
skip: number;
limit: number;
totalItems?: number;
onPageChange: (newSkip: number) => void;
onLimitChange: (newLimit: number) => void;
};
columns: ColumnDef<TData, TValue>[];
data: TData[];
title?: string;
onAddNew?: () => void;
addButtonText?: string;
isLoading?: boolean;
onEdit?: (item: TData) => void;
onDelete?: (item: TData) => void;
pagination?: {
skip: number;
limit: number;
totalItems?: number;
onPageChange: (newSkip: number) => void;
onLimitChange: (newLimit: number) => void;
};
}
export function DataTable<TData, TValue>({
columns: initialColumns,
data,
title,
onAddNew,
addButtonText,
isLoading = false,
onEdit,
onDelete,
pagination,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [sorting, setSorting] = React.useState<SortingState>([]);
const columns = React.useMemo(() => {
const cols = [...initialColumns];
if (onEdit || onDelete) {
cols.push({
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex justify-end gap-2">
{onEdit && (
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(item)}
className="h-8 w-8"
>
<Edit2 className="h-4 w-4" />
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(item)}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
);
},
});
}
return cols;
}, [initialColumns, onEdit, onDelete]);
const table = useReactTable({
columns: initialColumns,
data,
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: !!pagination,
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
state: {
rowSelection,
sorting,
pagination: pagination ? {
pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit,
} : undefined,
},
onPaginationChange: (updater) => {
if (typeof updater === 'function' && pagination) {
const newState = updater({
title,
onAddNew,
addButtonText,
isLoading = false,
onEdit,
onDelete,
pagination,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [sorting, setSorting] = React.useState<SortingState>([]);
const columns = React.useMemo(() => {
const cols: ColumnDef<TData, TValue>[] = [
...initialColumns,
];
if (onEdit || onDelete) {
cols.push({
id: "actions",
header: () => <div className="text-right px-4">Action</div>,
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex justify-end gap-3 px-4">
{onEdit && (
<button
onClick={(e) => {
e.stopPropagation();
onEdit(item);
}}
className="flex items-center justify-center h-8 w-8 rounded-md text-primary hover:bg-foreground/10 transition-all duration-200"
>
<Edit3 className="h-4.5 w-4.5" />
</button>
)}
{onDelete && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete(item);
}}
className="flex items-center justify-center h-8 w-8 rounded-md text-red-500 hover:bg-foreground/10 transition-all duration-200"
>
<Trash2 className="h-4.5 w-4.5" />
</button>
)}
</div>
);
},
});
}
return cols;
}, [initialColumns, onEdit, onDelete]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: !!pagination,
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
state: {
rowSelection,
sorting,
pagination: pagination ? {
pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit,
});
pagination.onLimitChange(newState.pageSize);
pagination.onPageChange(newState.pageIndex * newState.pageSize);
}
},
initialState: {
pagination: {
pageSize: 10,
pageIndex: 0,
},
},
});
} : undefined,
},
onPaginationChange: (updater) => {
if (typeof updater === 'function' && pagination) {
const newState = updater({
pageIndex: Math.floor(pagination.skip / pagination.limit),
pageSize: pagination.limit,
});
pagination.onLimitChange(newState.pageSize);
pagination.onPageChange(newState.pageIndex * newState.pageSize);
}
},
});
return (
<div className="rounded-md border">
<TopHeader
title={title}
itemCount={data.length}
onAddNew={onAddNew}
addButtonText={addButtonText}
/>
return (
<div className="rounded-xl border border-border/50 bg-muted/20 p-1 space-y-6">
<div className="bg-background rounded-xl border border-border/50 overflow-hidden transition-all duration-300">
<TopHeader
title={title}
itemCount={data.length}
onAddNew={onAddNew}
addButtonText={addButtonText}
/>
<div className="relative">
<Table>
<TableHeader table={table} />
<TableBody className="bg-transparent">
{isLoading ? (
Array.from({ length: 5 }).map((_, idx) => (
<TableRow key={idx}>
{columns.map((_, colIdx) => (
<TableCell key={colIdx} className="px-8 py-4">
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))
) : 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-32 text-center text-sm"
>
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<TableFooter table={table} />
</div>
);
<div className="px-6 py-2">
<Table containerClassName="max-h-[calc(100vh-300px)] overflow-y-auto scrollbar-thin" className="border-separate border-spacing-0">
<TableHeader table={table} />
<TableBody>
{isLoading ? (
Array.from({ length: 5 }).map((_, idx) => (
<TableRow key={idx} className="border-b border-border/30 last:border-0 hover:bg-muted/5">
{columns.map((_, colIdx) => (
<TableCell key={colIdx} className="px-6 py-6 border-b border-border/30">
<Skeleton className="h-4 w-full max-w-[140px] opacity-20" />
</TableCell>
))}
</TableRow>
))
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="group hover:bg-muted/10 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="px-4 py-3 border-b border-border/50 align-middle text-muted-foreground text-sm font-medium group-hover:text-foreground">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-48 text-center"
>
<div className="flex flex-col items-center justify-center text-muted-foreground gap-1">
<p className="font-bold text-sm tracking-tight">No results found.</p>
<p className="text-xs opacity-60 font-medium">Try adjusting your filters or search terms.</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<TableFooter table={table} />
</div>
</div>
);
}