mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-09 21:49:06 +01:00
Redesign expense form
This commit is contained in:
31
package-lock.json
generated
31
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
11
src/components/ui/collapsible.tsx
Normal file
11
src/components/ui/collapsible.tsx
Normal 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 }
|
||||||
Reference in New Issue
Block a user