diff --git a/package.json b/package.json index ccb3ce2..5fcf6a7 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.41.5", "react-hot-toast": "^2.4.0", + "react-intersection-observer": "^9.4.2", "react-tooltip": "^5.7.2", "sharp": "^0.31.3", "string-similarity": "^4.0.4", diff --git a/src/constants/index.ts b/src/constants/index.ts index 6cbbf1c..124b5bb 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3,6 +3,7 @@ import { Visibility } from '@prisma/client' import type { Option } from '~/components/radio-group' export const MAX_TOPICS_PER_DECK = 5 +export const ITEMS_PER_PAGE = 30 export const DECK_VISIBILITY_OPTIONS: Array> = [ { diff --git a/src/pages/decks/review/index.tsx b/src/pages/decks/review/index.tsx index a6aa75a..72aa906 100644 --- a/src/pages/decks/review/index.tsx +++ b/src/pages/decks/review/index.tsx @@ -1,21 +1,42 @@ +import { InView } from 'react-intersection-observer' + import { type NextPage } from 'next' import Head from 'next/head' import { DeckCardList } from '~/components/deck-card-list' +import { Loader } from '~/components/loader' import type { WithAuthentication } from '~/types/auth' import { api } from '~/utils/api' -// TODO emiliosheinz: Add pagination const DecksToBeReviewed: WithAuthentication = () => { - const { isLoading, isError, data, refetch } = api.decks.toBeReviewed.useQuery( - { page: 0 }, + const { + data, + isError, + refetch, + isLoading, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = api.decks.toBeReviewed.useInfiniteQuery( + {}, + { + getNextPageParam: lastPage => lastPage.nextCursor, + refetchOnWindowFocus: false, + keepPreviousData: true, + }, ) + const decks = data?.pages.flatMap(page => page.decks) ?? [] + const hasLoadedDecks = decks.length > 0 + const renderContent = () => { if (isLoading) return - if (isError) return - return + if (!hasLoadedDecks && isError) { + return + } + + return } return ( @@ -27,7 +48,18 @@ const DecksToBeReviewed: WithAuthentication = () => { content='Lista de decks que vocĂȘ precisa revisar' /> -
{renderContent()}
+ {renderContent()} + { + if (inView && hasNextPage && !isError && !isFetchingNextPage) { + fetchNextPage() + } + }} + > + {isFetchingNextPage ? : null} + ) } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1a1da34..fca94b9 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,23 +1,58 @@ +import { InView } from 'react-intersection-observer' + import { type NextPage } from 'next' import { DeckCardList } from '~/components/deck-card-list' +import { Loader } from '~/components/loader' import { api } from '~/utils/api' const Home: NextPage = () => { - // TODO emiliosheinz: Add pagination - const { isLoading, isError, data, refetch } = - api.decks.getPublicDecks.useQuery({ - page: 0, - }) + const { + data, + isError, + refetch, + isLoading, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = api.decks.getPublicDecks.useInfiniteQuery( + {}, + { + getNextPageParam: lastPage => lastPage.nextCursor, + refetchOnWindowFocus: false, + keepPreviousData: true, + }, + ) + + const decks = data?.pages.flatMap(page => page.decks) ?? [] + const hasLoadedDecks = decks.length > 0 const renderContent = () => { if (isLoading) return - if (isError) return - return + if (!hasLoadedDecks && isError) { + return + } + + return } - return
{renderContent()}
+ return ( +
+ {renderContent()} + { + if (inView && hasNextPage && !isError && !isFetchingNextPage) { + fetchNextPage() + } + }} + > + {isFetchingNextPage ? : null} + +
+ ) } export default Home diff --git a/src/server/api/routers/decks/decks.router.ts b/src/server/api/routers/decks/decks.router.ts index a6a56a7..d53bd58 100644 --- a/src/server/api/routers/decks/decks.router.ts +++ b/src/server/api/routers/decks/decks.router.ts @@ -1,6 +1,7 @@ import { Visibility } from '@prisma/client' import { z } from 'zod' +import { ITEMS_PER_PAGE } from '~/constants' import { createTRPCRouter, protectedProcedure, @@ -95,38 +96,47 @@ export const decksRouter = createTRPCRouter({ await deleteObjectFromS3(deck.image) await ctx.prisma.deck.delete({ where: { id } }) }), + /** + * Based on https://trpc.io/docs/useInfiniteQuery + */ getPublicDecks: publicProcedure .input( z.object({ - page: z.number(), + cursor: z.string().nullish(), }), ) - .query(async ({ input: { page }, ctx }) => { + .query(async ({ input: { cursor }, ctx }) => { const decks = await ctx.prisma.deck.findMany({ where: { visibility: Visibility.Public }, orderBy: { createdAt: 'desc' }, - take: 30, - skip: page * 30, + take: ITEMS_PER_PAGE + 1, // get an extra item at the end which we'll use as next cursor + cursor: cursor ? { id: cursor } : undefined, }) - return decks.map(deck => ({ - ...deck, - image: getS3ImageUrl(deck.image), - })) + const hasNextCursor = decks.length > ITEMS_PER_PAGE + const nextCursor = hasNextCursor ? decks.pop()!.id : undefined + + return { + nextCursor, + decks: decks.map(deck => ({ + ...deck, + image: getS3ImageUrl(deck.image), + })), + } }), toBeReviewed: protectedProcedure .input( z.object({ - page: z.number(), + cursor: z.string().nullish(), }), ) - .query(async ({ input: { page }, ctx }) => { + .query(async ({ input: { cursor }, ctx }) => { const { user } = ctx.session const now = new Date() const decks = await ctx.prisma.deck.findMany({ - take: 10, - skip: page * 10, + take: ITEMS_PER_PAGE + 1, + cursor: cursor ? { id: cursor } : undefined, orderBy: { createdAt: 'desc', }, @@ -145,9 +155,15 @@ export const decksRouter = createTRPCRouter({ }, }) - return decks.map(deck => ({ - ...deck, - image: getS3ImageUrl(deck.image), - })) + const hasNextCursor = decks.length > ITEMS_PER_PAGE + const nextCursor = hasNextCursor ? decks.pop()!.id : undefined + + return { + decks: decks.map(deck => ({ + ...deck, + image: getS3ImageUrl(deck.image), + })), + nextCursor, + } }), }) diff --git a/yarn.lock b/yarn.lock index a0c824a..a78a830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7251,6 +7251,11 @@ react-hot-toast@^2.4.0: dependencies: goober "^2.1.10" +react-intersection-observer@^9.4.2: + version "9.4.2" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.2.tgz#a80401d290715d8b89941d037bc4ad1398b8397f" + integrity sha512-AdK+ryzZ7U9ZJYttDUZ8q2Am3nqE0exg5Ryl5Y124KeVsix/1hGZPbdu58EqA98TwnzwDNWHxg/kwNawmIiUig== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"