mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-22 07:26: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
@@ -0,0 +1,138 @@
|
||||
'use client'
|
||||
|
||||
import { BalancesList } from '@/app/groups/[groupId]/balances-list'
|
||||
import { ReimbursementList } from '@/app/groups/[groupId]/reimbursement-list'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getGroup } from '@/lib/api'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
|
||||
export default function BalancesAndReimbursements({
|
||||
group,
|
||||
}: {
|
||||
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
||||
}) {
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
useEffect(() => {
|
||||
// Until we use tRPC more widely and can invalidate the cache on expense
|
||||
// update, it's easier and safer to invalidate the cache on page load.
|
||||
utils.groups.balances.invalidate()
|
||||
}, [utils])
|
||||
|
||||
const t = useTranslations('Balances')
|
||||
|
||||
const { data, isLoading } = trpc.groups.balances.list.useQuery({
|
||||
groupId: group.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading || !data ? (
|
||||
<BalancesLoading participantCount={group.participants.length} />
|
||||
) : (
|
||||
<BalancesList
|
||||
balances={data.balances}
|
||||
participants={group.participants}
|
||||
currency={group.currency}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('Reimbursements.title')}</CardTitle>
|
||||
<CardDescription>{t('Reimbursements.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading || !data ? (
|
||||
<ReimbursementsLoading
|
||||
participantCount={group.participants.length}
|
||||
/>
|
||||
) : (
|
||||
<ReimbursementList
|
||||
reimbursements={data.reimbursements}
|
||||
participants={group.participants}
|
||||
currency={group.currency}
|
||||
groupId={group.id}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ReimbursementsLoading = ({
|
||||
participantCount,
|
||||
}: {
|
||||
participantCount: number
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{Array(participantCount - 1)
|
||||
.fill(undefined)
|
||||
.map((_, index) => (
|
||||
<div key={index} className="flex justify-between py-5">
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
<Skeleton className="h-3 w-32" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BalancesLoading = ({
|
||||
participantCount,
|
||||
}: {
|
||||
participantCount: number
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 py-1 gap-y-2">
|
||||
{Array(participantCount)
|
||||
.fill(undefined)
|
||||
.map((_, index) =>
|
||||
index % 2 === 0 ? (
|
||||
<Fragment key={index}>
|
||||
<div className="flex items-center justify-end pr-2">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
<div className="self-start">
|
||||
<Skeleton
|
||||
className={`h-7 w-${(index % 3) + 1}/3 rounded-l-none`}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment key={index}>
|
||||
<div className="flex items-center justify-end">
|
||||
<Skeleton
|
||||
className={`h-7 w-${(index % 3) + 1}/3 rounded-r-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center pl-2">
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
import { cached } from '@/app/cached-functions'
|
||||
import { BalancesList } from '@/app/groups/[groupId]/balances-list'
|
||||
import { ReimbursementList } from '@/app/groups/[groupId]/reimbursement-list'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { getGroupExpenses } from '@/lib/api'
|
||||
import {
|
||||
getBalances,
|
||||
getPublicBalances,
|
||||
getSuggestedReimbursements,
|
||||
} from '@/lib/balances'
|
||||
import BalancesAndReimbursements from '@/app/groups/[groupId]/balances/balances-and-reimbursements'
|
||||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -27,44 +12,8 @@ export default async function GroupPage({
|
||||
}: {
|
||||
params: { groupId: string }
|
||||
}) {
|
||||
const t = await getTranslations('Balances')
|
||||
const group = await cached.getGroup(groupId)
|
||||
if (!group) notFound()
|
||||
|
||||
const expenses = await getGroupExpenses(groupId)
|
||||
const balances = getBalances(expenses)
|
||||
const reimbursements = getSuggestedReimbursements(balances)
|
||||
const publicBalances = getPublicBalances(reimbursements)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BalancesList
|
||||
balances={publicBalances}
|
||||
participants={group.participants}
|
||||
currency={group.currency}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('Reimbursements.title')}</CardTitle>
|
||||
<CardDescription>{t('Reimbursements.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ReimbursementList
|
||||
reimbursements={reimbursements}
|
||||
participants={group.participants}
|
||||
currency={group.currency}
|
||||
groupId={groupId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
return <BalancesAndReimbursements group={group} />
|
||||
}
|
||||
|
||||
@@ -4,21 +4,22 @@ import { getGroupExpensesAction } from '@/app/groups/[groupId]/expenses/expense-
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SearchBar } from '@/components/ui/search-bar'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { normalizeString } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { Participant } from '@prisma/client'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { forwardRef, useEffect, useMemo, useState } from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
const PAGE_SIZE = 200
|
||||
|
||||
type ExpensesType = NonNullable<
|
||||
Awaited<ReturnType<typeof getGroupExpensesAction>>
|
||||
>
|
||||
|
||||
type Props = {
|
||||
expensesFirstPage: ExpensesType
|
||||
expenseCount: number
|
||||
participants: Participant[]
|
||||
currency: string
|
||||
groupId: string
|
||||
@@ -62,22 +63,9 @@ function getGroupedExpensesByDate(expenses: ExpensesType) {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function ExpenseList({
|
||||
expensesFirstPage,
|
||||
expenseCount,
|
||||
currency,
|
||||
participants,
|
||||
groupId,
|
||||
}: Props) {
|
||||
const firstLen = expensesFirstPage.length
|
||||
export function ExpenseList({ currency, participants, groupId }: Props) {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [dataIndex, setDataIndex] = useState(firstLen)
|
||||
const [dataLen, setDataLen] = useState(firstLen)
|
||||
const [hasMoreData, setHasMoreData] = useState(expenseCount > firstLen)
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [expenses, setExpenses] = useState(expensesFirstPage)
|
||||
const { ref, inView } = useInView()
|
||||
const t = useTranslations('Expenses')
|
||||
const [debouncedSearchText] = useDebounce(searchText, 300)
|
||||
|
||||
useEffect(() => {
|
||||
const activeUser = localStorage.getItem('newGroup-activeUser')
|
||||
@@ -98,57 +86,74 @@ export function ExpenseList({
|
||||
}
|
||||
}, [groupId, participants])
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar onValueChange={(value) => setSearchText(value)} />
|
||||
<ExpenseListForSearch
|
||||
groupId={groupId}
|
||||
currency={currency}
|
||||
searchText={debouncedSearchText}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpenseListForSearch = ({
|
||||
currency,
|
||||
groupId,
|
||||
searchText,
|
||||
}: {
|
||||
currency: string
|
||||
groupId: string
|
||||
searchText: string
|
||||
}) => {
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNextPage = async () => {
|
||||
setIsFetching(true)
|
||||
// Until we use tRPC more widely and can invalidate the cache on expense
|
||||
// update, it's easier and safer to invalidate the cache on page load.
|
||||
utils.groups.expenses.invalidate()
|
||||
}, [utils])
|
||||
|
||||
const newExpenses = await getGroupExpensesAction(groupId, {
|
||||
offset: dataIndex,
|
||||
length: dataLen,
|
||||
})
|
||||
const t = useTranslations('Expenses')
|
||||
const { ref: loadingRef, inView } = useInView()
|
||||
|
||||
if (newExpenses !== null) {
|
||||
const exp = expenses.concat(newExpenses)
|
||||
setExpenses(exp)
|
||||
setHasMoreData(exp.length < expenseCount)
|
||||
setDataIndex(dataIndex + dataLen)
|
||||
setDataLen(Math.ceil(1.5 * dataLen))
|
||||
}
|
||||
const { data, isLoading, isError, fetchNextPage } =
|
||||
trpc.groups.expenses.list.useInfiniteQuery(
|
||||
{ groupId, limit: PAGE_SIZE, filter: searchText },
|
||||
{ getNextPageParam: ({ nextCursor }) => nextCursor },
|
||||
)
|
||||
const expenses = data?.pages.flatMap((page) => page.expenses)
|
||||
const hasMore = data?.pages.at(-1)?.hasMore ?? false
|
||||
|
||||
setTimeout(() => setIsFetching(false), 500)
|
||||
}
|
||||
|
||||
if (inView && hasMoreData && !isFetching) fetchNextPage()
|
||||
}, [
|
||||
dataIndex,
|
||||
dataLen,
|
||||
expenseCount,
|
||||
expenses,
|
||||
groupId,
|
||||
hasMoreData,
|
||||
inView,
|
||||
isFetching,
|
||||
])
|
||||
useEffect(() => {
|
||||
if (inView && hasMore && !isLoading) fetchNextPage()
|
||||
}, [fetchNextPage, hasMore, inView, isLoading])
|
||||
|
||||
const groupedExpensesByDate = useMemo(
|
||||
() => getGroupedExpensesByDate(expenses),
|
||||
() => (expenses ? getGroupedExpensesByDate(expenses) : {}),
|
||||
[expenses],
|
||||
)
|
||||
|
||||
return expenses.length > 0 ? (
|
||||
if (isLoading) return <ExpensesLoading />
|
||||
|
||||
if (!expenses || expenses?.length === 0)
|
||||
return (
|
||||
<p className="px-6 text-sm py-6">
|
||||
{t('noExpenses')}{' '}
|
||||
<Button variant="link" asChild className="-m-4">
|
||||
<Link href={`/groups/${groupId}/expenses/create`}>
|
||||
{t('createFirst')}
|
||||
</Link>
|
||||
</Button>
|
||||
</p>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar
|
||||
onValueChange={(value) => setSearchText(normalizeString(value))}
|
||||
/>
|
||||
{Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => {
|
||||
let groupExpenses = groupedExpensesByDate[expenseGroup]
|
||||
if (!groupExpenses) return null
|
||||
|
||||
groupExpenses = groupExpenses.filter(({ title }) =>
|
||||
normalizeString(title).includes(searchText),
|
||||
)
|
||||
|
||||
if (groupExpenses.length === 0) return null
|
||||
if (!groupExpenses || groupExpenses.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={expenseGroup}>
|
||||
@@ -170,31 +175,34 @@ export function ExpenseList({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{expenses.length < expenseCount &&
|
||||
[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-t flex justify-between items-center px-6 py-4 text-sm"
|
||||
ref={i === 0 ? ref : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
<Skeleton className="h-4 w-32 rounded-full" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{hasMore && <ExpensesLoading ref={loadingRef} />}
|
||||
</>
|
||||
) : (
|
||||
<p className="px-6 text-sm py-6">
|
||||
{t('noExpenses')}{' '}
|
||||
<Button variant="link" asChild className="-m-4">
|
||||
<Link href={`/groups/${groupId}/expenses/create`}>
|
||||
{t('createFirst')}
|
||||
</Link>
|
||||
</Button>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpensesLoading = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Skeleton className="mx-4 sm:mx-6 mb-2 h-4 w-32 rounded-full" />
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between items-start px-2 sm:px-6 py-4 text-sm gap-2"
|
||||
>
|
||||
<div className="flex-0 pl-2 pr-1">
|
||||
<Skeleton className="h-4 w-4 rounded-full" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
<Skeleton className="h-4 w-32 rounded-full" />
|
||||
</div>
|
||||
<div className="flex-0 flex flex-col gap-2 items-end mr-2 sm:mr-12">
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
<Skeleton className="h-4 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ExpensesLoading.displayName = 'ExpensesLoading'
|
||||
|
||||
@@ -10,19 +10,13 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
getCategories,
|
||||
getGroupExpenseCount,
|
||||
getGroupExpenses,
|
||||
} from '@/lib/api'
|
||||
import { getCategories } from '@/lib/api'
|
||||
import { env } from '@/lib/env'
|
||||
import { Download, Plus } from 'lucide-react'
|
||||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
@@ -79,24 +73,11 @@ export default async function GroupExpensesPage({
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 pt-2 pb-4 sm:pb-6 flex flex-col gap-4 relative">
|
||||
<Suspense
|
||||
fallback={[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-t flex justify-between items-center px-6 py-4 text-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
<Skeleton className="h-4 w-32 rounded-full" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-16 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
<Expenses group={group} />
|
||||
</Suspense>
|
||||
<ExpenseList
|
||||
groupId={group.id}
|
||||
currency={group.currency}
|
||||
participants={group.participants}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -104,26 +85,3 @@ export default async function GroupExpensesPage({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
group: NonNullable<Awaited<ReturnType<typeof cached.getGroup>>>
|
||||
}
|
||||
|
||||
async function Expenses({ group }: Props) {
|
||||
const expenseCount = await getGroupExpenseCount(group.id)
|
||||
|
||||
const expenses = await getGroupExpenses(group.id, {
|
||||
offset: 0,
|
||||
length: 200,
|
||||
})
|
||||
|
||||
return (
|
||||
<ExpenseList
|
||||
expensesFirstPage={expenses}
|
||||
expenseCount={expenseCount}
|
||||
groupId={group.id}
|
||||
currency={group.currency}
|
||||
participants={group.participants}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
52
src/app/groups/[groupId]/information/group-information.tsx
Normal file
52
src/app/groups/[groupId]/information/group-information.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { Pencil } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function GroupInformation({ groupId }: { groupId: string }) {
|
||||
const t = useTranslations('Information')
|
||||
const { data, isLoading } = trpc.groups.information.get.useQuery({ groupId })
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex justify-between">
|
||||
<span>{t('title')}</span>
|
||||
<Button size="icon" asChild className="-mb-12">
|
||||
<Link href={`/groups/${groupId}/edit`}>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription className="mr-12">
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm sm:prose-base max-w-full whitespace-break-spaces">
|
||||
{isLoading || !data ? (
|
||||
<div className="py-1 flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
) : (
|
||||
data.information || (
|
||||
<p className="text-muted-foreground text-sm">{t('empty')}</p>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +1,14 @@
|
||||
import { cached } from '@/app/cached-functions'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Pencil } from 'lucide-react'
|
||||
import GroupInformation from '@/app/groups/[groupId]/information/group-information'
|
||||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Totals',
|
||||
title: 'Group Information',
|
||||
}
|
||||
|
||||
export default async function InformationPage({
|
||||
export default function InformationPage({
|
||||
params: { groupId },
|
||||
}: {
|
||||
params: { groupId: string }
|
||||
}) {
|
||||
const group = await cached.getGroup(groupId)
|
||||
if (!group) notFound()
|
||||
|
||||
const t = await getTranslations('Information')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex justify-between">
|
||||
<span>{t('title')}</span>
|
||||
<Button size="icon" asChild className="-mb-12">
|
||||
<Link href={`/groups/${groupId}/edit`}>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription className="mr-12">
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-sm sm:prose-base max-w-full whitespace-break-spaces">
|
||||
{group.information || (
|
||||
<p className="text-muted-foreground italic">{t('empty')}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
return <GroupInformation groupId={groupId} />
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ReimbursementList({
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{reimbursements.map((reimbursement, index) => (
|
||||
<div className="border-t px-6 py-4 flex justify-between" key={index}>
|
||||
<div className="py-4 flex justify-between" key={index}>
|
||||
<div className="flex flex-col gap-1 items-start sm:flex-row sm:items-baseline sm:gap-4">
|
||||
<div>
|
||||
{t.rich('owes', {
|
||||
|
||||
@@ -15,7 +15,6 @@ export function Totals({
|
||||
totalGroupSpendings: number
|
||||
}) {
|
||||
const activeUser = useActiveUser(group.id)
|
||||
console.log('activeUser', activeUser)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user