Skip to content

Commit 6e71d59

Browse files
committed
Merge remote-tracking branch 'base/main'
2 parents e85c993 + 21d0c02 commit 6e71d59

File tree

12 files changed

+237
-89
lines changed

12 files changed

+237
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,21 @@
1-
import { cached } from '@/app/cached-functions'
2-
import { ExpenseForm } from '@/components/expense-form'
3-
import {
4-
deleteExpense,
5-
getCategories,
6-
getExpense,
7-
updateExpense,
8-
} from '@/lib/api'
1+
import { EditExpenseForm } from '@/app/groups/[groupId]/expenses/edit-expense-form'
92
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
10-
import { expenseFormSchema } from '@/lib/schemas'
113
import { Metadata } from 'next'
12-
import { notFound, redirect } from 'next/navigation'
13-
import { Suspense } from 'react'
144

155
export const metadata: Metadata = {
16-
title: 'Edit expense',
6+
title: 'Edit Expense',
177
}
188

199
export default async function EditExpensePage({
2010
params: { groupId, expenseId },
2111
}: {
2212
params: { groupId: string; expenseId: string }
2313
}) {
24-
const categories = await getCategories()
25-
const group = await cached.getGroup(groupId)
26-
if (!group) notFound()
27-
const expense = await getExpense(groupId, expenseId)
28-
if (!expense) notFound()
29-
30-
async function updateExpenseAction(values: unknown, participantId?: string) {
31-
'use server'
32-
const expenseFormValues = expenseFormSchema.parse(values)
33-
await updateExpense(groupId, expenseId, expenseFormValues, participantId)
34-
redirect(`/groups/${groupId}`)
35-
}
36-
37-
async function deleteExpenseAction(participantId?: string) {
38-
'use server'
39-
await deleteExpense(groupId, expenseId, participantId)
40-
redirect(`/groups/${groupId}`)
41-
}
42-
4314
return (
44-
<Suspense>
45-
<ExpenseForm
46-
group={group}
47-
expense={expense}
48-
categories={categories}
49-
onSubmit={updateExpenseAction}
50-
onDelete={deleteExpenseAction}
51-
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
52-
/>
53-
</Suspense>
15+
<EditExpenseForm
16+
groupId={groupId}
17+
expenseId={expenseId}
18+
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
19+
/>
5420
)
5521
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use client'
2+
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
3+
import { trpc } from '@/trpc/client'
4+
import { useRouter } from 'next/navigation'
5+
import { ExpenseForm } from './expense-form'
6+
7+
export function CreateExpenseForm({
8+
groupId,
9+
runtimeFeatureFlags,
10+
}: {
11+
groupId: string
12+
expenseId?: string
13+
runtimeFeatureFlags: RuntimeFeatureFlags
14+
}) {
15+
const { data: groupData } = trpc.groups.get.useQuery({ groupId })
16+
const group = groupData?.group
17+
18+
const { data: categoriesData } = trpc.categories.list.useQuery()
19+
const categories = categoriesData?.categories
20+
21+
const { mutateAsync: createExpenseMutateAsync } =
22+
trpc.groups.expenses.create.useMutation()
23+
24+
const utils = trpc.useUtils()
25+
const router = useRouter()
26+
27+
if (!group || !categories) return null
28+
29+
return (
30+
<ExpenseForm
31+
group={group}
32+
categories={categories}
33+
onSubmit={async (expenseFormValues, participantId) => {
34+
await createExpenseMutateAsync({
35+
groupId,
36+
expenseFormValues,
37+
participantId,
38+
})
39+
utils.groups.expenses.invalidate()
40+
router.push(`/groups/${group.id}`)
41+
}}
42+
runtimeFeatureFlags={runtimeFeatureFlags}
43+
/>
44+
)
45+
}
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,20 @@
1-
import { cached } from '@/app/cached-functions'
2-
import { ExpenseForm } from '@/components/expense-form'
3-
import { createExpense, getCategories } from '@/lib/api'
1+
import { CreateExpenseForm } from '@/app/groups/[groupId]/expenses/create-expense-form'
42
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
5-
import { expenseFormSchema } from '@/lib/schemas'
63
import { Metadata } from 'next'
7-
import { notFound, redirect } from 'next/navigation'
8-
import { Suspense } from 'react'
94

105
export const metadata: Metadata = {
11-
title: 'Create expense',
6+
title: 'Create Expense',
127
}
138

149
export default async function ExpensePage({
1510
params: { groupId },
1611
}: {
1712
params: { groupId: string }
1813
}) {
19-
const categories = await getCategories()
20-
const group = await cached.getGroup(groupId)
21-
if (!group) notFound()
22-
23-
async function createExpenseAction(values: unknown, participantId?: string) {
24-
'use server'
25-
const expenseFormValues = expenseFormSchema.parse(values)
26-
await createExpense(expenseFormValues, groupId, participantId)
27-
redirect(`/groups/${groupId}`)
28-
}
29-
3014
return (
31-
<Suspense>
32-
<ExpenseForm
33-
group={group}
34-
categories={categories}
35-
onSubmit={createExpenseAction}
36-
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
37-
/>
38-
</Suspense>
15+
<CreateExpenseForm
16+
groupId={groupId}
17+
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
18+
/>
3919
)
4020
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client'
2+
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
3+
import { trpc } from '@/trpc/client'
4+
import { useRouter } from 'next/navigation'
5+
import { ExpenseForm } from './expense-form'
6+
7+
export function EditExpenseForm({
8+
groupId,
9+
expenseId,
10+
runtimeFeatureFlags,
11+
}: {
12+
groupId: string
13+
expenseId: string
14+
runtimeFeatureFlags: RuntimeFeatureFlags
15+
}) {
16+
const { data: groupData } = trpc.groups.get.useQuery({ groupId })
17+
const group = groupData?.group
18+
19+
const { data: categoriesData } = trpc.categories.list.useQuery()
20+
const categories = categoriesData?.categories
21+
22+
const { data: expenseData } = trpc.groups.expenses.get.useQuery({
23+
groupId,
24+
expenseId,
25+
})
26+
const expense = expenseData?.expense
27+
28+
const { mutateAsync: updateExpenseMutateAsync } =
29+
trpc.groups.expenses.update.useMutation()
30+
const { mutateAsync: deleteExpenseMutateAsync } =
31+
trpc.groups.expenses.delete.useMutation()
32+
33+
const utils = trpc.useUtils()
34+
const router = useRouter()
35+
36+
if (!group || !categories || !expense) return null
37+
38+
return (
39+
<ExpenseForm
40+
group={group}
41+
expense={expense}
42+
categories={categories}
43+
onSubmit={async (expenseFormValues, participantId) => {
44+
await updateExpenseMutateAsync({
45+
expenseId,
46+
groupId,
47+
expenseFormValues,
48+
participantId,
49+
})
50+
utils.groups.expenses.invalidate()
51+
router.push(`/groups/${group.id}`)
52+
}}
53+
onDelete={async (participantId) => {
54+
await deleteExpenseMutateAsync({
55+
expenseId,
56+
groupId,
57+
participantId,
58+
})
59+
utils.groups.expenses.invalidate()
60+
router.push(`/groups/${group.id}`)
61+
}}
62+
runtimeFeatureFlags={runtimeFeatureFlags}
63+
/>
64+
)
65+
}

src/components/expense-form.tsx src/app/groups/[groupId]/expenses/expense-form.tsx

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
'use client'
21
import { CategorySelector } from '@/components/category-selector'
32
import { ExpenseDocumentsInput } from '@/components/expense-documents-input'
43
import { SubmitButton } from '@/components/submit-button'
@@ -34,7 +33,7 @@ import {
3433
SelectTrigger,
3534
SelectValue,
3635
} from '@/components/ui/select'
37-
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
36+
import { randomId } from '@/lib/api'
3837
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
3938
import { useActiveUser } from '@/lib/hooks'
4039
import {
@@ -43,6 +42,7 @@ import {
4342
expenseFormSchema,
4443
} from '@/lib/schemas'
4544
import { cn } from '@/lib/utils'
45+
import { AppRouterOutput } from '@/trpc/routers/_app'
4646
import { zodResolver } from '@hookform/resolvers/zod'
4747
import { Save } from 'lucide-react'
4848
import { useTranslations } from 'next-intl'
@@ -51,18 +51,9 @@ import { useSearchParams } from 'next/navigation'
5151
import { useEffect, useState } from 'react'
5252
import { useForm } from 'react-hook-form'
5353
import { match } from 'ts-pattern'
54-
import { DeletePopup } from './delete-popup'
55-
import { extractCategoryFromTitle } from './expense-form-actions'
56-
import { Textarea } from './ui/textarea'
57-
58-
export type Props = {
59-
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
60-
expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
61-
categories: NonNullable<Awaited<ReturnType<typeof getCategories>>>
62-
onSubmit: (values: ExpenseFormValues, participantId?: string) => Promise<void>
63-
onDelete?: (participantId?: string) => Promise<void>
64-
runtimeFeatureFlags: RuntimeFeatureFlags
65-
}
54+
import { DeletePopup } from '../../../../components/delete-popup'
55+
import { extractCategoryFromTitle } from '../../../../components/expense-form-actions'
56+
import { Textarea } from '../../../../components/ui/textarea'
6657

6758
const enforceCurrencyPattern = (value: string) =>
6859
value
@@ -73,7 +64,9 @@ const enforceCurrencyPattern = (value: string) =>
7364
.replace(/#/, '.') // change back # to dot
7465
.replace(/[^-\d.]/g, '') // remove all non-numeric characters
7566

76-
const getDefaultSplittingOptions = (group: Props['group']) => {
67+
const getDefaultSplittingOptions = (
68+
group: AppRouterOutput['groups']['get']['group'],
69+
) => {
7770
const defaultValue = {
7871
splitMode: 'EVENLY' as const,
7972
paidFor: group.participants.map(({ id }) => ({
@@ -147,15 +140,23 @@ async function persistDefaultSplittingOptions(
147140

148141
export function ExpenseForm({
149142
group,
150-
expense,
151143
categories,
144+
expense,
152145
onSubmit,
153146
onDelete,
154147
runtimeFeatureFlags,
155-
}: Props) {
148+
}: {
149+
group: AppRouterOutput['groups']['get']['group']
150+
categories: AppRouterOutput['categories']['list']['categories']
151+
expense?: AppRouterOutput['groups']['expenses']['get']['expense']
152+
onSubmit: (value: ExpenseFormValues, participantId?: string) => Promise<void>
153+
onDelete?: (participantId?: string) => Promise<void>
154+
runtimeFeatureFlags: RuntimeFeatureFlags
155+
}) {
156156
const t = useTranslations('ExpenseForm')
157157
const isCreate = expense === undefined
158158
const searchParams = useSearchParams()
159+
159160
const getSelectedPayer = (field?: { value: string }) => {
160161
if (isCreate && typeof window !== 'undefined') {
161162
const activeUser = localStorage.getItem(`${group.id}-activeUser`)

src/components/delete-popup.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use client'
2-
31
import { Trash2 } from 'lucide-react'
42
import { useTranslations } from 'next-intl'
53
import { AsyncButton } from './async-button'

src/lib/hooks.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ export function useLocalStorageState<T>(
8080
/**
8181
* @returns The active user, or `null` until it is fetched from local storage
8282
*/
83-
export function useActiveUser(groupId: string) {
83+
export function useActiveUser(groupId?: string) {
8484
const [activeUser, setActiveUser] = useState<string | null>(null)
8585

8686
useEffect(() => {
87-
const activeUser = localStorage.getItem(`${groupId}-activeUser`)
88-
if (activeUser) setActiveUser(activeUser)
87+
if (groupId) {
88+
const activeUser = localStorage.getItem(`${groupId}-activeUser`)
89+
if (activeUser) setActiveUser(activeUser)
90+
}
8991
}, [groupId])
9092

9193
return activeUser
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createExpense } from '@/lib/api'
2+
import { expenseFormSchema } from '@/lib/schemas'
3+
import { baseProcedure } from '@/trpc/init'
4+
import { z } from 'zod'
5+
6+
export const createGroupExpenseProcedure = baseProcedure
7+
.input(
8+
z.object({
9+
groupId: z.string().min(1),
10+
expenseFormValues: expenseFormSchema,
11+
participantId: z.string().optional(),
12+
}),
13+
)
14+
.mutation(
15+
async ({ input: { groupId, expenseFormValues, participantId } }) => {
16+
const expense = await createExpense(
17+
expenseFormValues,
18+
groupId,
19+
participantId,
20+
)
21+
return { expenseId: expense.id }
22+
},
23+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { deleteExpense } from '@/lib/api'
2+
import { baseProcedure } from '@/trpc/init'
3+
import { z } from 'zod'
4+
5+
export const deleteGroupExpenseProcedure = baseProcedure
6+
.input(
7+
z.object({
8+
expenseId: z.string().min(1),
9+
groupId: z.string().min(1),
10+
participantId: z.string().optional(),
11+
}),
12+
)
13+
.mutation(async ({ input: { expenseId, groupId, participantId } }) => {
14+
await deleteExpense(groupId, expenseId, participantId)
15+
return {}
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getExpense } from '@/lib/api'
2+
import { baseProcedure } from '@/trpc/init'
3+
import { TRPCError } from '@trpc/server'
4+
import { z } from 'zod'
5+
6+
export const getGroupExpenseProcedure = baseProcedure
7+
.input(z.object({ groupId: z.string().min(1), expenseId: z.string().min(1) }))
8+
.query(async ({ input: { groupId, expenseId } }) => {
9+
const expense = await getExpense(groupId, expenseId)
10+
if (!expense) {
11+
throw new TRPCError({
12+
code: 'NOT_FOUND',
13+
message: 'Expense not found',
14+
})
15+
}
16+
return { expense }
17+
})

0 commit comments

Comments
 (0)