mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-22 21:36:25 +02:00
⚡️ Use infinite query on polls page (#1200)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
e985a5db9f
commit
43c450aa91
4 changed files with 182 additions and 66 deletions
|
@ -22,6 +22,7 @@ import {
|
||||||
import { PollStatusBadge } from "@/components/poll-status";
|
import { PollStatusBadge } from "@/components/poll-status";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
import { VisibilityTrigger } from "@/components/visibility-trigger";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
function PollCount({ count }: { count?: number }) {
|
function PollCount({ count }: { count?: number }) {
|
||||||
|
@ -29,26 +30,36 @@ function PollCount({ count }: { count?: number }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilteredPolls({ status }: { status: PollStatus }) {
|
function FilteredPolls({ status }: { status: PollStatus }) {
|
||||||
const { data, isFetching } = trpc.polls.list.useQuery(
|
const { data, fetchNextPage, hasNextPage } =
|
||||||
{
|
trpc.polls.infiniteList.useInfiniteQuery(
|
||||||
status,
|
{
|
||||||
},
|
status,
|
||||||
{
|
limit: 30,
|
||||||
keepPreviousData: true,
|
},
|
||||||
},
|
{
|
||||||
);
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="space-y-6">
|
||||||
className={cn({
|
<ol className="space-y-4">
|
||||||
"animate-pulse": isFetching,
|
{data.pages.map((page, i) => (
|
||||||
})}
|
<li key={i}>
|
||||||
>
|
<PollsListView data={page.polls} />
|
||||||
<PollsListView data={data} />
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{hasNextPage ? (
|
||||||
|
<VisibilityTrigger onVisible={fetchNextPage} className="mt-6">
|
||||||
|
<Spinner />
|
||||||
|
</VisibilityTrigger>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
51
apps/web/src/components/visibility-trigger.tsx
Normal file
51
apps/web/src/components/visibility-trigger.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function useVisibilityTrigger<T extends Element>(onVisible: () => void) {
|
||||||
|
const triggerRef = React.useRef<T | null>(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<HTMLDivElement>(onVisible);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} ref={triggerRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
// START LEGACY ROUTES
|
||||||
create: possiblyPublicProcedure
|
create: possiblyPublicProcedure
|
||||||
|
|
|
@ -7,62 +7,64 @@ const prisma = new PrismaClient();
|
||||||
const randInt = (max = 1, floor = 0) => {
|
const randInt = (max = 1, floor = 0) => {
|
||||||
return Math.round(Math.random() * max) + floor;
|
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) {
|
async function createPollsForUser(userId: string) {
|
||||||
// Create some polls
|
// Create some polls
|
||||||
const polls = await Promise.all(
|
const polls = await Promise.all(
|
||||||
Array.from({ length: 5 }).map(async (_, i) => {
|
Array.from({ length: 100 }).map(() => createPollForUser(userId)),
|
||||||
// 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;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create some votes
|
// Create some votes
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue