diff --git a/src/app/groups/[groupId]/activity/activity-item.tsx b/src/app/groups/[groupId]/activity/activity-item.tsx index 69f64e11..a3a38dba 100644 --- a/src/app/groups/[groupId]/activity/activity-item.tsx +++ b/src/app/groups/[groupId]/activity/activity-item.tsx @@ -1,18 +1,20 @@ 'use client' import { Button } from '@/components/ui/button' -import { getGroupExpenses } from '@/lib/api' import { DateTimeStyle, cn, formatDate } from '@/lib/utils' -import { Activity, ActivityType, Participant } from '@prisma/client' +import { AppRouterOutput } from '@/trpc/routers/_app' +import { ActivityType, Participant } from '@prisma/client' import { ChevronRight } from 'lucide-react' import { useLocale, useTranslations } from 'next-intl' import Link from 'next/link' import { useRouter } from 'next/navigation' +export type Activity = + AppRouterOutput['groups']['activities']['list']['activities'][number] + type Props = { groupId: string activity: Activity participant?: Participant - expense?: Awaited>[number] dateStyle: DateTimeStyle } @@ -44,13 +46,12 @@ export function ActivityItem({ groupId, activity, participant, - expense, dateStyle, }: Props) { const router = useRouter() const locale = useLocale() - const expenseExists = expense !== undefined + const expenseExists = activity.expense !== undefined const summary = useSummary(activity, participant?.name) return ( diff --git a/src/app/groups/[groupId]/activity/activity-list.tsx b/src/app/groups/[groupId]/activity/activity-list.tsx index cf2c010f..04717172 100644 --- a/src/app/groups/[groupId]/activity/activity-list.tsx +++ b/src/app/groups/[groupId]/activity/activity-list.tsx @@ -1,15 +1,16 @@ -import { ActivityItem } from '@/app/groups/[groupId]/activity/activity-item' -import { getGroupExpenses } from '@/lib/api' -import { Activity, Participant } from '@prisma/client' +'use client' +import { + Activity, + ActivityItem, +} from '@/app/groups/[groupId]/activity/activity-item' +import { Skeleton } from '@/components/ui/skeleton' +import { trpc } from '@/trpc/client' import dayjs, { type Dayjs } from 'dayjs' import { useTranslations } from 'next-intl' +import { forwardRef, useEffect } from 'react' +import { useInView } from 'react-intersection-observer' -type Props = { - groupId: string - participants: Participant[] - expenses: Awaited> - activities: Activity[] -} +const PAGE_SIZE = 20 const DATE_GROUPS = { TODAY: 'today', @@ -48,23 +49,64 @@ function getDateGroup(date: Dayjs, today: Dayjs) { function getGroupedActivitiesByDate(activities: Activity[]) { const today = dayjs() return activities.reduce( - (result: { [key: string]: Activity[] }, activity: Activity) => { + (result, activity) => { const activityGroup = getDateGroup(dayjs(activity.time), today) result[activityGroup] = result[activityGroup] ?? [] result[activityGroup].push(activity) return result }, - {}, + {} as { + [key: string]: Activity[] + }, ) } -export function ActivityList({ - groupId, - participants, - expenses, - activities, -}: Props) { +const ActivitiesLoading = forwardRef((_, ref) => { + return ( +
+ + {Array(5) + .fill(undefined) + .map((_, index) => ( +
+
+ +
+
+ +
+
+ ))} +
+ ) +}) +ActivitiesLoading.displayName = 'ActivitiesLoading' + +export function ActivityList({ groupId }: { groupId: string }) { const t = useTranslations('Activity') + + const { data: groupData, isLoading: groupIsLoading } = + trpc.groups.get.useQuery({ groupId }) + + const { + data: activitiesData, + isLoading, + fetchNextPage, + } = trpc.groups.activities.list.useInfiniteQuery( + { groupId, limit: PAGE_SIZE }, + { getNextPageParam: ({ nextCursor }) => nextCursor }, + ) + const { ref: loadingRef, inView } = useInView() + + const activities = activitiesData?.pages.flatMap((page) => page.activities) + const hasMore = activitiesData?.pages.at(-1)?.hasMore ?? false + + useEffect(() => { + if (inView && hasMore && !isLoading) fetchNextPage() + }, [fetchNextPage, hasMore, inView, isLoading]) + + if (isLoading || !activities || !groupData) return + const groupedActivitiesByDate = getGroupedActivitiesByDate(activities) return activities.length > 0 ? ( @@ -86,27 +128,29 @@ export function ActivityList({ > {t(`Groups.${dateGroup}`)} - {groupActivities.map((activity: Activity) => { + {groupActivities.map((activity) => { const participant = activity.participantId !== null - ? participants.find((p) => p.id === activity.participantId) - : undefined - const expense = - activity.expenseId !== null - ? expenses.find((e) => e.id === activity.expenseId) + ? groupData.group.participants.find( + (p) => p.id === activity.participantId, + ) : undefined return ( ) })} ) })} + {hasMore && } ) : ( -

{t('noActivity')}

+

{t('noActivity')}

) } diff --git a/src/app/groups/[groupId]/activity/page.client.tsx b/src/app/groups/[groupId]/activity/page.client.tsx new file mode 100644 index 00000000..4f3318cd --- /dev/null +++ b/src/app/groups/[groupId]/activity/page.client.tsx @@ -0,0 +1,32 @@ +import { ActivityList } from '@/app/groups/[groupId]/activity/activity-list' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Metadata } from 'next' +import { useTranslations } from 'next-intl' + +export const metadata: Metadata = { + title: 'Activity', +} + +export function ActivityPageClient({ groupId }: { groupId: string }) { + const t = useTranslations('Activity') + + return ( + <> + + + {t('title')} + {t('description')} + + + + + + + ) +} diff --git a/src/app/groups/[groupId]/activity/page.tsx b/src/app/groups/[groupId]/activity/page.tsx index d02debf1..43408214 100644 --- a/src/app/groups/[groupId]/activity/page.tsx +++ b/src/app/groups/[groupId]/activity/page.tsx @@ -1,16 +1,5 @@ -import { cached } from '@/app/cached-functions' -import { ActivityList } from '@/app/groups/[groupId]/activity/activity-list' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { getActivities, getGroupExpenses } from '@/lib/api' +import { ActivityPageClient } from '@/app/groups/[groupId]/activity/page.client' import { Metadata } from 'next' -import { getTranslations } from 'next-intl/server' -import { notFound } from 'next/navigation' export const metadata: Metadata = { title: 'Activity', @@ -21,31 +10,5 @@ export default async function ActivityPage({ }: { params: { groupId: string } }) { - const t = await getTranslations('Activity') - const group = await cached.getGroup(groupId) - if (!group) notFound() - - const expenses = await getGroupExpenses(groupId) - const activities = await getActivities(groupId) - - return ( - <> - - - {t('title')} - {t('description')} - - - - - - - ) + return } diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index 28e45e3a..66ef409f 100644 --- a/src/app/groups/[groupId]/expenses/expense-list.tsx +++ b/src/app/groups/[groupId]/expenses/expense-list.tsx @@ -12,7 +12,7 @@ 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> diff --git a/src/app/groups/[groupId]/reimbursement-list.tsx b/src/app/groups/[groupId]/reimbursement-list.tsx index 6a7ca529..c9d4d50e 100644 --- a/src/app/groups/[groupId]/reimbursement-list.tsx +++ b/src/app/groups/[groupId]/reimbursement-list.tsx @@ -21,7 +21,7 @@ export function ReimbursementList({ const locale = useLocale() const t = useTranslations('Balances.Reimbursements') if (reimbursements.length === 0) { - return

{t('noImbursements')}

+ return

{t('noImbursements')}

} const getParticipant = (id: string) => participants.find((p) => p.id === id) diff --git a/src/lib/api.ts b/src/lib/api.ts index 2f8efacf..d4b467a2 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -310,11 +310,34 @@ export async function getExpense(groupId: string, expenseId: string) { }) } -export async function getActivities(groupId: string) { - return prisma.activity.findMany({ +export async function getActivities( + groupId: string, + options?: { offset?: number; length?: number }, +) { + const activities = await prisma.activity.findMany({ where: { groupId }, orderBy: [{ time: 'desc' }], + skip: options?.offset, + take: options?.length, + }) + + const expenseIds = activities + .map((activity) => activity.expenseId) + .filter(Boolean) + const expenses = await prisma.expense.findMany({ + where: { + groupId, + id: { in: expenseIds }, + }, }) + + return activities.map((activity) => ({ + ...activity, + expense: + activity.expenseId !== null + ? expenses.find((expense) => expense.id === activity.expenseId) + : undefined, + })) } export async function logActivity( diff --git a/src/trpc/routers/groups/activities/index.ts b/src/trpc/routers/groups/activities/index.ts new file mode 100644 index 00000000..55356006 --- /dev/null +++ b/src/trpc/routers/groups/activities/index.ts @@ -0,0 +1,6 @@ +import { createTRPCRouter } from '@/trpc/init' +import { listGroupActivitiesProcedure } from '@/trpc/routers/groups/activities/list.procedure' + +export const activitiesRouter = createTRPCRouter({ + list: listGroupActivitiesProcedure, +}) diff --git a/src/trpc/routers/groups/activities/list.procedure.ts b/src/trpc/routers/groups/activities/list.procedure.ts new file mode 100644 index 00000000..7669d2d3 --- /dev/null +++ b/src/trpc/routers/groups/activities/list.procedure.ts @@ -0,0 +1,23 @@ +import { getActivities } from '@/lib/api' +import { baseProcedure } from '@/trpc/init' +import { z } from 'zod' + +export const listGroupActivitiesProcedure = baseProcedure + .input( + z.object({ + groupId: z.string(), + cursor: z.number().optional().default(0), + limit: z.number().optional().default(5), + }), + ) + .query(async ({ input: { groupId, cursor, limit } }) => { + const activities = await getActivities(groupId, { + offset: cursor, + length: limit + 1, + }) + return { + activities: activities.slice(0, limit), + hasMore: !!activities[limit], + nextCursor: cursor + limit, + } + }) diff --git a/src/trpc/routers/groups/index.ts b/src/trpc/routers/groups/index.ts index cb098cc2..cf581cfe 100644 --- a/src/trpc/routers/groups/index.ts +++ b/src/trpc/routers/groups/index.ts @@ -1,4 +1,5 @@ import { createTRPCRouter } from '@/trpc/init' +import { activitiesRouter } from '@/trpc/routers/groups/activities' import { groupBalancesRouter } from '@/trpc/routers/groups/balances' import { groupExpensesRouter } from '@/trpc/routers/groups/expenses' import { getGroupProcedure } from '@/trpc/routers/groups/get.procedure' @@ -9,6 +10,7 @@ export const groupsRouter = createTRPCRouter({ expenses: groupExpensesRouter, balances: groupBalancesRouter, stats: groupStatsRouter, + activities: activitiesRouter, get: getGroupProcedure, update: updateGroupProcedure,