Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
feat: infinite scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidRouyer committed Sep 19, 2023
1 parent 1e28054 commit bfc969b
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 16 deletions.
1 change: 1 addition & 0 deletions apps/customer-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.46.1",
"react-intersection-observer": "^9.5.2",
"react-intl": "^6.4.7",
"superjson": "^1.13.1",
"zod": "^3.22.2"
Expand Down
63 changes: 50 additions & 13 deletions apps/customer-service/src/components/tickets/ticket-list.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'use client';

import { FC, useEffect } from 'react';
import { FC, Fragment, useEffect } from 'react';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { PartyPopper } from 'lucide-react';
import { useInView } from 'react-intersection-observer';
import { FormattedMessage } from 'react-intl';

import { TicketStatus } from '@cs/database/schema/ticket';

import { TicketListItem } from '~/components/tickets/ticket-list-item';
import { TicketListItemSkeleton } from '~/components/tickets/ticket-list-item-skeleton';
import { api } from '~/utils/api';

export const TicketList: FC<{
Expand All @@ -18,31 +20,66 @@ export const TicketList: FC<{
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();

const [ticketsData] = api.ticket.all.useSuspenseQuery({
filter: filter,
status: status,
orderBy: orderBy,
const { ref, inView } = useInView({
triggerOnce: true,
threshold: 1,
});

const [data, allTicketsQuery] = api.ticket.all.useSuspenseInfiniteQuery(
{
filter: filter,
status: status,
orderBy: orderBy,
},
{
getNextPageParam(lastPage) {
return lastPage.nextCursor;
},
}
);

const { isFetching, fetchNextPage, hasNextPage } = allTicketsQuery;

useEffect(() => {
if (!ticketsData || ticketsData.length === 0 || params.id) return;
if (!data.pages || data.pages.length === 0 || params.id) return;

const firstTicketIdFromList = ticketsData?.[0]?.id;
const firstTicketIdFromList = data.pages[0]?.data?.[0]?.id;
if (!firstTicketIdFromList) return;

router.replace(
`/tickets/${firstTicketIdFromList}?${searchParams.toString()}`
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ticketsData]);
}, [data]);

useEffect(() => {
if (inView && hasNextPage && !isFetching) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetching, fetchNextPage]);

return (
<div className="no-scrollbar flex-auto overflow-y-auto">
{ticketsData?.length > 0 ? (
ticketsData.map((ticket) => (
<TicketListItem key={ticket.id} ticket={ticket} />
))
{data?.pages?.length > 0 ? (
<>
<div>
{data?.pages.map((page) => (
<Fragment key={page.nextCursor}>
{page.data.map((ticket) => (
<TicketListItem key={ticket.id} ticket={ticket} />
))}
</Fragment>
))}
</div>
{isFetching && (
<div className="flex w-full flex-col gap-4">
<TicketListItemSkeleton />
<TicketListItemSkeleton />
<TicketListItemSkeleton />
</div>
)}
<div ref={ref} className="h-px w-full" />
</>
) : (
<div className="py-10">
<div className="text-center">
Expand Down
22 changes: 19 additions & 3 deletions packages/api/src/router/ticket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';

import { and, asc, desc, eq, isNull, not, schema } from '@cs/database';
import { and, asc, desc, eq, gt, isNull, lt, not, schema } from '@cs/database';
import { TicketStatus } from '@cs/database/schema/ticket';
import {
TicketActivityType,
Expand All @@ -19,16 +19,26 @@ export const ticketRouter = createTRPCRouter({
filter: z.enum(['all', 'me', 'unassigned']),
status: z.enum([TicketStatus.Open, TicketStatus.Resolved]),
orderBy: z.enum(['newest', 'oldest']),
cursor: z.string().nullish(),
})
)
.query(({ ctx, input }) => {
return ctx.db.query.tickets.findMany({
.query(async ({ ctx, input }) => {
const PAGE_SIZE = 10;
const tickets = await ctx.db.query.tickets.findMany({
orderBy: {
newest: desc(schema.tickets.createdAt),
oldest: asc(schema.tickets.createdAt),
}[input.orderBy],
where: and(
eq(schema.tickets.status, input.status),
{
newest: input.cursor
? lt(schema.tickets.createdAt, new Date(input.cursor))
: undefined,
oldest: input.cursor
? gt(schema.tickets.createdAt, new Date(input.cursor))
: undefined,
}[input.orderBy],
{
all: undefined,
me: eq(
Expand All @@ -39,7 +49,13 @@ export const ticketRouter = createTRPCRouter({
}[input.filter]
),
with: { author: true },
limit: PAGE_SIZE,
});

const nextCursor =
tickets[tickets.length - 1]?.createdAt.toISOString() ?? null;

return { data: tickets, nextCursor };
}),

byId: protectedProcedure
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4518,6 +4518,11 @@ react-hook-form@^7.46.1:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.46.1.tgz#39347dbff19d980cb41087ac32a57abdc6045bb3"
integrity sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==

react-intersection-observer@^9.5.2:
version "9.5.2"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz#f68363a1ff292323c0808201b58134307a1626d0"
integrity sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==

react-intl@^6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.4.7.tgz#28ec40350ff791a6a773f5e76b9e12835ae17e19"
Expand Down

0 comments on commit bfc969b

Please sign in to comment.