mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-05-18 23:42:24 +00:00
feat(admin/problemset): add interactive problemset data table
- Implement responsive data table with TanStack React Table - Add dynamic filtering for difficulty and search - Support server-side pagination controls - Include row selection and batch delete functionality - Add column visibility toggling - Style table with custom hover states and accessibility features
This commit is contained in:
parent
139c0ba76d
commit
ce41afa383
641
src/components/features/dashboard/admin/problemset/table.tsx
Normal file
641
src/components/features/dashboard/admin/problemset/table.tsx
Normal file
@ -0,0 +1,641 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronFirstIcon,
|
||||||
|
ChevronLastIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
CircleAlertIcon,
|
||||||
|
CircleXIcon,
|
||||||
|
Columns3Icon,
|
||||||
|
EllipsisIcon,
|
||||||
|
FilterIcon,
|
||||||
|
ListFilterIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
FilterFn,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
SortingState,
|
||||||
|
useReactTable,
|
||||||
|
VisibilityState,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Difficulty, Problem } from "@prisma/client";
|
||||||
|
import { cn, getDifficultyColorClass } from "@/lib/utils";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
|
||||||
|
type ProblemTableItem = Pick<Problem, "id" | "displayId" | "title" | "difficulty">;
|
||||||
|
|
||||||
|
interface ProblemTableProps {
|
||||||
|
data: ProblemTableItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom filter function for multi-column searching
|
||||||
|
const multiColumnFilterFn: FilterFn<ProblemTableItem> = (row, _columnId, filterValue) => {
|
||||||
|
const searchableRowContent = `${row.original.displayId} ${row.original.title}`.toLowerCase();
|
||||||
|
const searchTerm = (filterValue ?? "").toLowerCase();
|
||||||
|
return searchableRowContent.includes(searchTerm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const difficultyFilterFn: FilterFn<ProblemTableItem> = (
|
||||||
|
row,
|
||||||
|
columnId,
|
||||||
|
filterValue: string[]
|
||||||
|
) => {
|
||||||
|
if (!filterValue?.length) return true;
|
||||||
|
const difficulty = row.getValue(columnId) as string;
|
||||||
|
return filterValue.includes(difficulty);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<ProblemTableItem>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 28,
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "DisplayId",
|
||||||
|
accessorKey: "displayId",
|
||||||
|
cell: ({ row }) => <div className="font-medium">{row.getValue("displayId")}</div>,
|
||||||
|
size: 90,
|
||||||
|
filterFn: multiColumnFilterFn,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Title",
|
||||||
|
accessorKey: "title",
|
||||||
|
cell: ({ row }) => <div className="font-medium">{row.getValue("title")}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Difficulty",
|
||||||
|
accessorKey: "difficulty",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const difficulty = row.getValue("difficulty") as Difficulty;
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className={getDifficultyColorClass(difficulty)}>
|
||||||
|
{difficulty}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 100,
|
||||||
|
filterFn: difficultyFilterFn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <span className="sr-only">Actions</span>,
|
||||||
|
cell: () => <RowActions />,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProblemsetTable({ data }: ProblemTableProps) {
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{ id: "displayId", desc: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDeleteRows = async () => {
|
||||||
|
const selectedRows = table.getSelectedRowModel().rows;
|
||||||
|
const selectedIds = selectedRows.map((row) => row.original.id);
|
||||||
|
console.log("🚀 ~ handleDeleteRows ~ selectedIds:", selectedIds)
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
enableSortingRemoval: false,
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
state: { sorting, pagination, columnFilters, columnVisibility },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get unique difficulty values
|
||||||
|
const uniqueDifficultyValues = useMemo(() => {
|
||||||
|
const difficultyColumn = table.getColumn("difficulty");
|
||||||
|
|
||||||
|
if (!difficultyColumn) return [];
|
||||||
|
|
||||||
|
const values = Array.from(difficultyColumn.getFacetedUniqueValues().keys());
|
||||||
|
|
||||||
|
return values.sort();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
|
||||||
|
|
||||||
|
// Get counts for each difficulty
|
||||||
|
const difficultyCounts = useMemo(() => {
|
||||||
|
const difficultyColumn = table.getColumn("difficulty");
|
||||||
|
if (!difficultyColumn) return new Map();
|
||||||
|
return difficultyColumn.getFacetedUniqueValues();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
|
||||||
|
|
||||||
|
const selectedDifficulties = useMemo(() => {
|
||||||
|
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
|
||||||
|
return filterValue ?? [];
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [table.getColumn("difficulty")?.getFilterValue()]);
|
||||||
|
|
||||||
|
const handleDifficultyChange = (checked: boolean, value: string) => {
|
||||||
|
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
|
||||||
|
const newFilterValue = filterValue ? [...filterValue] : [];
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
newFilterValue.push(value);
|
||||||
|
} else {
|
||||||
|
const index = newFilterValue.indexOf(value);
|
||||||
|
if (index > -1) {
|
||||||
|
newFilterValue.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
.getColumn("difficulty")
|
||||||
|
?.setFilterValue(newFilterValue.length ? newFilterValue : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 pb-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
className={cn(
|
||||||
|
"peer min-w-60 ps-9",
|
||||||
|
Boolean(table.getColumn("displayId")?.getFilterValue()) && "pe-9"
|
||||||
|
)}
|
||||||
|
value={
|
||||||
|
(table.getColumn("displayId")?.getFilterValue() ?? "") as string
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
table.getColumn("displayId")?.setFilterValue(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="DisplayId or Title..."
|
||||||
|
type="text"
|
||||||
|
aria-label="Filter by displayId or title"
|
||||||
|
/>
|
||||||
|
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50">
|
||||||
|
<ListFilterIcon size={16} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
{Boolean(table.getColumn("displayId")?.getFilterValue()) && (
|
||||||
|
<button
|
||||||
|
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
aria-label="Clear filter"
|
||||||
|
onClick={() => {
|
||||||
|
table.getColumn("displayId")?.setFilterValue("");
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircleXIcon size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<FilterIcon
|
||||||
|
className="-ms-1 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Difficulty
|
||||||
|
{selectedDifficulties.length > 0 && (
|
||||||
|
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
|
||||||
|
{selectedDifficulties.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto min-w-36 p-3" align="start">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-muted-foreground text-xs font-medium">
|
||||||
|
Filters
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{uniqueDifficultyValues.map((value) => (
|
||||||
|
<div key={value} className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedDifficulties.includes(value)}
|
||||||
|
onCheckedChange={(checked: boolean) =>
|
||||||
|
handleDifficultyChange(checked, value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label className="flex grow justify-between gap-2 font-normal">
|
||||||
|
{value}{" "}
|
||||||
|
<span className="text-muted-foreground ms-2 text-xs">
|
||||||
|
{difficultyCounts.get(value)}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Columns3Icon
|
||||||
|
className="-ms-1 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
column.toggleVisibility(!!value)
|
||||||
|
}
|
||||||
|
onSelect={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{table.getSelectedRowModel().rows.length > 0 && (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button className="ml-auto" variant="outline">
|
||||||
|
<TrashIcon
|
||||||
|
className="-ms-1 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
|
||||||
|
{table.getSelectedRowModel().rows.length}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
|
||||||
|
<div
|
||||||
|
className="flex size-9 shrink-0 items-center justify-center rounded-full border"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<CircleAlertIcon className="opacity-80" size={16} />
|
||||||
|
</div>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you absolutely sure?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete{" "}
|
||||||
|
{table.getSelectedRowModel().rows.length} selected{" "}
|
||||||
|
{table.getSelectedRowModel().rows.length === 1
|
||||||
|
? "row"
|
||||||
|
: "rows"}
|
||||||
|
.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
</div>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteRows}>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)}
|
||||||
|
<Button className="ml-auto" variant="outline">
|
||||||
|
<PlusIcon
|
||||||
|
className="-ms-1 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Add Problem
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-background overflow-hidden rounded-md">
|
||||||
|
<Table className="table-fixed">
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id} className="hover:bg-transparent">
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead
|
||||||
|
key={header.id}
|
||||||
|
style={{ width: `${header.getSize()}px` }}
|
||||||
|
className="h-11"
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full cursor-pointer items-center justify-between gap-2 select-none"
|
||||||
|
)}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
header.column.getCanSort() &&
|
||||||
|
(e.key === "Enter" || e.key === " ")
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
header.column.getToggleSortingHandler()?.(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={header.column.getCanSort() ? 0 : undefined}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
{
|
||||||
|
asc: (
|
||||||
|
<ChevronUpIcon
|
||||||
|
className="shrink-0 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: (
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="shrink-0 opacity-60"
|
||||||
|
size={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}[header.column.getIsSorted() as string] ?? null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
className="h-10 border-b-0 cursor-pointer odd:bg-muted/25 hover:text-blue-500 hover:bg-muted"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="last:py-0">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-8">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Label className="max-sm:sr-only">Rows per page</Label>
|
||||||
|
<Select
|
||||||
|
value={table.getState().pagination.pageSize.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-fit whitespace-nowrap">
|
||||||
|
<SelectValue placeholder="Select number of results" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="[&_*[role=option]]:ps-2 [&_*[role=option]]:pe-8 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:end-2">
|
||||||
|
{[5, 10, 25, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||||
|
<span className="mr-2">{pageSize}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex grow justify-end text-sm whitespace-nowrap">
|
||||||
|
<p
|
||||||
|
className="text-muted-foreground text-sm whitespace-nowrap"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span className="text-foreground">
|
||||||
|
{table.getState().pagination.pageIndex *
|
||||||
|
table.getState().pagination.pageSize +
|
||||||
|
1}
|
||||||
|
-
|
||||||
|
{Math.min(
|
||||||
|
Math.max(
|
||||||
|
table.getState().pagination.pageIndex *
|
||||||
|
table.getState().pagination.pageSize +
|
||||||
|
table.getState().pagination.pageSize,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
table.getRowCount()
|
||||||
|
)}
|
||||||
|
</span>{" "}
|
||||||
|
of{" "}
|
||||||
|
<span className="text-foreground">
|
||||||
|
{table.getRowCount().toString()}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
onClick={() => table.firstPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
aria-label="Go to first page"
|
||||||
|
>
|
||||||
|
<ChevronFirstIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
aria-label="Go to next page"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
onClick={() => table.lastPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
aria-label="Go to last page"
|
||||||
|
>
|
||||||
|
<ChevronLastIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RowActions() {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="shadow-none"
|
||||||
|
aria-label="Edit item"
|
||||||
|
>
|
||||||
|
<EllipsisIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<span>Edit</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
||||||
|
<span>Delete</span>
|
||||||
|
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user