feat: modify table design
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Table } from '@tanstack/react-table';
|
import { Table } from '@tanstack/react-table';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -25,26 +30,33 @@ export function TableFooter<TData>({
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
onPageSizeChange?: (pageSize: number) => void;
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const rows = table.getRowModel().rows.length;
|
|
||||||
const currentPageSize = pageSize ?? table.getState().pagination.pageSize;
|
const currentPageSize = pageSize ?? table.getState().pagination.pageSize;
|
||||||
|
const selectedRowCount = Object.keys(table.getState().rowSelection).length;
|
||||||
|
const pageIndex = table.getState().pagination.pageIndex;
|
||||||
|
const rawPageCount = table.getPageCount();
|
||||||
|
const pageCount = Math.max(
|
||||||
|
1,
|
||||||
|
Number.isFinite(rawPageCount) && rawPageCount > 0 ? rawPageCount : 1,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 px-4 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div>
|
||||||
<span>
|
{selectedRowCount} of {totalItems} row(s) selected.
|
||||||
Showing {rows} of {totalItems}
|
</div>
|
||||||
</span>
|
|
||||||
|
<div className="flex flex-wrap items-center gap-4 sm:justify-end">
|
||||||
{onPageSizeChange ? (
|
{onPageSizeChange ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 text-foreground">
|
||||||
<span>Rows per page</span>
|
<span className="font-medium">Rows per page</span>
|
||||||
<Select
|
<Select
|
||||||
value={String(currentPageSize)}
|
value={String(currentPageSize)}
|
||||||
onValueChange={(value) => onPageSizeChange(Number(value))}
|
onValueChange={(value) => table.setPageSize(Number(value))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-18 bg-background">
|
<SelectTrigger className="h-8 w-20 border-border bg-background shadow-none">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent align="end">
|
||||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||||
<SelectItem key={size} value={String(size)}>
|
<SelectItem key={size} value={String(size)}>
|
||||||
{size}
|
{size}
|
||||||
@@ -54,26 +66,57 @@ export function TableFooter<TData>({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
<div className="min-w-24 text-foreground font-medium">
|
||||||
<Button
|
Page {pageIndex + 1} of {pageCount}
|
||||||
variant="outline"
|
</div>
|
||||||
size="sm"
|
|
||||||
onClick={() => table.previousPage()}
|
<div className="flex items-center gap-2">
|
||||||
disabled={!table.getCanPreviousPage()}
|
<Button
|
||||||
>
|
type="button"
|
||||||
<ChevronLeft className="size-4" />
|
variant="outline"
|
||||||
Previous
|
size="icon-sm"
|
||||||
</Button>
|
className="size-8 shadow-none"
|
||||||
<Button
|
onClick={() => table.setPageIndex(0)}
|
||||||
variant="outline"
|
disabled={!table.getCanPreviousPage()}
|
||||||
size="sm"
|
aria-label="Go to first page"
|
||||||
onClick={() => table.nextPage()}
|
>
|
||||||
disabled={!table.getCanNextPage()}
|
<ChevronsLeft className="size-4" />
|
||||||
>
|
</Button>
|
||||||
Next
|
<Button
|
||||||
<ChevronRight className="size-4" />
|
type="button"
|
||||||
</Button>
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-8 shadow-none"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-8 shadow-none"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
aria-label="Go to next page"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-8 shadow-none"
|
||||||
|
onClick={() => table.setPageIndex(pageCount - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
aria-label="Go to last page"
|
||||||
|
>
|
||||||
|
<ChevronsRight className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,26 +21,35 @@ const TopHeader = ({
|
|||||||
tableTools,
|
tableTools,
|
||||||
}: TopHeaderProps) => {
|
}: TopHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
{(title || onAddNew) && (
|
{(title || onAddNew) && (
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
{title ? <h2>{title}</h2> : null}
|
{title ? <h2 className="text-lg font-semibold">{title}</h2> : null}
|
||||||
<p className="text-muted-foreground">Total {itemCount ?? 0}</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Total {itemCount ?? 0}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{onAddNew ? (
|
{onAddNew ? (
|
||||||
<Button onClick={onAddNew}>
|
<Button onClick={onAddNew} size="sm" className="w-fit">
|
||||||
<Plus className="mr-2 size-4" />
|
<Plus className="size-4" />
|
||||||
{addButtonText}
|
{addButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(toolbar || tableTools) && (
|
{(toolbar || tableTools) && (
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="min-w-0 flex-1">{toolbar}</div>
|
{toolbar ? (
|
||||||
|
<div className="min-w-0 flex-1 **:data-[slot=input-group]:border-border [&_[data-slot=input-group]]:bg-background [&_[data-slot=input-group]]:shadow-none [&_[data-slot=select-trigger]]:border-border [&_[data-slot=select-trigger]]:bg-background [&_[data-slot=select-trigger]]:shadow-none">
|
||||||
|
{toolbar}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{tableTools ? (
|
{tableTools ? (
|
||||||
<div className="flex shrink-0 justify-end">{tableTools}</div>
|
<div className="flex shrink-0 justify-end **:data-[slot=button]:shadow-none">
|
||||||
|
{tableTools}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { getPinnedColumnStyles } from './columnStyles';
|
|||||||
|
|
||||||
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
||||||
return (
|
return (
|
||||||
<ShadTableHeader className="sticky top-0 z-20 bg-card">
|
<ShadTableHeader className="sticky top-0 z-20 bg-muted/70 backdrop-blur supports-backdrop-filter:bg-muted/60">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
@@ -39,9 +39,9 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
|||||||
canSort ? header.column.getToggleSortingHandler() : undefined
|
canSort ? header.column.getToggleSortingHandler() : undefined
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-10 max-w-0 overflow-hidden border-b border-border bg-card px-2 text-foreground transition-colors',
|
'h-11 max-w-0 overflow-hidden border-b border-border bg-muted/70 px-4 text-sm font-semibold text-foreground transition-colors',
|
||||||
canSort && 'cursor-pointer select-none hover:bg-muted/60',
|
canSort && 'cursor-pointer select-none hover:bg-muted',
|
||||||
sortDirection && 'bg-muted/70',
|
sortDirection && 'bg-muted',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : (
|
{header.isPlaceholder ? null : (
|
||||||
|
|||||||
@@ -23,14 +23,20 @@ export function TableSearchInput({
|
|||||||
className,
|
className,
|
||||||
}: TableSearchInputProps) {
|
}: TableSearchInputProps) {
|
||||||
return (
|
return (
|
||||||
<InputGroup className={cn('md:max-w-xs', className)}>
|
<InputGroup
|
||||||
|
className={cn(
|
||||||
|
'h-9 border-border bg-background shadow-none md:max-w-xs',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<Search />
|
<Search className="size-4" />
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
className="placeholder:text-muted-foreground/70"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -210,8 +210,8 @@ function DataTableContent<TData, TValue>({
|
|||||||
},
|
},
|
||||||
manualPagination: !!pagination,
|
manualPagination: !!pagination,
|
||||||
manualSorting: isManualSorting,
|
manualSorting: isManualSorting,
|
||||||
pageCount: pagination?.totalItems
|
pageCount: pagination
|
||||||
? Math.ceil(pagination.totalItems / pagination.limit)
|
? Math.ceil((pagination.totalItems ?? data.length) / pagination.limit)
|
||||||
: -1,
|
: -1,
|
||||||
state: {
|
state: {
|
||||||
rowSelection,
|
rowSelection,
|
||||||
@@ -247,7 +247,7 @@ function DataTableContent<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="space-y-4 rounded-lg border bg-card p-4">
|
<section className="space-y-3">
|
||||||
<TopHeader
|
<TopHeader
|
||||||
title={onAddNew ? title : undefined}
|
title={onAddNew ? title : undefined}
|
||||||
itemCount={pagination?.totalItems ?? data.length}
|
itemCount={pagination?.totalItems ?? data.length}
|
||||||
@@ -259,9 +259,9 @@ function DataTableContent<TData, TValue>({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div className="overflow-hidden rounded-lg border border-border bg-background">
|
||||||
<Table
|
<Table
|
||||||
containerClassName="max-h-[calc(100vh-350px)] overflow-y-auto"
|
containerClassName="max-h-[calc(100vh-350px)] overflow-auto"
|
||||||
className="table-fixed border-separate border-spacing-0"
|
className="table-fixed border-separate border-spacing-0"
|
||||||
style={{ width: '100%', minWidth: table.getTotalSize() }}
|
style={{ width: '100%', minWidth: table.getTotalSize() }}
|
||||||
>
|
>
|
||||||
@@ -273,7 +273,7 @@ function DataTableContent<TData, TValue>({
|
|||||||
{columns.map((_, colIdx) => (
|
{columns.map((_, colIdx) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={colIdx}
|
key={colIdx}
|
||||||
className="border-b border-border"
|
className="h-13 border-b border-border px-4 py-3"
|
||||||
>
|
>
|
||||||
<Skeleton className="h-4 w-full max-w-35" />
|
<Skeleton className="h-4 w-full max-w-35" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -290,7 +290,9 @@ function DataTableContent<TData, TValue>({
|
|||||||
const truncate = shouldTruncateCell(cell.column);
|
const truncate = shouldTruncateCell(cell.column);
|
||||||
const pinned = cell.column.getIsPinned();
|
const pinned = cell.column.getIsPinned();
|
||||||
const isActionsCell = cell.column.id === 'actions';
|
const isActionsCell = cell.column.id === 'actions';
|
||||||
const isRowClickable = Boolean(onRowClick && !isActionsCell);
|
const isRowClickable = Boolean(
|
||||||
|
onRowClick && !isActionsCell,
|
||||||
|
);
|
||||||
const content = flexRender(
|
const content = flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext(),
|
cell.getContext(),
|
||||||
@@ -306,8 +308,8 @@ function DataTableContent<TData, TValue>({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-0 overflow-hidden border-b border-border',
|
'h-13 max-w-0 overflow-hidden border-b border-border px-4 py-3 text-sm',
|
||||||
pinned && 'bg-card',
|
pinned && 'bg-background',
|
||||||
isRowClickable && 'cursor-pointer',
|
isRowClickable && 'cursor-pointer',
|
||||||
isActionsCell && 'cursor-default',
|
isActionsCell && 'cursor-default',
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user