@dyrected/admin 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE.md +50 -0
  3. package/README.md +73 -0
  4. package/components.json +17 -0
  5. package/eslint.config.js +22 -0
  6. package/index.html +13 -0
  7. package/package.json +99 -0
  8. package/postcss.config.js +6 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/icons.svg +24 -0
  11. package/src/App.css +184 -0
  12. package/src/App.tsx +25 -0
  13. package/src/assets/dyrected.svg +155 -0
  14. package/src/assets/hero.png +0 -0
  15. package/src/assets/react.svg +1 -0
  16. package/src/assets/vite.svg +1 -0
  17. package/src/components/auth/auth-gate.tsx +64 -0
  18. package/src/components/error-boundary.tsx +45 -0
  19. package/src/components/forms/field-renderer.tsx +111 -0
  20. package/src/components/forms/fields/block-builder.tsx +213 -0
  21. package/src/components/forms/fields/date-picker.tsx +60 -0
  22. package/src/components/forms/fields/json-editor.tsx +62 -0
  23. package/src/components/forms/fields/media-picker.tsx +286 -0
  24. package/src/components/forms/fields/multi-select.tsx +145 -0
  25. package/src/components/forms/fields/radio-field.tsx +51 -0
  26. package/src/components/forms/fields/relationship-picker.tsx +143 -0
  27. package/src/components/forms/fields/rich-text-editor.tsx +224 -0
  28. package/src/components/forms/fields/select-field.tsx +35 -0
  29. package/src/components/forms/fields/switch-field.tsx +16 -0
  30. package/src/components/forms/fields/text-area-field.tsx +15 -0
  31. package/src/components/forms/fields/text-field.tsx +24 -0
  32. package/src/components/forms/form-engine.tsx +87 -0
  33. package/src/components/forms/form-field-renderer.tsx +269 -0
  34. package/src/components/forms/utils.ts +97 -0
  35. package/src/components/layout/admin-shell.tsx +479 -0
  36. package/src/components/layout/branding-provider.tsx +112 -0
  37. package/src/components/live-preview/LivePreviewPane.tsx +128 -0
  38. package/src/components/media/focal-point-picker.tsx +66 -0
  39. package/src/components/media/media-card.tsx +44 -0
  40. package/src/components/media/media-grid.tsx +32 -0
  41. package/src/components/media/media-library-dialog.tsx +465 -0
  42. package/src/components/ui/aspect-ratio.tsx +7 -0
  43. package/src/components/ui/badge.tsx +36 -0
  44. package/src/components/ui/button.tsx +56 -0
  45. package/src/components/ui/calendar.tsx +214 -0
  46. package/src/components/ui/card.tsx +79 -0
  47. package/src/components/ui/checkbox.tsx +28 -0
  48. package/src/components/ui/command.tsx +151 -0
  49. package/src/components/ui/data-table.tsx +219 -0
  50. package/src/components/ui/dialog.tsx +122 -0
  51. package/src/components/ui/dropdown-menu.tsx +200 -0
  52. package/src/components/ui/form.tsx +178 -0
  53. package/src/components/ui/input.tsx +24 -0
  54. package/src/components/ui/label.tsx +24 -0
  55. package/src/components/ui/page-header.tsx +30 -0
  56. package/src/components/ui/pagination.tsx +57 -0
  57. package/src/components/ui/popover.tsx +29 -0
  58. package/src/components/ui/progress.tsx +26 -0
  59. package/src/components/ui/radio-group.tsx +42 -0
  60. package/src/components/ui/render-cell.tsx +110 -0
  61. package/src/components/ui/scroll-area.tsx +46 -0
  62. package/src/components/ui/select.tsx +160 -0
  63. package/src/components/ui/separator.tsx +29 -0
  64. package/src/components/ui/sheet.tsx +140 -0
  65. package/src/components/ui/sidebar.tsx +771 -0
  66. package/src/components/ui/skeleton.tsx +15 -0
  67. package/src/components/ui/sonner.tsx +27 -0
  68. package/src/components/ui/switch.tsx +27 -0
  69. package/src/components/ui/table.tsx +117 -0
  70. package/src/components/ui/tabs.tsx +53 -0
  71. package/src/components/ui/textarea.tsx +22 -0
  72. package/src/components/ui/toggle.tsx +43 -0
  73. package/src/components/ui/tooltip.tsx +28 -0
  74. package/src/hooks/use-mobile.tsx +19 -0
  75. package/src/hooks/use-preferences.ts +56 -0
  76. package/src/index.css +111 -0
  77. package/src/index.tsx +198 -0
  78. package/src/lib/utils.ts +32 -0
  79. package/src/main.tsx +10 -0
  80. package/src/pages/auth/first-user-page.tsx +115 -0
  81. package/src/pages/auth/login-page.tsx +91 -0
  82. package/src/pages/collections/edit-page.tsx +280 -0
  83. package/src/pages/collections/list-page.tsx +343 -0
  84. package/src/pages/dashboard/dashboard.tsx +150 -0
  85. package/src/pages/globals/editor-page.tsx +122 -0
  86. package/src/pages/media/media-page.tsx +564 -0
  87. package/src/pages/setup/setup-prompt.tsx +152 -0
  88. package/src/providers/dyrected-provider.tsx +122 -0
  89. package/src/providers/query-provider.tsx +19 -0
  90. package/src/types/jexl.d.ts +11 -0
  91. package/tailwind.config.ts +102 -0
  92. package/tsconfig.app.json +29 -0
  93. package/tsconfig.json +12 -0
  94. package/tsconfig.node.json +27 -0
  95. package/vite.config.ts +36 -0
@@ -0,0 +1,214 @@
1
+ import * as React from "react"
2
+ import {
3
+ ChevronDownIcon,
4
+ ChevronLeftIcon,
5
+ ChevronRightIcon,
6
+ } from "lucide-react"
7
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
8
+
9
+ import { cn } from "../../lib/utils"
10
+ import { Button, buttonVariants } from "../../components/ui/button"
11
+
12
+ function Calendar({
13
+ className,
14
+ classNames,
15
+ showOutsideDays = true,
16
+ captionLayout = "label",
17
+ buttonVariant = "ghost",
18
+ formatters,
19
+ components,
20
+ ...props
21
+ }: React.ComponentProps<typeof DayPicker> & {
22
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
23
+ }) {
24
+ const defaultClassNames = getDefaultClassNames()
25
+
26
+ return (
27
+ <DayPicker
28
+ showOutsideDays={showOutsideDays}
29
+ className={cn(
30
+ "bg-white group/calendar p-4 [--cell-size:2.25rem] shadow-sm rounded-xl border border-border/40",
31
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
32
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
33
+ className
34
+ )}
35
+ captionLayout={captionLayout}
36
+ formatters={{
37
+ formatMonthDropdown: (date) =>
38
+ date.toLocaleString("default", { month: "short" }),
39
+ ...formatters,
40
+ }}
41
+ classNames={{
42
+ root: cn("w-fit", defaultClassNames.root),
43
+ months: cn(
44
+ "relative flex flex-col gap-4 md:flex-row",
45
+ defaultClassNames.months
46
+ ),
47
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
48
+ nav: cn(
49
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
50
+ defaultClassNames.nav
51
+ ),
52
+ button_previous: cn(
53
+ buttonVariants({ variant: buttonVariant }),
54
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
55
+ defaultClassNames.button_previous
56
+ ),
57
+ button_next: cn(
58
+ buttonVariants({ variant: buttonVariant }),
59
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
60
+ defaultClassNames.button_next
61
+ ),
62
+ month_caption: cn(
63
+ "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size] mb-4",
64
+ defaultClassNames.month_caption
65
+ ),
66
+ dropdowns: cn(
67
+ "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
68
+ defaultClassNames.dropdowns
69
+ ),
70
+ dropdown_root: cn(
71
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
72
+ defaultClassNames.dropdown_root
73
+ ),
74
+ dropdown: cn(
75
+ "bg-popover absolute inset-0 opacity-0",
76
+ defaultClassNames.dropdown
77
+ ),
78
+ caption_label: cn(
79
+ "select-none font-semibold text-lg tracking-tight",
80
+ captionLayout === "label"
81
+ ? "text-sm"
82
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
83
+ defaultClassNames.caption_label
84
+ ),
85
+ table: "w-full border-collapse",
86
+ weekdays: cn("grid grid-cols-7 mb-2", defaultClassNames.weekdays),
87
+ weekday: cn(
88
+ "text-muted-foreground text-center select-none text-[0.8rem] font-medium",
89
+ defaultClassNames.weekday
90
+ ),
91
+ week: cn("grid grid-cols-7 w-full mt-1", defaultClassNames.week),
92
+ week_number_header: cn(
93
+ "w-[--cell-size] select-none",
94
+ defaultClassNames.week_number_header
95
+ ),
96
+ week_number: cn(
97
+ "text-muted-foreground select-none text-[0.8rem]",
98
+ defaultClassNames.week_number
99
+ ),
100
+ day: cn(
101
+ "group/day relative flex items-center justify-center h-[--cell-size] w-full select-none p-0 text-center",
102
+ defaultClassNames.day
103
+ ),
104
+ range_start: cn(
105
+ "bg-primary text-primary-foreground rounded-l-md",
106
+ defaultClassNames.range_start
107
+ ),
108
+ range_middle: cn("bg-accent text-accent-foreground rounded-none", defaultClassNames.range_middle),
109
+ range_end: cn("bg-primary text-primary-foreground rounded-r-md", defaultClassNames.range_end),
110
+ today: cn(
111
+ "bg-accent/50 text-accent-foreground font-bold rounded-md",
112
+ defaultClassNames.today
113
+ ),
114
+ outside: cn(
115
+ "text-muted-foreground/40 aria-selected:text-muted-foreground/40 opacity-50",
116
+ defaultClassNames.outside
117
+ ),
118
+ disabled: cn(
119
+ "text-muted-foreground opacity-50",
120
+ defaultClassNames.disabled
121
+ ),
122
+ hidden: cn("invisible", defaultClassNames.hidden),
123
+ ...classNames,
124
+ }}
125
+ components={{
126
+ Root: ({ className, rootRef, ...props }) => {
127
+ return (
128
+ <div
129
+ data-slot="calendar"
130
+ ref={rootRef}
131
+ className={cn(className)}
132
+ {...props}
133
+ />
134
+ )
135
+ },
136
+ Chevron: ({ className, orientation, ...props }) => {
137
+ if (orientation === "left") {
138
+ return (
139
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
140
+ )
141
+ }
142
+
143
+ if (orientation === "right") {
144
+ return (
145
+ <ChevronRightIcon
146
+ className={cn("size-4", className)}
147
+ {...props}
148
+ />
149
+ )
150
+ }
151
+
152
+ return (
153
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
154
+ )
155
+ },
156
+ DayButton: CalendarDayButton,
157
+ WeekNumber: ({ children, ...props }) => {
158
+ return (
159
+ <td {...props}>
160
+ <div className="flex size-[--cell-size] items-center justify-center text-center">
161
+ {children}
162
+ </div>
163
+ </td>
164
+ )
165
+ },
166
+ ...components,
167
+ }}
168
+ {...props}
169
+ />
170
+ )
171
+ }
172
+
173
+ function CalendarDayButton({
174
+ className,
175
+ day,
176
+ modifiers,
177
+ ...props
178
+ }: React.ComponentProps<typeof DayButton>) {
179
+ const ref = React.useRef<HTMLButtonElement>(null)
180
+ React.useEffect(() => {
181
+ if (modifiers.focused) ref.current?.focus()
182
+ }, [modifiers.focused])
183
+
184
+ return (
185
+ <Button
186
+ ref={ref}
187
+ variant="ghost"
188
+ size="icon"
189
+ data-day={day.date.toLocaleDateString()}
190
+ data-selected-single={
191
+ modifiers.selected &&
192
+ !modifiers.range_start &&
193
+ !modifiers.range_end &&
194
+ !modifiers.range_middle
195
+ }
196
+ data-range-start={modifiers.range_start}
197
+ data-range-end={modifiers.range_end}
198
+ data-range-middle={modifiers.range_middle}
199
+ className={cn(
200
+ "flex items-center justify-center h-[--cell-size] w-full min-w-[--cell-size] p-0 font-normal transition-all",
201
+ "hover:bg-accent hover:text-accent-foreground",
202
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:rounded-full data-[selected-single=true]:shadow-lg",
203
+ "data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-middle=true]:rounded-none",
204
+ "data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-start=true]:rounded-l-md",
205
+ "data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-end=true]:rounded-r-md",
206
+ "group-data-[focused=true]/day:ring-2 group-data-[focused=true]/day:ring-ring group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10",
207
+ className
208
+ )}
209
+ {...props}
210
+ />
211
+ )
212
+ }
213
+
214
+ export { Calendar, CalendarDayButton }
@@ -0,0 +1,79 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.HTMLAttributes<HTMLDivElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,28 @@
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("grid place-content-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
@@ -0,0 +1,151 @@
1
+ import * as React from "react"
2
+ import { type DialogProps } from "@radix-ui/react-dialog"
3
+ import { Command as CommandPrimitive } from "cmdk"
4
+ import { Search } from "lucide-react"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Dialog, DialogContent } from "../../components/ui/dialog"
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-popover-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ ))
22
+ Command.displayName = CommandPrimitive.displayName
23
+
24
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
25
+ return (
26
+ <Dialog {...props}>
27
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
28
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
29
+ {children}
30
+ </Command>
31
+ </DialogContent>
32
+ </Dialog>
33
+ )
34
+ }
35
+
36
+ const CommandInput = React.forwardRef<
37
+ React.ElementRef<typeof CommandPrimitive.Input>,
38
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
39
+ >(({ className, ...props }, ref) => (
40
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
41
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
42
+ <CommandPrimitive.Input
43
+ ref={ref}
44
+ className={cn(
45
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </div>
51
+ ))
52
+
53
+ CommandInput.displayName = CommandPrimitive.Input.displayName
54
+
55
+ const CommandList = React.forwardRef<
56
+ React.ElementRef<typeof CommandPrimitive.List>,
57
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
58
+ >(({ className, ...props }, ref) => (
59
+ <CommandPrimitive.List
60
+ ref={ref}
61
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
62
+ {...props}
63
+ />
64
+ ))
65
+
66
+ CommandList.displayName = CommandPrimitive.List.displayName
67
+
68
+ const CommandEmpty = React.forwardRef<
69
+ React.ElementRef<typeof CommandPrimitive.Empty>,
70
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
71
+ >((props, ref) => (
72
+ <CommandPrimitive.Empty
73
+ ref={ref}
74
+ className="py-6 text-center text-sm"
75
+ {...props}
76
+ />
77
+ ))
78
+
79
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
80
+
81
+ const CommandGroup = React.forwardRef<
82
+ React.ElementRef<typeof CommandPrimitive.Group>,
83
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
84
+ >(({ className, ...props }, ref) => (
85
+ <CommandPrimitive.Group
86
+ ref={ref}
87
+ className={cn(
88
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ ))
94
+
95
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
96
+
97
+ const CommandSeparator = React.forwardRef<
98
+ React.ElementRef<typeof CommandPrimitive.Separator>,
99
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
100
+ >(({ className, ...props }, ref) => (
101
+ <CommandPrimitive.Separator
102
+ ref={ref}
103
+ className={cn("-mx-1 h-px bg-border", className)}
104
+ {...props}
105
+ />
106
+ ))
107
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
108
+
109
+ const CommandItem = React.forwardRef<
110
+ React.ElementRef<typeof CommandPrimitive.Item>,
111
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
112
+ >(({ className, ...props }, ref) => (
113
+ <CommandPrimitive.Item
114
+ ref={ref}
115
+ className={cn(
116
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
117
+ className
118
+ )}
119
+ {...props}
120
+ />
121
+ ))
122
+
123
+ CommandItem.displayName = CommandPrimitive.Item.displayName
124
+
125
+ const CommandShortcut = ({
126
+ className,
127
+ ...props
128
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
129
+ return (
130
+ <span
131
+ className={cn(
132
+ "ml-auto text-xs tracking-widest text-muted-foreground",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+ CommandShortcut.displayName = "CommandShortcut"
140
+
141
+ export {
142
+ Command,
143
+ CommandDialog,
144
+ CommandInput,
145
+ CommandList,
146
+ CommandEmpty,
147
+ CommandGroup,
148
+ CommandItem,
149
+ CommandShortcut,
150
+ CommandSeparator,
151
+ }
@@ -0,0 +1,219 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ type ColumnDef,
6
+ type ColumnFiltersState,
7
+ type SortingState,
8
+ type VisibilityState,
9
+ flexRender,
10
+ getCoreRowModel,
11
+ getFilteredRowModel,
12
+ getPaginationRowModel,
13
+ getSortedRowModel,
14
+ useReactTable,
15
+ } from "@tanstack/react-table"
16
+
17
+ import {
18
+ Table,
19
+ TableBody,
20
+ TableCell,
21
+ TableHead,
22
+ TableHeader,
23
+ TableRow,
24
+ } from "../../components/ui/table"
25
+
26
+ import { Button } from "../../components/ui/button"
27
+ import { Input } from "../../components/ui/input"
28
+ import {
29
+ DropdownMenu,
30
+ DropdownMenuCheckboxItem,
31
+ DropdownMenuContent,
32
+ DropdownMenuTrigger,
33
+ } from "../../components/ui/dropdown-menu"
34
+ import { ChevronLeft, ChevronRight, Settings2 } from "lucide-react"
35
+ import { usePreferences } from "../../hooks/use-preferences"
36
+
37
+ interface DataTableProps<TData, TValue> {
38
+ columns: ColumnDef<TData, TValue>[]
39
+ data: TData[]
40
+ searchKey?: string
41
+ rowSelection?: any
42
+ onRowSelectionChange?: any
43
+ bulkActions?: (selectedIds: string[]) => React.ReactNode
44
+ persistenceKey?: string
45
+ initialColumnVisibility?: VisibilityState
46
+ }
47
+
48
+ export function DataTable<TData, TValue>({
49
+ columns,
50
+ data,
51
+ searchKey,
52
+ rowSelection: externalRowSelection,
53
+ onRowSelectionChange,
54
+ bulkActions,
55
+ persistenceKey,
56
+ initialColumnVisibility = {},
57
+ }: DataTableProps<TData, TValue>) {
58
+ const [sorting, setSorting] = React.useState<SortingState>([])
59
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
60
+ []
61
+ )
62
+ const [columnVisibility, setColumnVisibility] = usePreferences<VisibilityState>(
63
+ persistenceKey ? `visibility_${persistenceKey}` : "temp_visibility",
64
+ initialColumnVisibility
65
+ )
66
+ const [internalRowSelection, setInternalRowSelection] = React.useState({})
67
+
68
+ const rowSelection = externalRowSelection || internalRowSelection
69
+ const setRowSelection = onRowSelectionChange || setInternalRowSelection
70
+
71
+ const table = useReactTable({
72
+ data,
73
+ columns,
74
+ onSortingChange: setSorting,
75
+ onColumnFiltersChange: setColumnFilters,
76
+ getCoreRowModel: getCoreRowModel(),
77
+ getPaginationRowModel: getPaginationRowModel(),
78
+ getSortedRowModel: getSortedRowModel(),
79
+ getFilteredRowModel: getFilteredRowModel(),
80
+ onColumnVisibilityChange: setColumnVisibility,
81
+ onRowSelectionChange: setRowSelection,
82
+ state: {
83
+ sorting,
84
+ columnFilters,
85
+ columnVisibility,
86
+ rowSelection,
87
+ },
88
+ })
89
+
90
+ return (
91
+ <div className="w-full space-y-4">
92
+ <div className="flex items-center gap-4">
93
+ {searchKey && (
94
+ <Input
95
+ placeholder={`Search ${searchKey}...`}
96
+ value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
97
+ onChange={(event) =>
98
+ table.getColumn(searchKey)?.setFilterValue(event.target.value)
99
+ }
100
+ className="max-w-sm"
101
+ />
102
+ )}
103
+ {bulkActions && table.getFilteredSelectedRowModel().rows.length > 0 && (
104
+ <div className="flex items-center gap-2 animate-in slide-in-from-left-2">
105
+ {bulkActions(
106
+ table
107
+ .getFilteredSelectedRowModel()
108
+ .rows.map((r) => (r.original as any).id)
109
+ )}
110
+ </div>
111
+ )}
112
+ <DropdownMenu>
113
+ <DropdownMenuTrigger asChild>
114
+ <Button variant="outline" size="sm" className="ml-auto flex h-8 gap-2">
115
+ <Settings2 className="h-4 w-4" />
116
+ View
117
+ </Button>
118
+ </DropdownMenuTrigger>
119
+ <DropdownMenuContent align="end">
120
+ {table
121
+ .getAllColumns()
122
+ .filter((column) => column.getCanHide())
123
+ .map((column) => {
124
+ return (
125
+ <DropdownMenuCheckboxItem
126
+ key={column.id}
127
+ className="capitalize"
128
+ checked={column.getIsVisible()}
129
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
130
+ >
131
+ {column.id}
132
+ </DropdownMenuCheckboxItem>
133
+ )
134
+ })}
135
+ </DropdownMenuContent>
136
+ </DropdownMenu>
137
+ </div>
138
+ <div className="overflow-hidden">
139
+ <Table>
140
+ <TableHeader>
141
+ {table.getHeaderGroups().map((headerGroup) => (
142
+ <TableRow key={headerGroup.id}>
143
+ {headerGroup.headers.map((header) => {
144
+ return (
145
+ <TableHead key={header.id}>
146
+ {header.isPlaceholder
147
+ ? null
148
+ : flexRender(
149
+ header.column.columnDef.header,
150
+ header.getContext()
151
+ )}
152
+ </TableHead>
153
+ )
154
+ })}
155
+ </TableRow>
156
+ ))}
157
+ </TableHeader>
158
+ <TableBody className="border-t border-border/40">
159
+ {table.getRowModel().rows?.length ? (
160
+ table.getRowModel().rows.map((row) => (
161
+ <TableRow
162
+ key={row.id}
163
+ data-state={row.getIsSelected() && "selected"}
164
+ className="border-none even:bg-primary/[0.03] hover:bg-primary/[0.06] transition-colors duration-200"
165
+ >
166
+ {row.getVisibleCells().map((cell) => (
167
+ <TableCell key={cell.id} className="py-4 px-4 border-none first:pl-4 last:pr-4">
168
+ {flexRender(
169
+ cell.column.columnDef.cell,
170
+ cell.getContext()
171
+ )}
172
+ </TableCell>
173
+ ))}
174
+ </TableRow>
175
+ ))
176
+ ) : (
177
+ <TableRow>
178
+ <TableCell
179
+ colSpan={columns.length}
180
+ className="h-24 text-center"
181
+ >
182
+ No results.
183
+ </TableCell>
184
+ </TableRow>
185
+ )}
186
+ </TableBody>
187
+ </Table>
188
+ </div>
189
+ <div className="flex items-center justify-between px-2">
190
+ <div className="flex-1 text-sm text-muted-foreground">
191
+ {table.getFilteredSelectedRowModel().rows.length} of{" "}
192
+ {table.getFilteredRowModel().rows.length} row(s) selected.
193
+ </div>
194
+ <div className="flex items-center space-x-2">
195
+ <Button
196
+ variant="outline"
197
+ size="sm"
198
+ onClick={() => table.previousPage()}
199
+ disabled={!table.getCanPreviousPage()}
200
+ >
201
+ <ChevronLeft className="h-4 w-4" />
202
+ </Button>
203
+ <span className="text-sm font-medium">
204
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
205
+ {table.getPageCount()}
206
+ </span>
207
+ <Button
208
+ variant="outline"
209
+ size="sm"
210
+ onClick={() => table.nextPage()}
211
+ disabled={!table.getCanNextPage()}
212
+ >
213
+ <ChevronRight className="h-4 w-4" />
214
+ </Button>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ )
219
+ }