diff --git a/src/components/BeerById/BeerPostLikeButton.tsx b/src/components/BeerById/BeerPostLikeButton.tsx index bc54a92..3847ce1 100644 --- a/src/components/BeerById/BeerPostLikeButton.tsx +++ b/src/components/BeerById/BeerPostLikeButton.tsx @@ -20,8 +20,8 @@ const BeerPostLikeButton: FC<{ try { setLoading(true); await sendLikeRequest(beerPostId); - await mutateCount(); - await mutateLikeStatus(); + + await Promise.all([mutateCount(), mutateLikeStatus()]); setLoading(false); } catch (e) { setLoading(false); diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index a498833..c4d0652 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -1,13 +1,19 @@ import Link from 'next/link'; -import { FC } from 'react'; +import { FC, useContext } from 'react'; import Image from 'next/image'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; +import UserContext from '@/contexts/userContext'; +import useGetLikeCount from '@/hooks/useGetLikeCount'; +import BeerPostLikeButton from '../BeerById/BeerPostLikeButton'; const BeerCard: FC<{ post: z.infer }> = ({ post }) => { + const { user } = useContext(UserContext); + const { mutate, likeCount } = useGetLikeCount(post.id); + return ( -
-
+
+
{post.beerImages.length > 0 && ( }> = ({ post }) = )}
-
+
+
+ +

+ {post.name} +

+ + +

+ {post.brewery.name} +

+ +
-

- {post.name} -

-

{post.brewery.name}

+
+

{post.type.name}

+
+ {post.abv}% ABV + {post.ibu} IBU +
+
+
+ liked by {likeCount} users + {user && } +
diff --git a/src/components/BeerIndex/BeerPostLoadingCard.tsx b/src/components/BeerIndex/BeerPostLoadingCard.tsx new file mode 100644 index 0000000..e8f7743 --- /dev/null +++ b/src/components/BeerIndex/BeerPostLoadingCard.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react'; + +const BeerPostLoadingCard: FC = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default BeerPostLoadingCard; diff --git a/src/hooks/useGetBeerPosts.ts b/src/hooks/useGetBeerPosts.ts new file mode 100644 index 0000000..9100402 --- /dev/null +++ b/src/hooks/useGetBeerPosts.ts @@ -0,0 +1,56 @@ +import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import useSWRInfinite from 'swr/infinite'; +import { z } from 'zod'; + +const useGetBeerPosts = ({ pageSize }: { pageSize: number }) => { + const fetcher = async (url: string) => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(response.statusText); + } + + const json = await response.json(); + const count = response.headers.get('X-Total-Count'); + + const parsed = APIResponseValidationSchema.safeParse(json); + if (!parsed.success) { + throw new Error('API response validation failed'); + } + + const parsedPayload = z.array(beerPostQueryResult).safeParse(parsed.data.payload); + if (!parsedPayload.success) { + throw new Error('API response validation failed'); + } + + const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize); + return { + beerPosts: parsedPayload.data, + pageCount, + }; + }; + + const { data, error, isLoading, setSize, size } = useSWRInfinite( + (index) => `/api/beers?pageNum=${index + 1}&pageSize=${pageSize}`, + fetcher, + ); + + const beerPosts = data?.flatMap((d) => d.beerPosts) ?? []; + const pageCount = data?.[0].pageCount ?? 0; + const isLoadingMore = + isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined'); + const isAtEnd = !(size < data?.[0].pageCount!); + + return { + beerPosts, + pageCount, + size, + setSize, + isLoading, + isLoadingMore, + isAtEnd, + error: error as unknown, + }; +}; + +export default useGetBeerPosts; diff --git a/src/pages/api/beers/index.ts b/src/pages/api/beers/index.ts new file mode 100644 index 0000000..31b921b --- /dev/null +++ b/src/pages/api/beers/index.ts @@ -0,0 +1,50 @@ +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +interface GetBeerPostsRequest extends NextApiRequest { + query: { + pageNum: string; + pageSize: string; + }; +} + +const getBeerPosts = async ( + req: GetBeerPostsRequest, + res: NextApiResponse>, +) => { + const pageNum = parseInt(req.query.pageNum, 10); + const pageSize = parseInt(req.query.pageSize, 10); + + const beerPosts = await getAllBeerPosts(pageNum, pageSize); + + res.status(200).json({ + message: 'Beer posts retrieved successfully', + statusCode: 200, + payload: beerPosts, + success: true, + }); +}; + +const router = createRouter< + GetBeerPostsRequest, + NextApiResponse> +>(); + +router.get( + validateRequest({ + querySchema: z.object({ + pageNum: z.string().regex(/^\d+$/), + pageSize: z.string().regex(/^\d+$/), + }), + }), + getBeerPosts, +); + +const handler = router.handler(); + +export default handler; diff --git a/src/pages/beers/index.tsx b/src/pages/beers/index.tsx index 2471037..cbb97b8 100644 --- a/src/pages/beers/index.tsx +++ b/src/pages/beers/index.tsx @@ -1,44 +1,48 @@ -import { GetServerSideProps, NextPage } from 'next'; -import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts'; - -import { useRouter } from 'next/router'; -import DBClient from '@/prisma/DBClient'; +import { NextPage } from 'next'; import Layout from '@/components/ui/Layout'; -import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar'; import BeerCard from '@/components/BeerIndex/BeerCard'; -import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import Head from 'next/head'; -import { z } from 'zod'; import Link from 'next/link'; import UserContext from '@/contexts/userContext'; -import { useContext } from 'react'; +import { MutableRefObject, useContext, useRef } from 'react'; -interface BeerPageProps { - initialBeerPosts: z.infer[]; - pageCount: number; -} - -const BeerPage: NextPage = ({ initialBeerPosts, pageCount }) => { - const router = useRouter(); - const { query } = router; +import { useInView } from 'react-intersection-observer'; +import Spinner from '@/components/ui/Spinner'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import useGetBeerPosts from '@/hooks/useGetBeerPosts'; +import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard'; +import { FaArrowUp } from 'react-icons/fa'; +const BeerPage: NextPage = () => { const { user } = useContext(UserContext); - const pageNum = parseInt(query.page_num as string, 10) || 1; + const isDesktop = useMediaQuery('(min-width: 1024px)'); + const PAGE_SIZE = isDesktop ? 3 : 1; + + const { beerPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = useGetBeerPosts( + { pageSize: PAGE_SIZE }, + ); + + const { ref: lastBeerPostRef } = useInView({ + onChange: (visible) => { + if (!visible) return; + setSize(size + 1); + }, + }); + + const pageRef: MutableRefObject = useRef(null); + return ( Beer -
-
+
+

The Biergarten Index

-

- Page {pageNum} of {pageCount} -

{!!user && (
@@ -48,31 +52,61 @@ const BeerPage: NextPage = ({ initialBeerPosts, pageCount }) => {
)}
-
- {initialBeerPosts.map((post) => { - return ; - })} -
-
- +
+ {!!beerPosts.length && !isLoading && ( + <> + {beerPosts.map((beerPost, i) => { + return ( +
+ +
+ ); + })} + + )} + {isLoadingMore && ( + <> + {Array.from({ length: PAGE_SIZE }, (_, i) => ( + + ))} + + )}
+ + {isLoadingMore && ( +
+ +
+ )} + + {!isLoadingMore && isAtEnd && ( +
+
+ +
+
+ )}
); }; -export const getServerSideProps: GetServerSideProps = async (context) => { - const { query } = context; - const pageNumber = parseInt(query.page_num as string, 10) || 1; - const pageSize = 12; - const numberOfPosts = await DBClient.instance.beerPost.count(); - const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0; - const beerPosts = await getAllBeerPosts(pageNumber, pageSize); - - return { - props: { initialBeerPosts: JSON.parse(JSON.stringify(beerPosts)), pageCount }, - }; -}; - export default BeerPage;