mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-05 12:16:13 +01:00
Responsive category selector with drawer
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user