Skip to content

Instantly share code, notes, and snippets.

@AntonioErdeljac
Last active April 27, 2025 14:46
Show Gist options
  • Save AntonioErdeljac/ee40539ec03bce178b6301d4436fc198 to your computer and use it in GitHub Desktop.
Save AntonioErdeljac/ee40539ec03bce178b6301d4436fc198 to your computer and use it in GitHub Desktop.
dice-ui-registry
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "data-table",
"type": "registry:component",
"title": "Data Table",
"description": "A feature-rich data table component with server-side filtering, sorting, and pagination",
"dependencies": [
"@tanstack/react-table",
"lucide-react",
"nuqs"
],
"registryDependencies": [
"badge",
"button",
"calendar",
"command",
"dropdown-menu",
"input",
"popover",
"select",
"separator",
"slider",
"table"
],
"files": [
{
"path": "src/components/data-table.tsx",
"content": "import { type Table as TanstackTable, flexRender } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTablePagination } from \"@/components/data-table-pagination\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: TanstackTable\u003CTData\u003E;\n actionBar?: React.ReactNode;\n}\n\nexport function DataTable\u003CTData\u003E({\n table,\n actionBar,\n children,\n className,\n ...props\n}: DataTableProps\u003CTData\u003E) {\n return (\n \u003Cdiv\n className={cn(\"flex w-full flex-col gap-2.5 overflow-auto\", className)}\n {...props}\n \u003E\n {children}\n \u003Cdiv className=\"overflow-hidden rounded-md border\"\u003E\n \u003CTable\u003E\n \u003CTableHeader\u003E\n {table.getHeaderGroups().map((headerGroup) =\u003E (\n \u003CTableRow key={headerGroup.id}\u003E\n {headerGroup.headers.map((header) =\u003E (\n \u003CTableHead\n key={header.id}\n colSpan={header.colSpan}\n style={{\n ...getCommonPinningStyles({ column: header.column }),\n }}\n \u003E\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n \u003C/TableHead\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableHeader\u003E\n \u003CTableBody\u003E\n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) =\u003E (\n \u003CTableRow\n key={row.id}\n data-state={row.getIsSelected() && \"selected\"}\n \u003E\n {row.getVisibleCells().map((cell) =\u003E (\n \u003CTableCell\n key={cell.id}\n style={{\n ...getCommonPinningStyles({ column: cell.column }),\n }}\n \u003E\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n \u003C/TableCell\u003E\n ))}\n \u003C/TableRow\u003E\n ))\n ) : (\n \u003CTableRow\u003E\n \u003CTableCell\n colSpan={table.getAllColumns().length}\n className=\"h-24 text-center\"\n \u003E\n No results.\n \u003C/TableCell\u003E\n \u003C/TableRow\u003E\n )}\n \u003C/TableBody\u003E\n \u003C/Table\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex flex-col gap-2.5\"\u003E\n \u003CDataTablePagination table={table} /\u003E\n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length \u003E 0 &&\n actionBar}\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-column-header.tsx",
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport {\n ChevronDown,\n ChevronUp,\n ChevronsUpDown,\n EyeOff,\n X,\n} from \"lucide-react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableColumnHeaderProps\u003CTData, TValue\u003E\n extends React.ComponentProps\u003Ctypeof DropdownMenuTrigger\u003E {\n column: Column\u003CTData, TValue\u003E;\n title: string;\n}\n\nexport function DataTableColumnHeader\u003CTData, TValue\u003E({\n column,\n title,\n className,\n ...props\n}: DataTableColumnHeaderProps\u003CTData, TValue\u003E) {\n if (!column.getCanSort() && !column.getCanHide()) {\n return \u003Cdiv className={cn(className)}\u003E{title}\u003C/div\u003E;\n }\n\n return (\n \u003CDropdownMenu\u003E\n \u003CDropdownMenuTrigger\n className={cn(\n \"-ml-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 hover:bg-accent focus:outline-none focus:ring-1 focus:ring-ring data-[state=open]:bg-accent [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground\",\n className,\n )}\n {...props}\n \u003E\n {title}\n {column.getCanSort() &&\n (column.getIsSorted() === \"desc\" ? (\n \u003CChevronDown /\u003E\n ) : column.getIsSorted() === \"asc\" ? (\n \u003CChevronUp /\u003E\n ) : (\n \u003CChevronsUpDown /\u003E\n ))}\n \u003C/DropdownMenuTrigger\u003E\n \u003CDropdownMenuContent align=\"start\" className=\"w-28\"\u003E\n {column.getCanSort() && (\n \u003C\u003E\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() =\u003E column.toggleSorting(false)}\n \u003E\n \u003CChevronUp /\u003E\n Asc\n \u003C/DropdownMenuCheckboxItem\u003E\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() =\u003E column.toggleSorting(true)}\n \u003E\n \u003CChevronDown /\u003E\n Desc\n \u003C/DropdownMenuCheckboxItem\u003E\n {column.getIsSorted() && (\n \u003CDropdownMenuItem\n className=\"pl-2 [&_svg]:text-muted-foreground\"\n onClick={() =\u003E column.clearSorting()}\n \u003E\n \u003CX /\u003E\n Reset\n \u003C/DropdownMenuItem\u003E\n )}\n \u003C/\u003E\n )}\n {column.getCanHide() && (\n \u003CDropdownMenuCheckboxItem\n className=\"relative pr-8 pl-2 [&\u003Espan:first-child]:right-2 [&\u003Espan:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={!column.getIsVisible()}\n onClick={() =\u003E column.toggleVisibility(false)}\n \u003E\n \u003CEyeOff /\u003E\n Hide\n \u003C/DropdownMenuCheckboxItem\u003E\n )}\n \u003C/DropdownMenuContent\u003E\n \u003C/DropdownMenu\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-pagination.tsx",
"content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: Table\u003CTData\u003E;\n pageSizeOptions?: number[];\n}\n\nexport function DataTablePagination\u003CTData\u003E({\n table,\n pageSizeOptions = [10, 20, 30, 40, 50],\n className,\n ...props\n}: DataTablePaginationProps\u003CTData\u003E) {\n return (\n \u003Cdiv\n className={cn(\n \"flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8\",\n className,\n )}\n {...props}\n \u003E\n \u003Cdiv className=\"flex-1 whitespace-nowrap text-muted-foreground text-sm\"\u003E\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n \u003C/div\u003E\n \u003Cdiv className=\"flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8\"\u003E\n \u003Cdiv className=\"flex items-center space-x-2\"\u003E\n \u003Cp className=\"whitespace-nowrap font-medium text-sm\"\u003ERows per page\u003C/p\u003E\n \u003CSelect\n value={`${table.getState().pagination.pageSize}`}\n onValueChange={(value) =\u003E {\n table.setPageSize(Number(value));\n }}\n \u003E\n \u003CSelectTrigger className=\"h-8 w-[4.5rem] [&[data-size]]:h-8\"\u003E\n \u003CSelectValue placeholder={table.getState().pagination.pageSize} /\u003E\n \u003C/SelectTrigger\u003E\n \u003CSelectContent side=\"top\"\u003E\n {pageSizeOptions.map((pageSize) =\u003E (\n \u003CSelectItem key={pageSize} value={`${pageSize}`}\u003E\n {pageSize}\n \u003C/SelectItem\u003E\n ))}\n \u003C/SelectContent\u003E\n \u003C/Select\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center justify-center font-medium text-sm\"\u003E\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center space-x-2\"\u003E\n \u003CButton\n aria-label=\"Go to first page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"hidden size-8 lg:flex\"\n onClick={() =\u003E table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n \u003E\n \u003CChevronsLeft /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to previous page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"size-8\"\n onClick={() =\u003E table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n \u003E\n \u003CChevronLeft /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to next page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"size-8\"\n onClick={() =\u003E table.nextPage()}\n disabled={!table.getCanNextPage()}\n \u003E\n \u003CChevronRight /\u003E\n \u003C/Button\u003E\n \u003CButton\n aria-label=\"Go to last page\"\n variant=\"outline\"\n size=\"icon\"\n className=\"hidden size-8 lg:flex\"\n onClick={() =\u003E table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n \u003E\n \u003CChevronsRight /\u003E\n \u003C/Button\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-view-options.tsx",
"content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, ChevronsUpDown, Settings2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableViewOptionsProps\u003CTData\u003E {\n table: Table\u003CTData\u003E;\n}\n\nexport function DataTableViewOptions\u003CTData\u003E({\n table,\n}: DataTableViewOptionsProps\u003CTData\u003E) {\n const columns = React.useMemo(\n () =\u003E\n table\n .getAllColumns()\n .filter(\n (column) =\u003E\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n ),\n [table],\n );\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton\n aria-label=\"Toggle columns\"\n role=\"combobox\"\n variant=\"outline\"\n size=\"sm\"\n className=\"ml-auto hidden h-8 lg:flex\"\n \u003E\n \u003CSettings2 /\u003E\n View\n \u003CChevronsUpDown className=\"ml-auto opacity-50\" /\u003E\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent align=\"end\" className=\"w-44 p-0\"\u003E\n \u003CCommand\u003E\n \u003CCommandInput placeholder=\"Search columns...\" /\u003E\n \u003CCommandList\u003E\n \u003CCommandEmpty\u003ENo columns found.\u003C/CommandEmpty\u003E\n \u003CCommandGroup\u003E\n {columns.map((column) =\u003E (\n \u003CCommandItem\n key={column.id}\n onSelect={() =\u003E\n column.toggleVisibility(!column.getIsVisible())\n }\n \u003E\n \u003Cspan className=\"truncate\"\u003E\n {column.columnDef.meta?.label ?? column.id}\n \u003C/span\u003E\n \u003CCheck\n className={cn(\n \"ml-auto size-4 shrink-0\",\n column.getIsVisible() ? \"opacity-100\" : \"opacity-0\",\n )}\n /\u003E\n \u003C/CommandItem\u003E\n ))}\n \u003C/CommandGroup\u003E\n \u003C/CommandList\u003E\n \u003C/Command\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-faceted-filter.tsx",
"content": "\"use client\";\n\nimport type { Option } from \"@/types/data-table\";\nimport type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ninterface DataTableFacetedFilterProps\u003CTData, TValue\u003E {\n column?: Column\u003CTData, TValue\u003E;\n title?: string;\n options: Option[];\n multiple?: boolean;\n}\n\nexport function DataTableFacetedFilter\u003CTData, TValue\u003E({\n column,\n title,\n options,\n multiple,\n}: DataTableFacetedFilterProps\u003CTData, TValue\u003E) {\n const [open, setOpen] = React.useState(false);\n\n const columnFilterValue = column?.getFilterValue();\n const selectedValues = new Set(\n Array.isArray(columnFilterValue) ? columnFilterValue : [],\n );\n\n const onItemSelect = React.useCallback(\n (option: Option, isSelected: boolean) =\u003E {\n if (!column) return;\n\n if (multiple) {\n const newSelectedValues = new Set(selectedValues);\n if (isSelected) {\n newSelectedValues.delete(option.value);\n } else {\n newSelectedValues.add(option.value);\n }\n const filterValues = Array.from(newSelectedValues);\n column.setFilterValue(filterValues.length ? filterValues : undefined);\n } else {\n column.setFilterValue(isSelected ? undefined : [option.value]);\n setOpen(false);\n }\n },\n [column, multiple, selectedValues],\n );\n\n const onReset = React.useCallback(\n (event?: React.MouseEvent) =\u003E {\n event?.stopPropagation();\n column?.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \u003CPopover open={open} onOpenChange={setOpen}\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {selectedValues?.size \u003E 0 ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n onClick={onReset}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CPlusCircle /\u003E\n )}\n {title}\n {selectedValues?.size \u003E 0 && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003CBadge\n variant=\"secondary\"\n className=\"rounded-sm px-1 font-normal lg:hidden\"\n \u003E\n {selectedValues.size}\n \u003C/Badge\u003E\n \u003Cdiv className=\"hidden items-center gap-1 lg:flex\"\u003E\n {selectedValues.size \u003E 2 ? (\n \u003CBadge\n variant=\"secondary\"\n className=\"rounded-sm px-1 font-normal\"\n \u003E\n {selectedValues.size} selected\n \u003C/Badge\u003E\n ) : (\n options\n .filter((option) =\u003E selectedValues.has(option.value))\n .map((option) =\u003E (\n \u003CBadge\n variant=\"secondary\"\n key={option.value}\n className=\"rounded-sm px-1 font-normal\"\n \u003E\n {option.label}\n \u003C/Badge\u003E\n ))\n )}\n \u003C/div\u003E\n \u003C/\u003E\n )}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent className=\"w-[12.5rem] p-0\" align=\"start\"\u003E\n \u003CCommand\u003E\n \u003CCommandInput placeholder={title} /\u003E\n \u003CCommandList className=\"max-h-full\"\u003E\n \u003CCommandEmpty\u003ENo results found.\u003C/CommandEmpty\u003E\n \u003CCommandGroup className=\"max-h-[18.75rem] overflow-y-auto overflow-x-hidden\"\u003E\n {options.map((option) =\u003E {\n const isSelected = selectedValues.has(option.value);\n\n return (\n \u003CCommandItem\n key={option.value}\n onSelect={() =\u003E onItemSelect(option, isSelected)}\n \u003E\n \u003Cdiv\n className={cn(\n \"flex size-4 items-center justify-center rounded-sm border border-primary\",\n isSelected\n ? \"bg-primary\"\n : \"opacity-50 [&_svg]:invisible\",\n )}\n \u003E\n \u003CCheck /\u003E\n \u003C/div\u003E\n {option.icon && \u003Coption.icon /\u003E}\n \u003Cspan className=\"truncate\"\u003E{option.label}\u003C/span\u003E\n {option.count && (\n \u003Cspan className=\"ml-auto font-mono text-xs\"\u003E\n {option.count}\n \u003C/span\u003E\n )}\n \u003C/CommandItem\u003E\n );\n })}\n \u003C/CommandGroup\u003E\n {selectedValues.size \u003E 0 && (\n \u003C\u003E\n \u003CCommandSeparator /\u003E\n \u003CCommandGroup\u003E\n \u003CCommandItem\n onSelect={() =\u003E onReset()}\n className=\"justify-center text-center\"\n \u003E\n Clear filters\n \u003C/CommandItem\u003E\n \u003C/CommandGroup\u003E\n \u003C/\u003E\n )}\n \u003C/CommandList\u003E\n \u003C/Command\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-toolbar.tsx",
"content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { DataTableDateFilter } from \"@/components/data-table-date-filter\";\nimport { DataTableFacetedFilter } from \"@/components/data-table-faceted-filter\";\nimport { DataTableSliderFilter } from \"@/components/data-table-slider-filter\";\nimport { DataTableViewOptions } from \"@/components/data-table-view-options\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableToolbarProps\u003CTData\u003E extends React.ComponentProps\u003C\"div\"\u003E {\n table: Table\u003CTData\u003E;\n}\n\nexport function DataTableToolbar\u003CTData\u003E({\n table,\n children,\n className,\n ...props\n}: DataTableToolbarProps\u003CTData\u003E) {\n const isFiltered = table.getState().columnFilters.length \u003E 0;\n\n const columns = React.useMemo(\n () =\u003E table.getAllColumns().filter((column) =\u003E column.getCanFilter()),\n [table],\n );\n\n const onReset = React.useCallback(() =\u003E {\n table.resetColumnFilters();\n }, [table]);\n\n return (\n \u003Cdiv\n role=\"toolbar\"\n aria-orientation=\"horizontal\"\n className={cn(\n \"flex w-full items-start justify-between gap-2 p-1\",\n className,\n )}\n {...props}\n \u003E\n \u003Cdiv className=\"flex flex-1 flex-wrap items-center gap-2\"\u003E\n {columns.map((column) =\u003E (\n \u003CDataTableToolbarFilter key={column.id} column={column} /\u003E\n ))}\n {isFiltered && (\n \u003CButton\n aria-label=\"Reset filters\"\n variant=\"outline\"\n size=\"sm\"\n className=\"border-dashed\"\n onClick={onReset}\n \u003E\n \u003CX /\u003E\n Reset\n \u003C/Button\u003E\n )}\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n {children}\n \u003CDataTableViewOptions table={table} /\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n );\n}\ninterface DataTableToolbarFilterProps\u003CTData\u003E {\n column: Column\u003CTData\u003E;\n}\n\nfunction DataTableToolbarFilter\u003CTData\u003E({\n column,\n}: DataTableToolbarFilterProps\u003CTData\u003E) {\n {\n const columnMeta = column.columnDef.meta;\n\n const onFilterRender = React.useCallback(() =\u003E {\n if (!columnMeta?.variant) return null;\n\n switch (columnMeta.variant) {\n case \"text\":\n return (\n \u003CInput\n placeholder={columnMeta.placeholder ?? columnMeta.label}\n value={(column.getFilterValue() as string) ?? \"\"}\n onChange={(event) =\u003E column.setFilterValue(event.target.value)}\n className=\"h-8 w-40 lg:w-56\"\n /\u003E\n );\n\n case \"number\":\n return (\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n type=\"number\"\n inputMode=\"numeric\"\n placeholder={columnMeta.placeholder ?? columnMeta.label}\n value={(column.getFilterValue() as string) ?? \"\"}\n onChange={(event) =\u003E column.setFilterValue(event.target.value)}\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\n /\u003E\n {columnMeta.unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {columnMeta.unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n );\n\n case \"range\":\n return (\n \u003CDataTableSliderFilter\n column={column}\n title={columnMeta.label ?? column.id}\n /\u003E\n );\n\n case \"date\":\n case \"dateRange\":\n return (\n \u003CDataTableDateFilter\n column={column}\n title={columnMeta.label ?? column.id}\n multiple={columnMeta.variant === \"dateRange\"}\n /\u003E\n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \u003CDataTableFacetedFilter\n column={column}\n title={columnMeta.label ?? column.id}\n options={columnMeta.options ?? []}\n multiple={columnMeta.variant === \"multiSelect\"}\n /\u003E\n );\n\n default:\n return null;\n }\n }, [column, columnMeta]);\n\n return onFilterRender();\n }\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-slider-filter.tsx",
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport { PlusCircle, XCircle } from \"lucide-react\";\n\ninterface Range {\n min: number;\n max: number;\n}\n\ntype RangeValue = [number, number];\n\nfunction getIsValidRange(value: unknown): value is RangeValue {\n return (\n Array.isArray(value) &&\n value.length === 2 &&\n typeof value[0] === \"number\" &&\n typeof value[1] === \"number\"\n );\n}\n\ninterface DataTableSliderFilterProps\u003CTData\u003E {\n column: Column\u003CTData, unknown\u003E;\n title?: string;\n}\n\nexport function DataTableSliderFilter\u003CTData\u003E({\n column,\n title,\n}: DataTableSliderFilterProps\u003CTData\u003E) {\n const id = React.useId();\n\n const columnFilterValue = getIsValidRange(column.getFilterValue())\n ? (column.getFilterValue() as RangeValue)\n : undefined;\n\n const defaultRange = column.columnDef.meta?.range;\n const unit = column.columnDef.meta?.unit;\n\n const { min, max, step } = React.useMemo\u003CRange & { step: number }\u003E(() =\u003E {\n let minValue = 0;\n let maxValue = 100;\n\n if (defaultRange && getIsValidRange(defaultRange)) {\n [minValue, maxValue] = defaultRange;\n } else {\n const values = column.getFacetedMinMaxValues();\n if (values && Array.isArray(values) && values.length === 2) {\n const [facetMinValue, facetMaxValue] = values;\n if (\n typeof facetMinValue === \"number\" &&\n typeof facetMaxValue === \"number\"\n ) {\n minValue = facetMinValue;\n maxValue = facetMaxValue;\n }\n }\n }\n\n const rangeSize = maxValue - minValue;\n const step =\n rangeSize \u003C= 20\n ? 1\n : rangeSize \u003C= 100\n ? Math.ceil(rangeSize / 20)\n : Math.ceil(rangeSize / 50);\n\n return { min: minValue, max: maxValue, step };\n }, [column, defaultRange]);\n\n const range = React.useMemo((): RangeValue =\u003E {\n return columnFilterValue ?? [min, max];\n }, [columnFilterValue, min, max]);\n\n const formatValue = React.useCallback((value: number) =\u003E {\n return value.toLocaleString(undefined, { maximumFractionDigits: 0 });\n }, []);\n\n const onFromInputChange = React.useCallback(\n (event: React.ChangeEvent\u003CHTMLInputElement\u003E) =\u003E {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue \u003E= min && numValue \u003C= range[1]) {\n column.setFilterValue([numValue, range[1]]);\n }\n },\n [column, min, range],\n );\n\n const onToInputChange = React.useCallback(\n (event: React.ChangeEvent\u003CHTMLInputElement\u003E) =\u003E {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue \u003C= max && numValue \u003E= range[0]) {\n column.setFilterValue([range[0], numValue]);\n }\n },\n [column, max, range],\n );\n\n const onSliderValueChange = React.useCallback(\n (value: RangeValue) =\u003E {\n if (Array.isArray(value) && value.length === 2) {\n column.setFilterValue(value);\n }\n },\n [column],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) =\u003E {\n if (event.target instanceof HTMLDivElement) {\n event.stopPropagation();\n }\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {columnFilterValue ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n onClick={onReset}\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CPlusCircle /\u003E\n )}\n \u003Cspan\u003E{title}\u003C/span\u003E\n {columnFilterValue ? (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n {formatValue(columnFilterValue[0])} -{\" \"}\n {formatValue(columnFilterValue[1])}\n {unit ? ` ${unit}` : \"\"}\n \u003C/\u003E\n ) : null}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent align=\"start\" className=\"flex w-auto flex-col gap-4\"\u003E\n \u003Cdiv className=\"flex flex-col gap-3\"\u003E\n \u003Cp className=\"font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\u003E\n {title}\n \u003C/p\u003E\n \u003Cdiv className=\"flex items-center gap-4\"\u003E\n \u003CLabel htmlFor={`${id}-from`} className=\"sr-only\"\u003E\n From\n \u003C/Label\u003E\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n id={`${id}-from`}\n type=\"number\"\n aria-valuemin={min}\n aria-valuemax={max}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n placeholder={min.toString()}\n min={min}\n max={max}\n value={range[0]?.toString()}\n onChange={onFromInputChange}\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\n /\u003E\n {unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n \u003CLabel htmlFor={`${id}-to`} className=\"sr-only\"\u003E\n to\n \u003C/Label\u003E\n \u003Cdiv className=\"relative\"\u003E\n \u003CInput\n id={`${id}-to`}\n type=\"number\"\n aria-valuemin={min}\n aria-valuemax={max}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n placeholder={max.toString()}\n min={min}\n max={max}\n value={range[1]?.toString()}\n onChange={onToInputChange}\n className={cn(\"h-8 w-24\", unit && \"pr-8\")}\n /\u003E\n {unit && (\n \u003Cspan className=\"absolute top-0 right-0 bottom-0 flex items-center rounded-r-md bg-accent px-2 text-muted-foreground text-sm\"\u003E\n {unit}\n \u003C/span\u003E\n )}\n \u003C/div\u003E\n \u003C/div\u003E\n \u003CLabel htmlFor={`${id}-slider`} className=\"sr-only\"\u003E\n {title} slider\n \u003C/Label\u003E\n \u003CSlider\n id={`${id}-slider`}\n min={min}\n max={max}\n step={step}\n value={range}\n onValueChange={onSliderValueChange}\n /\u003E\n \u003C/div\u003E\n \u003CButton\n aria-label={`Clear ${title} filter`}\n variant=\"outline\"\n size=\"sm\"\n onClick={onReset}\n \u003E\n Clear\n \u003C/Button\u003E\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-date-filter.tsx",
"content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { CalendarIcon, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { formatDate } from \"@/lib/format\";\n\ntype DateSelection = Date[] | DateRange;\n\nfunction getIsDateRange(value: DateSelection): value is DateRange {\n return value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction parseAsDate(timestamp: number | string | undefined): Date | undefined {\n if (!timestamp) return undefined;\n const numericTimestamp =\n typeof timestamp === \"string\" ? Number(timestamp) : timestamp;\n const date = new Date(numericTimestamp);\n return !Number.isNaN(date.getTime()) ? date : undefined;\n}\n\nfunction parseColumnFilterValue(value: unknown) {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.map((item) =\u003E {\n if (typeof item === \"number\" || typeof item === \"string\") {\n return item;\n }\n return undefined;\n });\n }\n\n if (typeof value === \"string\" || typeof value === \"number\") {\n return [value];\n }\n\n return [];\n}\n\ninterface DataTableDateFilterProps\u003CTData\u003E {\n column: Column\u003CTData, unknown\u003E;\n title?: string;\n multiple?: boolean;\n}\n\nexport function DataTableDateFilter\u003CTData\u003E({\n column,\n title,\n multiple,\n}: DataTableDateFilterProps\u003CTData\u003E) {\n const columnFilterValue = column.getFilterValue();\n\n const selectedDates = React.useMemo\u003CDateSelection\u003E(() =\u003E {\n if (!columnFilterValue) {\n return multiple ? { from: undefined, to: undefined } : [];\n }\n\n if (multiple) {\n const timestamps = parseColumnFilterValue(columnFilterValue);\n return {\n from: parseAsDate(timestamps[0]),\n to: parseAsDate(timestamps[1]),\n };\n }\n\n const timestamps = parseColumnFilterValue(columnFilterValue);\n const date = parseAsDate(timestamps[0]);\n return date ? [date] : [];\n }, [columnFilterValue, multiple]);\n\n const onSelect = React.useCallback(\n (date: Date | DateRange | undefined) =\u003E {\n if (!date) {\n column.setFilterValue(undefined);\n return;\n }\n\n if (multiple && !(\"getTime\" in date)) {\n const from = date.from?.getTime();\n const to = date.to?.getTime();\n column.setFilterValue(from || to ? [from, to] : undefined);\n } else if (!multiple && \"getTime\" in date) {\n column.setFilterValue(date.getTime());\n }\n },\n [column, multiple],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) =\u003E {\n event.stopPropagation();\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n const hasValue = React.useMemo(() =\u003E {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return false;\n return selectedDates.from || selectedDates.to;\n }\n if (!Array.isArray(selectedDates)) return false;\n return selectedDates.length \u003E 0;\n }, [multiple, selectedDates]);\n\n const formatDateRange = React.useCallback((range: DateRange) =\u003E {\n if (!range.from && !range.to) return \"\";\n if (range.from && range.to) {\n return `${formatDate(range.from)} - ${formatDate(range.to)}`;\n }\n return formatDate(range.from ?? range.to);\n }, []);\n\n const label = React.useMemo(() =\u003E {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDates = selectedDates.from || selectedDates.to;\n const dateText = hasSelectedDates\n ? formatDateRange(selectedDates)\n : \"Select date range\";\n\n return (\n \u003Cspan className=\"flex items-center gap-2\"\u003E\n \u003Cspan\u003E{title}\u003C/span\u003E\n {hasSelectedDates && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003Cspan\u003E{dateText}\u003C/span\u003E\n \u003C/\u003E\n )}\n \u003C/span\u003E\n );\n }\n\n if (getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDate = selectedDates.length \u003E 0;\n const dateText = hasSelectedDate\n ? formatDate(selectedDates[0])\n : \"Select date\";\n\n return (\n \u003Cspan className=\"flex items-center gap-2\"\u003E\n \u003Cspan\u003E{title}\u003C/span\u003E\n {hasSelectedDate && (\n \u003C\u003E\n \u003CSeparator\n orientation=\"vertical\"\n className=\"mx-0.5 data-[orientation=vertical]:h-4\"\n /\u003E\n \u003Cspan\u003E{dateText}\u003C/span\u003E\n \u003C/\u003E\n )}\n \u003C/span\u003E\n );\n }, [selectedDates, multiple, formatDateRange, title]);\n\n return (\n \u003CPopover\u003E\n \u003CPopoverTrigger asChild\u003E\n \u003CButton variant=\"outline\" size=\"sm\" className=\"border-dashed\"\u003E\n {hasValue ? (\n \u003Cdiv\n role=\"button\"\n aria-label={`Clear ${title} filter`}\n tabIndex={0}\n onClick={onReset}\n className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n \u003E\n \u003CXCircle /\u003E\n \u003C/div\u003E\n ) : (\n \u003CCalendarIcon /\u003E\n )}\n {label}\n \u003C/Button\u003E\n \u003C/PopoverTrigger\u003E\n \u003CPopoverContent className=\"w-auto p-0\" align=\"start\"\u003E\n {multiple ? (\n \u003CCalendar\n initialFocus\n mode=\"range\"\n selected={\n getIsDateRange(selectedDates)\n ? selectedDates\n : { from: undefined, to: undefined }\n }\n onSelect={onSelect}\n /\u003E\n ) : (\n \u003CCalendar\n initialFocus\n mode=\"single\"\n selected={\n !getIsDateRange(selectedDates) ? selectedDates[0] : undefined\n }\n onSelect={onSelect}\n /\u003E\n )}\n \u003C/PopoverContent\u003E\n \u003C/Popover\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/components/data-table-skeleton.tsx",
"content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableSkeletonProps extends React.ComponentProps\u003C\"div\"\u003E {\n columnCount: number;\n rowCount?: number;\n filterCount?: number;\n cellWidths?: string[];\n withViewOptions?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n}\n\nexport function DataTableSkeleton({\n columnCount,\n rowCount = 10,\n filterCount = 0,\n cellWidths = [\"auto\"],\n withViewOptions = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: DataTableSkeletonProps) {\n const cozyCellWidths = Array.from(\n { length: columnCount },\n (_, index) =\u003E cellWidths[index % cellWidths.length] ?? \"auto\",\n );\n\n return (\n \u003Cdiv\n className={cn(\"flex w-full flex-col gap-2.5 overflow-auto\", className)}\n {...props}\n \u003E\n \u003Cdiv className=\"flex w-full items-center justify-between gap-2 overflow-auto p-1\"\u003E\n \u003Cdiv className=\"flex flex-1 items-center gap-2\"\u003E\n {filterCount \u003E 0\n ? Array.from({ length: filterCount }).map((_, i) =\u003E (\n \u003CSkeleton key={i} className=\"h-7 w-[4.5rem] border-dashed\" /\u003E\n ))\n : null}\n \u003C/div\u003E\n {withViewOptions ? (\n \u003CSkeleton className=\"ml-auto hidden h-7 w-[4.5rem] lg:flex\" /\u003E\n ) : null}\n \u003C/div\u003E\n \u003Cdiv className=\"rounded-md border\"\u003E\n \u003CTable\u003E\n \u003CTableHeader\u003E\n {Array.from({ length: 1 }).map((_, i) =\u003E (\n \u003CTableRow key={i} className=\"hover:bg-transparent\"\u003E\n {Array.from({ length: columnCount }).map((_, j) =\u003E (\n \u003CTableHead\n key={j}\n style={{\n width: cozyCellWidths[j],\n minWidth: shrinkZero ? cozyCellWidths[j] : \"auto\",\n }}\n \u003E\n \u003CSkeleton className=\"h-6 w-full\" /\u003E\n \u003C/TableHead\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableHeader\u003E\n \u003CTableBody\u003E\n {Array.from({ length: rowCount }).map((_, i) =\u003E (\n \u003CTableRow key={i} className=\"hover:bg-transparent\"\u003E\n {Array.from({ length: columnCount }).map((_, j) =\u003E (\n \u003CTableCell\n key={j}\n style={{\n width: cozyCellWidths[j],\n minWidth: shrinkZero ? cozyCellWidths[j] : \"auto\",\n }}\n \u003E\n \u003CSkeleton className=\"h-6 w-full\" /\u003E\n \u003C/TableCell\u003E\n ))}\n \u003C/TableRow\u003E\n ))}\n \u003C/TableBody\u003E\n \u003C/Table\u003E\n \u003C/div\u003E\n {withPagination ? (\n \u003Cdiv className=\"flex w-full items-center justify-between gap-4 overflow-auto p-1 sm:gap-8\"\u003E\n \u003CSkeleton className=\"h-7 w-40 shrink-0\" /\u003E\n \u003Cdiv className=\"flex items-center gap-4 sm:gap-6 lg:gap-8\"\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n \u003CSkeleton className=\"h-7 w-24\" /\u003E\n \u003CSkeleton className=\"h-7 w-[4.5rem]\" /\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center justify-center font-medium text-sm\"\u003E\n \u003CSkeleton className=\"h-7 w-20\" /\u003E\n \u003C/div\u003E\n \u003Cdiv className=\"flex items-center gap-2\"\u003E\n \u003CSkeleton className=\"hidden size-7 lg:block\" /\u003E\n \u003CSkeleton className=\"size-7\" /\u003E\n \u003CSkeleton className=\"size-7\" /\u003E\n \u003CSkeleton className=\"hidden size-7 lg:block\" /\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n \u003C/div\u003E\n ) : null}\n \u003C/div\u003E\n );\n}\n",
"type": "registry:component"
},
{
"path": "src/hooks/use-callback-ref.ts",
"content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef\u003CT extends (...args: never[]) =\u003E unknown\u003E(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() =\u003E {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () =\u003E ((...args) =\u003E callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n",
"type": "registry:hook"
},
{
"path": "src/hooks/use-data-table.ts",
"content": "\"use client\";\n\nimport {\n type ColumnFiltersState,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type TableState,\n type Updater,\n type VisibilityState,\n getCoreRowModel,\n getFacetedMinMaxValues,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport {\n type Parser,\n type UseQueryStateOptions,\n parseAsArrayOf,\n parseAsInteger,\n parseAsString,\n useQueryState,\n useQueryStates,\n} from \"nuqs\";\nimport * as React from \"react\";\n\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getSortingStateParser } from \"@/lib/parsers\";\nimport type { ExtendedColumnSort } from \"@/types/data-table\";\n\nconst PAGE_KEY = \"page\";\nconst PER_PAGE_KEY = \"perPage\";\nconst SORT_KEY = \"sort\";\nconst ARRAY_SEPARATOR = \",\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\n\ninterface UseDataTableProps\u003CTData\u003E\n extends Omit\u003C\n TableOptions\u003CTData\u003E,\n | \"state\"\n | \"pageCount\"\n | \"getCoreRowModel\"\n | \"manualFiltering\"\n | \"manualPagination\"\n | \"manualSorting\"\n \u003E,\n Required\u003CPick\u003CTableOptions\u003CTData\u003E, \"pageCount\"\u003E\u003E {\n initialState?: Omit\u003CPartial\u003CTableState\u003E, \"sorting\"\u003E & {\n sorting?: ExtendedColumnSort\u003CTData\u003E[];\n };\n history?: \"push\" | \"replace\";\n debounceMs?: number;\n throttleMs?: number;\n clearOnDefault?: boolean;\n enableAdvancedFilter?: boolean;\n scroll?: boolean;\n shallow?: boolean;\n startTransition?: React.TransitionStartFunction;\n}\n\nexport function useDataTable\u003CTData\u003E(props: UseDataTableProps\u003CTData\u003E) {\n const {\n columns,\n pageCount = -1,\n initialState,\n history = \"replace\",\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n clearOnDefault = false,\n enableAdvancedFilter = false,\n scroll = false,\n shallow = true,\n startTransition,\n ...tableProps\n } = props;\n\n const queryStateOptions = React.useMemo\u003C\n Omit\u003CUseQueryStateOptions\u003Cstring\u003E, \"parse\"\u003E\n \u003E(\n () =\u003E ({\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n }),\n [\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n ],\n );\n\n const [rowSelection, setRowSelection] = React.useState\u003CRowSelectionState\u003E(\n initialState?.rowSelection ?? {},\n );\n const [columnVisibility, setColumnVisibility] =\n React.useState\u003CVisibilityState\u003E(initialState?.columnVisibility ?? {});\n\n const [page, setPage] = useQueryState(\n PAGE_KEY,\n parseAsInteger.withOptions(queryStateOptions).withDefault(1),\n );\n const [perPage, setPerPage] = useQueryState(\n PER_PAGE_KEY,\n parseAsInteger\n .withOptions(queryStateOptions)\n .withDefault(initialState?.pagination?.pageSize ?? 10),\n );\n\n const pagination: PaginationState = React.useMemo(() =\u003E {\n return {\n pageIndex: page - 1, // zero-based index -\u003E one-based index\n pageSize: perPage,\n };\n }, [page, perPage]);\n\n const onPaginationChange = React.useCallback(\n (updaterOrValue: Updater\u003CPaginationState\u003E) =\u003E {\n if (typeof updaterOrValue === \"function\") {\n const newPagination = updaterOrValue(pagination);\n void setPage(newPagination.pageIndex + 1);\n void setPerPage(newPagination.pageSize);\n } else {\n void setPage(updaterOrValue.pageIndex + 1);\n void setPerPage(updaterOrValue.pageSize);\n }\n },\n [pagination, setPage, setPerPage],\n );\n\n const columnIds = React.useMemo(() =\u003E {\n return new Set(\n columns.map((column) =\u003E column.id).filter(Boolean) as string[],\n );\n }, [columns]);\n\n const [sorting, setSorting] = useQueryState(\n SORT_KEY,\n getSortingStateParser\u003CTData\u003E(columnIds)\n .withOptions(queryStateOptions)\n .withDefault(initialState?.sorting ?? []),\n );\n\n const onSortingChange = React.useCallback(\n (updaterOrValue: Updater\u003CSortingState\u003E) =\u003E {\n if (typeof updaterOrValue === \"function\") {\n const newSorting = updaterOrValue(sorting);\n setSorting(newSorting as ExtendedColumnSort\u003CTData\u003E[]);\n } else {\n setSorting(updaterOrValue as ExtendedColumnSort\u003CTData\u003E[]);\n }\n },\n [sorting, setSorting],\n );\n\n const filterableColumns = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return [];\n\n return columns.filter((column) =\u003E column.enableColumnFilter);\n }, [columns, enableAdvancedFilter]);\n\n const filterParsers = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return {};\n\n return filterableColumns.reduce\u003C\n Record\u003Cstring, Parser\u003Cstring\u003E | Parser\u003Cstring[]\u003E\u003E\n \u003E((acc, column) =\u003E {\n if (column.meta?.options) {\n acc[column.id ?? \"\"] = parseAsArrayOf(\n parseAsString,\n ARRAY_SEPARATOR,\n ).withOptions(queryStateOptions);\n } else {\n acc[column.id ?? \"\"] = parseAsString.withOptions(queryStateOptions);\n }\n return acc;\n }, {});\n }, [filterableColumns, queryStateOptions, enableAdvancedFilter]);\n\n const [filterValues, setFilterValues] = useQueryStates(filterParsers);\n\n const debouncedSetFilterValues = useDebouncedCallback(\n (values: typeof filterValues) =\u003E {\n void setPage(1);\n void setFilterValues(values);\n },\n debounceMs,\n );\n\n const initialColumnFilters: ColumnFiltersState = React.useMemo(() =\u003E {\n if (enableAdvancedFilter) return [];\n\n return Object.entries(filterValues).reduce\u003CColumnFiltersState\u003E(\n (filters, [key, value]) =\u003E {\n if (value !== null) {\n const processedValue = Array.isArray(value)\n ? value\n : typeof value === \"string\" && /[^a-zA-Z0-9]/.test(value)\n ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)\n : [value];\n\n filters.push({\n id: key,\n value: processedValue,\n });\n }\n return filters;\n },\n [],\n );\n }, [filterValues, enableAdvancedFilter]);\n\n const [columnFilters, setColumnFilters] =\n React.useState\u003CColumnFiltersState\u003E(initialColumnFilters);\n\n const onColumnFiltersChange = React.useCallback(\n (updaterOrValue: Updater\u003CColumnFiltersState\u003E) =\u003E {\n if (enableAdvancedFilter) return;\n\n setColumnFilters((prev) =\u003E {\n const next =\n typeof updaterOrValue === \"function\"\n ? updaterOrValue(prev)\n : updaterOrValue;\n\n const filterUpdates = next.reduce\u003C\n Record\u003Cstring, string | string[] | null\u003E\n \u003E((acc, filter) =\u003E {\n if (filterableColumns.find((column) =\u003E column.id === filter.id)) {\n acc[filter.id] = filter.value as string | string[];\n }\n return acc;\n }, {});\n\n for (const prevFilter of prev) {\n if (!next.some((filter) =\u003E filter.id === prevFilter.id)) {\n filterUpdates[prevFilter.id] = null;\n }\n }\n\n debouncedSetFilterValues(filterUpdates);\n return next;\n });\n },\n [debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],\n );\n\n const table = useReactTable({\n ...tableProps,\n columns,\n initialState,\n pageCount,\n state: {\n pagination,\n sorting,\n columnVisibility,\n rowSelection,\n columnFilters,\n },\n defaultColumn: {\n ...tableProps.defaultColumn,\n enableColumnFilter: false,\n },\n enableRowSelection: true,\n onRowSelectionChange: setRowSelection,\n onPaginationChange,\n onSortingChange,\n onColumnFiltersChange,\n onColumnVisibilityChange: setColumnVisibility,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\n manualPagination: true,\n manualSorting: true,\n manualFiltering: true,\n });\n\n return { table, shallow, debounceMs, throttleMs };\n}\n",
"type": "registry:hook"
},
{
"path": "src/hooks/use-debounced-callback.ts",
"content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback\u003CT extends (...args: never[]) =\u003E unknown\u003E(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () =\u003E () =\u003E window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters\u003CT\u003E) =\u003E {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () =\u003E handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n",
"type": "registry:hook"
},
{
"path": "src/lib/data-table.ts",
"content": "import type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/types/data-table\";\nimport type { Column } from \"@tanstack/react-table\";\n\nimport { dataTableConfig } from \"@/config/data-table\";\n\nexport function getCommonPinningStyles\u003CTData\u003E({\n column,\n withBorder = false,\n}: {\n column: Column\u003CTData\u003E;\n withBorder?: boolean;\n}): React.CSSProperties {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px hsl(var(--border)) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px hsl(var(--border)) inset\"\n : undefined\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: isPinned ? \"hsl(var(--background))\" : \"hsl(var(--background))\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : 0,\n };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n const operatorMap: Record\u003C\n FilterVariant,\n { label: string; value: FilterOperator }[]\n \u003E = {\n text: dataTableConfig.textOperators,\n number: dataTableConfig.numericOperators,\n range: dataTableConfig.numericOperators,\n date: dataTableConfig.dateOperators,\n dateRange: dataTableConfig.dateOperators,\n boolean: dataTableConfig.booleanOperators,\n select: dataTableConfig.selectOperators,\n multiSelect: dataTableConfig.multiSelectOperators,\n };\n\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n const operators = getFilterOperators(filterVariant);\n\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters\u003CTData\u003E(\n filters: ExtendedColumnFilter\u003CTData\u003E[],\n): ExtendedColumnFilter\u003CTData\u003E[] {\n return filters.filter(\n (filter) =\u003E\n filter.operator === \"isEmpty\" ||\n filter.operator === \"isNotEmpty\" ||\n (Array.isArray(filter.value)\n ? filter.value.length \u003E 0\n : filter.value !== \"\" &&\n filter.value !== null &&\n filter.value !== undefined),\n );\n}\n",
"type": "registry:lib"
},
{
"path": "src/lib/format.ts",
"content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch (_err) {\n return \"\";\n }\n}\n",
"type": "registry:lib"
},
{
"path": "src/lib/parsers.ts",
"content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport { dataTableConfig } from \"@/config/data-table\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/types/data-table\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = \u003CTData\u003E(\n columnIds?: string[] | Set\u003Cstring\u003E,\n) =\u003E {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) =\u003E {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) =\u003E !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort\u003CTData\u003E[];\n } catch {\n return null;\n }\n },\n serialize: (value) =\u003E JSON.stringify(value),\n eq: (a, b) =\u003E\n a.length === b.length &&\n a.every(\n (item, index) =\u003E\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer\u003Ctypeof filterItemSchema\u003E;\n\nexport const getFiltersStateParser = \u003CTData\u003E(\n columnIds?: string[] | Set\u003Cstring\u003E,\n) =\u003E {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) =\u003E {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) =\u003E !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter\u003CTData\u003E[];\n } catch {\n return null;\n }\n },\n serialize: (value) =\u003E JSON.stringify(value),\n eq: (a, b) =\u003E\n a.length === b.length &&\n a.every(\n (filter, index) =\u003E\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n",
"type": "registry:lib"
},
{
"path": "src/config/data-table.ts",
"content": "export type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n",
"type": "registry:file",
"target": "src/config/data-table.ts"
},
{
"path": "src/types/data-table.ts",
"content": "import type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: \u003Cexplanation\u003E\n interface ColumnMeta\u003CTData extends RowData, TValue\u003E {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC\u003CReact.SVGProps\u003CSVGSVGElement\u003E\u003E;\n }\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC\u003CReact.SVGProps\u003CSVGSVGElement\u003E\u003E;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort\u003CTData\u003E extends Omit\u003CColumnSort, \"id\"\u003E {\n id: Extract\u003Ckeyof TData, string\u003E;\n}\n\nexport interface ExtendedColumnFilter\u003CTData\u003E extends FilterItemSchema {\n id: Extract\u003Ckeyof TData, string\u003E;\n}\n\nexport interface DataTableRowAction\u003CTData\u003E {\n row: Row\u003CTData\u003E;\n variant: \"update\" | \"delete\";\n}\n",
"type": "registry:file",
"target": "src/types/data-table.ts"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment