diff --git a/src/components/features/dashboard/admin/problemset/table.tsx b/src/components/features/dashboard/admin/problemset/table.tsx new file mode 100644 index 0000000..552a63d --- /dev/null +++ b/src/components/features/dashboard/admin/problemset/table.tsx @@ -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; + +interface ProblemTableProps { + data: ProblemTableItem[]; +} + +// Custom filter function for multi-column searching +const multiColumnFilterFn: FilterFn = (row, _columnId, filterValue) => { + const searchableRowContent = `${row.original.displayId} ${row.original.title}`.toLowerCase(); + const searchTerm = (filterValue ?? "").toLowerCase(); + return searchableRowContent.includes(searchTerm); +}; + +const difficultyFilterFn: FilterFn = ( + row, + columnId, + filterValue: string[] +) => { + if (!filterValue?.length) return true; + const difficulty = row.getValue(columnId) as string; + return filterValue.includes(difficulty); +}; + +const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 28, + enableSorting: false, + enableHiding: false, + }, + { + header: "DisplayId", + accessorKey: "displayId", + cell: ({ row }) =>
{row.getValue("displayId")}
, + size: 90, + filterFn: multiColumnFilterFn, + enableHiding: false, + }, + { + header: "Title", + accessorKey: "title", + cell: ({ row }) =>
{row.getValue("title")}
, + }, + { + header: "Difficulty", + accessorKey: "difficulty", + cell: ({ row }) => { + const difficulty = row.getValue("difficulty") as Difficulty; + return ( + + {difficulty} + + ); + }, + size: 100, + filterFn: difficultyFilterFn, + }, + { + id: "actions", + header: () => Actions, + cell: () => , + enableHiding: false, + }, +]; + +export function ProblemsetTable({ data }: ProblemTableProps) { + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const inputRef = useRef(null); + + const [sorting, setSorting] = useState([ + { 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 ( +
+
+
+
+ + table.getColumn("displayId")?.setFilterValue(e.target.value) + } + placeholder="DisplayId or Title..." + type="text" + aria-label="Filter by displayId or title" + /> +
+
+ {Boolean(table.getColumn("displayId")?.getFilterValue()) && ( + + )} +
+ + + + + +
+
+ Filters +
+
+ {uniqueDifficultyValues.map((value) => ( +
+ + handleDifficultyChange(checked, value) + } + /> + +
+ ))} +
+
+
+
+ + + + + + Toggle columns + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + onSelect={(event) => event.preventDefault()} + > + {column.id} + + ); + })} + + +
+
+ {table.getSelectedRowModel().rows.length > 0 && ( + + + + + +
+ + + + Are you absolutely sure? + + + This action cannot be undone. This will permanently delete{" "} + {table.getSelectedRowModel().rows.length} selected{" "} + {table.getSelectedRowModel().rows.length === 1 + ? "row" + : "rows"} + . + + +
+ + Cancel + + Delete + + +
+
+ )} + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : header.column.getCanSort() ? ( +
{ + 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: ( +
+ ) : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} +
+ ))} +
+ ))} +
+ + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ + +
+
+

+ + {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() + )} + {" "} + of{" "} + + {table.getRowCount().toString()} + +

+
+
+ + + + + + + + + + + + + + + + +
+
+
+ ); +} + +function RowActions() { + return ( + + +
+ +
+
+ + + + Edit + + + + + Delete + ⌘⌫ + + +
+ ); +}