From ed55c696cd083bfaa5429082a9c5edd4ebde0cd8 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Tue, 5 Dec 2023 17:39:05 -0500 Subject: First version --- src/components/expense-form.tsx | 186 ++++++++++++++++++++++++++++++++++++++++ src/components/group-form.tsx | 136 +++++++++++++++++++++++++++++ src/components/ui/badge.tsx | 36 ++++++++ src/components/ui/button.tsx | 56 ++++++++++++ src/components/ui/card.tsx | 79 +++++++++++++++++ src/components/ui/checkbox.tsx | 30 +++++++ src/components/ui/form.tsx | 176 +++++++++++++++++++++++++++++++++++++ src/components/ui/input.tsx | 25 ++++++ src/components/ui/label.tsx | 26 ++++++ src/components/ui/select.tsx | 160 ++++++++++++++++++++++++++++++++++ src/components/ui/table.tsx | 117 +++++++++++++++++++++++++ 11 files changed, 1027 insertions(+) create mode 100644 src/components/expense-form.tsx create mode 100644 src/components/group-form.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/table.tsx (limited to 'src/components') diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx new file mode 100644 index 0000000..056d25e --- /dev/null +++ b/src/components/expense-form.tsx @@ -0,0 +1,186 @@ +'use client' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { getExpense, getGroup } from '@/lib/api' +import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' + +export type Props = { + group: NonNullable>> + expense?: NonNullable>> + onSubmit: (values: ExpenseFormValues) => void +} + +export function ExpenseForm({ group, expense, onSubmit }: Props) { + const form = useForm({ + resolver: zodResolver(expenseFormSchema), + defaultValues: expense + ? { + title: expense.title, + amount: expense.amount, + paidBy: expense.paidById, + paidFor: expense.paidFor.map(({ participantId }) => participantId), + } + : { title: '', amount: 0, paidFor: [] }, + }) + + return ( +
+ onSubmit(values))}> + + + Expense information + + + ( + + Expense title + + + + + Enter a description for the expense. + + + + )} + /> + + ( + + Paid by + + + Select the participant who paid the expense. + + + + )} + /> + + ( + + Amount + + + + Enter the expense amount. + + + )} + /> + + ( + +
+ Paid for + + Select who the expense was paid for. + +
+ {group.participants.map(({ id, name }) => ( + { + return ( + + + { + return checked + ? field.onChange([...field.value, id]) + : field.onChange( + field.value?.filter( + (value) => value !== id, + ), + ) + }} + /> + + + {name} + + + ) + }} + /> + ))} + +
+ )} + /> +
+ + + + +
+
+ + ) +} diff --git a/src/components/group-form.tsx b/src/components/group-form.tsx new file mode 100644 index 0000000..0b6e560 --- /dev/null +++ b/src/components/group-form.tsx @@ -0,0 +1,136 @@ +'use client' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { getGroup } from '@/lib/api' +import { GroupFormValues, groupFormSchema } from '@/lib/schemas' +import { zodResolver } from '@hookform/resolvers/zod' +import { useFieldArray, useForm } from 'react-hook-form' + +export type Props = { + group?: NonNullable>> + onSubmit: (groupFormValues: GroupFormValues) => void +} + +export function GroupForm({ group, onSubmit }: Props) { + const form = useForm({ + resolver: zodResolver(groupFormSchema), + defaultValues: group + ? { + name: group.name, + participants: group.participants, + } + : { + name: '', + participants: [{ name: 'John' }, { name: 'Jane' }, { name: 'Jack' }], + }, + }) + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'participants', + }) + + return ( +
+ { + onSubmit(values) + })} + className="space-y-8" + > + + + Group information + + + ( + + Group name + + + + + Enter a name for your group. + + + + )} + /> + + + + + Participants + + Enter the name for each participant + + + +
    + {fields.map((item, index) => ( +
  • + ( + + + Participant #{index + 1} + + +
    + + +
    +
    + +
    + )} + /> +
  • + ))} +
+
+ + + +
+ + +
+ + ) +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +