From 58d30b605f8fa50b8cb9dd4449f5a3b44fab50ad Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 23 Apr 2023 17:25:39 -0400 Subject: [PATCH] Continue work on brewery page, implement like system --- src/components/BeerById/BeerInfoHeader.tsx | 4 +- .../BeerById/BeerPostLikeButton.tsx | 35 +--- src/components/BeerIndex/BeerCard.tsx | 12 +- src/components/BreweryIndex/BreweryCard.tsx | 43 +++++ .../BreweryIndex/BreweryPostLikeButton.tsx | 30 ++++ src/components/ui/LikeButton.tsx | 37 +++++ .../LoadingCard.tsx} | 4 +- .../withPageAuthRequired.ts | 4 +- ...etLikeCount.ts => useBeerPostLikeCount.ts} | 4 +- src/hooks/useBreweryPosts.ts | 62 +++++++ src/hooks/useCheckIfUserLikesBreweryPost.ts | 45 +++++ src/hooks/useGetBreweryPostLikeCount.ts | 40 +++++ src/pages/api/beers/[id]/like/index.ts | 4 +- src/pages/api/breweries/[id]/like/index.ts | 97 +++++++++++ src/pages/api/breweries/[id]/like/is-liked.ts | 49 ++++++ src/pages/api/breweries/index.ts | 54 ++++++ src/pages/beers/create.tsx | 4 +- src/pages/beers/index.tsx | 9 +- src/pages/breweries/index.tsx | 155 +++++++++++------- .../migrations/20230423163714_/migration.sql | 16 ++ src/prisma/schema.prisma | 32 ++-- .../seed/create/createNewBreweryPostLikes.ts | 33 ++++ src/prisma/seed/index.ts | 5 + ...eRequest.ts => sendBeerPostLikeRequest.ts} | 8 +- src/requests/sendBreweryPostLikeRequest.ts | 25 +++ .../BreweryPost/getAllBreweryPosts.ts | 7 +- tailwind.config.js | 6 +- 27 files changed, 699 insertions(+), 125 deletions(-) create mode 100644 src/components/BreweryIndex/BreweryCard.tsx create mode 100644 src/components/BreweryIndex/BreweryPostLikeButton.tsx create mode 100644 src/components/ui/LikeButton.tsx rename src/components/{BeerIndex/BeerPostLoadingCard.tsx => ui/LoadingCard.tsx} (91%) rename src/hooks/{useGetLikeCount.ts => useBeerPostLikeCount.ts} (92%) create mode 100644 src/hooks/useBreweryPosts.ts create mode 100644 src/hooks/useCheckIfUserLikesBreweryPost.ts create mode 100644 src/hooks/useGetBreweryPostLikeCount.ts create mode 100644 src/pages/api/breweries/[id]/like/index.ts create mode 100644 src/pages/api/breweries/[id]/like/is-liked.ts create mode 100644 src/pages/api/breweries/index.ts create mode 100644 src/prisma/migrations/20230423163714_/migration.sql create mode 100644 src/prisma/seed/create/createNewBreweryPostLikes.ts rename src/requests/{sendLikeRequest.ts => sendBeerPostLikeRequest.ts} (75%) create mode 100644 src/requests/sendBreweryPostLikeRequest.ts diff --git a/src/components/BeerById/BeerInfoHeader.tsx b/src/components/BeerById/BeerInfoHeader.tsx index 3a9979d..a6f62c1 100644 --- a/src/components/BeerById/BeerInfoHeader.tsx +++ b/src/components/BeerById/BeerInfoHeader.tsx @@ -6,7 +6,7 @@ import UserContext from '@/contexts/userContext'; import { FaRegEdit } from 'react-icons/fa'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; -import useGetLikeCount from '@/hooks/useGetLikeCount'; +import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; import useTimeDistance from '@/hooks/useTimeDistance'; import BeerPostLikeButton from './BeerPostLikeButton'; @@ -20,7 +20,7 @@ const BeerInfoHeader: FC<{ const idMatches = user && beerPost.postedBy.id === user.id; const isPostOwner = !!(user && idMatches); - const { likeCount, mutate } = useGetLikeCount(beerPost.id); + const { likeCount, mutate } = useGetBeerPostLikeCount(beerPost.id); return (
diff --git a/src/components/BeerById/BeerPostLikeButton.tsx b/src/components/BeerById/BeerPostLikeButton.tsx index e7d9f42..fbc3806 100644 --- a/src/components/BeerById/BeerPostLikeButton.tsx +++ b/src/components/BeerById/BeerPostLikeButton.tsx @@ -1,13 +1,13 @@ import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost'; -import sendLikeRequest from '@/requests/sendLikeRequest'; +import sendBeerPostLikeRequest from '@/requests/sendBeerPostLikeRequest'; import { FC, useEffect, useState } from 'react'; -import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; -import useGetLikeCount from '@/hooks/useGetLikeCount'; +import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; +import LikeButton from '../ui/LikeButton'; const BeerPostLikeButton: FC<{ beerPostId: string; - mutateCount: ReturnType['mutate']; + mutateCount: ReturnType['mutate']; }> = ({ beerPostId, mutateCount }) => { const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId); const [loading, setLoading] = useState(true); @@ -19,7 +19,7 @@ const BeerPostLikeButton: FC<{ const handleLike = async () => { try { setLoading(true); - await sendLikeRequest(beerPostId); + await sendBeerPostLikeRequest(beerPostId); await Promise.all([mutateCount(), mutateLikeStatus()]); setLoading(false); @@ -28,30 +28,7 @@ const BeerPostLikeButton: FC<{ } }; - return ( - - ); + return ; }; export default BeerPostLikeButton; diff --git a/src/components/BeerIndex/BeerCard.tsx b/src/components/BeerIndex/BeerCard.tsx index ef03600..26b1a73 100644 --- a/src/components/BeerIndex/BeerCard.tsx +++ b/src/components/BeerIndex/BeerCard.tsx @@ -4,12 +4,12 @@ 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 useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount'; import BeerPostLikeButton from '../BeerById/BeerPostLikeButton'; const BeerCard: FC<{ post: z.infer }> = ({ post }) => { const { user } = useContext(UserContext); - const { mutate, likeCount } = useGetLikeCount(post.id); + const { mutate, likeCount } = useGetBeerPostLikeCount(post.id); return (
@@ -27,14 +27,14 @@ const BeerCard: FC<{ post: z.infer }> = ({ post }) =
-

+

{post.name} -

+ -

+

{post.brewery.name} -

+
diff --git a/src/components/BreweryIndex/BreweryCard.tsx b/src/components/BreweryIndex/BreweryCard.tsx new file mode 100644 index 0000000..b7e97c1 --- /dev/null +++ b/src/components/BreweryIndex/BreweryCard.tsx @@ -0,0 +1,43 @@ +import UserContext from '@/contexts/userContext'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { FC, useContext } from 'react'; +import { Link } from 'react-daisyui'; +import { z } from 'zod'; +import Image from 'next/image'; +import BreweryPostLikeButton from './BreweryPostLikeButton'; + +const BreweryCard: FC<{ brewery: z.infer }> = ({ + brewery, +}) => { + const { user } = useContext(UserContext); + const { likeCount, mutate } = useGetBreweryPostLikeCount(brewery.id); + return ( +
+
+ {brewery.breweryImages.length > 0 && ( + {brewery.name} + )} +
+
+
+

+ {brewery.name} +

+

{brewery.location}

+
+ liked by {likeCount} users + {user && ( + + )} +
+
+ ); +}; + +export default BreweryCard; diff --git a/src/components/BreweryIndex/BreweryPostLikeButton.tsx b/src/components/BreweryIndex/BreweryPostLikeButton.tsx new file mode 100644 index 0000000..b990420 --- /dev/null +++ b/src/components/BreweryIndex/BreweryPostLikeButton.tsx @@ -0,0 +1,30 @@ +import useCheckIfUserLikesBreweryPost from '@/hooks/useCheckIfUserLikesBreweryPost'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import sendBreweryPostLikeRequest from '@/requests/sendBreweryPostLikeRequest'; +import { FC, useState } from 'react'; +import LikeButton from '../ui/LikeButton'; + +const BreweryPostLikeButton: FC<{ + breweryPostId: string; + mutateCount: ReturnType['mutate']; +}> = ({ breweryPostId, mutateCount }) => { + const { isLiked, mutate: mutateLikeStatus } = + useCheckIfUserLikesBreweryPost(breweryPostId); + + const [isLoading, setIsLoading] = useState(false); + + const handleLike = async () => { + try { + setIsLoading(true); + await sendBreweryPostLikeRequest(breweryPostId); + await Promise.all([mutateCount(), mutateLikeStatus()]); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + } + }; + + return ; +}; + +export default BreweryPostLikeButton; diff --git a/src/components/ui/LikeButton.tsx b/src/components/ui/LikeButton.tsx new file mode 100644 index 0000000..5e49838 --- /dev/null +++ b/src/components/ui/LikeButton.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; +import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; + +interface LikeButtonProps { + isLiked: boolean; + handleLike: () => Promise; + loading: boolean; +} + +const LikeButton: FC = ({ isLiked, handleLike, loading }) => { + return ( + + ); +}; + +export default LikeButton; diff --git a/src/components/BeerIndex/BeerPostLoadingCard.tsx b/src/components/ui/LoadingCard.tsx similarity index 91% rename from src/components/BeerIndex/BeerPostLoadingCard.tsx rename to src/components/ui/LoadingCard.tsx index e8f7743..6e3bac8 100644 --- a/src/components/BeerIndex/BeerPostLoadingCard.tsx +++ b/src/components/ui/LoadingCard.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; -const BeerPostLoadingCard: FC = () => { +const LoadingCard: FC = () => { return (
@@ -23,4 +23,4 @@ const BeerPostLoadingCard: FC = () => { ); }; -export default BeerPostLoadingCard; +export default LoadingCard; diff --git a/src/getServerSideProps/withPageAuthRequired.ts b/src/getServerSideProps/withPageAuthRequired.ts index faaea66..fb3521c 100644 --- a/src/getServerSideProps/withPageAuthRequired.ts +++ b/src/getServerSideProps/withPageAuthRequired.ts @@ -27,8 +27,8 @@ export type ExtendedGetServerSideProps< ) => Promise>; /** - * A Higher Order Function that adds authentication requirement to a Next.js server-side - * page component. + * A Higher Order Function that adds an authentication requirement to a Next.js + * server-side page component. * * @param fn An async function that receives the GetServerSidePropsContext and * authenticated session as arguments and returns a GetServerSidePropsResult with props diff --git a/src/hooks/useGetLikeCount.ts b/src/hooks/useBeerPostLikeCount.ts similarity index 92% rename from src/hooks/useGetLikeCount.ts rename to src/hooks/useBeerPostLikeCount.ts index 1c9e514..513c06d 100644 --- a/src/hooks/useGetLikeCount.ts +++ b/src/hooks/useBeerPostLikeCount.ts @@ -10,7 +10,7 @@ import useSWR from 'swr'; * state of the request. */ -const useGetLikeCount = (beerPostId: string) => { +const useGetBeerPostLikeCount = (beerPostId: string) => { const { error, mutate, data, isLoading } = useSWR( `/api/beers/${beerPostId}/like`, async (url) => { @@ -45,4 +45,4 @@ const useGetLikeCount = (beerPostId: string) => { }; }; -export default useGetLikeCount; +export default useGetBeerPostLikeCount; diff --git a/src/hooks/useBreweryPosts.ts b/src/hooks/useBreweryPosts.ts new file mode 100644 index 0000000..ed73414 --- /dev/null +++ b/src/hooks/useBreweryPosts.ts @@ -0,0 +1,62 @@ +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import useSWRInfinite from 'swr/infinite'; +import { z } from 'zod'; + +/** + * A custom hook using SWR to fetch brewery posts from the API. + * + * @param options The options to use when fetching brewery posts. + * @param options.pageSize The number of brewery posts to fetch per page. + * @returns An object containing the brewery posts, page count, and loading state. + */ +const useBreweryPosts = ({ 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(BreweryPostQueryResult).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 { + breweryPosts: parsedPayload.data, + pageCount, + }; + }; + + const { data, error, isLoading, setSize, size } = useSWRInfinite( + (index) => `/api/breweries?pageNum=${index + 1}&pageSize=${pageSize}`, + fetcher, + ); + + const breweryPosts = data?.flatMap((d) => d.breweryPosts) ?? []; + const pageCount = data?.[0].pageCount ?? 0; + const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined'; + const isAtEnd = !(size < data?.[0].pageCount!); + + return { + breweryPosts, + pageCount, + size, + setSize, + isLoading, + isLoadingMore, + isAtEnd, + error: error as unknown, + }; +}; + +export default useBreweryPosts; diff --git a/src/hooks/useCheckIfUserLikesBreweryPost.ts b/src/hooks/useCheckIfUserLikesBreweryPost.ts new file mode 100644 index 0000000..7574d55 --- /dev/null +++ b/src/hooks/useCheckIfUserLikesBreweryPost.ts @@ -0,0 +1,45 @@ +import UserContext from '@/contexts/userContext'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { useContext } from 'react'; +import useSWR from 'swr'; +import { z } from 'zod'; + +const useCheckIfUserLikesBreweryPost = (breweryPostId: string) => { + const { user } = useContext(UserContext); + const { data, error, isLoading, mutate } = useSWR( + `/api/breweries/${breweryPostId}/like/is-liked`, + async () => { + if (!user) { + throw new Error('User is not logged in.'); + } + + const response = await fetch(`/api/breweries/${breweryPostId}/like/is-liked`); + const json = await response.json(); + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + const { payload } = parsed.data; + const parsedPayload = z.object({ isLiked: z.boolean() }).safeParse(payload); + + if (!parsedPayload.success) { + throw new Error('Invalid API response.'); + } + + const { isLiked } = parsedPayload.data; + + return isLiked; + }, + ); + + return { + isLiked: data, + error: error as unknown, + isLoading, + mutate, + }; +}; + +export default useCheckIfUserLikesBreweryPost; diff --git a/src/hooks/useGetBreweryPostLikeCount.ts b/src/hooks/useGetBreweryPostLikeCount.ts new file mode 100644 index 0000000..0a1a20e --- /dev/null +++ b/src/hooks/useGetBreweryPostLikeCount.ts @@ -0,0 +1,40 @@ +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import useSWR from 'swr'; +import { z } from 'zod'; + +const useGetBreweryPostLikeCount = (breweryPostId: string) => { + const { error, mutate, data, isLoading } = useSWR( + `/api/breweries/${breweryPostId}/like`, + async (url) => { + const response = await fetch(url); + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Failed to parse API response'); + } + + const parsedPayload = z + .object({ + likeCount: z.number(), + }) + .safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error('Failed to parse API response payload'); + } + + return parsedPayload.data.likeCount; + }, + ); + + return { + error: error as unknown, + isLoading, + mutate, + likeCount: data as number | undefined, + }; +}; + +export default useGetBreweryPostLikeCount; diff --git a/src/pages/api/beers/[id]/like/index.ts b/src/pages/api/beers/[id]/like/index.ts index a0af1be..dbf76ec 100644 --- a/src/pages/api/beers/[id]/like/index.ts +++ b/src/pages/api/beers/[id]/like/index.ts @@ -50,7 +50,7 @@ const getLikeCount = async ( ) => { const id = req.query.id as string; - const likes = await DBClient.instance.beerPostLike.count({ + const likeCount = await DBClient.instance.beerPostLike.count({ where: { beerPostId: id }, }); @@ -58,7 +58,7 @@ const getLikeCount = async ( success: true, message: 'Successfully retrieved like count.', statusCode: 200, - payload: { likeCount: likes }, + payload: { likeCount }, }); }; diff --git a/src/pages/api/breweries/[id]/like/index.ts b/src/pages/api/breweries/[id]/like/index.ts new file mode 100644 index 0000000..4c75729 --- /dev/null +++ b/src/pages/api/breweries/[id]/like/index.ts @@ -0,0 +1,97 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import ServerError from '@/config/util/ServerError'; +import DBClient from '@/prisma/DBClient'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +const sendLikeRequest = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse>, +) => { + const id = req.query.id! as string; + const user = req.user!; + + const breweryPost = await DBClient.instance.breweryPost.findUnique({ + where: { id }, + }); + + if (!breweryPost) { + throw new ServerError('Could not find a brewery post with that id', 404); + } + + const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({ + where: { breweryPostId: breweryPost.id, likedById: user.id }, + }); + + const jsonResponse = { + success: true as const, + message: '', + statusCode: 200 as const, + }; + + if (alreadyLiked) { + await DBClient.instance.breweryPostLike.delete({ + where: { id: alreadyLiked.id }, + }); + jsonResponse.message = 'Successfully unliked brewery post'; + } else { + await DBClient.instance.breweryPostLike.create({ + data: { breweryPostId: breweryPost.id, likedById: user.id }, + }); + jsonResponse.message = 'Successfully liked brewery post'; + } + + res.status(200).json(jsonResponse); +}; + +const getLikeCount = async ( + req: NextApiRequest, + res: NextApiResponse>, +) => { + const id = req.query.id! as string; + + const breweryPost = await DBClient.instance.breweryPost.findUnique({ + where: { id }, + }); + + if (!breweryPost) { + throw new ServerError('Could not find a brewery post with that id', 404); + } + + const likeCount = await DBClient.instance.breweryPostLike.count({ + where: { breweryPostId: breweryPost.id }, + }); + + res.status(200).json({ + success: true, + message: 'Successfully retrieved like count', + statusCode: 200, + payload: { likeCount }, + }); +}; + +const router = createRouter< + UserExtendedNextApiRequest, + NextApiResponse> +>(); + +router.post( + getCurrentUser, + validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + sendLikeRequest, +); + +router.get( + validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }), + getLikeCount, +); + +const handler = router.handler(NextConnectOptions); + +export default handler; diff --git a/src/pages/api/breweries/[id]/like/is-liked.ts b/src/pages/api/breweries/[id]/like/is-liked.ts new file mode 100644 index 0000000..7f7a0f6 --- /dev/null +++ b/src/pages/api/breweries/[id]/like/is-liked.ts @@ -0,0 +1,49 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +const checkIfLiked = async ( + req: UserExtendedNextApiRequest, + res: NextApiResponse>, +) => { + const user = req.user!; + const id = req.query.id as string; + + const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({ + where: { + breweryPostId: id, + likedById: user.id, + }, + }); + + res.status(200).json({ + success: true, + message: alreadyLiked ? 'Brewery post is liked.' : 'Brewery post is not liked.', + statusCode: 200, + payload: { isLiked: !!alreadyLiked }, + }); +}; + +const router = createRouter< + UserExtendedNextApiRequest, + NextApiResponse> +>(); + +router.get( + getCurrentUser, + validateRequest({ + querySchema: z.object({ + id: z.string().uuid(), + }), + }), + checkIfLiked, +); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/api/breweries/index.ts b/src/pages/api/breweries/index.ts new file mode 100644 index 0000000..bd1d183 --- /dev/null +++ b/src/pages/api/breweries/index.ts @@ -0,0 +1,54 @@ +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import DBClient from '@/prisma/DBClient'; +import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { createRouter } from 'next-connect'; +import { z } from 'zod'; + +interface GetBreweryPostsRequest extends NextApiRequest { + query: { + pageNum: string; + pageSize: string; + }; +} + +const getBreweryPosts = async ( + req: GetBreweryPostsRequest, + res: NextApiResponse>, +) => { + const pageNum = parseInt(req.query.pageNum, 10); + const pageSize = parseInt(req.query.pageSize, 10); + + const breweryPosts = await getAllBreweryPosts(pageNum, pageSize); + const breweryPostCount = await DBClient.instance.breweryPost.count(); + + res.setHeader('X-Total-Count', breweryPostCount); + + res.status(200).json({ + message: 'Brewery posts retrieved successfully', + statusCode: 200, + payload: breweryPosts, + success: true, + }); +}; + +const router = createRouter< + GetBreweryPostsRequest, + NextApiResponse> +>(); + +router.get( + validateRequest({ + querySchema: z.object({ + pageNum: z.string().regex(/^\d+$/), + pageSize: z.string().regex(/^\d+$/), + }), + }), + getBreweryPosts, +); + +const handler = router.handler(); + +export default handler; diff --git a/src/pages/beers/create.tsx b/src/pages/beers/create.tsx index 861d276..24bf470 100644 --- a/src/pages/beers/create.tsx +++ b/src/pages/beers/create.tsx @@ -15,7 +15,7 @@ interface CreateBeerPageProps { types: BeerType[]; } -const Create: NextPage = ({ breweries, types }) => { +const CreateBeerPost: NextPage = ({ breweries, types }) => { return ( (asyn }; }); -export default Create; +export default CreateBeerPost; diff --git a/src/pages/beers/index.tsx b/src/pages/beers/index.tsx index 31b5c8a..f73db99 100644 --- a/src/pages/beers/index.tsx +++ b/src/pages/beers/index.tsx @@ -10,8 +10,8 @@ import { useInView } from 'react-intersection-observer'; import Spinner from '@/components/ui/Spinner'; import useBeerPosts from '@/hooks/useBeerPosts'; -import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard'; import { FaArrowUp, FaPlus } from 'react-icons/fa'; +import LoadingCard from '@/components/ui/LoadingCard'; const BeerPage: NextPage = () => { const { user } = useContext(UserContext); @@ -40,7 +40,10 @@ const BeerPage: NextPage = () => {
-

The Biergarten Index

+
+

The Biergarten Index

+

Beers

+
{!!user && (
{ {(isLoading || isLoadingMore) && ( <> {Array.from({ length: PAGE_SIZE }, (_, i) => ( - + ))} )} diff --git a/src/pages/breweries/index.tsx b/src/pages/breweries/index.tsx index 90258ce..2fbd3a1 100644 --- a/src/pages/breweries/index.tsx +++ b/src/pages/breweries/index.tsx @@ -1,70 +1,115 @@ -import { GetServerSideProps, NextPage } from 'next'; - -import Link from 'next/link'; -import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; +import BreweryCard from '@/components/BreweryIndex/BreweryCard'; +import LoadingCard from '@/components/ui/LoadingCard'; +import Spinner from '@/components/ui/Spinner'; +import UserContext from '@/contexts/userContext'; +import useBreweryPosts from '@/hooks/useBreweryPosts'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; - -import { FC } from 'react'; -import Image from 'next/image'; +import { NextPage } from 'next'; +import { useContext, MutableRefObject, useRef } from 'react'; +import { Link } from 'react-daisyui'; +import { FaPlus, FaArrowUp } from 'react-icons/fa'; +import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; interface BreweryPageProps { breweryPosts: z.infer[]; } -const BreweryCard: FC<{ brewery: z.infer }> = ({ - brewery, -}) => { +const BreweryPage: NextPage = () => { + const PAGE_SIZE = 6; + + const { breweryPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = + useBreweryPosts({ + pageSize: PAGE_SIZE, + }); + + const { ref: lastBreweryPostRef } = useInView({ + onChange: (visible) => { + if (!visible || isAtEnd) return; + setSize(size + 1); + }, + }); + + const { user } = useContext(UserContext); + + const pageRef: MutableRefObject = useRef(null); return ( -
-
- {brewery.breweryImages.length > 0 && ( - {brewery.name} - )} -
-
-
-

- {brewery.name} -

-

{brewery.location}

+
+
+
+
+

The Biergarten Index

+

Breweries

+
+ {!!user && ( +
+ + + +
+ )} +
+
+ {!!breweryPosts.length && !isLoading && ( + <> + {breweryPosts.map((breweryPost) => { + return ( +
+ +
+ ); + })} + + )} + {(isLoading || isLoadingMore) && ( + <> + {Array.from({ length: PAGE_SIZE }, (_, i) => ( + + ))} + + )}
+ + {(isLoading || isLoadingMore) && ( +
+ +
+ )} + {isAtEnd && !isLoading && ( +
+
+ +
+
+ )}
); }; -const BreweryPage: NextPage = ({ breweryPosts }) => { - return ( - <> -
-
-
-
-

Breweries

-
-
-
- {breweryPosts.map((brewery) => { - return ; - })} -
-
-
- - ); -}; - -export const getServerSideProps: GetServerSideProps = async () => { - const breweryPosts = await getAllBreweryPosts(); - return { - props: { breweryPosts: JSON.parse(JSON.stringify(breweryPosts)) }, - }; -}; - export default BreweryPage; diff --git a/src/prisma/migrations/20230423163714_/migration.sql b/src/prisma/migrations/20230423163714_/migration.sql new file mode 100644 index 0000000..3c23040 --- /dev/null +++ b/src/prisma/migrations/20230423163714_/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "BreweryPostLike" ( + "id" STRING NOT NULL, + "breweryPostId" STRING NOT NULL, + "likedById" STRING NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3), + + CONSTRAINT "BreweryPostLike_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 37eb553..96eb136 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -11,15 +11,15 @@ datasource db { } model User { - id String @id @default(uuid()) - username String @unique + id String @id @default(uuid()) + username String @unique firstName String lastName String hash String - email String @unique - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime? @updatedAt @db.Timestamptz(3) - isAccountVerified Boolean @default(false) + email String @unique + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + isAccountVerified Boolean @default(false) dateOfBirth DateTime beerPosts BeerPost[] beerTypes BeerType[] @@ -29,6 +29,7 @@ model User { BeerPostLikes BeerPostLike[] BeerImage BeerImage[] BreweryImage BreweryImage[] + BreweryPostLike BreweryPostLike[] } model BeerPost { @@ -60,6 +61,16 @@ model BeerPostLike { updatedAt DateTime? @updatedAt @db.Timestamptz(3) } +model BreweryPostLike { + id String @id @default(uuid()) + breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) + breweryPostId String + likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade) + likedById String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) +} + model BeerComment { id String @id @default(uuid()) rating Int @@ -83,17 +94,18 @@ model BeerType { } model BreweryPost { - id String @id @default(uuid()) + id String @id @default(uuid()) name String location String beers BeerPost[] description String - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime? @updatedAt @db.Timestamptz(3) - postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedById String breweryComments BreweryComment[] breweryImages BreweryImage[] + BreweryPostLike BreweryPostLike[] } model BreweryComment { diff --git a/src/prisma/seed/create/createNewBreweryPostLikes.ts b/src/prisma/seed/create/createNewBreweryPostLikes.ts new file mode 100644 index 0000000..17888d0 --- /dev/null +++ b/src/prisma/seed/create/createNewBreweryPostLikes.ts @@ -0,0 +1,33 @@ +import type { BreweryPost, BreweryPostLike, User } from '@prisma/client'; +import DBClient from '../../DBClient'; + +const createNewBreweryPostLikes = async ({ + joinData: { breweryPosts, users }, + numberOfLikes, +}: { + joinData: { + breweryPosts: BreweryPost[]; + users: User[]; + }; + numberOfLikes: number; +}) => { + const breweryPostLikePromises: Promise[] = []; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < numberOfLikes; i++) { + const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; + const user = users[Math.floor(Math.random() * users.length)]; + + breweryPostLikePromises.push( + DBClient.instance.breweryPostLike.create({ + data: { + breweryPost: { connect: { id: breweryPost.id } }, + likedBy: { connect: { id: user.id } }, + }, + }), + ); + } + + return Promise.all(breweryPostLikePromises); +}; + +export default createNewBreweryPostLikes; diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 6d4ae14..ecf4038 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -13,6 +13,7 @@ import createNewBreweryImages from './create/createNewBreweryImages'; import createNewBreweryPostComments from './create/createNewBreweryPostComments'; import createNewBreweryPosts from './create/createNewBreweryPosts'; import createNewUsers from './create/createNewUsers'; +import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; (async () => { try { @@ -52,6 +53,10 @@ import createNewUsers from './create/createNewUsers'; numberOfLikes: 10000, joinData: { beerPosts, users }, }), + createNewBreweryPostLikes({ + numberOfLikes: 10000, + joinData: { breweryPosts, users }, + }), createNewBeerImages({ numberOfImages: 1000, joinData: { beerPosts, users }, diff --git a/src/requests/sendLikeRequest.ts b/src/requests/sendBeerPostLikeRequest.ts similarity index 75% rename from src/requests/sendLikeRequest.ts rename to src/requests/sendBeerPostLikeRequest.ts index 2e8e807..f736027 100644 --- a/src/requests/sendLikeRequest.ts +++ b/src/requests/sendBeerPostLikeRequest.ts @@ -1,12 +1,8 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -const sendLikeRequest = async (beerPostId: string) => { +const sendBeerPostLikeRequest = async (beerPostId: string) => { const response = await fetch(`/api/beers/${beerPostId}/like`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: '', }); if (!response.ok) { @@ -26,4 +22,4 @@ const sendLikeRequest = async (beerPostId: string) => { return { success, message }; }; -export default sendLikeRequest; +export default sendBeerPostLikeRequest; diff --git a/src/requests/sendBreweryPostLikeRequest.ts b/src/requests/sendBreweryPostLikeRequest.ts new file mode 100644 index 0000000..47cfee2 --- /dev/null +++ b/src/requests/sendBreweryPostLikeRequest.ts @@ -0,0 +1,25 @@ +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; + +const sendBreweryPostLikeRequest = async (breweryPostId: string) => { + const response = await fetch(`/api/breweries/${breweryPostId}/like`, { + method: 'POST', + }); + + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + if (!parsed.success) { + throw new Error('Invalid API response.'); + } + + const { success, message } = parsed.data; + + return { success, message }; +}; + +export default sendBreweryPostLikeRequest; diff --git a/src/services/BreweryPost/getAllBreweryPosts.ts b/src/services/BreweryPost/getAllBreweryPosts.ts index cf35eb9..6c9ee2c 100644 --- a/src/services/BreweryPost/getAllBreweryPosts.ts +++ b/src/services/BreweryPost/getAllBreweryPosts.ts @@ -4,9 +4,14 @@ import { z } from 'zod'; const prisma = DBClient.instance; -const getAllBreweryPosts = async () => { +const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => { + const skip = pageNum && pageSize ? (pageNum - 1) * pageSize : undefined; + const take = pageNum && pageSize ? pageSize : undefined; + const breweryPosts: z.infer[] = await prisma.breweryPost.findMany({ + skip, + take, select: { id: true, location: true, diff --git a/tailwind.config.js b/tailwind.config.js index 7822c55..11b30c5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -27,9 +27,9 @@ const myThemes = { warning: 'hsl(40, 76%, 73%)', 'primary-content': 'hsl(0, 0%, 0%)', 'error-content': 'hsl(0, 0%, 0%)', - 'base-100': 'hsl(180, 8%, 94%)', - 'base-200': 'hsl(180, 8%, 92%)', - 'base-300': 'hsl(180, 8%, 88%)', + 'base-300': 'hsl(180, 10%, 88%)', + 'base-200': 'hsl(180, 10%, 92%)', + 'base-100': 'hsl(180, 10%, 95%)', }, };