From 8093817ccc8f26cf088c40253600ec3b07ce79fb Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 30 Oct 2023 12:21:51 -0400 Subject: [PATCH 1/2] Fix: Update page count calc --- .../BeerStyleById/BeerStyleBeerSection.tsx | 100 ++++++++++++++++++ .../api/beers/styles/[id]/beers/index.ts | 71 +++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/components/BeerStyleById/BeerStyleBeerSection.tsx create mode 100644 src/pages/api/beers/styles/[id]/beers/index.ts diff --git a/src/components/BeerStyleById/BeerStyleBeerSection.tsx b/src/components/BeerStyleById/BeerStyleBeerSection.tsx new file mode 100644 index 0000000..c0c4d45 --- /dev/null +++ b/src/components/BeerStyleById/BeerStyleBeerSection.tsx @@ -0,0 +1,100 @@ +import Link from 'next/link'; +import { FC, MutableRefObject, useRef } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { z } from 'zod'; + +import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult'; +import useBeerPostsByBeerStyle from '@/hooks/data-fetching/beer-posts/useBeerPostsByBeerStyles'; +import BeerRecommendationLoadingComponent from '../BeerById/BeerRecommendationLoadingComponent'; + +interface BeerStyleBeerSectionProps { + beerStyle: z.infer; +} + +const BeerStyleBeerSection: FC = ({ beerStyle }) => { + const PAGE_SIZE = 2; + + const { beerPosts, isAtEnd, isLoadingMore, setSize, size } = useBeerPostsByBeerStyle({ + beerStyleId: beerStyle.id, + pageSize: PAGE_SIZE, + }); + const { ref: penultimateBeerPostRef } = useInView({ + /** + * When the last beer post comes into view, call setSize from useBeerPostsByBeerStyle + * to load more beer posts. + */ + onChange: (visible) => { + if (!visible || isAtEnd) return; + setSize(size + 1); + }, + }); + + const beerRecommendationsRef: MutableRefObject = useRef(null); + + return ( +
+
+ <> +
+
+

Brews

+
+
+ + {!!beerPosts.length && ( +
+ {beerPosts.map((beerPost, index) => { + const isPenultimateBeerPost = index === beerPosts.length - 2; + + /** + * Attach a ref to the second last beer post in the list. When it comes + * into view, the component will call setSize to load more beer posts. + */ + + return ( +
+
+ + {beerPost.name} + +
+
+ + + {beerPost.brewery.name} + + +
+ +
+ {beerPost.abv}% ABV + {beerPost.ibu} IBU +
+
+ ); + })} +
+ )} + + { + /** + * If there are more beer posts to load, show a loading component with a + * skeleton loader and a loading spinner. + */ + !!isLoadingMore && !isAtEnd && ( + + ) + } + +
+
+ ); +}; + +export default BeerStyleBeerSection; diff --git a/src/pages/api/beers/styles/[id]/beers/index.ts b/src/pages/api/beers/styles/[id]/beers/index.ts new file mode 100644 index 0000000..ca66936 --- /dev/null +++ b/src/pages/api/beers/styles/[id]/beers/index.ts @@ -0,0 +1,71 @@ +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +interface GetAllBeersByBeerStyleRequest extends NextApiRequest { + query: { page_size: string; page_num: string; id: string }; +} + +const getAllBeersByBeerStyle = async ( + req: GetAllBeersByBeerStyleRequest, + res: NextApiResponse>, +) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num, id } = req.query; + + const beers: z.infer[] = + await DBClient.instance.beerPost.findMany({ + where: { styleId: id }, + take: parseInt(page_size, 10), + skip: parseInt(page_num, 10) * parseInt(page_size, 10), + select: { + id: true, + name: true, + ibu: true, + abv: true, + createdAt: true, + updatedAt: true, + description: true, + postedBy: { select: { username: true, id: true } }, + brewery: { select: { name: true, id: true } }, + style: { select: { name: true, id: true, description: true } }, + beerImages: { select: { alt: true, path: true, caption: true, id: true } }, + }, + }); + + const pageCount = await DBClient.instance.beerPost.count({ where: { styleId: id } }); + + res.setHeader('X-Total-Count', pageCount); + + res.status(200).json({ + message: 'Beers fetched successfully', + statusCode: 200, + payload: beers, + success: true, + }); +}; + +const router = createRouter< + GetAllBeersByBeerStyleRequest, + NextApiResponse> +>(); + +router.get( + validateRequest({ + querySchema: z.object({ + page_size: z.string().min(1), + page_num: z.string().min(1), + id: z.string().min(1), + }), + }), + getAllBeersByBeerStyle, +); + +const handler = router.handler(NextConnectOptions); + +export default handler; From 51c29702d3f68e6a209dc2da48474bb2329d11a3 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 30 Oct 2023 12:37:29 -0400 Subject: [PATCH 2/2] Refactor: extract logic out of api route --- .../[id]/beers/getAllBeersByBeerStyle.ts | 45 +++++++++++++++++++ .../api/beers/styles/[id]/beers/index.ts | 26 +++-------- .../BeerPost/getBeerPostsByBeerStyleId.ts | 38 ++++++++++++++++ .../BeerPost/getBeerPostsByBreweryId.ts | 38 ++++++++++++++++ 4 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 src/pages/api/beers/styles/[id]/beers/getAllBeersByBeerStyle.ts create mode 100644 src/services/BeerPost/getBeerPostsByBeerStyleId.ts create mode 100644 src/services/BeerPost/getBeerPostsByBreweryId.ts diff --git a/src/pages/api/beers/styles/[id]/beers/getAllBeersByBeerStyle.ts b/src/pages/api/beers/styles/[id]/beers/getAllBeersByBeerStyle.ts new file mode 100644 index 0000000..1524073 --- /dev/null +++ b/src/pages/api/beers/styles/[id]/beers/getAllBeersByBeerStyle.ts @@ -0,0 +1,45 @@ +import DBClient from '@/prisma/DBClient'; +import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { z } from 'zod'; +import { GetAllBeersByBeerStyleRequest } from '.'; + +export const getAllBeersByBeerStyle = async ( + req: GetAllBeersByBeerStyleRequest, + res: NextApiResponse>, +) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num, id } = req.query; + + const beers: z.infer[] = + await DBClient.instance.beerPost.findMany({ + where: { styleId: id }, + take: parseInt(page_size, 10), + skip: parseInt(page_num, 10) * parseInt(page_size, 10), + select: { + id: true, + name: true, + ibu: true, + abv: true, + createdAt: true, + updatedAt: true, + description: true, + postedBy: { select: { username: true, id: true } }, + brewery: { select: { name: true, id: true } }, + style: { select: { name: true, id: true, description: true } }, + beerImages: { select: { alt: true, path: true, caption: true, id: true } }, + }, + }); + + const pageCount = await DBClient.instance.beerPost.count({ where: { styleId: id } }); + + res.setHeader('X-Total-Count', pageCount); + + res.status(200).json({ + message: 'Beers fetched successfully', + statusCode: 200, + payload: beers, + success: true, + }); +}; diff --git a/src/pages/api/beers/styles/[id]/beers/index.ts b/src/pages/api/beers/styles/[id]/beers/index.ts index ca66936..532c903 100644 --- a/src/pages/api/beers/styles/[id]/beers/index.ts +++ b/src/pages/api/beers/styles/[id]/beers/index.ts @@ -1,7 +1,7 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import DBClient from '@/prisma/DBClient'; -import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import getBeerPostsByBeerStyleId from '@/services/BeerPost/getBeerPostsByBeerStyleId'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiRequest, NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; @@ -18,25 +18,11 @@ const getAllBeersByBeerStyle = async ( // eslint-disable-next-line @typescript-eslint/naming-convention const { page_size, page_num, id } = req.query; - const beers: z.infer[] = - await DBClient.instance.beerPost.findMany({ - where: { styleId: id }, - take: parseInt(page_size, 10), - skip: parseInt(page_num, 10) * parseInt(page_size, 10), - select: { - id: true, - name: true, - ibu: true, - abv: true, - createdAt: true, - updatedAt: true, - description: true, - postedBy: { select: { username: true, id: true } }, - brewery: { select: { name: true, id: true } }, - style: { select: { name: true, id: true, description: true } }, - beerImages: { select: { alt: true, path: true, caption: true, id: true } }, - }, - }); + const beers = getBeerPostsByBeerStyleId({ + pageNum: parseInt(page_num, 10), + pageSize: parseInt(page_size, 10), + styleId: id, + }); const pageCount = await DBClient.instance.beerPost.count({ where: { styleId: id } }); diff --git a/src/services/BeerPost/getBeerPostsByBeerStyleId.ts b/src/services/BeerPost/getBeerPostsByBeerStyleId.ts new file mode 100644 index 0000000..adc1fc7 --- /dev/null +++ b/src/services/BeerPost/getBeerPostsByBeerStyleId.ts @@ -0,0 +1,38 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import BeerPostQueryResult from './schema/BeerPostQueryResult'; + +interface GetBeerPostsByBeerStyleIdArgs { + styleId: string; + pageSize: number; + pageNum: number; +} + +const getBeerPostsByBeerStyleId = async ({ + pageNum, + pageSize, + styleId, +}: GetBeerPostsByBeerStyleIdArgs): Promise[]> => { + const beers = await DBClient.instance.beerPost.findMany({ + where: { styleId }, + take: pageSize, + skip: pageNum * pageSize, + select: { + id: true, + name: true, + ibu: true, + abv: true, + createdAt: true, + updatedAt: true, + description: true, + postedBy: { select: { username: true, id: true } }, + brewery: { select: { name: true, id: true } }, + style: { select: { name: true, id: true, description: true } }, + beerImages: { select: { alt: true, path: true, caption: true, id: true } }, + }, + }); + + return beers; +}; + +export default getBeerPostsByBeerStyleId; diff --git a/src/services/BeerPost/getBeerPostsByBreweryId.ts b/src/services/BeerPost/getBeerPostsByBreweryId.ts new file mode 100644 index 0000000..910391b --- /dev/null +++ b/src/services/BeerPost/getBeerPostsByBreweryId.ts @@ -0,0 +1,38 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import BeerPostQueryResult from './schema/BeerPostQueryResult'; + +interface GetBeerPostsByBeerStyleIdArgs { + breweryId: string; + pageSize: number; + pageNum: number; +} + +const getBeerPostsByBeerStyleId = async ({ + pageNum, + pageSize, + breweryId, +}: GetBeerPostsByBeerStyleIdArgs): Promise[]> => { + const beers = await DBClient.instance.beerPost.findMany({ + where: { breweryId }, + take: pageSize, + skip: pageNum * pageSize, + select: { + id: true, + name: true, + ibu: true, + abv: true, + createdAt: true, + updatedAt: true, + description: true, + postedBy: { select: { username: true, id: true } }, + brewery: { select: { name: true, id: true } }, + style: { select: { name: true, id: true, description: true } }, + beerImages: { select: { alt: true, path: true, caption: true, id: true } }, + }, + }); + + return beers; +}; + +export default getBeerPostsByBeerStyleId;