From 1b9e624004cfb64135b744f877e5fa87e5810abd Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Tue, 9 Jan 2024 08:53:51 -0500 Subject: Ask the user who they are when opening a group for the first time (#7) --- .../[groupId]/expenses/active-user-modal.tsx | 134 +++++++++++++++++++++ src/app/groups/[groupId]/expenses/expense-list.tsx | 14 ++- src/app/groups/[groupId]/expenses/page.tsx | 80 ++++++------ src/components/ui/dialog.tsx | 122 +++++++++++++++++++ src/components/ui/drawer.tsx | 118 ++++++++++++++++++ src/components/ui/radio-group.tsx | 44 +++++++ src/lib/hooks.ts | 42 +++++++ 7 files changed, 513 insertions(+), 41 deletions(-) create mode 100644 src/app/groups/[groupId]/expenses/active-user-modal.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/lib/hooks.ts (limited to 'src') 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>> +} + +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 ( + + + + Who are you? + + Tell us which participant you are to let us customize how the + information is displayed. + + + setOpen(false)} /> + +

+ This setting can be changed later in the group settings. +

+
+
+
+ ) + } + + return ( + + + + Who are you? + + Tell us which participant you are to let us customize how the + information is displayed. + + + setOpen(false)} + /> + +

+ This setting can be changed later in the group settings. +

+
+
+
+ ) +} + +function ActiveUserForm({ + group, + close, + className, +}: ComponentProps<'form'> & { group: Props['group']; close: () => void }) { + const [selected, setSelected] = useState('None') + + return ( +
{ + event.preventDefault() + localStorage.setItem(`${group.id}-activeUser`, selected) + close() + }} + > + +
+
+ + +
+ {group.participants.map((participant) => ( +
+ + +
+ ))} +
+
+ +
+ ) +} 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 ( - -
- - Expenses - - Here are the expenses that you created for your group. - - - - - -
+ <> + +
+ + Expenses + + Here are the expenses that you created for your 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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +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) => ( + +) +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/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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +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(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 +} -- cgit v1.3