aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorSebastien Castiel <sebastien@castiel.me>2023-12-19 09:44:09 -0500
committerSebastien Castiel <sebastien@castiel.me>2023-12-19 09:44:09 -0500
commitf881aff5f9993c3a6b4b0d8ed7e67d2f812f39d6 (patch)
tree685731e70ad838dbc2b3d70450e090e1e5fc31c9 /src/components
parent1e66efe5169dfad9a0c62ba42fbf31ce977bf49e (diff)
Revert "Use modal dialogs for expense creation & edition (#10)"
This reverts commit 1e66efe5169dfad9a0c62ba42fbf31ce977bf49e.
Diffstat (limited to 'src/components')
-rw-r--r--src/components/expense-form.tsx388
-rw-r--r--src/components/ui/dialog.tsx122
2 files changed, 193 insertions, 317 deletions
diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx
index 0f677e6..bdee9d0 100644
--- a/src/components/expense-form.tsx
+++ b/src/components/expense-form.tsx
@@ -1,12 +1,14 @@
'use client'
-import {
- createExpenseAction,
- deleteExpenseAction,
- updateExpenseAction,
-} from '@/app/groups/[groupId]/expenses/actions'
import { AsyncButton } from '@/components/async-button'
import { SubmitButton } from '@/components/submit-button'
import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
@@ -28,15 +30,17 @@ import {
import { getExpense, getGroup } from '@/lib/api'
import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas'
import { zodResolver } from '@hookform/resolvers/zod'
-import { useRouter, useSearchParams } from 'next/navigation'
+import { useSearchParams } from 'next/navigation'
import { useForm } from 'react-hook-form'
export type Props = {
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
+ onSubmit: (values: ExpenseFormValues) => Promise<void>
+ onDelete?: () => Promise<void>
}
-export function ExpenseForm({ group, expense }: Props) {
+export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
const isCreate = expense === undefined
const searchParams = useSearchParams()
const form = useForm<ExpenseFormValues>({
@@ -61,210 +65,204 @@ export function ExpenseForm({ group, expense }: Props) {
}
: { title: '', amount: 0, paidFor: [], isReimbursement: false },
})
- const router = useRouter()
return (
<Form {...form}>
- <form
- onSubmit={form.handleSubmit(async (values) => {
- if (expense) {
- await updateExpenseAction(group.id, expense.id, values)
- } else {
- await createExpenseAction(group.id, values)
- }
- router.back()
- })}
- >
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
- <FormField
- control={form.control}
- name="title"
- render={({ field }) => (
- <FormItem className="order-1">
- <FormLabel>Expense title</FormLabel>
- <FormControl>
- <Input
- placeholder="Monday evening restaurant"
- className="text-base"
- {...field}
- />
- </FormControl>
- <FormDescription>
- Enter a description for the expense.
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="paidBy"
- render={({ field }) => (
- <FormItem className="order-3 sm:order-2">
- <FormLabel>Paid by</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- >
- <SelectTrigger>
- <SelectValue placeholder="Select a participant" />
- </SelectTrigger>
- <SelectContent>
- {group.participants.map(({ id, name }) => (
- <SelectItem key={id} value={id}>
- {name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormDescription>
- Select the participant who paid the expense.
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="amount"
- render={({ field }) => (
- <FormItem className="order-2 sm:order-3">
- <FormLabel>Amount</FormLabel>
- <div className="flex items-baseline gap-2">
- <span>{group.currency}</span>
+ <form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
+ <Card>
+ <CardHeader>
+ <CardTitle>
+ {isCreate ? <>Create expense</> : <>Edit expense</>}
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="grid grid-cols-1 sm:grid-cols-2 gap-6">
+ <FormField
+ control={form.control}
+ name="title"
+ render={({ field }) => (
+ <FormItem className="order-1">
+ <FormLabel>Expense title</FormLabel>
<FormControl>
<Input
- className="text-base max-w-[120px]"
- type="number"
- inputMode="decimal"
- step={0.01}
- placeholder="0.00"
+ placeholder="Monday evening restaurant"
+ className="text-base"
{...field}
/>
</FormControl>
- </div>
- <FormMessage />
-
- <FormField
- control={form.control}
- name="isReimbursement"
- render={({ field }) => (
- <FormItem className="flex flex-row gap-2 items-center space-y-0 pt-2">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div>
- <FormLabel>This is a reimbursement</FormLabel>
- </div>
- </FormItem>
- )}
- />
- </FormItem>
- )}
- />
+ <FormDescription>
+ Enter a description for the expense.
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
- <FormField
- control={form.control}
- name="paidFor"
- render={() => (
- <FormItem className="order-5">
- <div className="mb-4">
- <FormLabel>
- Paid for
- <Button
- variant="link"
- type="button"
- className="-m-2"
- onClick={() => {
- const paidFor = form.getValues().paidFor
- const allSelected =
- paidFor.length === group.participants.length
- const newPairFor = allSelected
- ? []
- : group.participants.map((p) => p.id)
- form.setValue('paidFor', newPairFor, {
- shouldDirty: true,
- shouldTouch: true,
- shouldValidate: true,
- })
- }}
- >
- {form.getValues().paidFor.length ===
- group.participants.length ? (
- <>Select none</>
- ) : (
- <>Select all</>
- )}
- </Button>
- </FormLabel>
+ <FormField
+ control={form.control}
+ name="paidBy"
+ render={({ field }) => (
+ <FormItem className="order-3 sm:order-2">
+ <FormLabel>Paid by</FormLabel>
+ <Select
+ onValueChange={field.onChange}
+ defaultValue={field.value}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="Select a participant" />
+ </SelectTrigger>
+ <SelectContent>
+ {group.participants.map(({ id, name }) => (
+ <SelectItem key={id} value={id}>
+ {name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
<FormDescription>
- Select who the expense was paid for.
+ Select the participant who paid the expense.
</FormDescription>
- </div>
- {group.participants.map(({ id, name }) => (
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="amount"
+ render={({ field }) => (
+ <FormItem className="order-2 sm:order-3">
+ <FormLabel>Amount</FormLabel>
+ <div className="flex items-baseline gap-2">
+ <span>{group.currency}</span>
+ <FormControl>
+ <Input
+ className="text-base max-w-[120px]"
+ type="number"
+ inputMode="decimal"
+ step={0.01}
+ placeholder="0.00"
+ {...field}
+ />
+ </FormControl>
+ </div>
+ <FormMessage />
+
<FormField
- key={id}
control={form.control}
- name="paidFor"
- render={({ field }) => {
- return (
- <FormItem
- key={id}
- className="flex flex-row items-start space-x-3 space-y-0"
- >
- <FormControl>
- <Checkbox
- checked={field.value?.includes(id)}
- onCheckedChange={(checked) => {
- return checked
- ? field.onChange([...field.value, id])
- : field.onChange(
- field.value?.filter(
- (value) => value !== id,
- ),
- )
- }}
- />
- </FormControl>
- <FormLabel className="text-sm font-normal">
- {name}
- </FormLabel>
- </FormItem>
- )
- }}
+ name="isReimbursement"
+ render={({ field }) => (
+ <FormItem className="flex flex-row gap-2 items-center space-y-0 pt-2">
+ <FormControl>
+ <Checkbox
+ checked={field.value}
+ onCheckedChange={field.onChange}
+ />
+ </FormControl>
+ <div>
+ <FormLabel>This is a reimbursement</FormLabel>
+ </div>
+ </FormItem>
+ )}
/>
- ))}
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
+ </FormItem>
+ )}
+ />
- <div className="mt-6 flex gap-2">
- <SubmitButton
- loadingContent={isCreate ? <>Creating…</> : <>Saving…</>}
- >
- {isCreate ? <>Create</> : <>Save</>}
- </SubmitButton>
- {!isCreate && (
- <AsyncButton
- type="button"
- variant="destructive"
- loadingContent="Deleting…"
- action={async () => {
- await deleteExpenseAction(group.id, expense.id)
- router.back()
- }}
+ <FormField
+ control={form.control}
+ name="paidFor"
+ render={() => (
+ <FormItem className="order-5">
+ <div className="mb-4">
+ <FormLabel>
+ Paid for
+ <Button
+ variant="link"
+ type="button"
+ className="-m-2"
+ onClick={() => {
+ const paidFor = form.getValues().paidFor
+ const allSelected =
+ paidFor.length === group.participants.length
+ const newPairFor = allSelected
+ ? []
+ : group.participants.map((p) => p.id)
+ form.setValue('paidFor', newPairFor, {
+ shouldDirty: true,
+ shouldTouch: true,
+ shouldValidate: true,
+ })
+ }}
+ >
+ {form.getValues().paidFor.length ===
+ group.participants.length ? (
+ <>Select none</>
+ ) : (
+ <>Select all</>
+ )}
+ </Button>
+ </FormLabel>
+ <FormDescription>
+ Select who the expense was paid for.
+ </FormDescription>
+ </div>
+ {group.participants.map(({ id, name }) => (
+ <FormField
+ key={id}
+ control={form.control}
+ name="paidFor"
+ render={({ field }) => {
+ return (
+ <FormItem
+ key={id}
+ className="flex flex-row items-start space-x-3 space-y-0"
+ >
+ <FormControl>
+ <Checkbox
+ checked={field.value?.includes(id)}
+ onCheckedChange={(checked) => {
+ return checked
+ ? field.onChange([...field.value, id])
+ : field.onChange(
+ field.value?.filter(
+ (value) => value !== id,
+ ),
+ )
+ }}
+ />
+ </FormControl>
+ <FormLabel className="text-sm font-normal">
+ {name}
+ </FormLabel>
+ </FormItem>
+ )
+ }}
+ />
+ ))}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+
+ <CardFooter className="gap-2">
+ <SubmitButton
+ loadingContent={isCreate ? <>Creating…</> : <>Saving…</>}
>
- Delete
- </AsyncButton>
- )}
- </div>
+ {isCreate ? <>Create</> : <>Save</>}
+ </SubmitButton>
+ {!isCreate && onDelete && (
+ <AsyncButton
+ type="button"
+ variant="destructive"
+ loadingContent="Deleting…"
+ action={onDelete}
+ >
+ Delete
+ </AsyncButton>
+ )}
+ </CardFooter>
+ </Card>
</form>
</Form>
)
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
deleted file mode 100644
index cad6f58..0000000
--- a/src/components/ui/dialog.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-"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-background/80 backdrop-blur-sm 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,
-}