diff --git a/dashboard/bun.lockb b/dashboard/bun.lockb index 457965a..92f5ffc 100755 Binary files a/dashboard/bun.lockb and b/dashboard/bun.lockb differ diff --git a/dashboard/package.json b/dashboard/package.json index 9641b8b..d5a5cb5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -14,11 +14,12 @@ "@ory/integrations": "^1.1.5", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", @@ -30,6 +31,7 @@ "@tanstack/react-table": "^8.20.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", + "cmdk": "1.0.0", "dotenv": "^16.4.7", "drizzle-orm": "^0.38.3", "lucide-react": "^0.462.0", diff --git a/dashboard/src/components/ui/command.tsx b/dashboard/src/components/ui/command.tsx new file mode 100644 index 0000000..f485776 --- /dev/null +++ b/dashboard/src/components/ui/command.tsx @@ -0,0 +1,154 @@ +'use client'; + +import * as React from 'react'; +import { type DialogProps } from '@radix-ui/react-dialog'; +import { Command as CommandPrimitive } from 'cmdk'; +import { Search } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props + }: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = 'CommandShortcut'; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/dashboard/src/components/ui/dialog.tsx b/dashboard/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c7b4160 --- /dev/null +++ b/dashboard/src/components/ui/dialog.tsx @@ -0,0 +1,123 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props + }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props + }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/dashboard/src/components/ui/multi-select.tsx b/dashboard/src/components/ui/multi-select.tsx new file mode 100644 index 0000000..266eea7 --- /dev/null +++ b/dashboard/src/components/ui/multi-select.tsx @@ -0,0 +1,129 @@ +'use client'; + +import * as React from 'react'; +import { X } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'; +import { Command as CommandPrimitive } from 'cmdk'; + +type MultiSelectItem = Record<'value' | 'label', string>; + +interface MultiSelectProps { + items: MultiSelectItem[]; + placeholder?: string; +} + +// Credits: https://github.com/mxkaske/mxkaske.dev/blob/main/components/craft/fancy-multi-select.tsx +export function MultiSelect({ items, placeholder }: MultiSelectProps) { + const inputRef = React.useRef(null); + const [open, setOpen] = React.useState(false); + const [selected, setSelected] = React.useState([]); + const [inputValue, setInputValue] = React.useState(''); + + const handleUnselect = React.useCallback((item: MultiSelectItem) => { + setSelected((prev) => prev.filter((s) => s.value !== item.value)); + }, []); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + const input = inputRef.current; + if (input) { + if (e.key === 'Delete' || e.key === 'Backspace') { + if (input.value === '') { + setSelected((prev) => { + const newSelected = [...prev]; + newSelected.pop(); + return newSelected; + }); + } + } + // This is not a default behaviour of the field + if (e.key === 'Escape') { + input.blur(); + } + } + }, + [], + ); + + const selectables = items.filter( + (item) => !selected.includes(item), + ); + + console.log(selectables, selected, inputValue); + + return ( + +
+
+ {selected.map((item) => { + return ( + + {item.label} + + + ); + })} + {/* Avoid having the "Search" Icon */} + setOpen(false)} + onFocus={() => setOpen(true)} + placeholder={placeholder ?? 'Select an item...'} + className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" + /> +
+
+
+ + {open && selectables.length > 0 ? ( +
+ + {selectables.map((item) => { + return ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onSelect={(value) => { + setInputValue(''); + setSelected((prev) => [...prev, item]); + }} + className={'cursor-pointer'} + > + {item.label} + + ); + })} + +
+ ) : null} +
+
+
+ ); +} \ No newline at end of file diff --git a/dashboard/src/components/ui/select.tsx b/dashboard/src/components/ui/select.tsx new file mode 100644 index 0000000..ddd77ac --- /dev/null +++ b/dashboard/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +'use client'; + +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +};