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,64 +1,69 @@
"use client";
import { Table } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
export function TableFooter<TData>({ table }: { table: Table<TData> }) {
return (
<div className="flex items-center justify-end px-4 py-4 border-t gap-6 lg:gap-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount() || 1}
</div>
return (
<div className="flex items-center justify-between px-6 py-4 border-t border-border/40 bg-muted/5">
<div className="flex items-center gap-6">
<div className="flex items-center space-x-2">
<p className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px] bg-transparent border-border/40 text-xs font-semibold">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top" className="min-w-[70px]">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`} className="text-xs">
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
);
<div className="flex items-center gap-8">
<div className="flex items-center text-[11px] font-bold text-muted-foreground uppercase tracking-widest gap-1">
<span className="text-foreground">Page {table.getState().pagination.pageIndex + 1}</span>
<span className="opacity-40">/</span>
<span>{table.getPageCount() || 1}</span>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="h-8 w-8 p-0 border-border/40 bg-transparent hover:bg-muted/50 transition-colors"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4 opacity-70" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0 border-border/40 bg-transparent hover:bg-muted/50 transition-colors"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4 opacity-70" />
</Button>
</div>
</div>
</div>
);
}

View File

@@ -3,15 +3,15 @@ import { SearchIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
const SearchBar = () => {
return (
<div className="relative w-full">
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search..."
className="pl-8 h-9 text-sm"
/>
</div>
);
return (
<div className="relative w-full">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70" />
<Input
placeholder="Search resources..."
className="pl-9 h-10 text-sm bg-muted/20 border-border/40 focus:bg-background transition-all"
/>
</div>
);
};
export default SearchBar;

View File

@@ -1,45 +1,47 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import SearchBar from "./SearchBar";
import { PlusIcon } from "lucide-react";
import { FolderPlus, Settings2, SlidersHorizontal } from "lucide-react";
import { Badge } from "@/components/ui/badge";
interface TopHeaderProps {
title?: string;
itemCount?: number;
onAddNew?: () => void;
addButtonText?: string;
title?: string;
itemCount?: number;
onAddNew?: () => void;
addButtonText?: string;
}
const TopHeader = ({ title, itemCount, onAddNew, addButtonText = "Add New" }: TopHeaderProps) => {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold tracking-tight">{title}</h2>
{itemCount !== undefined && (
<span className="flex items-center justify-center bg-secondary text-secondary-foreground min-w-[24px] h-[24px] px-2 text-xs font-bold rounded-full">
{itemCount}
</span>
)}
return (
<div className="flex flex-col gap-1 p-3 w-full">
{/* Connected Summary Block */}
<div className="w-full bg-muted/30 border border-border/50 rounded-lg p-4 flex items-center">
<div className="flex items-center gap-2 text-muted-foreground font-semibold tracking-tight">
<span className="text-base text-foreground/80">Total {title || "Items"} :</span>
<span className="text-primary font-bold text-lg">{itemCount || 0}</span>
</div>
</div>
<div className="flex items-center justify-between gap-4">
{/* Search Bar Hidden for now as per requirement */}
{/* <div className="flex w-full max-w-sm">
<SearchBar />
</div> */}
{/* Add New Button moved to PageHeader */}
{/* {onAddNew && (
<Button
onClick={onAddNew}
className="h-11 px-6 rounded-xl bg-primary hover:bg-primary/90 text-primary-foreground font-bold text-sm shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5"
>
<FolderPlus className="mr-2 h-5 w-5" />
{addButtonText}
</Button>
)} */}
</div>
</div>
{onAddNew && (
<Button
onClick={onAddNew}
size="sm"
className="flex items-center gap-2"
>
<PlusIcon className="w-4 h-4" />
{addButtonText}
</Button>
)}
</div>
<div className="flex w-full max-w-sm ml-auto">
<SearchBar />
</div>
</div>
);
);
};
export default TopHeader;

View File

@@ -1,33 +1,38 @@
"use client";
import { flexRender } from "@tanstack/react-table";
import {
TableHead,
TableHeader as ShadTableHeader,
TableRow,
TableHead,
TableHeader as ShadTableHeader,
TableRow,
} from "@/components/ui/table";
import { Table } from "@tanstack/react-table";
import { ChevronDown, ChevronsUpDown } from "lucide-react";
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return (
<ShadTableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</ShadTableHeader>
);
return (
<ShadTableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-transparent border-b border-border/30">
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="h-14 px-6 text-muted-foreground border-b border-border/30 font-medium text-sm">
{header.isPlaceholder
? null
: (
<div className="flex items-center gap-2 group cursor-pointer select-none">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
)}
</TableHead>
);
})}
</TableRow>
))}
</ShadTableHeader>
);
};
export default TableHeader;

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>
);
}