Responsive category selector with drawer

This commit is contained in:
Sebastien Castiel
2024-01-17 12:27:31 -05:00
parent 92156b29cb
commit 314eba284b

View File

@@ -1,7 +1,7 @@
import { ChevronDown } from 'lucide-react' import { ChevronDown } from 'lucide-react'
import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon'
import { Button } from '@/components/ui/button' import { Button, ButtonProps } from '@/components/ui/button'
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -9,13 +9,15 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
} from '@/components/ui/command' } from '@/components/ui/command'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { useMediaQuery } from '@/lib/hooks'
import { Category } from '@prisma/client' import { Category } from '@prisma/client'
import { useState } from 'react' import { forwardRef, useState } from 'react'
type Props = { type Props = {
categories: Category[] categories: Category[]
@@ -30,7 +32,57 @@ export function CategorySelector({
}: Props) { }: Props) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [value, setValue] = useState<number>(defaultValue) const [value, setValue] = useState<number>(defaultValue)
const isDesktop = useMediaQuery('(min-width: 768px)')
const selectedCategory =
categories.find((category) => category.id === value) ?? categories[0]
if (isDesktop) {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<CategoryButton category={selectedCategory} open={open} />
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<CategoryCommand
categories={categories}
onValueChange={(id) => {
setValue(id)
onValueChange(id)
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
)
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<CategoryButton category={selectedCategory} open={open} />
</DrawerTrigger>
<DrawerContent className="p-0">
<CategoryCommand
categories={categories}
onValueChange={(id) => {
setValue(id)
onValueChange(id)
setOpen(false)
}}
/>
</DrawerContent>
</Drawer>
)
}
function CategoryCommand({
categories,
onValueChange,
}: {
categories: Category[]
onValueChange: (categoryId: Category['id']) => void
}) {
const categoriesByGroup = categories.reduce<Record<string, Category[]>>( const categoriesByGroup = categories.reduce<Record<string, Category[]>>(
(acc, category) => ({ (acc, category) => ({
...acc, ...acc,
@@ -39,56 +91,62 @@ export function CategorySelector({
{}, {},
) )
const selectedCategory =
categories.find((category) => category.id === value) ?? categories[0]
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Command>
<PopoverTrigger asChild> <CommandInput placeholder="Search category..." className="text-base" />
<Button <CommandEmpty>No category found.</CommandEmpty>
variant="outline" <div className="w-full max-h-[300px] overflow-y-auto">
role="combobox" {Object.entries(categoriesByGroup).map(
aria-expanded={open} ([group, groupCategories], index) => (
className="flex w-full justify-between" <CommandGroup key={index} heading={group}>
> {groupCategories.map((category) => (
<div className="flex items-center gap-3"> <CommandItem
<CategoryIcon category={selectedCategory} className="w-4 h-4" /> key={category.id}
{selectedCategory.name} value={`${category.id} ${category.grouping} ${category.name}`}
</div> onSelect={(currentValue) => {
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> const id = Number(currentValue.split(' ')[0])
</Button> onValueChange(id)
</PopoverTrigger> }}
<PopoverContent className="p-0" align="start"> >
<Command> <CategoryLabel category={category} />
<CommandInput placeholder="Search category..." /> </CommandItem>
<CommandEmpty>No category found.</CommandEmpty> ))}
<div className="w-full max-h-[300px] overflow-y-auto"> </CommandGroup>
{Object.entries(categoriesByGroup).map( ),
([group, groupCategories], index) => ( )}
<CommandGroup key={index} heading={group}> </div>
{groupCategories.map((category) => ( </Command>
<CommandItem )
key={category.id} }
value={`${category.id} ${category.grouping} ${category.name}`}
onSelect={(currentValue) => { type CategoryButtonProps = {
const id = Number(currentValue.split(' ')[0]) category: Category
setValue(id) open: boolean
onValueChange(id) }
setOpen(false) const CategoryButton = forwardRef<HTMLButtonElement, CategoryButtonProps>(
}} ({ category, open, ...props }: ButtonProps & CategoryButtonProps, ref) => {
> return (
<div className="flex items-center gap-3"> <Button
<CategoryIcon category={category} className="w-4 h-4" /> variant="outline"
{category.name} role="combobox"
</div> aria-expanded={open}
</CommandItem> className="flex w-full justify-between"
))} ref={ref}
</CommandGroup> {...props}
), >
)} <CategoryLabel category={category} />
</div> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Command> </Button>
</PopoverContent> )
</Popover> },
)
CategoryButton.displayName = 'CategoryButton'
function CategoryLabel({ category }: { category: Category }) {
return (
<div className="flex items-center gap-3">
<CategoryIcon category={category} className="w-4 h-4" />
{category.name}
</div>
) )
} }