From 314eba284b04352639db3d8590cd6b33233d2841 Mon Sep 17 00:00:00 2001
From: Sebastien Castiel <sebastien@castiel.me>
Date: Wed, 17 Jan 2024 12:27:31 -0500
Subject: Responsive category selector with drawer

---
 src/components/category-selector.tsx | 160 ++++++++++++++++++++++++-----------
 1 file changed, 109 insertions(+), 51 deletions(-)

(limited to 'src/components/category-selector.tsx')

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

