mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-25 08:56:13 +01:00
Add tRPC, use it for group expenses, balances and information page (#246)
* Add tRPC, use it for group expense list * Use tRPC for balances * Use tRPC in group information + better loading states
This commit is contained in:
committed by
GitHub
parent
727803ea5c
commit
66e15e419e
60
src/trpc/client.tsx
Normal file
60
src/trpc/client.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client' // <-- to make sure we can mount the Provider from a server component
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { httpBatchLink } from '@trpc/client'
|
||||
import { createTRPCReact } from '@trpc/react-query'
|
||||
import { useState } from 'react'
|
||||
import superjson from 'superjson'
|
||||
import { makeQueryClient } from './query-client'
|
||||
import type { AppRouter } from './routers/_app'
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>()
|
||||
|
||||
let clientQueryClientSingleton: QueryClient
|
||||
|
||||
function getQueryClient() {
|
||||
if (typeof window === 'undefined') {
|
||||
// Server: always make a new query client
|
||||
return makeQueryClient()
|
||||
}
|
||||
// Browser: use singleton pattern to keep the same query client
|
||||
return (clientQueryClientSingleton ??= makeQueryClient())
|
||||
}
|
||||
|
||||
function getUrl() {
|
||||
const base = (() => {
|
||||
if (typeof window !== 'undefined') return ''
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
|
||||
return 'http://localhost:3000'
|
||||
})()
|
||||
return `${base}/api/trpc`
|
||||
}
|
||||
|
||||
export function TRPCProvider(
|
||||
props: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>,
|
||||
) {
|
||||
// NOTE: Avoid useState when initializing the query client if you don't
|
||||
// have a suspense boundary between this and the code that may
|
||||
// suspend because React will throw away the client on the initial
|
||||
// render if it suspends and there is no boundary
|
||||
const queryClient = getQueryClient()
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
transformer: superjson,
|
||||
url: getUrl(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{props.children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
)
|
||||
}
|
||||
25
src/trpc/init.ts
Normal file
25
src/trpc/init.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { initTRPC } from '@trpc/server'
|
||||
import { cache } from 'react'
|
||||
import superjson from 'superjson'
|
||||
|
||||
export const createTRPCContext = cache(async () => {
|
||||
/**
|
||||
* @see: https://trpc.io/docs/server/context
|
||||
*/
|
||||
return {}
|
||||
})
|
||||
|
||||
// Avoid exporting the entire t-object
|
||||
// since it's not very descriptive.
|
||||
// For instance, the use of a t variable
|
||||
// is common in i18n libraries.
|
||||
const t = initTRPC.create({
|
||||
/**
|
||||
* @see https://trpc.io/docs/server/data-transformers
|
||||
*/
|
||||
transformer: superjson,
|
||||
})
|
||||
|
||||
// Base router and procedure helpers
|
||||
export const createTRPCRouter = t.router
|
||||
export const baseProcedure = t.procedure
|
||||
21
src/trpc/query-client.ts
Normal file
21
src/trpc/query-client.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'
|
||||
import superjson from 'superjson'
|
||||
|
||||
export function makeQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30 * 1000,
|
||||
},
|
||||
dehydrate: {
|
||||
serializeData: superjson.serialize,
|
||||
shouldDehydrateQuery: (query) =>
|
||||
defaultShouldDehydrateQuery(query) ||
|
||||
query.state.status === 'pending',
|
||||
},
|
||||
hydrate: {
|
||||
deserializeData: superjson.deserialize,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
10
src/trpc/routers/_app.ts
Normal file
10
src/trpc/routers/_app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { groupsRouter } from '@/trpc/routers/groups'
|
||||
import { inferRouterOutputs } from '@trpc/server'
|
||||
import { createTRPCRouter } from '../init'
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
groups: groupsRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
export type AppRouterOutput = inferRouterOutputs<AppRouter>
|
||||
6
src/trpc/routers/groups/balances/index.ts
Normal file
6
src/trpc/routers/groups/balances/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createTRPCRouter } from '@/trpc/init'
|
||||
import { listGroupBalancesProcedure } from '@/trpc/routers/groups/balances/list.procedure'
|
||||
|
||||
export const groupBalancesRouter = createTRPCRouter({
|
||||
list: listGroupBalancesProcedure,
|
||||
})
|
||||
19
src/trpc/routers/groups/balances/list.procedure.ts
Normal file
19
src/trpc/routers/groups/balances/list.procedure.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getGroupExpenses } from '@/lib/api'
|
||||
import {
|
||||
getBalances,
|
||||
getPublicBalances,
|
||||
getSuggestedReimbursements,
|
||||
} from '@/lib/balances'
|
||||
import { baseProcedure } from '@/trpc/init'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listGroupBalancesProcedure = baseProcedure
|
||||
.input(z.object({ groupId: z.string().min(1) }))
|
||||
.query(async ({ input: { groupId } }) => {
|
||||
const expenses = await getGroupExpenses(groupId)
|
||||
const balances = getBalances(expenses)
|
||||
const reimbursements = getSuggestedReimbursements(balances)
|
||||
const publicBalances = getPublicBalances(reimbursements)
|
||||
|
||||
return { balances: publicBalances, reimbursements }
|
||||
})
|
||||
6
src/trpc/routers/groups/expenses/index.ts
Normal file
6
src/trpc/routers/groups/expenses/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createTRPCRouter } from '@/trpc/init'
|
||||
import { listGroupExpensesProcedure } from '@/trpc/routers/groups/expenses/list.procedure'
|
||||
|
||||
export const groupExpensesRouter = createTRPCRouter({
|
||||
list: listGroupExpensesProcedure,
|
||||
})
|
||||
29
src/trpc/routers/groups/expenses/list.procedure.ts
Normal file
29
src/trpc/routers/groups/expenses/list.procedure.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getGroupExpenses } from '@/lib/api'
|
||||
import { baseProcedure } from '@/trpc/init'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listGroupExpensesProcedure = baseProcedure
|
||||
.input(
|
||||
z.object({
|
||||
groupId: z.string().min(1),
|
||||
cursor: z.number().optional(),
|
||||
limit: z.number().optional(),
|
||||
filter: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { groupId, cursor = 0, limit = 10, filter } }) => {
|
||||
const expenses = await getGroupExpenses(groupId, {
|
||||
offset: cursor,
|
||||
length: limit + 1,
|
||||
filter,
|
||||
})
|
||||
return {
|
||||
expenses: expenses.slice(0, limit).map((expense) => ({
|
||||
...expense,
|
||||
createdAt: new Date(expense.createdAt),
|
||||
expenseDate: new Date(expense.expenseDate),
|
||||
})),
|
||||
hasMore: !!expenses[limit],
|
||||
nextCursor: cursor + limit,
|
||||
}
|
||||
})
|
||||
10
src/trpc/routers/groups/index.ts
Normal file
10
src/trpc/routers/groups/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createTRPCRouter } from '@/trpc/init'
|
||||
import { groupBalancesRouter } from '@/trpc/routers/groups/balances'
|
||||
import { groupExpensesRouter } from '@/trpc/routers/groups/expenses'
|
||||
import { groupInformationRouter } from '@/trpc/routers/groups/information'
|
||||
|
||||
export const groupsRouter = createTRPCRouter({
|
||||
expenses: groupExpensesRouter,
|
||||
balances: groupBalancesRouter,
|
||||
information: groupInformationRouter,
|
||||
})
|
||||
21
src/trpc/routers/groups/information/get.procedure.ts
Normal file
21
src/trpc/routers/groups/information/get.procedure.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getGroup } from '@/lib/api'
|
||||
import { baseProcedure } from '@/trpc/init'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const getGroupInformationProcedure = baseProcedure
|
||||
.input(
|
||||
z.object({
|
||||
groupId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { groupId } }) => {
|
||||
const group = await getGroup(groupId)
|
||||
if (!group) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Group not found.',
|
||||
})
|
||||
}
|
||||
return { information: group.information ?? '' }
|
||||
})
|
||||
6
src/trpc/routers/groups/information/index.ts
Normal file
6
src/trpc/routers/groups/information/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createTRPCRouter } from '@/trpc/init'
|
||||
import { getGroupInformationProcedure } from '@/trpc/routers/groups/information/get.procedure'
|
||||
|
||||
export const groupInformationRouter = createTRPCRouter({
|
||||
get: getGroupInformationProcedure,
|
||||
})
|
||||
Reference in New Issue
Block a user