all_src_files (1)
all_src_files (1)
import "@/styles/globals.css";
interface IndexPageProps {
searchParams: Promise<SearchParams>;
}
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";
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);
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>
);
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>
);
}
interface DataTableAdvancedToolbarProps<TData>
extends React.ComponentProps<"div"> {
table: Table<TData>;
}
"use client";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
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";
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "number" || typeof item === "string") {
return item;
}
return undefined;
});
}
return [];
}
interface DataTableDateFilterProps<TData> {
column: Column<TData, unknown>;
title?: string;
multiple?: boolean;
}
if (multiple) {
const timestamps = parseColumnFilterValue(columnFilterValue);
return {
from: parseAsDate(timestamps[0]),
to: parseAsDate(timestamps[1]),
};
}
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>
);
}
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";
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],
);
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";
interface DataTableFilterListProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
debounceMs?: number;
throttleMs?: number;
shallow?: boolean;
}
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]);
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);
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;
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`;
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 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";
interface DataTableFilterMenuProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
debounceMs?: number;
throttleMs?: number;
shallow?: boolean;
}
if (!open) {
setTimeout(() => {
setSelectedColumn(null);
setInputValue("");
}, 100);
}
}, []);
const filterValue =
column.columnDef.meta?.variant === "multiSelect" ? [value] : value;
setTimeout(() => {
setSelectedColumn(null);
setInputValue("");
}, 100);
},
[filters, 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]);
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);
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 "{value}"</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`;
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 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;
}
}
"use client";
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>
);
}
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";
interface Range {
min: number;
max: number;
}
interface DataTableSliderFilterProps<TData> {
column: Column<TData, unknown>;
title?: string;
}
const { min, max, step } = React.useMemo<Range & { step: number }>(() => {
let minValue = 0;
let maxValue = 100;
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";
interface DataTableSortListProps<TData>
extends React.ComponentProps<typeof PopoverContent> {
table: Table<TData>;
}
if (!sortingIds.has(column.id)) {
availableColumns.push({ id: column.id, label });
}
}
return {
columnLabels: labels,
columns: availableColumns,
};
}, [sorting, table]);
onSortingChange((prevSorting) => [
...prevSorting,
{ id: firstColumn.id, desc: false },
]);
}, [columns, onSortingChange]);
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]);
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`;
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";
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;
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";
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>
);
}
"use client";
/**
* @see https://github.com/dubinc/dub/blob/main/packages/ui/src/animated-size-
container.tsx
*/
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;
return () => {
observer.disconnect();
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [width, height]);
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 };
"use client";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
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 };
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>
);
}
process.exit(0);
}
runMigrate().catch((err) => {
console.error("❌ Migration failed");
console.error(err);
process.exit(1);
});
process.exit(0);
}
runSeed().catch((err) => {
console.error("❌ Seed failed");
console.error(err);
process.exit(1);
});
/**
* @see https://gist.github.com/rphlmr/0d1722a794ed5a16da0fdf6652902b15
*/
/**
* 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}`);
if (!first) {
throw new Error(errorMessage ?? "Item not found");
}
return first;
}
/**
* @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";
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;
}
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]);
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]);
setColumnFilters((prev) => {
const next =
typeof updaterOrValue === "function"
? updaterOrValue(prev)
: updaterOrValue;
debouncedSetFilterValues(filterUpdates);
return next;
});
},
[debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],
);
/**
* @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/
src/use-debounced-callback/use-debounced-callback.ts
*/
return setValue;
}
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}
return value;
}
/**
* 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
*/
/**
* 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);
}
/**
* 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);
}
import type {
ExtendedColumnFilter,
FilterOperator,
FilterVariant,
} from "@/types/data-table";
import type { Column } from "@tanstack/react-table";
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,
};
}
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");
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;
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;
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}`);
}
});
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 "";
}
}
if (isRedirectError(err)) {
throw err;
}
return unknownError;
}
interface GenerateIdOptions {
length?: number;
separator?: string;
}
const prefix =
typeof prefixOrOptions === "object" ? undefined : prefixOrOptions;
import type {
ExtendedColumnFilter,
ExtendedColumnSort,
} from "@/types/data-table";
return createParser({
parse: (value) => {
try {
const parsed = JSON.parse(value);
const result = z.array(sortingItemSchema).safeParse(parsed);
return createParser({
parse: (value) => {
try {
const parsed = JSON.parse(value);
const result = z.array(filterItemSchema).safeParse(parsed);
/**
* @see https://github.com/ethanniser/NextMaster/blob/main/src/lib/unstable-
cache.ts
*/
@import "tailwindcss";
@import "tw-animate-css";
: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;
/**
* 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;
}
/**
* The container to mount the portal into.
* @default document.body
*/
container?: Element | DocumentFragment | null;
}
/**
* 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;
}
/**
* The options of the pagination.
* @default [10, 20, 30, 40, 50]
*/
pageSizeOptions?: number[];
}
export interface DataTableViewOptionsProps<TData> {
/** The table instance. */
table: Table<TData>;
}
/**
* 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;
}