Redesign expense form

This commit is contained in:
Sebastien Castiel
2024-01-08 10:22:06 -05:00
parent f4b31c805d
commit c9a92408d7
4 changed files with 164 additions and 100 deletions

31
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@hookform/resolvers": "^3.3.2",
"@prisma/client": "5.6.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
@@ -628,6 +629,36 @@
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz",
"integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==",
"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-id": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "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-collection": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",

View File

@@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.3.2",
"@prisma/client": "5.6.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",

View File

@@ -5,11 +5,16 @@ import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardFooter,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
Form,
FormControl,
@@ -31,6 +36,7 @@ import { getExpense, getGroup } from '@/lib/api'
import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { Save, Trash2 } from 'lucide-react'
import { useSearchParams } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { match } from 'ts-pattern'
@@ -177,89 +183,52 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
</FormItem>
)}
/>
</CardContent>
</Card>
<FormField
control={form.control}
name="splitMode"
render={({ field }) => (
<FormItem className="sm:order-2">
<FormLabel>Split mode</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
form.setValue('splitMode', value as any, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
})
}}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EVENLY">Evenly</SelectItem>
<SelectItem value="BY_SHARES">
Unevenly By shares
</SelectItem>
<SelectItem value="BY_PERCENTAGE">
Unevenly By percentage
</SelectItem>
{/* <SelectItem value="BY_AMOUNT">
Unevenly By amount
</SelectItem> */}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Select how to split the expense.
</FormDescription>
</FormItem>
)}
/>
<Card className="mt-4">
<CardHeader>
<CardTitle className="flex justify-between">
<span>Paid for</span>
<Button
variant="link"
type="button"
className="-my-2 -mx-4"
onClick={() => {
const paidFor = form.getValues().paidFor
const allSelected =
paidFor.length === group.participants.length
const newPaidFor = allSelected
? []
: group.participants.map((p) => ({
participant: p.id,
shares: 1,
}))
form.setValue('paidFor', newPaidFor, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
})
}}
>
{form.getValues().paidFor.length ===
group.participants.length ? (
<>Select none</>
) : (
<>Select all</>
)}
</Button>
</CardTitle>
<CardDescription>
Select who the expense was paid for.
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="paidFor"
render={() => (
<FormItem className="sm:order-4 row-span-2">
<div className="">
<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 newPaidFor = allSelected
? []
: group.participants.map((p) => ({
participant: p.id,
shares: 1,
}))
form.setValue('paidFor', newPaidFor, {
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>
<FormItem className="sm:order-4 row-span-2 space-y-0">
{group.participants.map(({ id, name }) => (
<FormField
key={id}
@@ -271,7 +240,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
data-id={`${id}/${form.getValues().splitMode}/${
group.currency
}`}
className="flex items-center"
className="flex items-center border-t last-of-type:border-b last-of-type:!mb-4 -mx-6 px-6 py-3"
>
<FormItem className="flex-1 flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -293,7 +262,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
}}
/>
</FormControl>
<FormLabel className="text-sm font-normal">
<FormLabel className="text-sm font-normal flex-1">
{name}
</FormLabel>
</FormItem>
@@ -313,7 +282,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
participant === id,
),
)}
className="text-base w-[80px]"
className="text-base w-[80px] -my-2"
type="number"
disabled={
!field.value?.some(
@@ -379,26 +348,78 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
</FormItem>
)}
/>
</CardContent>
<CardFooter className="gap-2">
<SubmitButton
loadingContent={isCreate ? <>Creating</> : <>Saving</>}
>
{isCreate ? <>Create</> : <>Save</>}
</SubmitButton>
{!isCreate && onDelete && (
<AsyncButton
type="button"
variant="destructive"
loadingContent="Deleting…"
action={onDelete}
>
Delete
</AsyncButton>
)}
</CardFooter>
<Collapsible className="mt-5">
<CollapsibleTrigger asChild>
<Button variant="link" className="-mx-4">
Advanced splitting options
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="grid sm:grid-cols-2 gap-6 pt-3">
<FormField
control={form.control}
name="splitMode"
render={({ field }) => (
<FormItem className="sm:order-2">
<FormLabel>Split mode</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
form.setValue('splitMode', value as any, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
})
}}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EVENLY">Evenly</SelectItem>
<SelectItem value="BY_SHARES">
Unevenly By shares
</SelectItem>
<SelectItem value="BY_PERCENTAGE">
Unevenly By percentage
</SelectItem>
{/* <SelectItem value="BY_AMOUNT">
Unevenly By amount
</SelectItem> */}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Select how to split the expense.
</FormDescription>
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</CardContent>
</Card>
<div className="flex mt-4 gap-2">
<SubmitButton
loadingContent={isCreate ? <>Creating</> : <>Saving</>}
>
<Save className="w-4 h-4 mr-2" />
{isCreate ? <>Create</> : <>Save</>}
</SubmitButton>
{!isCreate && onDelete && (
<AsyncButton
type="button"
variant="destructive"
loadingContent="Deleting…"
action={onDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</AsyncButton>
)}
</div>
</form>
</Form>
)

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }