0% found this document useful (0 votes)
3 views

all_src_files (1)

The document outlines a React application structure utilizing Next.js, including components for layout, theming, and data tables. It features metadata configuration for SEO and social media, as well as a root layout that incorporates a header, main content area, and a toaster for notifications. Additionally, it includes functionalities for filtering and sorting data in tables, with support for date selection and responsive design elements.

Uploaded by

Anshu Aditya
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

all_src_files (1)

The document outlines a React application structure utilizing Next.js, including components for layout, theming, and data tables. It features metadata configuration for SEO and social media, as well as a root layout that incorporates a header, main content area, and a toaster for notifications. Additionally, it includes functionalities for filtering and sorting data in tables, with support for date selection and responsive design elements.

Uploaded by

Anshu Aditya
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 102

import { SiteHeader } from "@/components/layouts/site-header";

import { ThemeProvider } from "@/components/providers";


import { TailwindIndicator } from "@/components/tailwind-indicator";
import { siteConfig } from "@/config/site";
import { cn } from "@/lib/utils";

import "@/styles/globals.css";

import type { Metadata, Viewport } from "next";

import { Toaster } from "@/components/ui/sonner";


import { fontMono, fontSans } from "@/lib/fonts";
import Script from "next/script";

export const metadata: Metadata = {


metadataBase: new URL(siteConfig.url),
title: {
default: siteConfig.name,
template: `%s - ${siteConfig.name}`,
},
description: siteConfig.description,
keywords: [
"nextjs",
"react",
"react server components",
"table",
"react-table",
"tanstack-table",
"shadcn-table",
],
authors: [
{
name: "sadmann7",
url: "https://www.sadmn.com",
},
],
creator: "sadmann7",
openGraph: {
type: "website",
locale: "en_US",
url: siteConfig.url,
title: siteConfig.name,
description: siteConfig.description,
siteName: siteConfig.name,
},
twitter: {
card: "summary_large_image",
title: siteConfig.name,
description: siteConfig.description,
images: [`${siteConfig.url}/og.jpg`],
creator: "@sadmann17",
},
icons: {
icon: "/icon.png",
},
manifest: `${siteConfig.url}/site.webmanifest`,
};

export const viewport: Viewport = {


colorScheme: "dark light",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};

export default function RootLayout({ children }: React.PropsWithChildren) {


return (
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
fontMono.variable,
)}
>
<Script
defer
data-site-id={siteConfig.url}
src="https://assets.onedollarstats.com/stonks.js"
/>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="relative flex min-h-screen flex-col">
<SiteHeader />
<main className="flex-1">{children}</main>
</div>
<TailwindIndicator />
</ThemeProvider>
<Toaster />
</body>
</html>
);
}

import type { SearchParams } from "@/types";


import * as React from "react";

import { DataTableSkeleton } from "@/components/data-table-skeleton";


import { Shell } from "@/components/shell";
import { getValidFilters } from "@/lib/data-table";

import { FeatureFlagsProvider } from "./_components/feature-flags-provider";


import { TasksTable } from "./_components/tasks-table";
import {
getEstimatedHoursRange,
getTaskPriorityCounts,
getTaskStatusCounts,
getTasks,
} from "./_lib/queries";
import { searchParamsCache } from "./_lib/validations";

interface IndexPageProps {
searchParams: Promise<SearchParams>;
}

export default async function IndexPage(props: IndexPageProps) {


const searchParams = await props.searchParams;
const search = searchParamsCache.parse(searchParams);

const validFilters = getValidFilters(search.filters);

const promises = Promise.all([


getTasks({
...search,
filters: validFilters,
}),
getTaskStatusCounts(),
getTaskPriorityCounts(),
getEstimatedHoursRange(),
]);

return (
<Shell className="gap-2">
<FeatureFlagsProvider>
<React.Suspense
fallback={
<DataTableSkeleton
columnCount={7}
filterCount={2}
cellWidths={[
"10rem",
"30rem",
"10rem",
"10rem",
"6rem",
"6rem",
"6rem",
]}
shrinkZero
/>
}
>
<TasksTable promises={promises} />
</React.Suspense>
</FeatureFlagsProvider>
</Shell>
);
}

"use client";

import { Button } from "@/components/ui/button";


import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { Table } from "@tanstack/react-table";
import { Loader } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import * as React from "react";
import * as ReactDOM from "react-dom";

interface DataTableActionBarProps<TData>
extends React.ComponentProps<typeof motion.div> {
table: Table<TData>;
visible?: boolean;
container?: Element | DocumentFragment | null;
}

function DataTableActionBar<TData>({
table,
visible: visibleProp,
container: containerProp,
children,
className,
...props
}: DataTableActionBarProps<TData>) {
const [mounted, setMounted] = React.useState(false);

React.useLayoutEffect(() => {
setMounted(true);
}, []);

React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
table.toggleAllRowsSelected(false);
}
}

window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [table]);

const container =
containerProp ?? (mounted ? globalThis.document?.body : null);

if (!container) return null;

const visible =
visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;

return ReactDOM.createPortal(
<AnimatePresence>
{visible && (
<motion.div
role="toolbar"
aria-orientation="horizontal"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className={cn(
"fixed inset-x-0 bottom-6 z-50 mx-auto flex w-fit flex-wrap items-
center justify-center gap-2 rounded-md border bg-background p-2 text-foreground
shadow-sm",
className,
)}
{...props}
>
{children}
</motion.div>
)}
</AnimatePresence>,
container,
);
}

interface DataTableActionBarActionProps
extends React.ComponentProps<typeof Button> {
tooltip?: string;
isPending?: boolean;
}

function DataTableActionBarAction({
size = "sm",
tooltip,
isPending,
disabled,
className,
children,
...props
}: DataTableActionBarActionProps) {
const trigger = (
<Button
variant="secondary"
size={size}
className={cn(
"gap-1.5 border border-secondary bg-secondary/50 hover:bg-secondary/70
[&>svg]:size-3.5",
size === "icon" ? "size-7" : "h-7",
className,
)}
disabled={disabled || isPending}
{...props}
>
{isPending ? <Loader className="animate-spin" /> : children}
</Button>
);

if (!tooltip) return trigger;

return (
<Tooltip>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
sideOffset={6}
className="border bg-accent font-semibold text-foreground dark:bg-zinc-900
[&>span]:hidden"
>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
);
}

export { DataTableActionBar, DataTableActionBarAction };


"use client";

import type { Table } from "@tanstack/react-table";


import type * as React from "react";

import { DataTableViewOptions } from "@/components/data-table-view-options";


import { cn } from "@/lib/utils";

interface DataTableAdvancedToolbarProps<TData>
extends React.ComponentProps<"div"> {
table: Table<TData>;
}

export function DataTableAdvancedToolbar<TData>({


table,
children,
className,
...props
}: DataTableAdvancedToolbarProps<TData>) {
return (
<div
role="toolbar"
aria-orientation="horizontal"
className={cn(
"flex w-full items-start justify-between gap-2 p-1",
className,
)}
{...props}
>
<div className="flex flex-1 flex-wrap items-center gap-2">{children}</div>
<div className="flex items-center gap-2">
<DataTableViewOptions table={table} />
</div>
</div>
);
}

"use client";

import type { Column } from "@tanstack/react-table";


import {
ChevronDown,
ChevronUp,
ChevronsUpDown,
EyeOff,
X,
} from "lucide-react";

import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

interface DataTableColumnHeaderProps<TData, TValue>


extends React.ComponentProps<typeof DropdownMenuTrigger> {
column: Column<TData, TValue>;
title: string;
}

export function DataTableColumnHeader<TData, TValue>({


column,
title,
className,
...props
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort() && !column.getCanHide()) {
return <div className={cn(className)}>{title}</div>;
}

return (
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
"-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",
className,
)}
{...props}
>
{title}
{column.getCanSort() &&
(column.getIsSorted() === "desc" ? (
<ChevronDown />
) : column.getIsSorted() === "asc" ? (
<ChevronUp />
) : (
<ChevronsUpDown />
))}
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-28">
{column.getCanSort() && (
<>
<DropdownMenuCheckboxItem
className="relative pr-8 pl-2 [&>span:first-child]:right-2
[&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
checked={column.getIsSorted() === "asc"}
onClick={() => column.toggleSorting(false)}
>
<ChevronUp />
Asc
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
className="relative pr-8 pl-2 [&>span:first-child]:right-2
[&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
checked={column.getIsSorted() === "desc"}
onClick={() => column.toggleSorting(true)}
>
<ChevronDown />
Desc
</DropdownMenuCheckboxItem>
{column.getIsSorted() && (
<DropdownMenuItem
className="pl-2 [&_svg]:text-muted-foreground"
onClick={() => column.clearSorting()}
>
<X />
Reset
</DropdownMenuItem>
)}
</>
)}
{column.getCanHide() && (
<DropdownMenuCheckboxItem
className="relative pr-8 pl-2 [&>span:first-child]:right-2
[&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
checked={!column.getIsVisible()}
onClick={() => column.toggleVisibility(false)}
>
<EyeOff />
Hide
</DropdownMenuCheckboxItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

"use client";

import type { Column } from "@tanstack/react-table";


import { CalendarIcon, XCircle } from "lucide-react";
import * as React from "react";
import type { DateRange } from "react-day-picker";

import { Button } from "@/components/ui/button";


import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { formatDate } from "@/lib/format";

type DateSelection = Date[] | DateRange;

function getIsDateRange(value: DateSelection): value is DateRange {


return value && typeof value === "object" && !Array.isArray(value);
}

function parseAsDate(timestamp: number | string | undefined): Date | undefined {


if (!timestamp) return undefined;
const numericTimestamp =
typeof timestamp === "string" ? Number(timestamp) : timestamp;
const date = new Date(numericTimestamp);
return !Number.isNaN(date.getTime()) ? date : undefined;
}

function parseColumnFilterValue(value: unknown) {


if (value === null || value === undefined) {
return [];
}

if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "number" || typeof item === "string") {
return item;
}
return undefined;
});
}

if (typeof value === "string" || typeof value === "number") {


return [value];
}

return [];
}

interface DataTableDateFilterProps<TData> {
column: Column<TData, unknown>;
title?: string;
multiple?: boolean;
}

export function DataTableDateFilter<TData>({


column,
title,
multiple,
}: DataTableDateFilterProps<TData>) {
const columnFilterValue = column.getFilterValue();

const selectedDates = React.useMemo<DateSelection>(() => {


if (!columnFilterValue) {
return multiple ? { from: undefined, to: undefined } : [];
}

if (multiple) {
const timestamps = parseColumnFilterValue(columnFilterValue);
return {
from: parseAsDate(timestamps[0]),
to: parseAsDate(timestamps[1]),
};
}

const timestamps = parseColumnFilterValue(columnFilterValue);


const date = parseAsDate(timestamps[0]);
return date ? [date] : [];
}, [columnFilterValue, multiple]);

const onSelect = React.useCallback(


(date: Date | DateRange | undefined) => {
if (!date) {
column.setFilterValue(undefined);
return;
}

if (multiple && !("getTime" in date)) {


const from = date.from?.getTime();
const to = date.to?.getTime();
column.setFilterValue(from || to ? [from, to] : undefined);
} else if (!multiple && "getTime" in date) {
column.setFilterValue(date.getTime());
}
},
[column, multiple],
);

const onReset = React.useCallback(


(event: React.MouseEvent) => {
event.stopPropagation();
column.setFilterValue(undefined);
},
[column],
);

const hasValue = React.useMemo(() => {


if (multiple) {
if (!getIsDateRange(selectedDates)) return false;
return selectedDates.from || selectedDates.to;
}
if (!Array.isArray(selectedDates)) return false;
return selectedDates.length > 0;
}, [multiple, selectedDates]);

const formatDateRange = React.useCallback((range: DateRange) => {


if (!range.from && !range.to) return "";
if (range.from && range.to) {
return `${formatDate(range.from)} - ${formatDate(range.to)}`;
}
return formatDate(range.from ?? range.to);
}, []);

const label = React.useMemo(() => {


if (multiple) {
if (!getIsDateRange(selectedDates)) return null;

const hasSelectedDates = selectedDates.from || selectedDates.to;


const dateText = hasSelectedDates
? formatDateRange(selectedDates)
: "Select date range";

return (
<span className="flex items-center gap-2">
<span>{title}</span>
{hasSelectedDates && (
<>
<Separator
orientation="vertical"
className="mx-0.5 data-[orientation=vertical]:h-4"
/>
<span>{dateText}</span>
</>
)}
</span>
);
}

if (getIsDateRange(selectedDates)) return null;


const hasSelectedDate = selectedDates.length > 0;
const dateText = hasSelectedDate
? formatDate(selectedDates[0])
: "Select date";

return (
<span className="flex items-center gap-2">
<span>{title}</span>
{hasSelectedDate && (
<>
<Separator
orientation="vertical"
className="mx-0.5 data-[orientation=vertical]:h-4"
/>
<span>{dateText}</span>
</>
)}
</span>
);
}, [selectedDates, multiple, formatDateRange, title]);

return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="border-dashed">
{hasValue ? (
<div
role="button"
aria-label={`Clear ${title} filter`}
tabIndex={0}
onClick={onReset}
className="rounded-sm opacity-70 transition-opacity hover:opacity-100
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<XCircle />
</div>
) : (
<CalendarIcon />
)}
{label}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
{multiple ? (
<Calendar
initialFocus
mode="range"
selected={
getIsDateRange(selectedDates)
? selectedDates
: { from: undefined, to: undefined }
}
onSelect={onSelect}
/>
) : (
<Calendar
initialFocus
mode="single"
selected={
!getIsDateRange(selectedDates) ? selectedDates[0] : undefined
}
onSelect={onSelect}
/>
)}
</PopoverContent>
</Popover>
);
}

"use client";

import type { Option } from "@/types/data-table";


import type { Column } from "@tanstack/react-table";
import { Check, PlusCircle, XCircle } from "lucide-react";

import { Badge } from "@/components/ui/badge";


import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import * as React from "react";

interface DataTableFacetedFilterProps<TData, TValue> {


column?: Column<TData, TValue>;
title?: string;
options: Option[];
multiple?: boolean;
}

export function DataTableFacetedFilter<TData, TValue>({


column,
title,
options,
multiple,
}: DataTableFacetedFilterProps<TData, TValue>) {
const [open, setOpen] = React.useState(false);

const columnFilterValue = column?.getFilterValue();


const selectedValues = new Set(
Array.isArray(columnFilterValue) ? columnFilterValue : [],
);

const onItemSelect = React.useCallback(


(option: Option, isSelected: boolean) => {
if (!column) return;

if (multiple) {
const newSelectedValues = new Set(selectedValues);
if (isSelected) {
newSelectedValues.delete(option.value);
} else {
newSelectedValues.add(option.value);
}
const filterValues = Array.from(newSelectedValues);
column.setFilterValue(filterValues.length ? filterValues : undefined);
} else {
column.setFilterValue(isSelected ? undefined : [option.value]);
setOpen(false);
}
},
[column, multiple, selectedValues],
);

const onReset = React.useCallback(


(event?: React.MouseEvent) => {
event?.stopPropagation();
column?.setFilterValue(undefined);
},
[column],
);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="border-dashed">
{selectedValues?.size > 0 ? (
<div
role="button"
aria-label={`Clear ${title} filter`}
tabIndex={0}
onClick={onReset}
className="rounded-sm opacity-70 transition-opacity hover:opacity-100
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<XCircle />
</div>
) : (
<PlusCircle />
)}
{title}
{selectedValues?.size > 0 && (
<>
<Separator
orientation="vertical"
className="mx-0.5 data-[orientation=vertical]:h-4"
/>
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden items-center gap-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[12.5rem] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList className="max-h-full">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup className="max-h-[18.75rem] overflow-y-auto overflow-x-
hidden">
{options.map((option) => {
const isSelected = selectedValues.has(option.value);

return (
<CommandItem
key={option.value}
onSelect={() => onItemSelect(option, isSelected)}
>
<div
className={cn(
"flex size-4 items-center justify-center rounded-sm border
border-primary",
isSelected
? "bg-primary"
: "opacity-50 [&_svg]:invisible",
)}
>
<Check />
</div>
{option.icon && <option.icon />}
<span className="truncate">{option.label}</span>
{option.count && (
<span className="ml-auto font-mono text-xs">
{option.count}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => onReset()}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

"use client";

import type { Column, ColumnMeta, Table } from "@tanstack/react-table";


import {
CalendarIcon,
Check,
ChevronsUpDown,
GripVertical,
ListFilter,
Trash2,
} from "lucide-react";
import { parseAsStringEnum, useQueryState } from "nuqs";
import * as React from "react";

import { DataTableRangeFilter } from "@/components/data-table-range-filter";


import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Faceted,
FacetedBadgeList,
FacetedContent,
FacetedEmpty,
FacetedGroup,
FacetedInput,
FacetedItem,
FacetedList,
FacetedTrigger,
} from "@/components/ui/faceted";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sortable,
SortableContent,
SortableItem,
SortableItemHandle,
SortableOverlay,
} from "@/components/ui/sortable";
import { dataTableConfig } from "@/config/data-table";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { getDefaultFilterOperator, getFilterOperators } from "@/lib/data-table";
import { formatDate } from "@/lib/format";
import { generateId } from "@/lib/id";
import { getFiltersStateParser } from "@/lib/parsers";
import { cn } from "@/lib/utils";
import type {
ExtendedColumnFilter,
FilterOperator,
JoinOperator,
} from "@/types/data-table";

const FILTERS_KEY = "filters";


const JOIN_OPERATOR_KEY = "joinOperator";
const DEBOUNCE_MS = 300;
const THROTTLE_MS = 50;
const OPEN_MENU_SHORTCUT = "f";
const REMOVE_FILTER_SHORTCUTS = ["backspace", "delete"];

interface DataTableFilterListProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
debounceMs?: number;
throttleMs?: number;
shallow?: boolean;
}

export function DataTableFilterList<TData>({


table,
debounceMs = DEBOUNCE_MS,
throttleMs = THROTTLE_MS,
shallow = true,
...props
}: DataTableFilterListProps<TData>) {
const id = React.useId();
const labelId = React.useId();
const descriptionId = React.useId();
const [open, setOpen] = React.useState(false);
const addButtonRef = React.useRef<HTMLButtonElement>(null);

const columns = React.useMemo(() => {


return table
.getAllColumns()
.filter((column) => column.columnDef.enableColumnFilter);
}, [table]);

const [filters, setFilters] = useQueryState(


FILTERS_KEY,
getFiltersStateParser<TData>(columns.map((field) => field.id))
.withDefault([])
.withOptions({
clearOnDefault: true,
shallow,
throttleMs,
}),
);
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);

console.log({ filters });

const [joinOperator, setJoinOperator] = useQueryState(


JOIN_OPERATOR_KEY,
parseAsStringEnum(["and", "or"]).withDefault("and").withOptions({
clearOnDefault: true,
shallow,
}),
);

const onFilterAdd = React.useCallback(() => {


const column = columns[0];

if (!column) return;

debouncedSetFilters([
...filters,
{
id: column.id as Extract<keyof TData, string>,
value: "",
variant: column.columnDef.meta?.variant ?? "text",
operator: getDefaultFilterOperator(
column.columnDef.meta?.variant ?? "text",
),
filterId: generateId({ length: 8 }),
},
]);
}, [columns, filters, debouncedSetFilters]);

const onFilterUpdate = React.useCallback(


(
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => {
debouncedSetFilters((prevFilters) => {
const updatedFilters = prevFilters.map((filter) => {
if (filter.filterId === filterId) {
return { ...filter, ...updates } as ExtendedColumnFilter<TData>;
}
return filter;
});
return updatedFilters;
});
},
[debouncedSetFilters],
);

const onFilterRemove = React.useCallback(


(filterId: string) => {
const updatedFilters = filters.filter(
(filter) => filter.filterId !== filterId,
);
void setFilters(updatedFilters);
requestAnimationFrame(() => {
addButtonRef.current?.focus();
});
},
[filters, setFilters],
);

const onFiltersReset = React.useCallback(() => {


void setFilters(null);
void setJoinOperator("and");
}, [setFilters, setJoinOperator]);

React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) {
event.preventDefault();
setOpen(true);
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
event.shiftKey &&
filters.length > 0
) {
event.preventDefault();
onFilterRemove(filters[filters.length - 1]?.filterId ?? "");
}
}

window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [filters, onFilterRemove]);
const onTriggerKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLButtonElement>) => {
if (
REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&
filters.length > 0
) {
event.preventDefault();
onFilterRemove(filters[filters.length - 1]?.filterId ?? "");
}
},
[filters, onFilterRemove],
);

return (
<Sortable
value={filters}
onValueChange={setFilters}
getItemValue={(item) => item.filterId}
>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" onKeyDown={onTriggerKeyDown}>
<ListFilter />
Filter
{filters.length > 0 && (
<Badge
variant="secondary"
className="h-[18.24px] rounded-[3.2px] px-[5.12px] font-mono font-
normal text-[10.4px]"
>
{filters.length}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent
aria-describedby={descriptionId}
aria-labelledby={labelId}
className="flex w-full max-w-[var(--radix-popover-content-available-
width)] origin-[var(--radix-popover-content-transform-origin)] flex-col gap-3.5 p-4
sm:min-w-[380px]"
{...props}
>
<div className="flex flex-col gap-1">
<h4 id={labelId} className="font-medium leading-none">
{filters.length > 0 ? "Filters" : "No filters applied"}
</h4>
<p
id={descriptionId}
className={cn(
"text-muted-foreground text-sm",
filters.length > 0 && "sr-only",
)}
>
{filters.length > 0
? "Modify filters to refine your rows."
: "Add filters to refine your rows."}
</p>
</div>
{filters.length > 0 ? (
<SortableContent asChild>
<div
role="list"
className="flex max-h-[300px] flex-col gap-2 overflow-y-auto p-1"
>
{filters.map((filter, index) => (
<DataTableFilterItem<TData>
key={filter.filterId}
filter={filter}
index={index}
filterItemId={`${id}-filter-${filter.filterId}`}
joinOperator={joinOperator}
setJoinOperator={setJoinOperator}
columns={columns}
onFilterUpdate={onFilterUpdate}
onFilterRemove={onFilterRemove}
/>
))}
</div>
</SortableContent>
) : null}
<div className="flex w-full items-center gap-2">
<Button
size="sm"
className="rounded"
ref={addButtonRef}
onClick={onFilterAdd}
>
Add filter
</Button>
{filters.length > 0 ? (
<Button
variant="outline"
size="sm"
className="rounded"
onClick={onFiltersReset}
>
Reset filters
</Button>
) : null}
</div>
</PopoverContent>
</Popover>
<SortableOverlay>
<div className="flex items-center gap-2">
<div className="h-8 min-w-[72px] rounded-sm bg-primary/10" />
<div className="h-8 w-32 rounded-sm bg-primary/10" />
<div className="h-8 w-32 rounded-sm bg-primary/10" />
<div className="h-8 min-w-36 flex-1 rounded-sm bg-primary/10" />
<div className="size-8 shrink-0 rounded-sm bg-primary/10" />
<div className="size-8 shrink-0 rounded-sm bg-primary/10" />
</div>
</SortableOverlay>
</Sortable>
);
}
interface DataTableFilterItemProps<TData> {
filter: ExtendedColumnFilter<TData>;
index: number;
filterItemId: string;
joinOperator: JoinOperator;
setJoinOperator: (value: JoinOperator) => void;
columns: Column<TData>[];
onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
onFilterRemove: (filterId: string) => void;
}

function DataTableFilterItem<TData>({
filter,
index,
filterItemId,
joinOperator,
setJoinOperator,
columns,
onFilterUpdate,
onFilterRemove,
}: DataTableFilterItemProps<TData>) {
const [showFieldSelector, setShowFieldSelector] = React.useState(false);
const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);
const [showValueSelector, setShowValueSelector] = React.useState(false);

const column = columns.find((column) => column.id === filter.id);


if (!column) return null;

const joinOperatorListboxId = `${filterItemId}-join-operator-listbox`;


const fieldListboxId = `${filterItemId}-field-listbox`;
const operatorListboxId = `${filterItemId}-operator-listbox`;
const inputId = `${filterItemId}-input`;

const columnMeta = column.columnDef.meta;


const filterOperators = getFilterOperators(filter.variant);

const onItemKeyDown = React.useCallback(


(event: React.KeyboardEvent<HTMLDivElement>) => {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}

if (showFieldSelector || showOperatorSelector || showValueSelector) {


return;
}

if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {
event.preventDefault();
onFilterRemove(filter.filterId);
}
},
[
filter.filterId,
showFieldSelector,
showOperatorSelector,
showValueSelector,
onFilterRemove,
],
);

return (
<SortableItem value={filter.filterId} asChild>
<div
role="listitem"
id={filterItemId}
tabIndex={-1}
className="flex items-center gap-2"
onKeyDown={onItemKeyDown}
>
<div className="min-w-[72px] text-center">
{index === 0 ? (
<span className="text-muted-foreground text-sm">Where</span>
) : index === 1 ? (
<Select
value={joinOperator}
onValueChange={(value: JoinOperator) => setJoinOperator(value)}
>
<SelectTrigger
aria-label="Select join operator"
aria-controls={joinOperatorListboxId}
className="h-8 rounded lowercase [&[data-size]]:h-8"
>
<SelectValue placeholder={joinOperator} />
</SelectTrigger>
<SelectContent
id={joinOperatorListboxId}
position="popper"
className="min-w-(--radix-select-trigger-width) lowercase"
>
{dataTableConfig.joinOperators.map((joinOperator) => (
<SelectItem key={joinOperator} value={joinOperator}>
{joinOperator}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span className="text-muted-foreground text-sm">
{joinOperator}
</span>
)}
</div>
<Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>
<PopoverTrigger asChild>
<Button
role="combobox"
aria-controls={fieldListboxId}
variant="outline"
size="sm"
className="w-32 justify-between rounded font-normal"
>
<span className="truncate">
{columns.find((column) => column.id === filter.id)?.columnDef
.meta?.label ?? "Select field"}
</span>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
id={fieldListboxId}
align="start"
className="w-40 origin-[var(--radix-popover-content-transform-origin)]
p-0"
>
<Command>
<CommandInput placeholder="Search fields..." />
<CommandList>
<CommandEmpty>No fields found.</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.id}
value={column.id}
onSelect={(value) => {
onFilterUpdate(filter.filterId, {
id: value as Extract<keyof TData, string>,
variant: column.columnDef.meta?.variant ?? "text",
operator: getDefaultFilterOperator(
column.columnDef.meta?.variant ?? "text",
),
value: "",
});

setShowFieldSelector(false);
}}
>
<span className="truncate">
{column.columnDef.meta?.label}
</span>
<Check
className={cn(
"ml-auto",
column.id === filter.id ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Select
open={showOperatorSelector}
onOpenChange={setShowOperatorSelector}
value={filter.operator}
onValueChange={(value: FilterOperator) =>
onFilterUpdate(filter.filterId, {
operator: value,
value:
value === "isEmpty" || value === "isNotEmpty"
? ""
: filter.value,
})
}
>
<SelectTrigger
aria-controls={operatorListboxId}
className="h-8 w-32 rounded lowercase [&[data-size]]:h-8"
>
<div className="truncate">
<SelectValue placeholder={filter.operator} />
</div>
</SelectTrigger>
<SelectContent
id={operatorListboxId}
className="origin-[var(--radix-select-content-transform-origin)]"
>
{filterOperators.map((operator) => (
<SelectItem
key={operator.value}
value={operator.value}
className="lowercase"
>
{operator.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="min-w-36 flex-1">
{onFilterInputRender({
filter,
inputId,
column,
columnMeta,
onFilterUpdate,
showValueSelector,
setShowValueSelector,
})}
</div>
<Button
aria-controls={filterItemId}
variant="outline"
size="icon"
className="size-8 rounded"
onClick={() => onFilterRemove(filter.filterId)}
>
<Trash2 />
</Button>
<SortableItemHandle asChild>
<Button variant="outline" size="icon" className="size-8 rounded">
<GripVertical />
</Button>
</SortableItemHandle>
</div>
</SortableItem>
);
}

function onFilterInputRender<TData>({
filter,
inputId,
column,
columnMeta,
onFilterUpdate,
showValueSelector,
setShowValueSelector,
}: {
filter: ExtendedColumnFilter<TData>;
inputId: string;
column: Column<TData>;
columnMeta?: ColumnMeta<TData, unknown>;
onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
showValueSelector: boolean;
setShowValueSelector: (value: boolean) => void;
}) {
if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty") {
return (
<div
id={inputId}
role="status"
aria-label={`${columnMeta?.label} filter is ${
filter.operator === "isEmpty" ? "empty" : "not empty"
}`}
aria-live="polite"
className="h-8 w-full rounded border bg-transparent dark:bg-input/30"
/>
);
}

switch (filter.variant) {
case "text":
case "number":
case "range": {
if (
(filter.variant === "range" && filter.operator === "isBetween") ||
filter.operator === "isBetween"
) {
return (
<DataTableRangeFilter
filter={filter}
column={column}
inputId={inputId}
onFilterUpdate={onFilterUpdate}
/>
);
}

const isNumber =
filter.variant === "number" || filter.variant === "range";

return (
<Input
id={inputId}
type={isNumber ? "number" : filter.variant}
aria-label={`${columnMeta?.label} filter value`}
aria-describedby={`${inputId}-description`}
inputMode={isNumber ? "numeric" : undefined}
placeholder={columnMeta?.placeholder ?? "Enter a value..."}
className="h-8 w-full rounded"
defaultValue={
typeof filter.value === "string" ? filter.value : undefined
}
onChange={(event) =>
onFilterUpdate(filter.filterId, {
value: event.target.value,
})
}
/>
);
}

case "boolean": {
if (Array.isArray(filter.value)) return null;

const inputListboxId = `${inputId}-listbox`;

return (
<Select
open={showValueSelector}
onOpenChange={setShowValueSelector}
value={filter.value}
onValueChange={(value) =>
onFilterUpdate(filter.filterId, {
value,
})
}
>
<SelectTrigger
id={inputId}
aria-controls={inputListboxId}
aria-label={`${columnMeta?.label} boolean filter`}
className="h-8 w-full rounded [&[data-size]]:h-8"
>
<SelectValue placeholder={filter.value ? "True" : "False"} />
</SelectTrigger>
<SelectContent id={inputListboxId}>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
);
}

case "select":
case "multiSelect": {
const inputListboxId = `${inputId}-listbox`;

const multiple = filter.variant === "multiSelect";


const selectedValues = multiple
? Array.isArray(filter.value)
? filter.value
: []
: typeof filter.value === "string"
? filter.value
: undefined;

return (
<Faceted
open={showValueSelector}
onOpenChange={setShowValueSelector}
value={selectedValues}
onValueChange={(value) => {
onFilterUpdate(filter.filterId, {
value,
});
}}
multiple={multiple}
>
<FacetedTrigger asChild>
<Button
id={inputId}
aria-controls={inputListboxId}
aria-label={`${columnMeta?.label} filter value${multiple ? "s" :
""}`}
variant="outline"
size="sm"
className="w-full rounded font-normal"
>
<FacetedBadgeList
options={columnMeta?.options}
placeholder={
columnMeta?.placeholder ??
`Select option${multiple ? "s" : ""}...`
}
/>
</Button>
</FacetedTrigger>
<FacetedContent
id={inputListboxId}
className="w-[200px] origin-[var(--radix-popover-content-transform-
origin)]"
>
<FacetedInput
aria-label={`Search ${columnMeta?.label} options`}
placeholder={columnMeta?.placeholder ?? "Search options..."}
/>
<FacetedList>
<FacetedEmpty>No options found.</FacetedEmpty>
<FacetedGroup>
{columnMeta?.options?.map((option) => (
<FacetedItem key={option.value} value={option.value}>
{option.icon && <option.icon />}
<span>{option.label}</span>
{option.count && (
<span className="ml-auto font-mono text-xs">
{option.count}
</span>
)}
</FacetedItem>
))}
</FacetedGroup>
</FacetedList>
</FacetedContent>
</Faceted>
);
}

case "date":
case "dateRange": {
const inputListboxId = `${inputId}-listbox`;

const dateValue = Array.isArray(filter.value)


? filter.value.filter(Boolean)
: [filter.value, filter.value].filter(Boolean);

const displayValue =
filter.operator === "isBetween" && dateValue.length === 2
? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(
new Date(Number(dateValue[1])),
)}`
: dateValue[0]
? formatDate(new Date(Number(dateValue[0])))
: "Pick a date";

return (
<Popover open={showValueSelector} onOpenChange={setShowValueSelector}>
<PopoverTrigger asChild>
<Button
id={inputId}
aria-controls={inputListboxId}
aria-label={`${columnMeta?.label} date filter`}
variant="outline"
size="sm"
className={cn(
"w-full justify-start rounded text-left font-normal",
!filter.value && "text-muted-foreground",
)}
>
<CalendarIcon />
<span className="truncate">{displayValue}</span>
</Button>
</PopoverTrigger>
<PopoverContent
id={inputListboxId}
align="start"
className="w-auto origin-[var(--radix-popover-content-transform-
origin)] p-0"
>
{filter.operator === "isBetween" ? (
<Calendar
aria-label={`Select ${columnMeta?.label} date range`}
mode="range"
initialFocus
selected={
dateValue.length === 2
? {
from: new Date(Number(dateValue[0])),
to: new Date(Number(dateValue[1])),
}
: {
from: new Date(),
to: new Date(),
}
}
onSelect={(date) => {
onFilterUpdate(filter.filterId, {
value: date
? [
(date.from?.getTime() ?? "").toString(),
(date.to?.getTime() ?? "").toString(),
]
: [],
});
}}
/>
) : (
<Calendar
aria-label={`Select ${columnMeta?.label} date`}
mode="single"
initialFocus
selected={
dateValue[0] ? new Date(Number(dateValue[0])) : undefined
}
onSelect={(date) => {
onFilterUpdate(filter.filterId, {
value: (date?.getTime() ?? "").toString(),
});
}}
/>
)}
</PopoverContent>
</Popover>
);
}

default:
return null;
}
}

"use client";

import type { Column, Table } from "@tanstack/react-table";


import {
BadgeCheck,
CalendarIcon,
Check,
ListFilter,
Text,
X,
} from "lucide-react";
import { useQueryState } from "nuqs";
import * as React from "react";

import { DataTableRangeFilter } from "@/components/data-table-range-filter";


import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { getDefaultFilterOperator, getFilterOperators } from "@/lib/data-table";
import { formatDate } from "@/lib/format";
import { generateId } from "@/lib/id";
import { getFiltersStateParser } from "@/lib/parsers";
import { cn } from "@/lib/utils";
import type { ExtendedColumnFilter, FilterOperator } from "@/types/data-table";

const FILTERS_KEY = "filters";


const DEBOUNCE_MS = 300;
const THROTTLE_MS = 50;
const OPEN_MENU_SHORTCUT = "f";
const REMOVE_FILTER_SHORTCUTS = ["backspace", "delete"];

interface DataTableFilterMenuProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
debounceMs?: number;
throttleMs?: number;
shallow?: boolean;
}

export function DataTableFilterMenu<TData>({


table,
debounceMs = DEBOUNCE_MS,
throttleMs = THROTTLE_MS,
shallow = true,
align = "start",
...props
}: DataTableFilterMenuProps<TData>) {
const id = React.useId();

const columns = React.useMemo(() => {


return table
.getAllColumns()
.filter((column) => column.columnDef.enableColumnFilter);
}, [table]);

const [open, setOpen] = React.useState(false);


const [selectedColumn, setSelectedColumn] =
React.useState<Column<TData> | null>(null);
const [inputValue, setInputValue] = React.useState("");
const triggerRef = React.useRef<HTMLButtonElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);

const onOpenChange = React.useCallback((open: boolean) => {


setOpen(open);

if (!open) {
setTimeout(() => {
setSelectedColumn(null);
setInputValue("");
}, 100);
}
}, []);

const onInputKeyDown = React.useCallback(


(event: React.KeyboardEvent<HTMLInputElement>) => {
if (
REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&
!inputValue &&
selectedColumn
) {
event.preventDefault();
setSelectedColumn(null);
}
},
[inputValue, selectedColumn],
);

const [filters, setFilters] = useQueryState(


FILTERS_KEY,
getFiltersStateParser<TData>(columns.map((field) => field.id))
.withDefault([])
.withOptions({
clearOnDefault: true,
shallow,
throttleMs,
}),
);
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);

const onFilterAdd = React.useCallback(


(column: Column<TData>, value: string) => {
if (!value.trim() && column.columnDef.meta?.variant !== "boolean") {
return;
}

const filterValue =
column.columnDef.meta?.variant === "multiSelect" ? [value] : value;

const newFilter: ExtendedColumnFilter<TData> = {


id: column.id as Extract<keyof TData, string>,
value: filterValue,
variant: column.columnDef.meta?.variant ?? "text",
operator: getDefaultFilterOperator(
column.columnDef.meta?.variant ?? "text",
),
filterId: generateId({ length: 8 }),
};
debouncedSetFilters([...filters, newFilter]);
setOpen(false);

setTimeout(() => {
setSelectedColumn(null);
setInputValue("");
}, 100);
},
[filters, debouncedSetFilters],
);

const onFilterRemove = React.useCallback(


(filterId: string) => {
const updatedFilters = filters.filter(
(filter) => filter.filterId !== filterId,
);
debouncedSetFilters(updatedFilters);
requestAnimationFrame(() => {
triggerRef.current?.focus();
});
},
[filters, debouncedSetFilters],
);

const onFilterUpdate = React.useCallback(


(
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => {
debouncedSetFilters((prevFilters) => {
const updatedFilters = prevFilters.map((filter) => {
if (filter.filterId === filterId) {
return { ...filter, ...updates } as ExtendedColumnFilter<TData>;
}
return filter;
});
return updatedFilters;
});
},
[debouncedSetFilters],
);

const onFiltersReset = React.useCallback(() => {


debouncedSetFilters([]);
}, [debouncedSetFilters]);

React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) {
event.preventDefault();
setOpen(true);
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
event.shiftKey &&
!open &&
filters.length > 0
) {
event.preventDefault();
onFilterRemove(filters[filters.length - 1]?.filterId ?? "");
}
}

window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [open, filters, onFilterRemove]);

const onTriggerKeyDown = React.useCallback(


(event: React.KeyboardEvent<HTMLButtonElement>) => {
if (
REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&
filters.length > 0
) {
event.preventDefault();
onFilterRemove(filters[filters.length - 1]?.filterId ?? "");
}
},
[filters, onFilterRemove],
);

return (
<div className="flex flex-wrap items-center gap-2">
{filters.map((filter) => (
<DataTableFilterItem
key={filter.filterId}
filter={filter}
filterItemId={`${id}-filter-${filter.filterId}`}
columns={columns}
onFilterUpdate={onFilterUpdate}
onFilterRemove={onFilterRemove}
/>
))}
{filters.length > 0 && (
<Button
aria-label="Reset all filters"
variant="outline"
size="icon"
className="size-8"
onClick={onFiltersReset}
>
<X />
</Button>
)}
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<Button
aria-label="Open filter command menu"
variant="outline"
size={filters.length > 0 ? "icon" : "sm"}
className={cn(filters.length > 0 && "size-8", "h-8")}
ref={triggerRef}
onKeyDown={onTriggerKeyDown}
>
<ListFilter />
{filters.length > 0 ? null : "Filter"}
</Button>
</PopoverTrigger>
<PopoverContent
align={align}
className="w-full max-w-[var(--radix-popover-content-available-width)]
origin-[var(--radix-popover-content-transform-origin)] p-0"
{...props}
>
<Command loop className="[&_[cmdk-input-wrapper]_svg]:hidden">
<CommandInput
ref={inputRef}
placeholder={
selectedColumn
? (selectedColumn.columnDef.meta?.label ?? selectedColumn.id)
: "Search fields..."
}
value={inputValue}
onValueChange={setInputValue}
onKeyDown={onInputKeyDown}
/>
<CommandList>
{selectedColumn ? (
<>
{selectedColumn.columnDef.meta?.options && (
<CommandEmpty>No options found.</CommandEmpty>
)}
<FilterValueSelector
column={selectedColumn}
value={inputValue}
onSelect={(value) => onFilterAdd(selectedColumn, value)}
/>
</>
) : (
<>
<CommandEmpty>No fields found.</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.id}
value={column.id}
onSelect={() => {
setSelectedColumn(column);
setInputValue("");
requestAnimationFrame(() => {
inputRef.current?.focus();
});
}}
>
{column.columnDef.meta?.icon && (
<column.columnDef.meta.icon />
)}
<span className="truncate">
{column.columnDef.meta?.label ?? column.id}
</span>
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
}

interface DataTableFilterItemProps<TData> {
filter: ExtendedColumnFilter<TData>;
filterItemId: string;
columns: Column<TData>[];
onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
onFilterRemove: (filterId: string) => void;
}

function DataTableFilterItem<TData>({
filter,
filterItemId,
columns,
onFilterUpdate,
onFilterRemove,
}: DataTableFilterItemProps<TData>) {
{
const [showFieldSelector, setShowFieldSelector] = React.useState(false);
const [showOperatorSelector, setShowOperatorSelector] =
React.useState(false);
const [showValueSelector, setShowValueSelector] = React.useState(false);

const column = columns.find((column) => column.id === filter.id);


if (!column) return null;

const operatorListboxId = `${filterItemId}-operator-listbox`;


const inputId = `${filterItemId}-input`;

const columnMeta = column.columnDef.meta;


const filterOperators = getFilterOperators(filter.variant);

const onItemKeyDown = React.useCallback(


(event: React.KeyboardEvent<HTMLDivElement>) => {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}
if (showFieldSelector || showOperatorSelector || showValueSelector) {
return;
}

if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {
event.preventDefault();
onFilterRemove(filter.filterId);
}
},
[
filter.filterId,
showFieldSelector,
showOperatorSelector,
showValueSelector,
onFilterRemove,
],
);

return (
<div
key={filter.filterId}
role="listitem"
id={filterItemId}
className="flex h-8 items-center rounded-md bg-background"
onKeyDown={onItemKeyDown}
>
<Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="rounded-none rounded-l-md border border-r-0 font-normal
dark:bg-input/30"
>
{columnMeta?.icon && (
<columnMeta.icon className="text-muted-foreground" />
)}
{columnMeta?.label ?? column.id}
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-48 origin-[var(--radix-popover-content-transform-origin)]
p-0"
>
<Command loop>
<CommandInput placeholder="Search fields..." />
<CommandList>
<CommandEmpty>No fields found.</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.id}
value={column.id}
onSelect={() => {
onFilterUpdate(filter.filterId, {
id: column.id as Extract<keyof TData, string>,
variant: column.columnDef.meta?.variant ?? "text",
operator: getDefaultFilterOperator(
column.columnDef.meta?.variant ?? "text",
),
value: "",
});

setShowFieldSelector(false);
}}
>
{column.columnDef.meta?.icon && (
<column.columnDef.meta.icon />
)}
<span className="truncate">
{column.columnDef.meta?.label ?? column.id}
</span>
<Check
className={cn(
"ml-auto",
column.id === filter.id ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Select
open={showOperatorSelector}
onOpenChange={setShowOperatorSelector}
value={filter.operator}
onValueChange={(value: FilterOperator) =>
onFilterUpdate(filter.filterId, {
operator: value,
value:
value === "isEmpty" || value === "isNotEmpty"
? ""
: filter.value,
})
}
>
<SelectTrigger
aria-controls={operatorListboxId}
className="h-8 rounded-none border-r-0 px-2.5 lowercase [&[data-
size]]:h-8 [&_svg]:hidden"
>
<SelectValue placeholder={filter.operator} />
</SelectTrigger>
<SelectContent
id={operatorListboxId}
className="origin-[var(--radix-select-content-transform-origin)]"
>
{filterOperators.map((operator) => (
<SelectItem
key={operator.value}
className="lowercase"
value={operator.value}
>
{operator.label}
</SelectItem>
))}
</SelectContent>
</Select>
{onFilterInputRender({
filter,
column,
inputId,
onFilterUpdate,
showValueSelector,
setShowValueSelector,
})}
<Button
aria-controls={filterItemId}
variant="ghost"
size="sm"
className="h-full rounded-none rounded-r-md border border-l-0 px-1.5
font-normal dark:bg-input/30"
onClick={() => onFilterRemove(filter.filterId)}
>
<X className="size-3.5" />
</Button>
</div>
);
}
}

interface FilterValueSelectorProps<TData> {
column: Column<TData>;
value: string;
onSelect: (value: string) => void;
}

function FilterValueSelector<TData>({
column,
value,
onSelect,
}: FilterValueSelectorProps<TData>) {
const variant = column.columnDef.meta?.variant ?? "text";

switch (variant) {
case "boolean":
return (
<CommandGroup>
<CommandItem value="true" onSelect={() => onSelect("true")}>
True
</CommandItem>
<CommandItem value="false" onSelect={() => onSelect("false")}>
False
</CommandItem>
</CommandGroup>
);

case "select":
case "multiSelect":
return (
<CommandGroup>
{column.columnDef.meta?.options?.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => onSelect(option.value)}
>
{option.icon && <option.icon />}
<span className="truncate">{option.label}</span>
{option.count && (
<span className="ml-auto font-mono text-xs">
{option.count}
</span>
)}
</CommandItem>
))}
</CommandGroup>
);

case "date":
case "dateRange":
return (
<Calendar
initialFocus
mode="single"
selected={value ? new Date(value) : undefined}
onSelect={(date) => onSelect(date?.getTime().toString() ?? "")}
/>
);

default: {
const isEmpty = !value.trim();

return (
<CommandGroup>
<CommandItem
value={value}
onSelect={() => onSelect(value)}
disabled={isEmpty}
>
{isEmpty ? (
<>
<Text />
<span>Type to add filter...</span>
</>
) : (
<>
<BadgeCheck />
<span className="truncate">Filter by &quot;{value}&quot;</span>
</>
)}
</CommandItem>
</CommandGroup>
);
}
}
}

function onFilterInputRender<TData>({
filter,
column,
inputId,
onFilterUpdate,
showValueSelector,
setShowValueSelector,
}: {
filter: ExtendedColumnFilter<TData>;
column: Column<TData>;
inputId: string;
onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
showValueSelector: boolean;
setShowValueSelector: (value: boolean) => void;
}) {
if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty") {
return (
<div
id={inputId}
role="status"
aria-label={`${column.columnDef.meta?.label} filter is ${
filter.operator === "isEmpty" ? "empty" : "not empty"
}`}
aria-live="polite"
className="h-full w-16 rounded-none border bg-transparent px-1.5 py-0.5
text-muted-foreground dark:bg-input/30"
/>
);
}

switch (filter.variant) {
case "text":
case "number":
case "range": {
if (
(filter.variant === "range" && filter.operator === "isBetween") ||
filter.operator === "isBetween"
) {
return (
<DataTableRangeFilter
filter={filter}
column={column}
inputId={inputId}
onFilterUpdate={onFilterUpdate}
className="size-full max-w-28 gap-0 [&_[data-slot='range-min']]:border-
r-0 [&_input]:rounded-none [&_input]:px-1.5"
/>
);
}

const isNumber =
filter.variant === "number" || filter.variant === "range";

return (
<Input
id={inputId}
type={isNumber ? "number" : "text"}
inputMode={isNumber ? "numeric" : undefined}
placeholder={column.columnDef.meta?.placeholder ?? "Enter value..."}
className="h-full w-24 rounded-none px-1.5"
defaultValue={typeof filter.value === "string" ? filter.value : ""}
onChange={(event) =>
onFilterUpdate(filter.filterId, { value: event.target.value })
}
/>
);
}

case "boolean": {
const inputListboxId = `${inputId}-listbox`;

return (
<Select
open={showValueSelector}
onOpenChange={setShowValueSelector}
value={typeof filter.value === "string" ? filter.value : "true"}
onValueChange={(value: "true" | "false") =>
onFilterUpdate(filter.filterId, { value })
}
>
<SelectTrigger
id={inputId}
aria-controls={inputListboxId}
className="rounded-none bg-transparent px-1.5 py-0.5 [&_svg]:hidden"
>
<SelectValue placeholder={filter.value ? "True" : "False"} />
</SelectTrigger>
<SelectContent id={inputListboxId}>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
);
}

case "select":
case "multiSelect": {
const inputListboxId = `${inputId}-listbox`;

const options = column.columnDef.meta?.options ?? [];


const selectedValues = Array.isArray(filter.value)
? filter.value
: [filter.value];

const selectedOptions = options.filter((option) =>


selectedValues.includes(option.value),
);

return (
<Popover open={showValueSelector} onOpenChange={setShowValueSelector}>
<PopoverTrigger asChild>
<Button
id={inputId}
aria-controls={inputListboxId}
variant="ghost"
size="sm"
className="h-full min-w-16 rounded-none border px-1.5 font-normal
dark:bg-input/30"
>
{selectedOptions.length === 0 ? (
filter.variant === "multiSelect" ? (
"Select options..."
) : (
"Select option..."
)
) : (
<>
<div className="-space-x-2 flex items-center rtl:space-x-
reverse">
{selectedOptions.map((selectedOption) =>
selectedOption.icon ? (
<div
key={selectedOption.value}
className="rounded-full border bg-background p-0.5"
>
<selectedOption.icon className="size-3.5" />
</div>
) : null,
)}
</div>
<span className="truncate">
{selectedOptions.length > 1
? `${selectedOptions.length} selected`
: selectedOptions[0]?.label}
</span>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent
id={inputListboxId}
align="start"
className="w-48 origin-[var(--radix-popover-content-transform-origin)]
p-0"
>
<Command>
<CommandInput placeholder="Search options..." />
<CommandList>
<CommandEmpty>No options found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
const value =
filter.variant === "multiSelect"
? selectedValues.includes(option.value)
? selectedValues.filter((v) => v !== option.value)
: [...selectedValues, option.value]
: option.value;
onFilterUpdate(filter.filterId, { value });
}}
>
{option.icon && <option.icon />}
<span className="truncate">{option.label}</span>
{filter.variant === "multiSelect" && (
<Check
className={cn(
"ml-auto",
selectedValues.includes(option.value)
? "opacity-100"
: "opacity-0",
)}
/>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

case "date":
case "dateRange": {
const inputListboxId = `${inputId}-listbox`;

const dateValue = Array.isArray(filter.value)


? filter.value.filter(Boolean)
: [filter.value, filter.value].filter(Boolean);

const displayValue =
filter.operator === "isBetween" && dateValue.length === 2
? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(
new Date(Number(dateValue[1])),
)}`
: dateValue[0]
? formatDate(new Date(Number(dateValue[0])))
: "Pick date...";

return (
<Popover open={showValueSelector} onOpenChange={setShowValueSelector}>
<PopoverTrigger asChild>
<Button
id={inputId}
aria-controls={inputListboxId}
variant="ghost"
size="sm"
className={cn(
"h-full rounded-none border px-1.5 font-normal dark:bg-input/30",
!filter.value && "text-muted-foreground",
)}
>
<CalendarIcon className="size-3.5" />
<span className="truncate">{displayValue}</span>
</Button>
</PopoverTrigger>
<PopoverContent
id={inputListboxId}
align="start"
className="w-auto origin-[var(--radix-popover-content-transform-
origin)] p-0"
>
{filter.operator === "isBetween" ? (
<Calendar
mode="range"
initialFocus
selected={
dateValue.length === 2
? {
from: new Date(Number(dateValue[0])),
to: new Date(Number(dateValue[1])),
}
: {
from: new Date(),
to: new Date(),
}
}
onSelect={(date) => {
onFilterUpdate(filter.filterId, {
value: date
? [
(date.from?.getTime() ?? "").toString(),
(date.to?.getTime() ?? "").toString(),
]
: [],
});
}}
/>
) : (
<Calendar
mode="single"
initialFocus
selected={
dateValue[0] ? new Date(Number(dateValue[0])) : undefined
}
onSelect={(date) => {
onFilterUpdate(filter.filterId, {
value: (date?.getTime() ?? "").toString(),
});
}}
/>
)}
</PopoverContent>
</Popover>
);
}

default:
return null;
}
}

import type { Table } from "@tanstack/react-table";


import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";

import { Button } from "@/components/ui/button";


import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

interface DataTablePaginationProps<TData> extends React.ComponentProps<"div"> {


table: Table<TData>;
pageSizeOptions?: number[];
}

export function DataTablePagination<TData>({


table,
pageSizeOptions = [10, 20, 30, 40, 50],
className,
...props
}: DataTablePaginationProps<TData>) {
return (
<div
className={cn(
"flex w-full flex-col-reverse items-center justify-between gap-4 overflow-
auto p-1 sm:flex-row sm:gap-8",
className,
)}
{...props}
>
<div className="flex-1 whitespace-nowrap text-muted-foreground text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6
lg:gap-8">
<div className="flex items-center space-x-2">
<p className="whitespace-nowrap font-medium text-sm">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[4.5rem] [&[data-size]]:h-8">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{pageSizeOptions.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-center font-medium text-sm">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
aria-label="Go to first page"
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeft />
</Button>
<Button
aria-label="Go to previous page"
variant="outline"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeft />
</Button>
<Button
aria-label="Go to next page"
variant="outline"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRight />
</Button>
<Button
aria-label="Go to last page"
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<ChevronsRight />
</Button>
</div>
</div>
</div>
);
}

"use client";

import type { Column } from "@tanstack/react-table";


import * as React from "react";

import { Input } from "@/components/ui/input";


import { cn } from "@/lib/utils";
import type { ExtendedColumnFilter } from "@/types/data-table";

interface DataTableRangeFilterProps<TData> extends React.ComponentProps<"div"> {


filter: ExtendedColumnFilter<TData>;
column: Column<TData>;
inputId: string;
onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
}

export function DataTableRangeFilter<TData>({


filter,
column,
inputId,
onFilterUpdate,
className,
...props
}: DataTableRangeFilterProps<TData>) {
const meta = column.columnDef.meta;

const [min, max] = React.useMemo(() => {


const range = column.columnDef.meta?.range;
if (range) return range;

const values = column.getFacetedMinMaxValues();


if (!values) return [0, 100];

return [values[0], values[1]];


}, [column]);

const formatValue = React.useCallback(


(value: string | number | undefined) => {
if (value === undefined || value === "") return "";
const numValue = Number(value);
return Number.isNaN(numValue)
? ""
: numValue.toLocaleString(undefined, {
maximumFractionDigits: 0,
});
},
[],
);

const value = React.useMemo(() => {


if (Array.isArray(filter.value)) return filter.value.map(formatValue);
return [formatValue(filter.value), ""];
}, [filter.value, formatValue]);

const onRangeValueChange = React.useCallback(


(value: string, isMin?: boolean) => {
const numValue = Number(value);
const currentValues = Array.isArray(filter.value)
? filter.value
: ["", ""];
const otherValue = isMin
? (currentValues[1] ?? "")
: (currentValues[0] ?? "");

if (
value === "" ||
(!Number.isNaN(numValue) &&
(isMin
? numValue >= min && numValue <= (Number(otherValue) || max)
: numValue <= max && numValue >= (Number(otherValue) || min)))
) {
onFilterUpdate(filter.filterId, {
value: isMin ? [value, otherValue] : [otherValue, value],
});
}
},
[filter.filterId, filter.value, min, max, onFilterUpdate],
);

return (
<div
data-slot="range"
className={cn("flex w-full items-center gap-2", className)}
{...props}
>
<Input
id={`${inputId}-min`}
type="number"
aria-label={`${meta?.label} minimum value`}
aria-valuemin={min}
aria-valuemax={max}
data-slot="range-min"
inputMode="numeric"
placeholder={min.toString()}
min={min}
max={max}
className="h-8 w-full rounded"
defaultValue={value[0]}
onChange={(event) => onRangeValueChange(event.target.value, true)}
/>
<span className="sr-only shrink-0 text-muted-foreground">to</span>
<Input
id={`${inputId}-max`}
type="number"
aria-label={`${meta?.label} maximum value`}
aria-valuemin={min}
aria-valuemax={max}
data-slot="range-max"
inputMode="numeric"
placeholder={max.toString()}
min={min}
max={max}
className="h-8 w-full rounded"
defaultValue={value[1]}
onChange={(event) => onRangeValueChange(event.target.value)}
/>
</div>
);
}

import { Skeleton } from "@/components/ui/skeleton";


import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";

interface DataTableSkeletonProps extends React.ComponentProps<"div"> {


columnCount: number;
rowCount?: number;
filterCount?: number;
cellWidths?: string[];
withViewOptions?: boolean;
withPagination?: boolean;
shrinkZero?: boolean;
}

export function DataTableSkeleton({


columnCount,
rowCount = 10,
filterCount = 0,
cellWidths = ["auto"],
withViewOptions = true,
withPagination = true,
shrinkZero = false,
className,
...props
}: DataTableSkeletonProps) {
const cozyCellWidths = Array.from(
{ length: columnCount },
(_, index) => cellWidths[index % cellWidths.length] ?? "auto",
);

return (
<div
className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
{...props}
>
<div className="flex w-full items-center justify-between gap-2 overflow-auto
p-1">
<div className="flex flex-1 items-center gap-2">
{filterCount > 0
? Array.from({ length: filterCount }).map((_, i) => (
<Skeleton key={i} className="h-7 w-[4.5rem] border-dashed" />
))
: null}
</div>
{withViewOptions ? (
<Skeleton className="ml-auto hidden h-7 w-[4.5rem] lg:flex" />
) : null}
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: 1 }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent">
{Array.from({ length: columnCount }).map((_, j) => (
<TableHead
key={j}
style={{
width: cozyCellWidths[j],
minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
}}
>
<Skeleton className="h-6 w-full" />
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rowCount }).map((_, i) => (
<TableRow key={i} className="hover:bg-transparent">
{Array.from({ length: columnCount }).map((_, j) => (
<TableCell
key={j}
style={{
width: cozyCellWidths[j],
minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
}}
>
<Skeleton className="h-6 w-full" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
{withPagination ? (
<div className="flex w-full items-center justify-between gap-4 overflow-
auto p-1 sm:gap-8">
<Skeleton className="h-7 w-40 shrink-0" />
<div className="flex items-center gap-4 sm:gap-6 lg:gap-8">
<div className="flex items-center gap-2">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-7 w-[4.5rem]" />
</div>
<div className="flex items-center justify-center font-medium text-sm">
<Skeleton className="h-7 w-20" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="hidden size-7 lg:block" />
<Skeleton className="size-7" />
<Skeleton className="size-7" />
<Skeleton className="hidden size-7 lg:block" />
</div>
</div>
</div>
) : null}
</div>
);
}

"use client";

import type { Column } from "@tanstack/react-table";


import * as React from "react";

import { Button } from "@/components/ui/button";


import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/lib/utils";
import { PlusCircle, XCircle } from "lucide-react";

interface Range {
min: number;
max: number;
}

type RangeValue = [number, number];

function getIsValidRange(value: unknown): value is RangeValue {


return (
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === "number" &&
typeof value[1] === "number"
);
}

interface DataTableSliderFilterProps<TData> {
column: Column<TData, unknown>;
title?: string;
}

export function DataTableSliderFilter<TData>({


column,
title,
}: DataTableSliderFilterProps<TData>) {
const id = React.useId();

const columnFilterValue = getIsValidRange(column.getFilterValue())


? (column.getFilterValue() as RangeValue)
: undefined;

const defaultRange = column.columnDef.meta?.range;


const unit = column.columnDef.meta?.unit;

const { min, max, step } = React.useMemo<Range & { step: number }>(() => {
let minValue = 0;
let maxValue = 100;

if (defaultRange && getIsValidRange(defaultRange)) {


[minValue, maxValue] = defaultRange;
} else {
const values = column.getFacetedMinMaxValues();
if (values && Array.isArray(values) && values.length === 2) {
const [facetMinValue, facetMaxValue] = values;
if (
typeof facetMinValue === "number" &&
typeof facetMaxValue === "number"
) {
minValue = facetMinValue;
maxValue = facetMaxValue;
}
}
}

const rangeSize = maxValue - minValue;


const step =
rangeSize <= 20
? 1
: rangeSize <= 100
? Math.ceil(rangeSize / 20)
: Math.ceil(rangeSize / 50);

return { min: minValue, max: maxValue, step };


}, [column, defaultRange]);

const range = React.useMemo((): RangeValue => {


return columnFilterValue ?? [min, max];
}, [columnFilterValue, min, max]);

const formatValue = React.useCallback((value: number) => {


return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
}, []);

const onFromInputChange = React.useCallback(


(event: React.ChangeEvent<HTMLInputElement>) => {
const numValue = Number(event.target.value);
if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {
column.setFilterValue([numValue, range[1]]);
}
},
[column, min, range],
);

const onToInputChange = React.useCallback(


(event: React.ChangeEvent<HTMLInputElement>) => {
const numValue = Number(event.target.value);
if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {
column.setFilterValue([range[0], numValue]);
}
},
[column, max, range],
);

const onSliderValueChange = React.useCallback(


(value: RangeValue) => {
if (Array.isArray(value) && value.length === 2) {
column.setFilterValue(value);
}
},
[column],
);

const onReset = React.useCallback(


(event: React.MouseEvent) => {
if (event.target instanceof HTMLDivElement) {
event.stopPropagation();
}
column.setFilterValue(undefined);
},
[column],
);

return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="border-dashed">
{columnFilterValue ? (
<div
role="button"
aria-label={`Clear ${title} filter`}
tabIndex={0}
className="rounded-sm opacity-70 transition-opacity hover:opacity-100
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
onClick={onReset}
>
<XCircle />
</div>
) : (
<PlusCircle />
)}
<span>{title}</span>
{columnFilterValue ? (
<>
<Separator
orientation="vertical"
className="mx-0.5 data-[orientation=vertical]:h-4"
/>
{formatValue(columnFilterValue[0])} -{" "}
{formatValue(columnFilterValue[1])}
{unit ? ` ${unit}` : ""}
</>
) : null}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="flex w-auto flex-col gap-4">
<div className="flex flex-col gap-3">
<p className="font-medium leading-none peer-disabled:cursor-not-allowed
peer-disabled:opacity-70">
{title}
</p>
<div className="flex items-center gap-4">
<Label htmlFor={`${id}-from`} className="sr-only">
From
</Label>
<div className="relative">
<Input
id={`${id}-from`}
type="number"
aria-valuemin={min}
aria-valuemax={max}
inputMode="numeric"
pattern="[0-9]*"
placeholder={min.toString()}
min={min}
max={max}
value={range[0]?.toString()}
onChange={onFromInputChange}
className={cn("h-8 w-24", unit && "pr-8")}
/>
{unit && (
<span className="absolute top-0 right-0 bottom-0 flex items-center
rounded-r-md bg-accent px-2 text-muted-foreground text-sm">
{unit}
</span>
)}
</div>
<Label htmlFor={`${id}-to`} className="sr-only">
to
</Label>
<div className="relative">
<Input
id={`${id}-to`}
type="number"
aria-valuemin={min}
aria-valuemax={max}
inputMode="numeric"
pattern="[0-9]*"
placeholder={max.toString()}
min={min}
max={max}
value={range[1]?.toString()}
onChange={onToInputChange}
className={cn("h-8 w-24", unit && "pr-8")}
/>
{unit && (
<span className="absolute top-0 right-0 bottom-0 flex items-center
rounded-r-md bg-accent px-2 text-muted-foreground text-sm">
{unit}
</span>
)}
</div>
</div>
<Label htmlFor={`${id}-slider`} className="sr-only">
{title} slider
</Label>
<Slider
id={`${id}-slider`}
min={min}
max={max}
step={step}
value={range}
onValueChange={onSliderValueChange}
/>
</div>
<Button
aria-label={`Clear ${title} filter`}
variant="outline"
size="sm"
onClick={onReset}
>
Clear
</Button>
</PopoverContent>
</Popover>
);
}

"use client";

import type { ColumnSort, SortDirection, Table } from "@tanstack/react-table";


import {
ArrowDownUp,
ChevronsUpDown,
GripVertical,
Trash2,
} from "lucide-react";
import * as React from "react";

import { Badge } from "@/components/ui/badge";


import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sortable,
SortableContent,
SortableItem,
SortableItemHandle,
SortableOverlay,
} from "@/components/ui/sortable";
import { dataTableConfig } from "@/config/data-table";
import { cn } from "@/lib/utils";

const OPEN_MENU_SHORTCUT = "s";


const REMOVE_SORT_SHORTCUTS = ["backspace", "delete"];

interface DataTableSortListProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
}

export function DataTableSortList<TData>({


table,
...props
}: DataTableSortListProps<TData>) {
const id = React.useId();
const labelId = React.useId();
const descriptionId = React.useId();
const [open, setOpen] = React.useState(false);
const addButtonRef = React.useRef<HTMLButtonElement>(null);

const sorting = table.getState().sorting;


const onSortingChange = table.setSorting;

const { columnLabels, columns } = React.useMemo(() => {


const labels = new Map<string, string>();
const sortingIds = new Set(sorting.map((s) => s.id));
const availableColumns: { id: string; label: string }[] = [];

for (const column of table.getAllColumns()) {


if (!column.getCanSort()) continue;

const label = column.columnDef.meta?.label ?? column.id;


labels.set(column.id, label);

if (!sortingIds.has(column.id)) {
availableColumns.push({ id: column.id, label });
}
}

return {
columnLabels: labels,
columns: availableColumns,
};
}, [sorting, table]);

const onSortAdd = React.useCallback(() => {


const firstColumn = columns[0];
if (!firstColumn) return;

onSortingChange((prevSorting) => [
...prevSorting,
{ id: firstColumn.id, desc: false },
]);
}, [columns, onSortingChange]);

const onSortUpdate = React.useCallback(


(sortId: string, updates: Partial<ColumnSort>) => {
onSortingChange((prevSorting) => {
if (!prevSorting) return prevSorting;
return prevSorting.map((sort) =>
sort.id === sortId ? { ...sort, ...updates } : sort,
);
});
},
[onSortingChange],
);

const onSortRemove = React.useCallback(


(sortId: string) => {
onSortingChange((prevSorting) =>
prevSorting.filter((item) => item.id !== sortId),
);
},
[onSortingChange],
);
const onSortingReset = React.useCallback(
() => onSortingChange(table.initialState.sorting),
[onSortingChange, table.initialState.sorting],
);

React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) {
event.preventDefault();
setOpen(true);
}

if (
event.key.toLowerCase() === OPEN_MENU_SHORTCUT &&
event.shiftKey &&
sorting.length > 0
) {
event.preventDefault();
onSortingReset();
}
}

window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [sorting.length, onSortingReset]);

const onTriggerKeyDown = React.useCallback(


(event: React.KeyboardEvent<HTMLButtonElement>) => {
if (
REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&
sorting.length > 0
) {
event.preventDefault();
onSortingReset();
}
},
[sorting.length, onSortingReset],
);

return (
<Sortable
value={sorting}
onValueChange={onSortingChange}
getItemValue={(item) => item.id}
>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" onKeyDown={onTriggerKeyDown}>
<ArrowDownUp />
Sort
{sorting.length > 0 && (
<Badge
variant="secondary"
className="h-[18.24px] rounded-[3.2px] px-[5.12px] font-mono font-
normal text-[10.4px]"
>
{sorting.length}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent
aria-labelledby={labelId}
aria-describedby={descriptionId}
className="flex w-full max-w-[var(--radix-popover-content-available-
width)] origin-[var(--radix-popover-content-transform-origin)] flex-col gap-3.5 p-4
sm:min-w-[380px]"
{...props}
>
<div className="flex flex-col gap-1">
<h4 id={labelId} className="font-medium leading-none">
{sorting.length > 0 ? "Sort by" : "No sorting applied"}
</h4>
<p
id={descriptionId}
className={cn(
"text-muted-foreground text-sm",
sorting.length > 0 && "sr-only",
)}
>
{sorting.length > 0
? "Modify sorting to organize your rows."
: "Add sorting to organize your rows."}
</p>
</div>
{sorting.length > 0 && (
<SortableContent asChild>
<div
role="list"
className="flex max-h-[300px] flex-col gap-2 overflow-y-auto p-1"
>
{sorting.map((sort) => (
<DataTableSortItem
key={sort.id}
sort={sort}
sortItemId={`${id}-sort-${sort.id}`}
columns={columns}
columnLabels={columnLabels}
onSortUpdate={onSortUpdate}
onSortRemove={onSortRemove}
/>
))}
</div>
</SortableContent>
)}
<div className="flex w-full items-center gap-2">
<Button
size="sm"
className="rounded"
ref={addButtonRef}
onClick={onSortAdd}
disabled={columns.length === 0}
>
Add sort
</Button>
{sorting.length > 0 && (
<Button
variant="outline"
size="sm"
className="rounded"
onClick={onSortingReset}
>
Reset sorting
</Button>
)}
</div>
</PopoverContent>
</Popover>
<SortableOverlay>
<div className="flex items-center gap-2">
<div className="h-8 w-[180px] rounded-sm bg-primary/10" />
<div className="h-8 w-24 rounded-sm bg-primary/10" />
<div className="size-8 shrink-0 rounded-sm bg-primary/10" />
<div className="size-8 shrink-0 rounded-sm bg-primary/10" />
</div>
</SortableOverlay>
</Sortable>
);
}

interface DataTableSortItemProps {
sort: ColumnSort;
sortItemId: string;
columns: { id: string; label: string }[];
columnLabels: Map<string, string>;
onSortUpdate: (sortId: string, updates: Partial<ColumnSort>) => void;
onSortRemove: (sortId: string) => void;
}

function DataTableSortItem({
sort,
sortItemId,
columns,
columnLabels,
onSortUpdate,
onSortRemove,
}: DataTableSortItemProps) {
const fieldListboxId = `${sortItemId}-field-listbox`;
const fieldTriggerId = `${sortItemId}-field-trigger`;
const directionListboxId = `${sortItemId}-direction-listbox`;

const [showFieldSelector, setShowFieldSelector] = React.useState(false);


const [showDirectionSelector, setShowDirectionSelector] =
React.useState(false);
const onItemKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}

if (showFieldSelector || showDirectionSelector) {
return;
}

if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {
event.preventDefault();
onSortRemove(sort.id);
}
},
[sort.id, showFieldSelector, showDirectionSelector, onSortRemove],
);

return (
<SortableItem value={sort.id} asChild>
<div
role="listitem"
id={sortItemId}
tabIndex={-1}
className="flex items-center gap-2"
onKeyDown={onItemKeyDown}
>
<Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>
<PopoverTrigger asChild>
<Button
id={fieldTriggerId}
role="combobox"
aria-controls={fieldListboxId}
variant="outline"
size="sm"
className="w-44 justify-between rounded font-normal"
>
<span className="truncate">{columnLabels.get(sort.id)}</span>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
id={fieldListboxId}
className="w-[var(--radix-popover-trigger-width)] origin-[var(--radix-
popover-content-transform-origin)] p-0"
>
<Command>
<CommandInput placeholder="Search fields..." />
<CommandList>
<CommandEmpty>No fields found.</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.id}
value={column.id}
onSelect={(value) => onSortUpdate(sort.id, { id: value })}
>
<span className="truncate">{column.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Select
open={showDirectionSelector}
onOpenChange={setShowDirectionSelector}
value={sort.desc ? "desc" : "asc"}
onValueChange={(value: SortDirection) =>
onSortUpdate(sort.id, { desc: value === "desc" })
}
>
<SelectTrigger
aria-controls={directionListboxId}
className="h-8 w-24 rounded [&[data-size]]:h-8"
>
<SelectValue />
</SelectTrigger>
<SelectContent
id={directionListboxId}
className="min-w-[var(--radix-select-trigger-width)] origin-[var(--
radix-select-content-transform-origin)]"
>
{dataTableConfig.sortOrders.map((order) => (
<SelectItem key={order.value} value={order.value}>
{order.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
aria-controls={sortItemId}
variant="outline"
size="icon"
className="size-8 shrink-0 rounded"
onClick={() => onSortRemove(sort.id)}
>
<Trash2 />
</Button>
<SortableItemHandle asChild>
<Button
variant="outline"
size="icon"
className="size-8 shrink-0 rounded"
>
<GripVertical />
</Button>
</SortableItemHandle>
</div>
</SortableItem>
);
}
"use client";

import type { Column, Table } from "@tanstack/react-table";


import { X } from "lucide-react";
import * as React from "react";

import { DataTableDateFilter } from "@/components/data-table-date-filter";


import { DataTableFacetedFilter } from "@/components/data-table-faceted-filter";
import { DataTableSliderFilter } from "@/components/data-table-slider-filter";
import { DataTableViewOptions } from "@/components/data-table-view-options";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";

interface DataTableToolbarProps<TData> extends React.ComponentProps<"div"> {


table: Table<TData>;
}

export function DataTableToolbar<TData>({


table,
children,
className,
...props
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;

const columns = React.useMemo(


() => table.getAllColumns().filter((column) => column.getCanFilter()),
[table],
);

const onReset = React.useCallback(() => {


table.resetColumnFilters();
}, [table]);

return (
<div
role="toolbar"
aria-orientation="horizontal"
className={cn(
"flex w-full items-start justify-between gap-2 p-1",
className,
)}
{...props}
>
<div className="flex flex-1 flex-wrap items-center gap-2">
{columns.map((column) => (
<DataTableToolbarFilter key={column.id} column={column} />
))}
{isFiltered && (
<Button
aria-label="Reset filters"
variant="outline"
size="sm"
className="border-dashed"
onClick={onReset}
>
<X />
Reset
</Button>
)}
</div>
<div className="flex items-center gap-2">
{children}
<DataTableViewOptions table={table} />
</div>
</div>
);
}
interface DataTableToolbarFilterProps<TData> {
column: Column<TData>;
}

function DataTableToolbarFilter<TData>({
column,
}: DataTableToolbarFilterProps<TData>) {
{
const columnMeta = column.columnDef.meta;

const onFilterRender = React.useCallback(() => {


if (!columnMeta?.variant) return null;

switch (columnMeta.variant) {
case "text":
return (
<Input
placeholder={columnMeta.placeholder ?? columnMeta.label}
value={(column.getFilterValue() as string) ?? ""}
onChange={(event) => column.setFilterValue(event.target.value)}
className="h-8 w-40 lg:w-56"
/>
);

case "number":
return (
<div className="relative">
<Input
type="number"
inputMode="numeric"
placeholder={columnMeta.placeholder ?? columnMeta.label}
value={(column.getFilterValue() as string) ?? ""}
onChange={(event) => column.setFilterValue(event.target.value)}
className={cn("h-8 w-[120px]", columnMeta.unit && "pr-8")}
/>
{columnMeta.unit && (
<span className="absolute top-0 right-0 bottom-0 flex items-center
rounded-r-md bg-accent px-2 text-muted-foreground text-sm">
{columnMeta.unit}
</span>
)}
</div>
);

case "range":
return (
<DataTableSliderFilter
column={column}
title={columnMeta.label ?? column.id}
/>
);

case "date":
case "dateRange":
return (
<DataTableDateFilter
column={column}
title={columnMeta.label ?? column.id}
multiple={columnMeta.variant === "dateRange"}
/>
);

case "select":
case "multiSelect":
return (
<DataTableFacetedFilter
column={column}
title={columnMeta.label ?? column.id}
options={columnMeta.options ?? []}
multiple={columnMeta.variant === "multiSelect"}
/>
);

default:
return null;
}
}, [column, columnMeta]);

return onFilterRender();
}
}

"use client";

import type { Table } from "@tanstack/react-table";


import { Check, ChevronsUpDown, Settings2 } from "lucide-react";

import { Button } from "@/components/ui/button";


import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import * as React from "react";

interface DataTableViewOptionsProps<TData> {
table: Table<TData>;
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
const columns = React.useMemo(
() =>
table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide(),
),
[table],
);

return (
<Popover>
<PopoverTrigger asChild>
<Button
aria-label="Toggle columns"
role="combobox"
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
>
<Settings2 />
View
<ChevronsUpDown className="ml-auto opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-44 p-0">
<Command>
<CommandInput placeholder="Search columns..." />
<CommandList>
<CommandEmpty>No columns found.</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.id}
onSelect={() =>
column.toggleVisibility(!column.getIsVisible())
}
>
<span className="truncate">
{column.columnDef.meta?.label ?? column.id}
</span>
<Check
className={cn(
"ml-auto size-4 shrink-0",
column.getIsVisible() ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

import { type Table as TanstackTable, flexRender } from "@tanstack/react-table";


import type * as React from "react";

import { DataTablePagination } from "@/components/data-table-pagination";


import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getCommonPinningStyles } from "@/lib/data-table";
import { cn } from "@/lib/utils";

interface DataTableProps<TData> extends React.ComponentProps<"div"> {


table: TanstackTable<TData>;
actionBar?: React.ReactNode;
}

export function DataTable<TData>({


table,
actionBar,
children,
className,
...props
}: DataTableProps<TData>) {
return (
<div
className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
{...props}
>
{children}
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{
...getCommonPinningStyles({ column: header.column }),
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
...getCommonPinningStyles({ column: cell.column }),
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col gap-2.5">
<DataTablePagination table={table} />
{actionBar &&
table.getFilteredSelectedRowModel().rows.length > 0 &&
actionBar}
</div>
</div>
);
}

"use client";

/**
* @see https://github.com/dubinc/dub/blob/main/packages/ui/src/animated-size-
container.tsx
*/

import * as React from "react";

import { cn } from "@/lib/utils";


import { type TargetAndTransition, motion } from "motion/react";

interface Dimensions extends TargetAndTransition {


width: string | number;
height: string | number;
}

interface DynamicContainerProps
extends React.ComponentProps<typeof motion.div> {
width?: boolean;
height?: boolean;
children?: React.ReactNode;
}

function DynamicContainer({
width,
height,
transition = {
type: "spring",
duration: 0.3,
stiffness: 100,
damping: 15,
},
className,
children,
...props
}: DynamicContainerProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = React.useState<Dimensions>({
width: "auto",
height: "auto",
});
const rafRef = React.useRef<number | null>(null);

React.useEffect(() => {
const node = containerRef?.current;
if (!node) return;

function updateDimensions([entry]: ResizeObserverEntry[]) {


if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}

rafRef.current = requestAnimationFrame(() => {


setDimensions({
width: width ? (entry?.contentRect?.width ?? "auto") : "auto",
height: height ? (entry?.contentRect?.height ?? "auto") : "auto",
});
});
}

const observer = new ResizeObserver(updateDimensions);


observer.observe(node);

return () => {
observer.disconnect();
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [width, height]);

const containerStyle = React.useMemo(


() => ({
height: height ? "max-content" : "auto",
width: width ? "max-content" : "auto",
}),
[height, width],
);

return (
<motion.div
animate={dimensions}
transition={transition}
className={cn("translate-z-0 transform overflow-hidden", className)}
{...props}
>
<div ref={containerRef} style={containerStyle}>
{children}
</div>
</motion.div>
);
}

export { DynamicContainer };

export type IconProps = React.HTMLAttributes<SVGElement>;

export const Icons = {


gitHub: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-
70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-
60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745
41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283
11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-
15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846
1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-
4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-
4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-
3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289
1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851
5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184
4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-
2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-
29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-
19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826
0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715
13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089
5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823
7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853
23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787
22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125
29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-
8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842
40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995
44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-
128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
),
};

"use client";

import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";

import { TooltipProvider } from "@/components/ui/tooltip";

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {


return (
<NextThemesProvider {...props}>
<TooltipProvider delayDuration={120}>
<NuqsAdapter>{children}</NuqsAdapter>
</TooltipProvider>
</NextThemesProvider>
);
}

import { type VariantProps, cva } from "class-variance-authority";


import type * as React from "react";

import { cn } from "@/lib/utils";

const shellVariants = cva("grid items-center gap-8 pt-6 pb-8 md:py-8", {


variants: {
variant: {
default: "container",
sidebar: "",
centered: "container flex h-dvh max-w-2xl flex-col justify-center py-16",
markdown: "container max-w-3xl py-8 md:py-10 lg:py-10",
},
},
defaultVariants: {
variant: "default",
},
});

interface ShellProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof shellVariants> {
as?: React.ElementType;
}

function Shell({
className,
as: Comp = "section",
variant,
...props
}: ShellProps) {
return (
<Comp className={cn(shellVariants({ variant }), className)} {...props} />
);
}
export { Shell, shellVariants };

import { env } from "@/env.js";

export function TailwindIndicator() {


if (env.NODE_ENV === "production") return null;

return (
<div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-
center rounded-full bg-gray-800 p-3 font-mono text-white text-xs">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
sm
</div>
<div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
<div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
);
}

export type DataTableConfig = typeof dataTableConfig;

export const dataTableConfig = {


textOperators: [
{ label: "Contains", value: "iLike" as const },
{ label: "Does not contain", value: "notILike" as const },
{ label: "Is", value: "eq" as const },
{ label: "Is not", value: "ne" as const },
{ label: "Is empty", value: "isEmpty" as const },
{ label: "Is not empty", value: "isNotEmpty" as const },
],
numericOperators: [
{ label: "Is", value: "eq" as const },
{ label: "Is not", value: "ne" as const },
{ label: "Is less than", value: "lt" as const },
{ label: "Is less than or equal to", value: "lte" as const },
{ label: "Is greater than", value: "gt" as const },
{ label: "Is greater than or equal to", value: "gte" as const },
{ label: "Is between", value: "isBetween" as const },
{ label: "Is empty", value: "isEmpty" as const },
{ label: "Is not empty", value: "isNotEmpty" as const },
],
dateOperators: [
{ label: "Is", value: "eq" as const },
{ label: "Is not", value: "ne" as const },
{ label: "Is before", value: "lt" as const },
{ label: "Is after", value: "gt" as const },
{ label: "Is on or before", value: "lte" as const },
{ label: "Is on or after", value: "gte" as const },
{ label: "Is between", value: "isBetween" as const },
{ label: "Is relative to today", value: "isRelativeToToday" as const },
{ label: "Is empty", value: "isEmpty" as const },
{ label: "Is not empty", value: "isNotEmpty" as const },
],
selectOperators: [
{ label: "Is", value: "eq" as const },
{ label: "Is not", value: "ne" as const },
{ label: "Is empty", value: "isEmpty" as const },
{ label: "Is not empty", value: "isNotEmpty" as const },
],
multiSelectOperators: [
{ label: "Has any of", value: "inArray" as const },
{ label: "Has none of", value: "notInArray" as const },
{ label: "Is empty", value: "isEmpty" as const },
{ label: "Is not empty", value: "isNotEmpty" as const },
],
booleanOperators: [
{ label: "Is", value: "eq" as const },
{ label: "Is not", value: "ne" as const },
],
sortOrders: [
{ label: "Asc", value: "asc" as const },
{ label: "Desc", value: "desc" as const },
],
filterVariants: [
"text",
"number",
"range",
"date",
"dateRange",
"boolean",
"select",
"multiSelect",
] as const,
operators: [
"iLike",
"notILike",
"eq",
"ne",
"inArray",
"notInArray",
"isEmpty",
"isNotEmpty",
"lt",
"lte",
"gt",
"gte",
"isBetween",
"isRelativeToToday",
] as const,
joinOperators: ["and", "or"] as const,
};

import { CommandIcon, FileSpreadsheetIcon } from "lucide-react";

export type FlagConfig = typeof flagConfig;

export const flagConfig = {


featureFlags: [
{
label: "Advanced filters",
value: "advancedFilters" as const,
icon: FileSpreadsheetIcon,
tooltipTitle: "Advanced filters",
tooltipDescription: "Airtable like advanced filters for filtering rows.",
},
{
label: "Command filters",
value: "commandFilters" as const,
icon: CommandIcon,
tooltipTitle: "Command filter chips",
tooltipDescription: "Linear like command palette for filtering rows.",
},
],
};

import { env } from "@/env";

export type SiteConfig = typeof siteConfig;

export const siteConfig = {


name: "Table",
description:
"Shadcn table with server side sorting, pagination, and filtering",
url:
env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://table.sadmn.com",
links: { github: "https://github.com/sadmann7/shadcn-table" },
};

import { env } from "@/env.js";


import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import * as schema from "./schema";

const client = postgres(env.DATABASE_URL);


export const db = drizzle(client, { schema });

import { migrate } from "drizzle-orm/postgres-js/migrator";

import { db } from ".";

export async function runMigrate() {


console.log("⏳ Running migrations...");

const start = Date.now();

await migrate(db, { migrationsFolder: "drizzle" });

const end = Date.now();

console.log(`✅ Migrations completed in ${end - start}ms`);

process.exit(0);
}

runMigrate().catch((err) => {
console.error("❌ Migration failed");
console.error(err);
process.exit(1);
});

import { pgTable } from "@/db/utils";


import { sql } from "drizzle-orm";
import { boolean, real, timestamp, varchar } from "drizzle-orm/pg-core";

import { generateId } from "@/lib/id";

export const tasks = pgTable("tasks", {


id: varchar("id", { length: 30 })
.$defaultFn(() => generateId())
.primaryKey(),
code: varchar("code", { length: 128 }).notNull().unique(),
title: varchar("title", { length: 128 }),
status: varchar("status", {
length: 30,
enum: ["todo", "in-progress", "done", "canceled"],
})
.notNull()
.default("todo"),
label: varchar("label", {
length: 30,
enum: ["bug", "feature", "enhancement", "documentation"],
})
.notNull()
.default("bug"),
priority: varchar("priority", {
length: 30,
enum: ["low", "medium", "high"],
})
.notNull()
.default("low"),
estimatedHours: real("estimated_hours").notNull().default(0),
archived: boolean("archived").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.default(sql`current_timestamp`)
.$onUpdate(() => new Date()),
});

export type Task = typeof tasks.$inferSelect;


export type NewTask = typeof tasks.$inferInsert;

import { seedTasks } from "@/app/_lib/seeds";

async function runSeed() {


console.log("⏳ Running seed...");

const start = Date.now();

await seedTasks({ count: 100 });

const end = Date.now();

console.log(`✅ Seed completed in ${end - start}ms`);

process.exit(0);
}

runSeed().catch((err) => {
console.error("❌ Seed failed");
console.error(err);
process.exit(1);
});

/**
* @see https://gist.github.com/rphlmr/0d1722a794ed5a16da0fdf6652902b15
*/

import { type AnyColumn, not, sql } from "drizzle-orm";


import { pgTableCreator } from "drizzle-orm/pg-core";

import { databasePrefix } from "@/lib/constants";

/**
* Allows a single database instance for multiple projects.
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const pgTable = pgTableCreator((name) => `${databasePrefix}_${name}`);

export function takeFirstOrNull<TData>(data: TData[]) {


return data[0] ?? null;
}

export function takeFirstOrThrow<TData>(data: TData[], errorMessage?: string) {


const first = takeFirstOrNull(data);

if (!first) {
throw new Error(errorMessage ?? "Item not found");
}

return first;
}

export function isEmpty<TColumn extends AnyColumn>(column: TColumn) {


return sql<boolean>`
case
when ${column} is null then true
when ${column} = '' then true
when ${column}::text = '[]' then true
when ${column}::text = '{}' then true
else false
end
`;
}

import * as React from "react";

/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-
callback-ref/src/useCallbackRef.tsx
*/

/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders
when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef<T extends (...args: never[]) => unknown>(
callback: T | undefined,
): T {
const callbackRef = React.useRef(callback);

React.useEffect(() => {
callbackRef.current = callback;
});

// https://github.com/facebook/react/issues/19240
return React.useMemo(
() => ((...args) => callbackRef.current?.(...args)) as T,
[],
);
}

export { useCallbackRef };

"use client";

import {
type ColumnFiltersState,
type PaginationState,
type RowSelectionState,
type SortingState,
type TableOptions,
type TableState,
type Updater,
type VisibilityState,
getCoreRowModel,
getFacetedMinMaxValues,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
type Parser,
type UseQueryStateOptions,
parseAsArrayOf,
parseAsInteger,
parseAsString,
useQueryState,
useQueryStates,
} from "nuqs";
import * as React from "react";

import { useDebouncedCallback } from "@/hooks/use-debounced-callback";


import { getSortingStateParser } from "@/lib/parsers";
import type { ExtendedColumnSort } from "@/types/data-table";

const PAGE_KEY = "page";


const PER_PAGE_KEY = "perPage";
const SORT_KEY = "sort";
const ARRAY_SEPARATOR = ",";
const DEBOUNCE_MS = 300;
const THROTTLE_MS = 50;

interface UseDataTableProps<TData>
extends Omit<
TableOptions<TData>,
| "state"
| "pageCount"
| "getCoreRowModel"
| "manualFiltering"
| "manualPagination"
| "manualSorting"
>,
Required<Pick<TableOptions<TData>, "pageCount">> {
initialState?: Omit<Partial<TableState>, "sorting"> & {
sorting?: ExtendedColumnSort<TData>[];
};
history?: "push" | "replace";
debounceMs?: number;
throttleMs?: number;
clearOnDefault?: boolean;
enableAdvancedFilter?: boolean;
scroll?: boolean;
shallow?: boolean;
startTransition?: React.TransitionStartFunction;
}

export function useDataTable<TData>(props: UseDataTableProps<TData>) {


const {
columns,
pageCount = -1,
initialState,
history = "replace",
debounceMs = DEBOUNCE_MS,
throttleMs = THROTTLE_MS,
clearOnDefault = false,
enableAdvancedFilter = false,
scroll = false,
shallow = true,
startTransition,
...tableProps
} = props;

const queryStateOptions = React.useMemo<


Omit<UseQueryStateOptions<string>, "parse">
>(
() => ({
history,
scroll,
shallow,
throttleMs,
debounceMs,
clearOnDefault,
startTransition,
}),
[
history,
scroll,
shallow,
throttleMs,
debounceMs,
clearOnDefault,
startTransition,
],
);

const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(


initialState?.rowSelection ?? {},
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>(initialState?.columnVisibility ?? {});

const [page, setPage] = useQueryState(


PAGE_KEY,
parseAsInteger.withOptions(queryStateOptions).withDefault(1),
);
const [perPage, setPerPage] = useQueryState(
PER_PAGE_KEY,
parseAsInteger
.withOptions(queryStateOptions)
.withDefault(initialState?.pagination?.pageSize ?? 10),
);

const pagination: PaginationState = React.useMemo(() => {


return {
pageIndex: page - 1, // zero-based index -> one-based index
pageSize: perPage,
};
}, [page, perPage]);

const onPaginationChange = React.useCallback(


(updaterOrValue: Updater<PaginationState>) => {
if (typeof updaterOrValue === "function") {
const newPagination = updaterOrValue(pagination);
void setPage(newPagination.pageIndex + 1);
void setPerPage(newPagination.pageSize);
} else {
void setPage(updaterOrValue.pageIndex + 1);
void setPerPage(updaterOrValue.pageSize);
}
},
[pagination, setPage, setPerPage],
);

const columnIds = React.useMemo(() => {


return new Set(
columns.map((column) => column.id).filter(Boolean) as string[],
);
}, [columns]);

const [sorting, setSorting] = useQueryState(


SORT_KEY,
getSortingStateParser<TData>(columnIds)
.withOptions(queryStateOptions)
.withDefault(initialState?.sorting ?? []),
);

const onSortingChange = React.useCallback(


(updaterOrValue: Updater<SortingState>) => {
if (typeof updaterOrValue === "function") {
const newSorting = updaterOrValue(sorting);
setSorting(newSorting as ExtendedColumnSort<TData>[]);
} else {
setSorting(updaterOrValue as ExtendedColumnSort<TData>[]);
}
},
[sorting, setSorting],
);

const filterableColumns = React.useMemo(() => {


if (enableAdvancedFilter) return [];

return columns.filter((column) => column.enableColumnFilter);


}, [columns, enableAdvancedFilter]);

const filterParsers = React.useMemo(() => {


if (enableAdvancedFilter) return {};

return filterableColumns.reduce<
Record<string, Parser<string> | Parser<string[]>>
>((acc, column) => {
if (column.meta?.options) {
acc[column.id ?? ""] = parseAsArrayOf(
parseAsString,
ARRAY_SEPARATOR,
).withOptions(queryStateOptions);
} else {
acc[column.id ?? ""] = parseAsString.withOptions(queryStateOptions);
}
return acc;
}, {});
}, [filterableColumns, queryStateOptions, enableAdvancedFilter]);

const [filterValues, setFilterValues] = useQueryStates(filterParsers);

const debouncedSetFilterValues = useDebouncedCallback(


(values: typeof filterValues) => {
void setPage(1);
void setFilterValues(values);
},
debounceMs,
);

const initialColumnFilters: ColumnFiltersState = React.useMemo(() => {


if (enableAdvancedFilter) return [];

return Object.entries(filterValues).reduce<ColumnFiltersState>(
(filters, [key, value]) => {
if (value !== null) {
const processedValue = Array.isArray(value)
? value
: typeof value === "string" && /[^a-zA-Z0-9]/.test(value)
? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)
: [value];

filters.push({
id: key,
value: processedValue,
});
}
return filters;
},
[],
);
}, [filterValues, enableAdvancedFilter]);

const [columnFilters, setColumnFilters] =


React.useState<ColumnFiltersState>(initialColumnFilters);

const onColumnFiltersChange = React.useCallback(


(updaterOrValue: Updater<ColumnFiltersState>) => {
if (enableAdvancedFilter) return;

setColumnFilters((prev) => {
const next =
typeof updaterOrValue === "function"
? updaterOrValue(prev)
: updaterOrValue;

const filterUpdates = next.reduce<


Record<string, string | string[] | null>
>((acc, filter) => {
if (filterableColumns.find((column) => column.id === filter.id)) {
acc[filter.id] = filter.value as string | string[];
}
return acc;
}, {});

for (const prevFilter of prev) {


if (!next.some((filter) => filter.id === prevFilter.id)) {
filterUpdates[prevFilter.id] = null;
}
}

debouncedSetFilterValues(filterUpdates);
return next;
});
},
[debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],
);

const table = useReactTable({


...tableProps,
columns,
initialState,
pageCount,
state: {
pagination,
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
defaultColumn: {
...tableProps.defaultColumn,
enableColumnFilter: false,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onPaginationChange,
onSortingChange,
onColumnFiltersChange,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
manualPagination: true,
manualSorting: true,
manualFiltering: true,
});

return { table, shallow, debounceMs, throttleMs };


}

/**
* @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/
src/use-debounced-callback/use-debounced-callback.ts
*/

import * as React from "react";

import { useCallbackRef } from "@/hooks/use-callback-ref";

export function useDebouncedCallback<T extends (...args: never[]) => unknown>(


callback: T,
delay: number,
) {
const handleCallback = useCallbackRef(callback);
const debounceTimerRef = React.useRef(0);
React.useEffect(
() => () => window.clearTimeout(debounceTimerRef.current),
[],
);

const setValue = React.useCallback(


(...args: Parameters<T>) => {
window.clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = window.setTimeout(
() => handleCallback(...args),
delay,
);
},
[handleCallback, delay],
);

return setValue;
}

import * as React from "react";

export function useMediaQuery(query: string) {


const [value, setValue] = React.useState(false);

React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}

const result = matchMedia(query);


result.addEventListener("change", onChange);
setValue(result.matches);

return () => result.removeEventListener("change", onChange);


}, [query]);

return value;
}

import * as React from "react";

/**
* A utility to compose multiple event handlers into a single event handler.
* Call originalEventHandler first, then ourEventHandler unless prevented.
*/
function composeEventHandlers<E>(
originalEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void,
{ checkForDefaultPrevented = true } = {},
) {
return function handleEvent(event: E) {
originalEventHandler?.(event);

if (
checkForDefaultPrevented === false ||
!(event as unknown as Event).defaultPrevented
) {
return ourEventHandler?.(event);
}
};
}

/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-
refs/src/compose-refs.tsx
*/

type PossibleRef<T> = React.Ref<T> | undefined;

/**
* Set a given ref to a given value.
* This utility takes care of different types of refs: callback refs and
RefObject(s).
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}

if (ref !== null && ref !== undefined) {


ref.current = value;
}
}

/**
* A utility to compose multiple refs together.
* Accepts callback refs and RefObject(s).
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});

// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}

/**
* A custom hook that composes multiple refs.
* Accepts callback refs and RefObject(s).
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(composeRefs(...refs), refs);
}

export { composeEventHandlers, composeRefs, useComposedRefs };

export const unknownError =


"An unknown error occurred. Please try again later.";

export const databasePrefix = "shadcn";

import type {
ExtendedColumnFilter,
FilterOperator,
FilterVariant,
} from "@/types/data-table";
import type { Column } from "@tanstack/react-table";

import { dataTableConfig } from "@/config/data-table";

export function getCommonPinningStyles<TData>({


column,
withBorder = false,
}: {
column: Column<TData>;
withBorder?: boolean;
}): React.CSSProperties {
const isPinned = column.getIsPinned();
const isLastLeftPinnedColumn =
isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinnedColumn =
isPinned === "right" && column.getIsFirstColumn("right");

return {
boxShadow: withBorder
? isLastLeftPinnedColumn
? "-4px 0 4px -4px hsl(var(--border)) inset"
: isFirstRightPinnedColumn
? "4px 0 4px -4px hsl(var(--border)) inset"
: undefined
: undefined,
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
opacity: isPinned ? 0.97 : 1,
position: isPinned ? "sticky" : "relative",
background: isPinned ? "hsl(var(--background))" : "hsl(var(--background))",
width: column.getSize(),
zIndex: isPinned ? 1 : 0,
};
}

export function getFilterOperators(filterVariant: FilterVariant) {


const operatorMap: Record<
FilterVariant,
{ label: string; value: FilterOperator }[]
> = {
text: dataTableConfig.textOperators,
number: dataTableConfig.numericOperators,
range: dataTableConfig.numericOperators,
date: dataTableConfig.dateOperators,
dateRange: dataTableConfig.dateOperators,
boolean: dataTableConfig.booleanOperators,
select: dataTableConfig.selectOperators,
multiSelect: dataTableConfig.multiSelectOperators,
};

return operatorMap[filterVariant] ?? dataTableConfig.textOperators;


}

export function getDefaultFilterOperator(filterVariant: FilterVariant) {


const operators = getFilterOperators(filterVariant);

return operators[0]?.value ?? (filterVariant === "text" ? "iLike" : "eq");


}

export function getValidFilters<TData>(


filters: ExtendedColumnFilter<TData>[],
): ExtendedColumnFilter<TData>[] {
return filters.filter(
(filter) =>
filter.operator === "isEmpty" ||
filter.operator === "isNotEmpty" ||
(Array.isArray(filter.value)
? filter.value.length > 0
: filter.value !== "" &&
filter.value !== null &&
filter.value !== undefined),
);
}

import type { Table } from "@tanstack/react-table";

export function exportTableToCSV<TData>(


table: Table<TData>,
opts: {
filename?: string;
excludeColumns?: (keyof TData | "select" | "actions")[];
onlySelected?: boolean;
} = {},
): void {
const {
filename = "table",
excludeColumns = [],
onlySelected = false,
} = opts;

const headers = table


.getAllLeafColumns()
.map((column) => column.id)
.filter((id) => !excludeColumns.includes(id));

const csvContent = [
headers.join(","),
...(onlySelected
? table.getFilteredSelectedRowModel().rows
: table.getRowModel().rows
).map((row) =>
headers
.map((header) => {
const cellValue = row.getValue(header);
return typeof cellValue === "string"
? `"${cellValue.replace(/"/g, '""')}"`
: cellValue;
})
.join(","),
),
].join("\n");

const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });

const url = URL.createObjectURL(blob);


const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${filename}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
import { isEmpty } from "@/db/utils";
import type { ExtendedColumnFilter, JoinOperator } from "@/types/data-table";
import { addDays, endOfDay, startOfDay } from "date-fns";
import {
type AnyColumn,
type SQL,
type Table,
and,
eq,
gt,
gte,
ilike,
inArray,
lt,
lte,
ne,
not,
notIlike,
notInArray,
or,
} from "drizzle-orm";

export function filterColumns<T extends Table>({


table,
filters,
joinOperator,
}: {
table: T;
filters: ExtendedColumnFilter<T>[];
joinOperator: JoinOperator;
}): SQL | undefined {
const joinFn = joinOperator === "and" ? and : or;

const conditions = filters.map((filter) => {


const column = getColumn(table, filter.id);

switch (filter.operator) {
case "iLike":
return filter.variant === "text" && typeof filter.value === "string"
? ilike(column, `%${filter.value}%`)
: undefined;

case "notILike":
return filter.variant === "text" && typeof filter.value === "string"
? notIlike(column, `%${filter.value}%`)
: undefined;

case "eq":
if (column.dataType === "boolean" && typeof filter.value === "string") {
return eq(column, filter.value === "true");
}
if (filter.variant === "date" || filter.variant === "dateRange") {
const date = new Date(Number(filter.value));
date.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return and(gte(column, date), lte(column, end));
}
return eq(column, filter.value);
case "ne":
if (column.dataType === "boolean" && typeof filter.value === "string") {
return ne(column, filter.value === "true");
}
if (filter.variant === "date" || filter.variant === "dateRange") {
const date = new Date(Number(filter.value));
date.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return or(lt(column, date), gt(column, end));
}
return ne(column, filter.value);

case "inArray":
if (Array.isArray(filter.value)) {
return inArray(column, filter.value);
}
return undefined;

case "notInArray":
if (Array.isArray(filter.value)) {
return notInArray(column, filter.value);
}
return undefined;

case "lt":
return filter.variant === "number" || filter.variant === "range"
? lt(column, filter.value)
: filter.variant === "date" && typeof filter.value === "string"
? lt(
column,
(() => {
const date = new Date(Number(filter.value));
date.setHours(23, 59, 59, 999);
return date;
})(),
)
: undefined;

case "lte":
return filter.variant === "number" || filter.variant === "range"
? lte(column, filter.value)
: filter.variant === "date" && typeof filter.value === "string"
? lte(
column,
(() => {
const date = new Date(Number(filter.value));
date.setHours(23, 59, 59, 999);
return date;
})(),
)
: undefined;

case "gt":
return filter.variant === "number" || filter.variant === "range"
? gt(column, filter.value)
: filter.variant === "date" && typeof filter.value === "string"
? gt(
column,
(() => {
const date = new Date(Number(filter.value));
date.setHours(0, 0, 0, 0);
return date;
})(),
)
: undefined;

case "gte":
return filter.variant === "number" || filter.variant === "range"
? gte(column, filter.value)
: filter.variant === "date" && typeof filter.value === "string"
? gte(
column,
(() => {
const date = new Date(Number(filter.value));
date.setHours(0, 0, 0, 0);
return date;
})(),
)
: undefined;

case "isBetween":
if (
(filter.variant === "date" || filter.variant === "dateRange") &&
Array.isArray(filter.value) &&
filter.value.length === 2
) {
return and(
filter.value[0]
? gte(
column,
(() => {
const date = new Date(Number(filter.value[0]));
date.setHours(0, 0, 0, 0);
return date;
})(),
)
: undefined,
filter.value[1]
? lte(
column,
(() => {
const date = new Date(Number(filter.value[1]));
date.setHours(23, 59, 59, 999);
return date;
})(),
)
: undefined,
);
}

if (
(filter.variant === "number" || filter.variant === "range") &&
Array.isArray(filter.value) &&
filter.value.length === 2
) {
const firstValue =
filter.value[0] && filter.value[0].trim() !== ""
? Number(filter.value[0])
: null;
const secondValue =
filter.value[1] && filter.value[1].trim() !== ""
? Number(filter.value[1])
: null;

if (firstValue === null && secondValue === null) {


return undefined;
}

if (firstValue !== null && secondValue === null) {


return eq(column, firstValue);
}

if (firstValue === null && secondValue !== null) {


return eq(column, secondValue);
}

return and(
firstValue !== null ? gte(column, firstValue) : undefined,
secondValue !== null ? lte(column, secondValue) : undefined,
);
}
return undefined;

case "isRelativeToToday":
if (
(filter.variant === "date" || filter.variant === "dateRange") &&
typeof filter.value === "string"
) {
const today = new Date();
const [amount, unit] = filter.value.split(" ") ?? [];
let startDate: Date;
let endDate: Date;

if (!amount || !unit) return undefined;

switch (unit) {
case "days":
startDate = startOfDay(addDays(today, Number.parseInt(amount)));
endDate = endOfDay(startDate);
break;
case "weeks":
startDate = startOfDay(
addDays(today, Number.parseInt(amount) * 7),
);
endDate = endOfDay(addDays(startDate, 6));
break;
case "months":
startDate = startOfDay(
addDays(today, Number.parseInt(amount) * 30),
);
endDate = endOfDay(addDays(startDate, 29));
break;
default:
return undefined;
}
return and(gte(column, startDate), lte(column, endDate));
}
return undefined;

case "isEmpty":
return isEmpty(column);

case "isNotEmpty":
return not(isEmpty(column));

default:
throw new Error(`Unsupported operator: ${filter.operator}`);
}
});

const validConditions = conditions.filter(


(condition) => condition !== undefined,
);

return validConditions.length > 0 ? joinFn(...validConditions) : undefined;


}

export function getColumn<T extends Table>(


table: T,
columnKey: keyof T,
): AnyColumn {
return table[columnKey] as AnyColumn;
}

import { GeistMono } from "geist/font/mono";


import { GeistSans } from "geist/font/sans";

export const fontSans = GeistSans;


export const fontMono = GeistMono;

export function formatDate(


date: Date | string | number | undefined,
opts: Intl.DateTimeFormatOptions = {},
) {
if (!date) return "";

try {
return new Intl.DateTimeFormat("en-US", {
month: opts.month ?? "long",
day: opts.day ?? "numeric",
year: opts.year ?? "numeric",
...opts,
}).format(new Date(date));
} catch (_err) {
return "";
}
}

import { isRedirectError } from "next/dist/client/components/redirect-error";


import { z } from "zod";

export function getErrorMessage(err: unknown) {


const unknownError = "Something went wrong, please try again later.";
if (err instanceof z.ZodError) {
const errors = err.issues.map((issue) => {
return issue.message;
});
return errors.join("\n");
}

if (err instanceof Error) {


return err.message;
}

if (isRedirectError(err)) {
throw err;
}

return unknownError;
}

import { customAlphabet } from "nanoid";

const prefixes: Record<string, unknown> = {};

interface GenerateIdOptions {
length?: number;
separator?: string;
}

export function generateId(


prefixOrOptions?: keyof typeof prefixes | GenerateIdOptions,
inputOptions: GenerateIdOptions = {},
) {
const finalOptions =
typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;

const prefix =
typeof prefixOrOptions === "object" ? undefined : prefixOrOptions;

const { length = 12, separator = "_" } = finalOptions;


const id = customAlphabet(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
length,
)();

return prefix ? `${prefixes[prefix]}${separator}${id}` : id;


}

import { createParser } from "nuqs/server";


import { z } from "zod";

import { dataTableConfig } from "@/config/data-table";

import type {
ExtendedColumnFilter,
ExtendedColumnSort,
} from "@/types/data-table";

const sortingItemSchema = z.object({


id: z.string(),
desc: z.boolean(),
});

export const getSortingStateParser = <TData>(


columnIds?: string[] | Set<string>,
) => {
const validKeys = columnIds
? columnIds instanceof Set
? columnIds
: new Set(columnIds)
: null;

return createParser({
parse: (value) => {
try {
const parsed = JSON.parse(value);
const result = z.array(sortingItemSchema).safeParse(parsed);

if (!result.success) return null;

if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {


return null;
}

return result.data as ExtendedColumnSort<TData>[];


} catch {
return null;
}
},
serialize: (value) => JSON.stringify(value),
eq: (a, b) =>
a.length === b.length &&
a.every(
(item, index) =>
item.id === b[index]?.id && item.desc === b[index]?.desc,
),
});
};

const filterItemSchema = z.object({


id: z.string(),
value: z.union([z.string(), z.array(z.string())]),
variant: z.enum(dataTableConfig.filterVariants),
operator: z.enum(dataTableConfig.operators),
filterId: z.string(),
});

export type FilterItemSchema = z.infer<typeof filterItemSchema>;

export const getFiltersStateParser = <TData>(


columnIds?: string[] | Set<string>,
) => {
const validKeys = columnIds
? columnIds instanceof Set
? columnIds
: new Set(columnIds)
: null;

return createParser({
parse: (value) => {
try {
const parsed = JSON.parse(value);
const result = z.array(filterItemSchema).safeParse(parsed);

if (!result.success) return null;

if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {


return null;
}

return result.data as ExtendedColumnFilter<TData>[];


} catch {
return null;
}
},
serialize: (value) => JSON.stringify(value),
eq: (a, b) =>
a.length === b.length &&
a.every(
(filter, index) =>
filter.id === b[index]?.id &&
filter.value === b[index]?.value &&
filter.variant === b[index]?.variant &&
filter.operator === b[index]?.operator,
),
});
};

/**
* @see https://github.com/ethanniser/NextMaster/blob/main/src/lib/unstable-
cache.ts
*/

import { unstable_cache as next_unstable_cache } from "next/cache";


import { cache } from "react";

// next_unstable_cache doesn't handle deduplication, so we wrap it in React's cache


export const unstable_cache = <Inputs extends unknown[], Output>(
cb: (...args: Inputs) => Promise<Output>,
keyParts: string[],
options?: {
/**
* The revalidation interval in seconds.
*/
revalidate?: number | false;
tags?: string[];
},
) => cache(next_unstable_cache(cb, keyParts, options));

import { type ClassValue, clsx } from "clsx";


import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {


return twMerge(clsx(inputs));
}

@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.5rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}

@theme inline {
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;

@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}

@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

@utility container {
margin-inline: auto;
padding-inline: 2rem;

@media (width >= --theme(--breakpoint-sm)) {


max-width: none;
}

@media (width >= 1400px) {


max-width: 1400px;
}

@media (max-width: 640px) {


@apply px-4;
}
}

import type { DataTableConfig } from "@/config/data-table";


import type { FilterItemSchema } from "@/lib/parsers";
import type { ColumnSort, Row, RowData } from "@tanstack/react-table";

declare module "@tanstack/react-table" {


// biome-ignore lint/correctness/noUnusedVariables: <explanation>
interface ColumnMeta<TData extends RowData, TValue> {
label?: string;
placeholder?: string;
variant?: FilterVariant;
options?: Option[];
range?: [number, number];
unit?: string;
icon?: React.FC<React.SVGProps<SVGSVGElement>>;
}
}

export interface Option {


label: string;
value: string;
count?: number;
icon?: React.FC<React.SVGProps<SVGSVGElement>>;
}

export type FilterOperator = DataTableConfig["operators"][number];


export type FilterVariant = DataTableConfig["filterVariants"][number];
export type JoinOperator = DataTableConfig["joinOperators"][number];

export interface ExtendedColumnSort<TData> extends Omit<ColumnSort, "id"> {


id: Extract<keyof TData, string>;
}

export interface ExtendedColumnFilter<TData> extends FilterItemSchema {


id: Extract<keyof TData, string>;
}

export interface DataTableRowAction<TData> {


row: Row<TData>;
variant: "update" | "delete";
}

import type { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";


import type { EmptyProps } from "@/types";
import type { ExtendedColumnFilter, Option } from "@/types/data-table";

import type { Column, Table, TableOptions } from "@tanstack/react-table";


import type { motion } from "motion/react";
import type * as React from "react";

export interface UseDataTableProps<TData>


extends Required<Pick<TableOptions<TData>, "pageCount">>,
Pick<
TableOptions<TData>,
"data" | "columns" | "getRowId" | "defaultColumn" | "initialState"
> {
/**
* Determines how query updates affect history.
* `push` creates a new history entry; `replace` (default) updates the current
entry.
* @default "replace"
*/
history?: "push" | "replace";

/**
* Debounce time (ms) for filter updates to enhance performance during rapid
input.
* @default 300
*/
debounceMs?: number;

/**
* Maximum time (ms) to wait between URL query string updates.
* Helps with browser rate-limiting. Minimum effective value is 50ms.
* @default 50
*/
throttleMs?: number;

/**
* Clear URL query key-value pair when state is set to default.
* Keep URL meaning consistent when defaults change.
* @default false
*/
clearOnDefault?: boolean;

/**
* Enable notion like column filters.
* Advanced filters and column filters cannot be used at the same time.
* @default false
* @type boolean
*/
enableAdvancedFilter?: boolean;

/**
* Whether the page should scroll to the top when the URL changes.
* @default false
*/
scroll?: boolean;

/**
* Whether to keep query states client-side, avoiding server calls.
* Setting to `false` triggers a network request with the updated querystring.
* @default true
*/
shallow?: boolean;

/**
* Observe Server Component loading states for non-shallow updates.
* Pass `startTransition` from `React.useTransition()`.
* Sets `shallow` to `false` automatically.
* So shallow: true` and `startTransition` cannot be used at the same time.
* @see https://react.dev/reference/react/useTransition
*/
startTransition?: React.TransitionStartFunction;
}

export interface DataTableProps<TData> extends EmptyProps<"div"> {


/** The table instance. */
table: Table<TData>;

/** The action bar to display above the table. */


actionBar?: React.ReactNode;
}

export interface DataTableToolbarProps<TData> extends EmptyProps<"div"> {


/** The table instance. */
table: Table<TData>;
}

export interface DataTableAdvancedToolbarProps<TData>


extends EmptyProps<"div"> {
/** The table instance. */
table: Table<TData>;
}
export interface DataTableActionBarProps<TData>
extends EmptyProps<typeof motion.div> {
/** The table instance. */
table: Table<TData>;

/** Whether the action bar is visible. */


visible?: boolean;

/**
* The container to mount the portal into.
* @default document.body
*/
container?: Element | DocumentFragment | null;
}

export interface DataTableColumnHeaderProps<TData, TValue>


extends EmptyProps<typeof DropdownMenuTrigger> {
/** The column instance. */
column: Column<TData, TValue>;

/** The column title. */


title: string;
}

export interface DataTableDateFilterProps<TData> {


/** The column instance. */
column: Column<TData, unknown>;

/** The title of the date picker. */


title?: string;

/** Whether to enable range selection. */


multiple?: boolean;
}

export interface DataTableFacetedFilterProps<TData, TValue> {


/** The column instance. */
column?: Column<TData, TValue>;

/** The title of the filter. */


title?: string;

/** The options of the filter. */


options: Option[];

/** Whether to enable multiple selection. */


multiple?: boolean;
}

export interface DataTableSliderFilterProps<TData> {


/** The column instance. */
column: Column<TData, unknown>;

/** The title of the slider filter. */


title?: string;
}

export interface DataTableRangeFilterProps<TData> extends EmptyProps<"div"> {


/** The extended column filter. */
filter: ExtendedColumnFilter<TData>;

/** The column instance. */


column: Column<TData>;

/** The input id for screen readers. */


inputId: string;

/** The function to update the filter. */


onFilterUpdate: (
filterId: string,
updates: Partial<Omit<ExtendedColumnFilter<TData>, "filterId">>,
) => void;
}

export interface DataTableFilterListProps<TData> {


/** The table instance. */
table: Table<TData>;

/**
* Debounce time (ms) for filter updates to enhance performance during rapid
input.
* @default 300
*/
debounceMs?: number;

/**
* Maximum time (ms) to wait between URL query string updates.
* Helps with browser rate-limiting. Minimum effective value is 50ms.
* @default 50
*/
throttleMs?: number;

/**
* Whether to keep query states client-side, avoiding server calls.
* Setting to `false` triggers a network request with the updated querystring.
* @default true
*/
shallow?: boolean;
}

export interface DataTableFilterMenuProps<TData>


extends DataTableFilterListProps<TData> {}

export interface DataTableSortListProps<TData>


extends DataTableFilterListProps<TData> {}

export interface DataTablePaginationProps<TData> extends EmptyProps<"div"> {


/** The table instance. */
table: Table<TData>;

/**
* The options of the pagination.
* @default [10, 20, 30, 40, 50]
*/
pageSizeOptions?: number[];
}
export interface DataTableViewOptionsProps<TData> {
/** The table instance. */
table: Table<TData>;
}

export interface DataTableSkeletonProps extends EmptyProps<"div"> {


/** The number of columns in the table. */
columnCount: number;

/**
* The number of rows in the table.
* @default 10
*/
rowCount?: number;

/**
* The number of filters in the table.
* @default 0
*/
filterCount?: number;

/**
* Array of CSS width values for each table column.
* The maximum length of the array must match columnCount, extra values will be
ignored.
* @default ["auto"]
*/
cellWidths?: string[];

/**
* Whether to show the view options.
* @default true
*/
withViewOptions?: boolean;

/**
* Whether to show the pagination bar.
* @default true
*/
withPagination?: boolean;

/**
* Whether to prevent the table cells from shrinking.
* @default false
*/
shrinkZero?: boolean;
}

import type { SQL } from "drizzle-orm";

export type Prettify<T> = {


[K in keyof T]: T[K];
} & {};

export type EmptyProps<T extends React.ElementType> = Omit<


React.ComponentProps<T>,
keyof React.ComponentProps<T>
>;
export interface SearchParams {
[key: string]: string | string[] | undefined;
}

export interface QueryBuilderOpts {


where?: SQL;
orderBy?: SQL;
distinct?: boolean;
nullish?: boolean;
}

You might also like