diff --git a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx index 795c53f9a..a7e72b515 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx @@ -22,6 +22,7 @@ import { import { PollStatusBadge } from "@/components/poll-status"; import { Spinner } from "@/components/spinner"; import { Trans } from "@/components/trans"; +import { VisibilityTrigger } from "@/components/visibility-trigger"; import { trpc } from "@/utils/trpc/client"; function PollCount({ count }: { count?: number }) { @@ -29,26 +30,36 @@ function PollCount({ count }: { count?: number }) { } function FilteredPolls({ status }: { status: PollStatus }) { - const { data, isFetching } = trpc.polls.list.useQuery( - { - status, - }, - { - keepPreviousData: true, - }, - ); + const { data, fetchNextPage, hasNextPage } = + trpc.polls.infiniteList.useInfiniteQuery( + { + status, + limit: 30, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + keepPreviousData: true, + }, + ); if (!data) { return ; } return ( -
- +
+
    + {data.pages.map((page, i) => ( +
  1. + +
  2. + ))} +
+ {hasNextPage ? ( + + + + ) : null}
); } diff --git a/apps/web/src/components/visibility-trigger.tsx b/apps/web/src/components/visibility-trigger.tsx new file mode 100644 index 000000000..1ce3949e3 --- /dev/null +++ b/apps/web/src/components/visibility-trigger.tsx @@ -0,0 +1,51 @@ +import React from "react"; + +export function useVisibilityTrigger(onVisible: () => void) { + const triggerRef = React.useRef(null); + + React.useEffect(() => { + const currentTriggerRef = triggerRef.current; + if (!currentTriggerRef) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + onVisible(); + } + }, + { + root: null, // Use the viewport as the root + rootMargin: "0px", + threshold: 1.0, // Trigger when 100% of the element is visible + }, + ); + + observer.observe(currentTriggerRef); + + return () => { + if (currentTriggerRef) { + observer.unobserve(currentTriggerRef); + } + }; + }, [onVisible]); + + return triggerRef; +} + +export function VisibilityTrigger({ + children, + onVisible, + className, +}: { + children: React.ReactNode; + onVisible: () => void; + className?: string; +}) { + const triggerRef = useVisibilityTrigger(onVisible); + + return ( +
+ {children} +
+ ); +} diff --git a/packages/backend/trpc/routers/polls.ts b/packages/backend/trpc/routers/polls.ts index 2144a9488..3f14ebb9c 100644 --- a/packages/backend/trpc/routers/polls.ts +++ b/packages/backend/trpc/routers/polls.ts @@ -93,6 +93,58 @@ export const polls = router({ }, }); }), + infiniteList: possiblyPublicProcedure + .input( + z.object({ + status: z.enum(["all", "live", "paused", "finalized"]), + cursor: z.string().optional(), + limit: z.number(), + }), + ) + .query(async ({ ctx, input }) => { + const { cursor, limit, status } = input; + const polls = await prisma.poll.findMany({ + where: { + userId: ctx.user.id, + status: status === "all" ? undefined : status, + }, + orderBy: [ + { + createdAt: "desc", + }, + { + title: "asc", + }, + ], + cursor: cursor ? { id: cursor } : undefined, + take: limit + 1, + select: { + id: true, + title: true, + location: true, + timeZone: true, + createdAt: true, + status: true, + userId: true, + participants: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + let nextCursor: typeof cursor | undefined = undefined; + if (polls.length > input.limit) { + const nextItem = polls.pop(); + nextCursor = nextItem!.id; + } + return { + polls, + nextCursor, + }; + }), // START LEGACY ROUTES create: possiblyPublicProcedure diff --git a/packages/database/prisma/seed.ts b/packages/database/prisma/seed.ts index fefe1e2cb..5f7507f3c 100644 --- a/packages/database/prisma/seed.ts +++ b/packages/database/prisma/seed.ts @@ -7,62 +7,64 @@ const prisma = new PrismaClient(); const randInt = (max = 1, floor = 0) => { return Math.round(Math.random() * max) + floor; }; +async function createPollForUser(userId: string) { + // create some polls with no duration (all day) and some with a random duration. + const duration = 60 * randInt(8); + let cursor = dayjs().add(randInt(30), "day").second(0).minute(0); + + const numberOfOptions = randInt(30, 2); + + const poll = await prisma.poll.create({ + include: { + participants: true, + options: true, + }, + data: { + id: faker.random.alpha(10), + title: `${faker.animal.cat()} Meetup ${faker.date.month()}`, + description: faker.lorem.paragraph(), + location: faker.address.streetAddress(), + deadline: faker.date.future(), + user: { + connect: { + id: userId, + }, + }, + timeZone: duration !== 0 ? "Europe/London" : undefined, + options: { + create: Array.from({ length: numberOfOptions }).map(() => { + const startTime = cursor.toDate(); + if (duration !== 0) { + cursor = cursor.add(randInt(72, 1), "hour"); + } else { + cursor = cursor.add(randInt(7, 1), "day"); + } + return { + startTime, + duration, + }; + }), + }, + participants: { + create: Array.from({ length: Math.round(Math.random() * 10) }).map( + () => ({ + name: faker.name.fullName(), + email: faker.internet.email(), + }), + ), + }, + adminUrlId: faker.random.alpha(10), + participantUrlId: faker.random.alpha(10), + }, + }); + + return poll; +} async function createPollsForUser(userId: string) { // Create some polls const polls = await Promise.all( - Array.from({ length: 5 }).map(async (_, i) => { - // create some polls with no duration (all day) and some with a random duration. - const duration = i % 2 === 0 ? 60 * randInt(8, 1) : 0; - let cursor = dayjs().add(randInt(30), "day").second(0).minute(0); - - const numberOfOptions = randInt(30, 2); - - const poll = await prisma.poll.create({ - include: { - participants: true, - options: true, - }, - data: { - id: faker.random.alpha(10), - title: `${faker.animal.cat()} Meetup ${faker.date.month()}`, - description: faker.lorem.paragraph(), - location: faker.address.streetAddress(), - deadline: faker.date.future(), - user: { - connect: { - id: userId, - }, - }, - timeZone: duration !== 0 ? "Europe/London" : undefined, - options: { - create: Array.from({ length: numberOfOptions }).map(() => { - const startTime = cursor.toDate(); - if (duration !== 0) { - cursor = cursor.add(randInt(72, 1), "hour"); - } else { - cursor = cursor.add(randInt(7, 1), "day"); - } - return { - startTime, - duration, - }; - }), - }, - participants: { - create: Array.from({ length: Math.round(Math.random() * 10) }).map( - () => ({ - name: faker.name.fullName(), - email: faker.internet.email(), - }), - ), - }, - adminUrlId: faker.random.alpha(10), - participantUrlId: faker.random.alpha(10), - }, - }); - return poll; - }), + Array.from({ length: 100 }).map(() => createPollForUser(userId)), ); // Create some votes