From 92156b29cbcc496af705982bdf18dd558e765acb Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Wed, 17 Jan 2024 12:07:03 -0500 Subject: Use combobox for category selector --- src/components/category-selector.tsx | 94 +++++++++++++++++++++ src/components/expense-form.tsx | 36 ++------ src/components/ui/command.tsx | 155 +++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 31 deletions(-) create mode 100644 src/components/category-selector.tsx create mode 100644 src/components/ui/command.tsx (limited to 'src') diff --git a/src/components/category-selector.tsx b/src/components/category-selector.tsx new file mode 100644 index 0000000..adb6582 --- /dev/null +++ b/src/components/category-selector.tsx @@ -0,0 +1,94 @@ +import { ChevronDown } from 'lucide-react' + +import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { Category } from '@prisma/client' +import { useState } from 'react' + +type Props = { + categories: Category[] + onValueChange: (categoryId: Category['id']) => void + defaultValue: Category['id'] +} + +export function CategorySelector({ + categories, + onValueChange, + defaultValue, +}: Props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState(defaultValue) + + const categoriesByGroup = categories.reduce>( + (acc, category) => ({ + ...acc, + [category.grouping]: [...(acc[category.grouping] ?? []), category], + }), + {}, + ) + + const selectedCategory = + categories.find((category) => category.id === value) ?? categories[0] + + return ( + + + + + + + + No category found. +
+ {Object.entries(categoriesByGroup).map( + ([group, groupCategories], index) => ( + + {groupCategories.map((category) => ( + { + const id = Number(currentValue.split(' ')[0]) + setValue(id) + onValueChange(id) + setOpen(false) + }} + > +
+ + {category.name} +
+
+ ))} +
+ ), + )} +
+
+
+
+ ) +} diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx index cfc749a..dfaaaa0 100644 --- a/src/components/expense-form.tsx +++ b/src/components/expense-form.tsx @@ -1,6 +1,6 @@ 'use client' -import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' import { AsyncButton } from '@/components/async-button' +import { CategorySelector } from '@/components/category-selector' import { SubmitButton } from '@/components/submit-button' import { Button } from '@/components/ui/button' import { @@ -29,9 +29,7 @@ import { Input } from '@/components/ui/input' import { Select, SelectContent, - SelectGroup, SelectItem, - SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' @@ -227,35 +225,11 @@ export function ExpenseForm({ render={({ field }) => ( Category - + /> Select the expense category. diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..17cc641 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"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 + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + 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, +} -- cgit v1.3