From 210c12b7efce18b79f334290020c997d5ada3b21 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Sat, 19 Oct 2024 21:29:53 -0400 Subject: [PATCH] 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 --- .../[groupId]/activity/activity-item.tsx | 11 +- .../[groupId]/activity/activity-list.tsx | 94 ++++-- .../groups/[groupId]/activity/page.client.tsx | 32 ++ src/app/groups/[groupId]/activity/page.tsx | 41 +-- .../balances/balances-and-reimbursements.tsx | 51 +-- src/app/groups/[groupId]/balances/page.tsx | 2 +- src/app/groups/[groupId]/edit/edit-group.tsx | 30 ++ src/app/groups/[groupId]/edit/page.tsx | 25 +- .../[groupId]/expenses/active-user-modal.tsx | 25 +- .../expenses/create-from-receipt-button.tsx | 308 +++++++++--------- .../[groupId]/expenses/expense-list.tsx | 44 +-- .../groups/[groupId]/expenses/page.client.tsx | 75 +++++ src/app/groups/[groupId]/expenses/page.tsx | 75 +---- src/app/groups/[groupId]/group-header.tsx | 30 ++ .../information/group-information.tsx | 4 +- src/app/groups/[groupId]/layout.tsx | 19 +- .../groups/[groupId]/reimbursement-list.tsx | 2 +- .../groups/[groupId]/stats/page.client.tsx | 27 ++ src/app/groups/[groupId]/stats/page.tsx | 39 +-- .../[groupId]/stats/totals-your-share.tsx | 32 +- .../[groupId]/stats/totals-your-spending.tsx | 29 +- src/app/groups/[groupId]/stats/totals.tsx | 51 ++- src/components/group-form.tsx | 3 +- src/lib/api.ts | 27 +- src/trpc/routers/_app.ts | 2 + src/trpc/routers/categories/index.ts | 6 + src/trpc/routers/categories/list.procedure.ts | 6 + src/trpc/routers/groups/activities/index.ts | 6 + .../groups/activities/list.procedure.ts | 23 ++ .../groups/{information => }/get.procedure.ts | 14 +- src/trpc/routers/groups/index.ts | 11 +- src/trpc/routers/groups/information/index.ts | 6 - .../routers/groups/stats/get.procedure.ts | 35 ++ src/trpc/routers/groups/stats/index.ts | 6 + src/trpc/routers/groups/update.procedure.ts | 16 + 35 files changed, 709 insertions(+), 498 deletions(-) create mode 100644 src/app/groups/[groupId]/activity/page.client.tsx create mode 100644 src/app/groups/[groupId]/edit/edit-group.tsx create mode 100644 src/app/groups/[groupId]/expenses/page.client.tsx create mode 100644 src/app/groups/[groupId]/group-header.tsx create mode 100644 src/app/groups/[groupId]/stats/page.client.tsx create mode 100644 src/trpc/routers/categories/index.ts create mode 100644 src/trpc/routers/categories/list.procedure.ts create mode 100644 src/trpc/routers/groups/activities/index.ts create mode 100644 src/trpc/routers/groups/activities/list.procedure.ts rename src/trpc/routers/groups/{information => }/get.procedure.ts (52%) delete mode 100644 src/trpc/routers/groups/information/index.ts create mode 100644 src/trpc/routers/groups/stats/get.procedure.ts create mode 100644 src/trpc/routers/groups/stats/index.ts create mode 100644 src/trpc/routers/groups/update.procedure.ts diff --git a/src/app/groups/[groupId]/activity/activity-item.tsx b/src/app/groups/[groupId]/activity/activity-item.tsx index 69f64e1..a3a38db 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 cf2c010..0471717 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 0000000..4f3318c --- /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 d02debf..4340821 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]/balances/balances-and-reimbursements.tsx b/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx index adf1fed..687f887 100644 --- a/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx +++ b/src/app/groups/[groupId]/balances/balances-and-reimbursements.tsx @@ -10,17 +10,23 @@ import { 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, + groupId, }: { - group: NonNullable>> + groupId: string }) { const utils = trpc.useUtils() + const { data: groupData, isLoading: groupIsLoading } = + trpc.groups.get.useQuery({ groupId }) + const { data: balancesData, isLoading: balancesAreLoading } = + trpc.groups.balances.list.useQuery({ + groupId, + }) + const t = useTranslations('Balances') useEffect(() => { // Until we use tRPC more widely and can invalidate the cache on expense @@ -28,11 +34,8 @@ export default function BalancesAndReimbursements({ utils.groups.balances.invalidate() }, [utils]) - const t = useTranslations('Balances') - - const { data, isLoading } = trpc.groups.balances.list.useQuery({ - groupId: group.id, - }) + const isLoading = + balancesAreLoading || !balancesData || groupIsLoading || !groupData?.group return ( <> @@ -42,13 +45,15 @@ export default function BalancesAndReimbursements({ {t('description')} - {isLoading || !data ? ( - + {isLoading ? ( + ) : ( )} @@ -59,16 +64,16 @@ export default function BalancesAndReimbursements({ {t('Reimbursements.description')} - {isLoading || !data ? ( + {isLoading ? ( ) : ( )} @@ -78,9 +83,9 @@ export default function BalancesAndReimbursements({ } const ReimbursementsLoading = ({ - participantCount, + participantCount = 3, }: { - participantCount: number + participantCount?: number }) => { return (
@@ -100,9 +105,9 @@ const ReimbursementsLoading = ({ } const BalancesLoading = ({ - participantCount, + participantCount = 3, }: { - participantCount: number + participantCount?: number }) => { return (
diff --git a/src/app/groups/[groupId]/balances/page.tsx b/src/app/groups/[groupId]/balances/page.tsx index 0a403aa..a9e7c81 100644 --- a/src/app/groups/[groupId]/balances/page.tsx +++ b/src/app/groups/[groupId]/balances/page.tsx @@ -15,5 +15,5 @@ export default async function GroupPage({ const group = await cached.getGroup(groupId) if (!group) notFound() - return + return } diff --git a/src/app/groups/[groupId]/edit/edit-group.tsx b/src/app/groups/[groupId]/edit/edit-group.tsx new file mode 100644 index 0000000..93f8b65 --- /dev/null +++ b/src/app/groups/[groupId]/edit/edit-group.tsx @@ -0,0 +1,30 @@ +'use client' + +import { GroupForm } from '@/components/group-form' +import { trpc } from '@/trpc/client' + +export const EditGroup = ({ groupId }: { groupId: string }) => { + const { data, isLoading } = trpc.groups.get.useQuery({ groupId }) + const { mutateAsync, isPending } = trpc.groups.update.useMutation() + const utils = trpc.useUtils() + + // async function updateGroupAction(values: unknown, participantId?: string) { + // 'use server' + // const groupFormValues = groupFormSchema.parse(values) + // const group = await updateGroup(groupId, groupFormValues, participantId) + // redirect(`/groups/${group.id}`) + // } + + if (isLoading) return <> + + return ( + { + await mutateAsync({ groupId, participantId, groupFormValues }) + await utils.groups.invalidate() + }} + protectedParticipantIds={data?.participantsWithExpenses} + /> + ) +} diff --git a/src/app/groups/[groupId]/edit/page.tsx b/src/app/groups/[groupId]/edit/page.tsx index ab2318f..66b83ed 100644 --- a/src/app/groups/[groupId]/edit/page.tsx +++ b/src/app/groups/[groupId]/edit/page.tsx @@ -1,9 +1,5 @@ -import { cached } from '@/app/cached-functions' -import { GroupForm } from '@/components/group-form' -import { getGroupExpensesParticipants, updateGroup } from '@/lib/api' -import { groupFormSchema } from '@/lib/schemas' +import { EditGroup } from '@/app/groups/[groupId]/edit/edit-group' import { Metadata } from 'next' -import { notFound, redirect } from 'next/navigation' export const metadata: Metadata = { title: 'Settings', @@ -14,22 +10,5 @@ export default async function EditGroupPage({ }: { params: { groupId: string } }) { - const group = await cached.getGroup(groupId) - if (!group) notFound() - - async function updateGroupAction(values: unknown, participantId?: string) { - 'use server' - const groupFormValues = groupFormSchema.parse(values) - const group = await updateGroup(groupId, groupFormValues, participantId) - redirect(`/groups/${group.id}`) - } - - const protectedParticipantIds = await getGroupExpensesParticipants(groupId) - return ( - - ) + return } diff --git a/src/app/groups/[groupId]/expenses/active-user-modal.tsx b/src/app/groups/[groupId]/expenses/active-user-modal.tsx index 27d8ad8..043e299 100644 --- a/src/app/groups/[groupId]/expenses/active-user-modal.tsx +++ b/src/app/groups/[groupId]/expenses/active-user-modal.tsx @@ -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>> -} - -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({
{ + if (!group) return + event.preventDefault() localStorage.setItem(`${group.id}-activeUser`, selected) close() @@ -114,7 +123,7 @@ function ActiveUserForm({ {t('nobody')}
- {group.participants.map((participant) => ( + {group?.participants.map((participant) => (