mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-09 13:49:04 +01:00
Use tRPC in other group pages (#249)
* Use tRPC in group edition + group layout * Use tRPC in group modals * Use tRPC in group stats * Use tRPC in group activity
This commit is contained in:
committed by
GitHub
parent
66e15e419e
commit
210c12b7ef
@@ -18,22 +18,24 @@ import {
|
||||
} from '@/components/ui/drawer'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { getGroup } from '@/lib/api'
|
||||
import { useMediaQuery } from '@/lib/hooks'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { AppRouterOutput } from '@/trpc/routers/_app'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { ComponentProps, useEffect, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
||||
}
|
||||
|
||||
export function ActiveUserModal({ group }: Props) {
|
||||
export function ActiveUserModal({ groupId }: { groupId: string }) {
|
||||
const t = useTranslations('Expenses.ActiveUserModal')
|
||||
const [open, setOpen] = useState(false)
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||
const { data: groupData } = trpc.groups.get.useQuery({ groupId })
|
||||
|
||||
const group = groupData?.group
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) return
|
||||
|
||||
const tempUser = localStorage.getItem(`newGroup-activeUser`)
|
||||
const activeUser = localStorage.getItem(`${group.id}-activeUser`)
|
||||
if (!tempUser && !activeUser) {
|
||||
@@ -42,6 +44,8 @@ export function ActiveUserModal({ group }: Props) {
|
||||
}, [group])
|
||||
|
||||
function updateOpen(open: boolean) {
|
||||
if (!group) return
|
||||
|
||||
if (!open && !localStorage.getItem(`${group.id}-activeUser`)) {
|
||||
localStorage.setItem(`${group.id}-activeUser`, 'None')
|
||||
}
|
||||
@@ -93,7 +97,10 @@ function ActiveUserForm({
|
||||
group,
|
||||
close,
|
||||
className,
|
||||
}: ComponentProps<'form'> & { group: Props['group']; close: () => void }) {
|
||||
}: ComponentProps<'form'> & {
|
||||
group?: AppRouterOutput['groups']['get']['group']
|
||||
close: () => void
|
||||
}) {
|
||||
const t = useTranslations('Expenses.ActiveUserModal')
|
||||
const [selected, setSelected] = useState('None')
|
||||
|
||||
@@ -101,6 +108,8 @@ function ActiveUserForm({
|
||||
<form
|
||||
className={cn('grid items-start gap-4', className)}
|
||||
onSubmit={(event) => {
|
||||
if (!group) return
|
||||
|
||||
event.preventDefault()
|
||||
localStorage.setItem(`${group.id}-activeUser`, selected)
|
||||
close()
|
||||
@@ -114,7 +123,7 @@ function ActiveUserForm({
|
||||
{t('nobody')}
|
||||
</Label>
|
||||
</div>
|
||||
{group.participants.map((participant) => (
|
||||
{group?.participants.map((participant) => (
|
||||
<div key={participant.id} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={participant.id} id={participant.id} />
|
||||
<Label htmlFor={participant.id} className="flex-1">
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ToastAction } from '@/components/ui/toast'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { useMediaQuery } from '@/lib/hooks'
|
||||
import { formatCurrency, formatDate, formatFileSize } from '@/lib/utils'
|
||||
import { Category } from '@prisma/client'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { ChevronRight, FileQuestion, Loader2, Receipt } from 'lucide-react'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { getImageData, usePresignedUpload } from 'next-s3-upload'
|
||||
@@ -35,19 +35,52 @@ import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
groupId: string
|
||||
groupCurrency: string
|
||||
categories: Category[]
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 ** 2
|
||||
|
||||
export function CreateFromReceiptButton({
|
||||
groupId,
|
||||
groupCurrency,
|
||||
categories,
|
||||
}: Props) {
|
||||
export function CreateFromReceiptButton({ groupId }: { groupId: string }) {
|
||||
return <CreateFromReceiptButton_ groupId={groupId} />
|
||||
}
|
||||
|
||||
function CreateFromReceiptButton_({ groupId }: { groupId: string }) {
|
||||
const t = useTranslations('CreateFromReceipt')
|
||||
const isDesktop = useMediaQuery('(min-width: 640px)')
|
||||
|
||||
const DialogOrDrawer = isDesktop
|
||||
? CreateFromReceiptDialog
|
||||
: CreateFromReceiptDrawer
|
||||
|
||||
return (
|
||||
<DialogOrDrawer
|
||||
trigger={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
title={t('Dialog.triggerTitle')}
|
||||
>
|
||||
<Receipt className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<span>{t('Dialog.title')}</span>
|
||||
<Badge className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600">
|
||||
Beta
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
description={<>{t('Dialog.description')}</>}
|
||||
>
|
||||
<ReceiptDialogContent groupId={groupId} />
|
||||
</DialogOrDrawer>
|
||||
)
|
||||
}
|
||||
|
||||
function ReceiptDialogContent({ groupId }: { groupId: string }) {
|
||||
const { data: groupData } = trpc.groups.get.useQuery({ groupId })
|
||||
const { data: categoriesData } = trpc.categories.list.useQuery()
|
||||
const group = groupData?.group
|
||||
const categories = categoriesData?.categories
|
||||
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('CreateFromReceipt')
|
||||
const [pending, setPending] = useState(false)
|
||||
@@ -58,7 +91,6 @@ export function CreateFromReceiptButton({
|
||||
| null
|
||||
| (ReceiptExtractedInfo & { url: string; width?: number; height?: number })
|
||||
>(null)
|
||||
const isDesktop = useMediaQuery('(min-width: 640px)')
|
||||
|
||||
const handleFileChange = async (file: File) => {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
@@ -107,160 +139,130 @@ export function CreateFromReceiptButton({
|
||||
|
||||
const receiptInfoCategory =
|
||||
(receiptInfo?.categoryId &&
|
||||
categories.find((c) => String(c.id) === receiptInfo.categoryId)) ||
|
||||
categories?.find((c) => String(c.id) === receiptInfo.categoryId)) ||
|
||||
null
|
||||
|
||||
const DialogOrDrawer = isDesktop
|
||||
? CreateFromReceiptDialog
|
||||
: CreateFromReceiptDrawer
|
||||
|
||||
return (
|
||||
<DialogOrDrawer
|
||||
trigger={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
title={t('Dialog.triggerTitle')}
|
||||
>
|
||||
<Receipt className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<span>{t('Dialog.title')}</span>
|
||||
<Badge className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600">
|
||||
Beta
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
description={<>{t('Dialog.description')}</>}
|
||||
>
|
||||
<div className="prose prose-sm dark:prose-invert">
|
||||
<p>{t('Dialog.body')}</p>
|
||||
<div>
|
||||
<FileInput
|
||||
onChange={handleFileChange}
|
||||
accept="image/jpeg,image/png"
|
||||
/>
|
||||
<div className="grid gap-x-4 gap-y-2 grid-cols-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="row-span-3 w-full h-full relative"
|
||||
title="Create expense from receipt"
|
||||
onClick={openFileDialog}
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
) : receiptInfo ? (
|
||||
<div className="absolute top-2 left-2 bottom-2 right-2">
|
||||
<Image
|
||||
src={receiptInfo.url}
|
||||
width={receiptInfo.width}
|
||||
height={receiptInfo.height}
|
||||
className="w-full h-full m-0 object-contain drop-shadow-lg"
|
||||
alt="Scanned receipt"
|
||||
/>
|
||||
</div>
|
||||
<div className="prose prose-sm dark:prose-invert">
|
||||
<p>{t('Dialog.body')}</p>
|
||||
<div>
|
||||
<FileInput onChange={handleFileChange} accept="image/jpeg,image/png" />
|
||||
<div className="grid gap-x-4 gap-y-2 grid-cols-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="row-span-3 w-full h-full relative"
|
||||
title="Create expense from receipt"
|
||||
onClick={openFileDialog}
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
) : receiptInfo ? (
|
||||
<div className="absolute top-2 left-2 bottom-2 right-2">
|
||||
<Image
|
||||
src={receiptInfo.url}
|
||||
width={receiptInfo.width}
|
||||
height={receiptInfo.height}
|
||||
className="w-full h-full m-0 object-contain drop-shadow-lg"
|
||||
alt="Scanned receipt"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs sm:text-sm text-muted-foreground">
|
||||
{t('Dialog.selectImage')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<div className="col-span-2">
|
||||
<strong>{t('Dialog.titleLabel')}</strong>
|
||||
<div>{receiptInfo ? receiptInfo.title ?? <Unknown /> : '…'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<strong>{t('Dialog.categoryLabel')}</strong>
|
||||
<div>
|
||||
{receiptInfo ? (
|
||||
receiptInfoCategory ? (
|
||||
<div className="flex items-center">
|
||||
<CategoryIcon
|
||||
category={receiptInfoCategory}
|
||||
className="inline w-4 h-4 mr-2"
|
||||
/>
|
||||
<span className="mr-1">{receiptInfoCategory.grouping}</span>
|
||||
<ChevronRight className="inline w-3 h-3 mr-1" />
|
||||
<span>{receiptInfoCategory.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Unknown />
|
||||
)
|
||||
) : (
|
||||
<span className="text-xs sm:text-sm text-muted-foreground">
|
||||
{t('Dialog.selectImage')}
|
||||
</span>
|
||||
'' || '…'
|
||||
)}
|
||||
</Button>
|
||||
<div className="col-span-2">
|
||||
<strong>{t('Dialog.titleLabel')}</strong>
|
||||
<div>{receiptInfo ? receiptInfo.title ?? <Unknown /> : '…'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<strong>{t('Dialog.categoryLabel')}</strong>
|
||||
<div>
|
||||
{receiptInfo ? (
|
||||
receiptInfoCategory ? (
|
||||
<div className="flex items-center">
|
||||
<CategoryIcon
|
||||
category={receiptInfoCategory}
|
||||
className="inline w-4 h-4 mr-2"
|
||||
/>
|
||||
<span className="mr-1">
|
||||
{receiptInfoCategory.grouping}
|
||||
</span>
|
||||
<ChevronRight className="inline w-3 h-3 mr-1" />
|
||||
<span>{receiptInfoCategory.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Unknown />
|
||||
)
|
||||
) : (
|
||||
'' || '…'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t('Dialog.amountLabel')}</strong>
|
||||
<div>
|
||||
<strong>{t('Dialog.amountLabel')}</strong>
|
||||
<div>
|
||||
{receiptInfo ? (
|
||||
receiptInfo.amount ? (
|
||||
<>
|
||||
{formatCurrency(
|
||||
groupCurrency,
|
||||
receiptInfo.amount,
|
||||
locale,
|
||||
true,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Unknown />
|
||||
)
|
||||
) : (
|
||||
'…'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t('Dialog.dateLabel')}</strong>
|
||||
<div>
|
||||
{receiptInfo ? (
|
||||
receiptInfo.date ? (
|
||||
formatDate(
|
||||
new Date(`${receiptInfo?.date}T12:00:00.000Z`),
|
||||
{receiptInfo && group ? (
|
||||
receiptInfo.amount ? (
|
||||
<>
|
||||
{formatCurrency(
|
||||
group.currency,
|
||||
receiptInfo.amount,
|
||||
locale,
|
||||
{ dateStyle: 'medium' },
|
||||
)
|
||||
) : (
|
||||
<Unknown />
|
||||
true,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Unknown />
|
||||
)
|
||||
) : (
|
||||
'…'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t('Dialog.dateLabel')}</strong>
|
||||
<div>
|
||||
{receiptInfo ? (
|
||||
receiptInfo.date ? (
|
||||
formatDate(
|
||||
new Date(`${receiptInfo?.date}T12:00:00.000Z`),
|
||||
locale,
|
||||
{ dateStyle: 'medium' },
|
||||
)
|
||||
) : (
|
||||
'…'
|
||||
)}
|
||||
</div>
|
||||
<Unknown />
|
||||
)
|
||||
) : (
|
||||
'…'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t('Dialog.editNext')}</p>
|
||||
<div className="text-center">
|
||||
<Button
|
||||
disabled={pending || !receiptInfo}
|
||||
onClick={() => {
|
||||
if (!receiptInfo) return
|
||||
router.push(
|
||||
`/groups/${groupId}/expenses/create?amount=${
|
||||
receiptInfo.amount
|
||||
}&categoryId=${receiptInfo.categoryId}&date=${
|
||||
receiptInfo.date
|
||||
}&title=${encodeURIComponent(
|
||||
receiptInfo.title ?? '',
|
||||
)}&imageUrl=${encodeURIComponent(receiptInfo.url)}&imageWidth=${
|
||||
receiptInfo.width
|
||||
}&imageHeight=${receiptInfo.height}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('Dialog.continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogOrDrawer>
|
||||
<p>{t('Dialog.editNext')}</p>
|
||||
<div className="text-center">
|
||||
<Button
|
||||
disabled={pending || !receiptInfo}
|
||||
onClick={() => {
|
||||
if (!receiptInfo || !group) return
|
||||
router.push(
|
||||
`/groups/${group.id}/expenses/create?amount=${
|
||||
receiptInfo.amount
|
||||
}&categoryId=${receiptInfo.categoryId}&date=${
|
||||
receiptInfo.date
|
||||
}&title=${encodeURIComponent(
|
||||
receiptInfo.title ?? '',
|
||||
)}&imageUrl=${encodeURIComponent(receiptInfo.url)}&imageWidth=${
|
||||
receiptInfo.width
|
||||
}&imageHeight=${receiptInfo.height}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('Dialog.continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button } from '@/components/ui/button'
|
||||
import { SearchBar } from '@/components/ui/search-bar'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
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'
|
||||
@@ -13,18 +12,12 @@ import { forwardRef, useEffect, useMemo, useState } from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
const PAGE_SIZE = 200
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
type ExpensesType = NonNullable<
|
||||
Awaited<ReturnType<typeof getGroupExpensesAction>>
|
||||
>
|
||||
|
||||
type Props = {
|
||||
participants: Participant[]
|
||||
currency: string
|
||||
groupId: string
|
||||
}
|
||||
|
||||
const EXPENSE_GROUPS = {
|
||||
UPCOMING: 'upcoming',
|
||||
THIS_WEEK: 'thisWeek',
|
||||
@@ -63,11 +56,16 @@ function getGroupedExpensesByDate(expenses: ExpensesType) {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function ExpenseList({ currency, participants, groupId }: Props) {
|
||||
export function ExpenseList({ groupId }: { groupId: string }) {
|
||||
const { data: groupData } = trpc.groups.get.useQuery({ groupId })
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [debouncedSearchText] = useDebounce(searchText, 300)
|
||||
|
||||
const participants = groupData?.group.participants
|
||||
|
||||
useEffect(() => {
|
||||
if (!participants) return
|
||||
|
||||
const activeUser = localStorage.getItem('newGroup-activeUser')
|
||||
const newUser = localStorage.getItem(`${groupId}-newUser`)
|
||||
if (activeUser || newUser) {
|
||||
@@ -91,7 +89,6 @@ export function ExpenseList({ currency, participants, groupId }: Props) {
|
||||
<SearchBar onValueChange={(value) => setSearchText(value)} />
|
||||
<ExpenseListForSearch
|
||||
groupId={groupId}
|
||||
currency={currency}
|
||||
searchText={debouncedSearchText}
|
||||
/>
|
||||
</>
|
||||
@@ -99,11 +96,9 @@ export function ExpenseList({ currency, participants, groupId }: Props) {
|
||||
}
|
||||
|
||||
const ExpenseListForSearch = ({
|
||||
currency,
|
||||
groupId,
|
||||
searchText,
|
||||
}: {
|
||||
currency: string
|
||||
groupId: string
|
||||
searchText: string
|
||||
}) => {
|
||||
@@ -118,14 +113,23 @@ const ExpenseListForSearch = ({
|
||||
const t = useTranslations('Expenses')
|
||||
const { ref: loadingRef, inView } = useInView()
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage } =
|
||||
trpc.groups.expenses.list.useInfiniteQuery(
|
||||
{ groupId, limit: PAGE_SIZE, filter: searchText },
|
||||
{ getNextPageParam: ({ nextCursor }) => nextCursor },
|
||||
)
|
||||
const {
|
||||
data,
|
||||
isLoading: expensesAreLoading,
|
||||
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
|
||||
|
||||
const { data: groupData, isLoading: groupIsLoading } =
|
||||
trpc.groups.get.useQuery({ groupId })
|
||||
|
||||
const isLoading =
|
||||
expensesAreLoading || !expenses || groupIsLoading || !groupData
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasMore && !isLoading) fetchNextPage()
|
||||
}, [fetchNextPage, hasMore, inView, isLoading])
|
||||
@@ -137,7 +141,7 @@ const ExpenseListForSearch = ({
|
||||
|
||||
if (isLoading) return <ExpensesLoading />
|
||||
|
||||
if (!expenses || expenses?.length === 0)
|
||||
if (expenses.length === 0)
|
||||
return (
|
||||
<p className="px-6 text-sm py-6">
|
||||
{t('noExpenses')}{' '}
|
||||
@@ -168,7 +172,7 @@ const ExpenseListForSearch = ({
|
||||
<ExpenseCard
|
||||
key={expense.id}
|
||||
expense={expense}
|
||||
currency={currency}
|
||||
currency={groupData.group.currency}
|
||||
groupId={groupId}
|
||||
/>
|
||||
))}
|
||||
@@ -183,7 +187,7 @@ const ExpenseListForSearch = ({
|
||||
const ExpensesLoading = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Skeleton className="mx-4 sm:mx-6 mb-2 h-4 w-32 rounded-full" />
|
||||
<Skeleton className="mx-4 sm:mx-6 mt-1 mb-2 h-3 w-32 rounded-full" />
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
|
||||
75
src/app/groups/[groupId]/expenses/page.client.tsx
Normal file
75
src/app/groups/[groupId]/expenses/page.client.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import { ActiveUserModal } from '@/app/groups/[groupId]/expenses/active-user-modal'
|
||||
import { CreateFromReceiptButton } from '@/app/groups/[groupId]/expenses/create-from-receipt-button'
|
||||
import { ExpenseList } from '@/app/groups/[groupId]/expenses/expense-list'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Download, Plus } from 'lucide-react'
|
||||
import { Metadata } from 'next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Expenses',
|
||||
}
|
||||
|
||||
export default function GroupExpensesPageClient({
|
||||
groupId,
|
||||
enableReceiptExtract,
|
||||
}: {
|
||||
groupId: string
|
||||
enableReceiptExtract: boolean
|
||||
}) {
|
||||
const t = useTranslations('Expenses')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4 rounded-none -mx-4 border-x-0 sm:border-x sm:rounded-lg sm:mx-0">
|
||||
<div className="flex flex-1">
|
||||
<CardHeader className="flex-1 p-4 sm:p-6">
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardHeader className="p-4 sm:p-6 flex flex-row space-y-0 gap-2">
|
||||
<Button variant="secondary" size="icon" asChild>
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/groups/${groupId}/expenses/export/json`}
|
||||
target="_blank"
|
||||
title={t('exportJson')}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
{enableReceiptExtract && (
|
||||
<CreateFromReceiptButton groupId={groupId} />
|
||||
)}
|
||||
<Button asChild size="icon">
|
||||
<Link
|
||||
href={`/groups/${groupId}/expenses/create`}
|
||||
title={t('create')}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 pt-2 pb-4 sm:pb-6 flex flex-col gap-4 relative">
|
||||
<ExpenseList groupId={groupId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ActiveUserModal groupId={groupId} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,6 @@
|
||||
import { cached } from '@/app/cached-functions'
|
||||
import { ActiveUserModal } from '@/app/groups/[groupId]/expenses/active-user-modal'
|
||||
import { CreateFromReceiptButton } from '@/app/groups/[groupId]/expenses/create-from-receipt-button'
|
||||
import { ExpenseList } from '@/app/groups/[groupId]/expenses/expense-list'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { getCategories } from '@/lib/api'
|
||||
import GroupExpensesPageClient from '@/app/groups/[groupId]/expenses/page.client'
|
||||
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'
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
@@ -29,59 +13,10 @@ export default async function GroupExpensesPage({
|
||||
}: {
|
||||
params: { groupId: string }
|
||||
}) {
|
||||
const t = await getTranslations('Expenses')
|
||||
const group = await cached.getGroup(groupId)
|
||||
if (!group) notFound()
|
||||
|
||||
const categories = await getCategories()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-4 rounded-none -mx-4 border-x-0 sm:border-x sm:rounded-lg sm:mx-0">
|
||||
<div className="flex flex-1">
|
||||
<CardHeader className="flex-1 p-4 sm:p-6">
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardHeader className="p-4 sm:p-6 flex flex-row space-y-0 gap-2">
|
||||
<Button variant="secondary" size="icon" asChild>
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/groups/${groupId}/expenses/export/json`}
|
||||
target="_blank"
|
||||
title={t('exportJson')}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
{env.NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT && (
|
||||
<CreateFromReceiptButton
|
||||
groupId={groupId}
|
||||
groupCurrency={group.currency}
|
||||
categories={categories}
|
||||
/>
|
||||
)}
|
||||
<Button asChild size="icon">
|
||||
<Link
|
||||
href={`/groups/${groupId}/expenses/create`}
|
||||
title={t('create')}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 pt-2 pb-4 sm:pb-6 flex flex-col gap-4 relative">
|
||||
<ExpenseList
|
||||
groupId={group.id}
|
||||
currency={group.currency}
|
||||
participants={group.participants}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ActiveUserModal group={group} />
|
||||
</>
|
||||
<GroupExpensesPageClient
|
||||
groupId={groupId}
|
||||
enableReceiptExtract={env.NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user