From b1a403e4f2343421d928d9bed2d54d1610becec0 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 15 Apr 2023 22:25:32 -0400 Subject: [PATCH 1/3] Slightly alter colours in themes, change cards to compact --- src/prisma/ERD.svg | 1 - src/styles/globals.css | 2 +- tailwind.config.js | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 src/prisma/ERD.svg diff --git a/src/prisma/ERD.svg b/src/prisma/ERD.svg deleted file mode 100644 index 9c4504e..0000000 --- a/src/prisma/ERD.svg +++ /dev/null @@ -1 +0,0 @@ -UserStringidPKStringusernameStringfirstNameStringlastNameStringhashStringemailDateTimecreatedAtDateTimeupdatedAtnullableDateTimedateOfBirthBeerPostStringidPKStringnameFloatibuFloatabvStringdescriptionStringpostedByIdStringbreweryIdStringtypeIdDateTimecreatedAtDateTimeupdatedAtnullableBeerPostLikeStringidPKStringbeerPostIdStringlikedByIdDateTimecreatedAtDateTimeupdatedAtnullableBeerCommentStringidPKIntratingStringbeerPostIdStringpostedByIdStringcontentDateTimecreatedAtDateTimeupdatedAtnullableBeerTypeStringidPKStringnameDateTimecreatedAtDateTimeupdatedAtnullableStringpostedByIdBreweryPostStringidPKStringnameStringlocationStringdescriptionDateTimecreatedAtDateTimeupdatedAtnullableStringpostedByIdBreweryCommentStringidPKIntratingStringbreweryPostIdStringpostedByIdStringcontentDateTimecreatedAtDateTimeupdatedAtnullableBeerImageStringidPKStringbeerPostIdStringpathStringaltStringcaptionDateTimecreatedAtDateTimeupdatedAtnullableStringpostedByIdBreweryImageStringidPKStringbreweryPostIdStringpathDateTimecreatedAtDateTimeupdatedAtnullableStringcaptionStringaltStringpostedByIdpostedBybrewerytypebeerPostlikedBybeerPostpostedBypostedBypostedBybreweryPostpostedBybeerPostpostedBybreweryPostpostedBy \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 2505039..4ccbf08 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -3,5 +3,5 @@ @tailwind utilities; .card { - @apply shadow-md + @apply shadow-md card-compact bg-base-300 } \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index c5a68a2..0d20479 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,7 +12,7 @@ const darkTheme = { warning: 'hsl(50, 98%, 50%)', 'primary-content': 'hsl(0, 0%, 98%)', 'error-content': 'hsl(0, 0%, 98%)', - 'base-100': 'hsl(190, 4%, 9%)', + 'base-100': 'hsl(190, 4%, 11%)', 'base-200': 'hsl(190, 4%, 8%)', 'base-300': 'hsl(190, 4%, 5%)', }, @@ -28,11 +28,11 @@ const pastelTheme = { info: 'hsl(163, 40%, 79%)', success: 'hsl(93, 27%, 73%)', warning: 'hsl(40, 76%, 73%)', - 'primary-content': 'hsl(0, 0%, 12%)', - 'error-content': 'hsl(0, 0%, 12%)', - 'base-100': 'hsl(0, 0%, 85%)', - 'base-200': 'hsl(0, 0%, 82%)', - 'base-300': 'hsl(0, 0%, 78%)', + 'primary-content': 'hsl(0, 0%, 0%)', + 'error-content': 'hsl(0, 0%, 0%)', + 'base-100': 'hsl(0, 0%, 94%)', + 'base-200': 'hsl(0, 0%, 90%)', + 'base-300': 'hsl(0, 0%, 85%)', }, }; From 60fe48b7c1b2030edb8a60558d4f410933ff4765 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 16 Apr 2023 20:32:42 -0400 Subject: [PATCH 2/3] Begin work on infinite beer scroll --- .../BeerById/BeerPostLikeButton.tsx | 4 +- src/components/BeerIndex/BeerCard.tsx | 41 ++++-- .../BeerIndex/BeerPostLoadingCard.tsx | 26 ++++ src/hooks/useGetBeerPosts.ts | 56 ++++++++ src/pages/api/beers/index.ts | 50 ++++++++ src/pages/beers/index.tsx | 120 +++++++++++------- 6 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 src/components/BeerIndex/BeerPostLoadingCard.tsx create mode 100644 src/hooks/useGetBeerPosts.ts create mode 100644 src/pages/api/beers/index.ts 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; From 788d77e745a217d450787112c7839528f24acf00 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 16 Apr 2023 23:20:05 -0400 Subject: [PATCH 3/3] Add page count to api route header, update docs --- package.json | 2 +- .../{useGetBeerPosts.ts => useBeerPosts.ts} | 14 ++++++--- src/pages/api/beers/index.ts | 4 +++ src/pages/beers/[id]/index.tsx | 2 +- src/pages/beers/index.tsx | 29 +++++++++---------- src/prisma/DBClient.ts | 2 +- 6 files changed, 31 insertions(+), 22 deletions(-) rename src/hooks/{useGetBeerPosts.ts => useBeerPosts.ts} (75%) diff --git a/package.json b/package.json index 3cfd4fe..2282de2 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "next lint", "format": "npx prettier . --write", - "seed": "npx ts-node ./prisma/seed/index.ts" + "seed": "npx ts-node ./src/prisma/seed/index.ts" }, "dependencies": { "@hapi/iron": "^7.0.1", diff --git a/src/hooks/useGetBeerPosts.ts b/src/hooks/useBeerPosts.ts similarity index 75% rename from src/hooks/useGetBeerPosts.ts rename to src/hooks/useBeerPosts.ts index 9100402..4cc28a2 100644 --- a/src/hooks/useGetBeerPosts.ts +++ b/src/hooks/useBeerPosts.ts @@ -3,7 +3,14 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem import useSWRInfinite from 'swr/infinite'; import { z } from 'zod'; -const useGetBeerPosts = ({ pageSize }: { pageSize: number }) => { +/** + * A custom hook using SWR to fetch beer posts from the API. + * + * @param options The options to use when fetching beer posts. + * @param options.pageSize The number of beer posts to fetch per page. + * @returns An object containing the beer posts, page count, and loading state. + */ +const useBeerPosts = ({ pageSize }: { pageSize: number }) => { const fetcher = async (url: string) => { const response = await fetch(url); if (!response.ok) { @@ -37,8 +44,7 @@ const useGetBeerPosts = ({ pageSize }: { pageSize: number }) => { 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 isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined'; const isAtEnd = !(size < data?.[0].pageCount!); return { @@ -53,4 +59,4 @@ const useGetBeerPosts = ({ pageSize }: { pageSize: number }) => { }; }; -export default useGetBeerPosts; +export default useBeerPosts; diff --git a/src/pages/api/beers/index.ts b/src/pages/api/beers/index.ts index 31b921b..6d9c3c8 100644 --- a/src/pages/api/beers/index.ts +++ b/src/pages/api/beers/index.ts @@ -1,4 +1,5 @@ import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; @@ -21,6 +22,9 @@ const getBeerPosts = async ( const pageSize = parseInt(req.query.pageSize, 10); const beerPosts = await getAllBeerPosts(pageNum, pageSize); + const beerPostCount = await DBClient.instance.beerPost.count(); + + res.setHeader('X-Total-Count', beerPostCount); res.status(200).json({ message: 'Beer posts retrieved successfully', diff --git a/src/pages/beers/[id]/index.tsx b/src/pages/beers/[id]/index.tsx index d3c21bb..70fdeff 100644 --- a/src/pages/beers/[id]/index.tsx +++ b/src/pages/beers/[id]/index.tsx @@ -61,7 +61,7 @@ const BeerByIdPage: NextPage = ({ beerPost, beerRecommendations }
-
+
{isDesktop ? ( diff --git a/src/pages/beers/index.tsx b/src/pages/beers/index.tsx index cbb97b8..484a367 100644 --- a/src/pages/beers/index.tsx +++ b/src/pages/beers/index.tsx @@ -8,24 +8,23 @@ import { MutableRefObject, useContext, useRef } from 'react'; import { useInView } from 'react-intersection-observer'; import Spinner from '@/components/ui/Spinner'; -import useMediaQuery from '@/hooks/useMediaQuery'; -import useGetBeerPosts from '@/hooks/useGetBeerPosts'; + +import useBeerPosts from '@/hooks/useBeerPosts'; import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard'; import { FaArrowUp } from 'react-icons/fa'; const BeerPage: NextPage = () => { const { user } = useContext(UserContext); - const isDesktop = useMediaQuery('(min-width: 1024px)'); - const PAGE_SIZE = isDesktop ? 3 : 1; + const PAGE_SIZE = 2; - const { beerPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = useGetBeerPosts( - { pageSize: PAGE_SIZE }, - ); + const { beerPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = useBeerPosts({ + pageSize: PAGE_SIZE, + }); const { ref: lastBeerPostRef } = useInView({ onChange: (visible) => { - if (!visible) return; + if (!visible || isAtEnd) return; setSize(size + 1); }, }); @@ -39,7 +38,7 @@ const BeerPage: NextPage = () => {
-
+

The Biergarten Index

@@ -52,7 +51,7 @@ const BeerPage: NextPage = () => {
)}
-
+
{!!beerPosts.length && !isLoading && ( <> {beerPosts.map((beerPost, i) => { @@ -67,7 +66,7 @@ const BeerPage: NextPage = () => { })} )} - {isLoadingMore && ( + {(isLoading || isLoadingMore) && ( <> {Array.from({ length: PAGE_SIZE }, (_, i) => ( @@ -76,13 +75,13 @@ const BeerPage: NextPage = () => { )}
- {isLoadingMore && ( -
- + {(isLoading || isLoadingMore) && ( +
+
)} - {!isLoadingMore && isAtEnd && ( + {isAtEnd && !isLoading && (