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", "@hookform/resolvers": "^3.3.2",
"@prisma/client": "5.6.0", "@prisma/client": "5.6.0",
"@radix-ui/react-checkbox": "^1.0.4", "@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-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0", "@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": { "node_modules/@radix-ui/react-collection": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", "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", "@hookform/resolvers": "^3.3.2",
"@prisma/client": "5.6.0", "@prisma/client": "5.6.0",
"@radix-ui/react-checkbox": "^1.0.4", "@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-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",

View File

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