diff --git a/admin-spa/package-lock.json b/admin-spa/package-lock.json index df989c8..cc5e675 100644 --- a/admin-spa/package-lock.json +++ b/admin-spa/package-lock.json @@ -35,6 +35,7 @@ "recharts": "^3.3.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "vaul": "^1.1.2", "zustand": "^5.0.8" }, "devDependencies": { @@ -7947,6 +7948,19 @@ "dev": true, "license": "MIT" }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", diff --git a/admin-spa/package.json b/admin-spa/package.json index 6640648..de92d7a 100644 --- a/admin-spa/package.json +++ b/admin-spa/package.json @@ -37,6 +37,7 @@ "recharts": "^3.3.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "vaul": "^1.1.2", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/admin-spa/src/components/ui/drawer.tsx b/admin-spa/src/components/ui/drawer.tsx new file mode 100644 index 0000000..c17b0cc --- /dev/null +++ b/admin-spa/src/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/admin-spa/src/components/ui/responsive-dialog.tsx b/admin-spa/src/components/ui/responsive-dialog.tsx new file mode 100644 index 0000000..d9be0e4 --- /dev/null +++ b/admin-spa/src/components/ui/responsive-dialog.tsx @@ -0,0 +1,72 @@ +import * as React from "react" +import { useMediaQuery } from "@/hooks/use-media-query" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, +} from "@/components/ui/drawer" + +interface ResponsiveDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + children: React.ReactNode + title?: string + description?: string + footer?: React.ReactNode + className?: string +} + +export function ResponsiveDialog({ + open, + onOpenChange, + children, + title, + description, + footer, + className, +}: ResponsiveDialogProps) { + const isDesktop = useMediaQuery("(min-width: 768px)") + + if (isDesktop) { + return ( + + + {(title || description) && ( + + {title && {title}} + {description && {description}} + + )} + {children} + {footer} + + + ) + } + + return ( + + + {(title || description) && ( + + {title && {title}} + {description && {description}} + + )} +
{children}
+ {footer && {footer}} +
+
+ ) +} diff --git a/admin-spa/src/hooks/use-media-query.ts b/admin-spa/src/hooks/use-media-query.ts new file mode 100644 index 0000000..95e552c --- /dev/null +++ b/admin-spa/src/hooks/use-media-query.ts @@ -0,0 +1,19 @@ +import * as React from "react" + +export function useMediaQuery(query: string) { + const [value, setValue] = React.useState(false) + + React.useEffect(() => { + function onChange(event: MediaQueryListEvent) { + setValue(event.matches) + } + + const result = matchMedia(query) + result.addEventListener("change", onChange) + setValue(result.matches) + + return () => result.removeEventListener("change", onChange) + }, [query]) + + return value +}