diff options
| -rw-r--r-- | package-lock.json | 83 | ||||
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/active-user-modal.tsx | 134 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/expense-list.tsx | 14 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/page.tsx | 80 | ||||
| -rw-r--r-- | src/components/ui/dialog.tsx | 122 | ||||
| -rw-r--r-- | src/components/ui/drawer.tsx | 118 | ||||
| -rw-r--r-- | src/components/ui/radio-group.tsx | 44 | ||||
| -rw-r--r-- | src/lib/hooks.ts | 42 |
9 files changed, 599 insertions, 41 deletions
diff --git a/package-lock.json b/package-lock.json index fcfcbfe..aec9969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@prisma/client": "5.6.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", @@ -37,6 +39,7 @@ "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.0.6", "uuid": "^9.0.1", + "vaul": "^0.8.0", "zod": "^3.22.4" }, "devDependencies": { @@ -720,6 +723,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -1096,6 +1135,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -6037,6 +6108,18 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vaul": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.8.0.tgz", + "integrity": "sha512-9nUU2jIObJvJZxeQU1oVr/syKo5XqbRoOMoTEt0hHlWify4QZFlqTh6QSN/yxoKzNrMeEQzxbc3XC/vkPLOIqw==", + "dependencies": { + "@radix-ui/react-dialog": "^1.0.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index e46173f..4381ff0 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "@prisma/client": "5.6.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", @@ -38,6 +40,7 @@ "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.0.6", "uuid": "^9.0.1", + "vaul": "^0.8.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/app/groups/[groupId]/expenses/active-user-modal.tsx b/src/app/groups/[groupId]/expenses/active-user-modal.tsx new file mode 100644 index 0000000..5d9d005 --- /dev/null +++ b/src/app/groups/[groupId]/expenses/active-user-modal.tsx @@ -0,0 +1,134 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, +} from '@/components/ui/drawer' +import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { getGroup } from '@/lib/api' +import { useMediaQuery } from '@/lib/hooks' +import { cn } from '@/lib/utils' +import { ComponentProps, useEffect, useState } from 'react' + +type Props = { + group: NonNullable<Awaited<ReturnType<typeof getGroup>>> +} + +export function ActiveUserModal({ group }: Props) { + const [open, setOpen] = useState(false) + const isDesktop = useMediaQuery('(min-width: 768px)') + + useEffect(() => { + const tempUser = localStorage.getItem(`newGroup-activeUser`) + const activeUser = localStorage.getItem(`${group.id}-activeUser`) + if (!tempUser && !activeUser) { + setOpen(true) + } + }, [group]) + + function updateOpen(open: boolean) { + if (!open && !localStorage.getItem(`${group.id}-activeUser`)) { + localStorage.setItem(`${group.id}-activeUser`, 'None') + } + setOpen(open) + } + + if (isDesktop) { + return ( + <Dialog open={open} onOpenChange={updateOpen}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle>Who are you?</DialogTitle> + <DialogDescription> + Tell us which participant you are to let us customize how the + information is displayed. + </DialogDescription> + </DialogHeader> + <ActiveUserForm group={group} close={() => setOpen(false)} /> + <DialogFooter className="sm:justify-center"> + <p className="text-sm text-center text-muted-foreground"> + This setting can be changed later in the group settings. + </p> + </DialogFooter> + </DialogContent> + </Dialog> + ) + } + + return ( + <Drawer open={open} onOpenChange={updateOpen}> + <DrawerContent> + <DrawerHeader className="text-left"> + <DrawerTitle>Who are you?</DrawerTitle> + <DrawerDescription> + Tell us which participant you are to let us customize how the + information is displayed. + </DrawerDescription> + </DrawerHeader> + <ActiveUserForm + className="px-4" + group={group} + close={() => setOpen(false)} + /> + <DrawerFooter className="pt-2"> + <p className="text-sm text-center text-muted-foreground"> + This setting can be changed later in the group settings. + </p> + </DrawerFooter> + </DrawerContent> + </Drawer> + ) +} + +function ActiveUserForm({ + group, + close, + className, +}: ComponentProps<'form'> & { group: Props['group']; close: () => void }) { + const [selected, setSelected] = useState('None') + + return ( + <form + className={cn('grid items-start gap-4', className)} + onSubmit={(event) => { + event.preventDefault() + localStorage.setItem(`${group.id}-activeUser`, selected) + close() + }} + > + <RadioGroup defaultValue="none" onValueChange={setSelected}> + <div className="flex flex-col gap-4 my-4"> + <div className="flex items-center space-x-2"> + <RadioGroupItem value="none" id="none" /> + <Label htmlFor="none" className="italic font-normal flex-1"> + I don’t want to select anyone + </Label> + </div> + {group.participants.map((participant) => ( + <div key={participant.id} className="flex items-center space-x-2"> + <RadioGroupItem value={participant.id} id={participant.id} /> + <Label htmlFor={participant.id} className="flex-1"> + {participant.name} + </Label> + </div> + ))} + </div> + </RadioGroup> + <Button type="submit">Save changes</Button> + </form> + ) +} diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index ef4903c..4578300 100644 --- a/src/app/groups/[groupId]/expenses/expense-list.tsx +++ b/src/app/groups/[groupId]/expenses/expense-list.tsx @@ -27,11 +27,15 @@ export function ExpenseList({ if (activeUser || newUser) { localStorage.removeItem('newGroup-activeUser') localStorage.removeItem(`${groupId}-newUser`) - const userId = participants.find( - (p) => p.name === (activeUser || newUser), - )?.id - if (userId) { - localStorage.setItem(`${groupId}-activeUser`, userId) + if (activeUser === 'None') { + localStorage.setItem(`${groupId}-activeUser`, 'None') + } else { + const userId = participants.find( + (p) => p.name === (activeUser || newUser), + )?.id + if (userId) { + localStorage.setItem(`${groupId}-activeUser`, userId) + } } } }, [groupId, participants]) diff --git a/src/app/groups/[groupId]/expenses/page.tsx b/src/app/groups/[groupId]/expenses/page.tsx index 5285cb8..2a1ac5f 100644 --- a/src/app/groups/[groupId]/expenses/page.tsx +++ b/src/app/groups/[groupId]/expenses/page.tsx @@ -1,3 +1,4 @@ +import { ActiveUserModal } from '@/app/groups/[groupId]/expenses/active-user-modal' import { ExpenseList } from '@/app/groups/[groupId]/expenses/expense-list' import { Button } from '@/components/ui/button' import { @@ -24,45 +25,52 @@ export default async function GroupExpensesPage({ }: { params: { groupId: string } }) { + const group = await getGroup(groupId) + if (!group) notFound() + return ( - <Card className="mb-4"> - <div className="flex flex-1"> - <CardHeader className="flex-1"> - <CardTitle>Expenses</CardTitle> - <CardDescription> - Here are the expenses that you created for your group. - </CardDescription> - </CardHeader> - <CardHeader> - <Button asChild size="icon"> - <Link href={`/groups/${groupId}/expenses/create`}> - <Plus /> - </Link> - </Button> - </CardHeader> - </div> + <> + <Card className="mb-4"> + <div className="flex flex-1"> + <CardHeader className="flex-1"> + <CardTitle>Expenses</CardTitle> + <CardDescription> + Here are the expenses that you created for your group. + </CardDescription> + </CardHeader> + <CardHeader> + <Button asChild size="icon"> + <Link href={`/groups/${groupId}/expenses/create`}> + <Plus /> + </Link> + </Button> + </CardHeader> + </div> - <CardContent className="p-0"> - <Suspense - fallback={[0, 1, 2].map((i) => ( - <div - key={i} - className="border-t flex justify-between items-center px-6 py-4 text-sm" - > - <div className="flex flex-col gap-2"> - <Skeleton className="h-4 w-16 rounded-full" /> - <Skeleton className="h-4 w-32 rounded-full" /> + <CardContent className="p-0"> + <Suspense + fallback={[0, 1, 2].map((i) => ( + <div + key={i} + className="border-t flex justify-between items-center px-6 py-4 text-sm" + > + <div className="flex flex-col gap-2"> + <Skeleton className="h-4 w-16 rounded-full" /> + <Skeleton className="h-4 w-32 rounded-full" /> + </div> + <div> + <Skeleton className="h-4 w-16 rounded-full" /> + </div> </div> - <div> - <Skeleton className="h-4 w-16 rounded-full" /> - </div> - </div> - ))} - > - <Expenses groupId={groupId} /> - </Suspense> - </CardContent> - </Card> + ))} + > + <Expenses groupId={groupId} /> + </Suspense> + </CardContent> + </Card> + + <ActiveUserModal group={group} /> + </> ) } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..01ff19c --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Overlay + ref={ref} + className={cn( + "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + className + )} + {...props} + /> +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> +>(({ className, children, ...props }, ref) => ( + <DialogPortal> + <DialogOverlay /> + <DialogPrimitive.Content + ref={ref} + className={cn( + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + className + )} + {...props} + > + {children} + <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> + <X className="h-4 w-4" /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + </DialogPrimitive.Content> + </DialogPortal> +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col space-y-1.5 text-center sm:text-left", + className + )} + {...props} + /> +) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", + className + )} + {...props} + /> +) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Title + ref={ref} + className={cn( + "text-lg font-semibold leading-none tracking-tight", + className + )} + {...props} + /> +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef<typeof DialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> +>(({ className, ...props }, ref) => ( + <DialogPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..6a0ef53 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,118 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( + <DrawerPrimitive.Root + shouldScaleBackground={shouldScaleBackground} + {...props} + /> +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Overlay + ref={ref} + className={cn("fixed inset-0 z-50 bg-black/80", className)} + {...props} + /> +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> +>(({ className, children, ...props }, ref) => ( + <DrawerPortal> + <DrawerOverlay /> + <DrawerPrimitive.Content + ref={ref} + className={cn( + "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", + className + )} + {...props} + > + <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> + {children} + </DrawerPrimitive.Content> + </DrawerPortal> +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} + {...props} + /> +) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> +) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Title + ref={ref} + className={cn( + "text-lg font-semibold leading-none tracking-tight", + className + )} + {...props} + /> +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..e9bde17 --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,44 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef<typeof RadioGroupPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <RadioGroupPrimitive.Root + className={cn("grid gap-2", className)} + {...props} + ref={ref} + /> + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef<typeof RadioGroupPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> +>(({ className, ...props }, ref) => { + return ( + <RadioGroupPrimitive.Item + ref={ref} + className={cn( + "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + > + <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> + <Circle className="h-2.5 w-2.5 fill-current text-current" /> + </RadioGroupPrimitive.Indicator> + </RadioGroupPrimitive.Item> + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts new file mode 100644 index 0000000..02e2543 --- /dev/null +++ b/src/lib/hooks.ts @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react' + +export function useMediaQuery(query: string): boolean { + const getMatches = (query: string): boolean => { + // Prevents SSR issues + if (typeof window !== 'undefined') { + return window.matchMedia(query).matches + } + return false + } + + const [matches, setMatches] = useState<boolean>(getMatches(query)) + + function handleChange() { + setMatches(getMatches(query)) + } + + useEffect(() => { + const matchMedia = window.matchMedia(query) + + // Triggered at the first client-side load and if query changes + handleChange() + + // Listen matchMedia + if (matchMedia.addListener) { + matchMedia.addListener(handleChange) + } else { + matchMedia.addEventListener('change', handleChange) + } + + return () => { + if (matchMedia.removeListener) { + matchMedia.removeListener(handleChange) + } else { + matchMedia.removeEventListener('change', handleChange) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]) + + return matches +} |
