diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/src/components/BeerById/BeerCommentForm.tsx b/src/components/BeerById/BeerCommentForm.tsx index 8ad90bf..c9f89cd 100644 --- a/src/components/BeerById/BeerCommentForm.tsx +++ b/src/components/BeerById/BeerCommentForm.tsx @@ -1,5 +1,5 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; + import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -9,6 +9,7 @@ import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import Button from '../ui/forms/Button'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; @@ -26,12 +27,12 @@ const BeerCommentForm: FunctionComponent = ({ mutate, }) => { const { register, handleSubmit, formState, reset, setValue } = useForm< - z.infer + z.infer >({ defaultValues: { rating: 0, }, - resolver: zodResolver(BeerCommentValidationSchema), + resolver: zodResolver(CreateCommentValidationSchema), }); const [rating, setRating] = useState(0); @@ -40,7 +41,7 @@ const BeerCommentForm: FunctionComponent = ({ reset({ rating: 0, content: '' }); }, [reset]); - const onSubmit: SubmitHandler> = async ( + const onSubmit: SubmitHandler> = async ( data, ) => { setValue('rating', 0); diff --git a/src/components/BeerById/BeerPostCommentsSection.tsx b/src/components/BeerById/BeerPostCommentsSection.tsx index 3211315..1d7a6b6 100644 --- a/src/components/BeerById/BeerPostCommentsSection.tsx +++ b/src/components/BeerById/BeerPostCommentsSection.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-nested-ternary */ import UserContext from '@/contexts/userContext'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; @@ -7,13 +6,10 @@ import { FC, MutableRefObject, useContext, useRef } from 'react'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; import { useRouter } from 'next/router'; -import { useInView } from 'react-intersection-observer'; -import { FaArrowUp } from 'react-icons/fa'; import BeerCommentForm from './BeerCommentForm'; -import CommentCardBody from './CommentCardBody'; -import NoCommentsCard from './NoCommentsCard'; import LoadingComponent from './LoadingComponent'; +import CommentsComponent from '../ui/CommentsComponent'; interface BeerPostCommentsSectionProps { beerPost: z.infer; @@ -33,20 +29,9 @@ const BeerPostCommentsSection: FC = ({ beerPost }) pageSize: PAGE_SIZE, }); - const { ref: lastCommentRef } = useInView({ - /** - * When the last comment comes into view, call setSize from useBeerPostComments to - * load more comments. - */ - onChange: (visible) => { - if (!visible || isAtEnd) return; - setSize(size + 1); - }, - }); - - const sectionRef: MutableRefObject = useRef(null); + const commentSectionRef: MutableRefObject = useRef(null); return ( -
+
{user ? ( @@ -69,66 +54,15 @@ const BeerPostCommentsSection: FC = ({ beerPost })
) : ( - <> - {!!comments.length && ( -
- {comments.map((comment, index) => { - const isPenulitmateComment = index === comments.length - 2; - - /** - * Attach a ref to the last comment in the list. When it comes into - * view, the component will call setSize to load more comments. - */ - return ( -
- -
- ); - })} - - { - /** - * If there are more comments to load, show a loading component with a - * skeleton loader and a loading spinner. - */ - !!isLoadingMore && - } - - { - /** - * If the user has scrolled to the end of the comments, show a button - * that will scroll them back to the top of the comments section. - */ - !!isAtEnd && ( -
-
- -
-
- ) - } -
- )} - - {!comments.length && } - + ) }
diff --git a/src/components/BeerById/CommentCardBody.tsx b/src/components/BeerById/CommentCardBody.tsx index 95becc6..47193e2 100644 --- a/src/components/BeerById/CommentCardBody.tsx +++ b/src/components/BeerById/CommentCardBody.tsx @@ -1,5 +1,5 @@ import useBeerPostComments from '@/hooks/useBeerPostComments'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { FC, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; @@ -7,7 +7,7 @@ import CommentContentBody from './CommentContentBody'; import EditCommentBody from './EditCommentBody'; interface CommentCardProps { - comment: z.infer; + comment: z.infer; mutate: ReturnType['mutate']; ref?: ReturnType['ref']; } diff --git a/src/components/BeerById/CommentCardDropdown.tsx b/src/components/BeerById/CommentCardDropdown.tsx index 69e103b..5d7f342 100644 --- a/src/components/BeerById/CommentCardDropdown.tsx +++ b/src/components/BeerById/CommentCardDropdown.tsx @@ -1,11 +1,11 @@ import UserContext from '@/contexts/userContext'; import { Dispatch, SetStateAction, FC, useContext } from 'react'; import { FaEllipsisH } from 'react-icons/fa'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { z } from 'zod'; interface CommentCardDropdownProps { - comment: z.infer; + comment: z.infer; setInEditMode: Dispatch>; } diff --git a/src/components/BeerById/CommentContentBody.tsx b/src/components/BeerById/CommentContentBody.tsx index c9b3416..ceafa89 100644 --- a/src/components/BeerById/CommentContentBody.tsx +++ b/src/components/BeerById/CommentContentBody.tsx @@ -3,13 +3,13 @@ import useTimeDistance from '@/hooks/useTimeDistance'; import { format } from 'date-fns'; import { Dispatch, FC, SetStateAction, useContext } from 'react'; import { Link, Rating } from 'react-daisyui'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; import CommentCardDropdown from './CommentCardDropdown'; interface CommentContentBodyProps { - comment: z.infer; + comment: z.infer; ref: ReturnType['ref'] | undefined; setInEditMode: Dispatch>; } diff --git a/src/components/BeerById/EditCommentBody.tsx b/src/components/BeerById/EditCommentBody.tsx index 8ac8636..4e317b7 100644 --- a/src/components/BeerById/EditCommentBody.tsx +++ b/src/components/BeerById/EditCommentBody.tsx @@ -1,12 +1,12 @@ -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; import { zodResolver } from '@hookform/resolvers/zod'; import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react'; import { Rating } from 'react-daisyui'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import useBeerPostComments from '@/hooks/useBeerPostComments'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { useInView } from 'react-intersection-observer'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; import FormLabel from '../ui/forms/FormLabel'; @@ -14,7 +14,7 @@ import FormSegment from '../ui/forms/FormSegment'; import FormTextArea from '../ui/forms/FormTextArea'; interface CommentCardDropdownProps { - comment: z.infer; + comment: z.infer; setInEditMode: Dispatch>; ref: ReturnType['ref'] | undefined; mutate: ReturnType['mutate']; @@ -27,13 +27,13 @@ const EditCommentBody: FC = ({ mutate, }) => { const { register, handleSubmit, formState, setValue, watch } = useForm< - z.infer + z.infer >({ defaultValues: { content: comment.content, rating: comment.rating, }, - resolver: zodResolver(BeerCommentValidationSchema), + resolver: zodResolver(CreateCommentValidationSchema), }); const { errors } = formState; @@ -59,7 +59,7 @@ const EditCommentBody: FC = ({ await mutate(); }; - const onSubmit: SubmitHandler> = async ( + const onSubmit: SubmitHandler> = async ( data, ) => { const response = await fetch(`/api/beer-comments/${comment.id}`, { diff --git a/src/components/BreweryById/BreweryBeerSection.tsx.tsx b/src/components/BreweryById/BreweryBeerSection.tsx.tsx new file mode 100644 index 0000000..0cbd7ff --- /dev/null +++ b/src/components/BreweryById/BreweryBeerSection.tsx.tsx @@ -0,0 +1,9 @@ +import { FC } from 'react'; + +interface BreweryCommentsSectionProps {} + +const BreweryBeersSection: FC = () => { + return
; +}; + +export default BreweryBeersSection; diff --git a/src/components/BreweryById/BreweryCommentsSection.tsx b/src/components/BreweryById/BreweryCommentsSection.tsx new file mode 100644 index 0000000..6b46355 --- /dev/null +++ b/src/components/BreweryById/BreweryCommentsSection.tsx @@ -0,0 +1,65 @@ +import UserContext from '@/contexts/userContext'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { FC, MutableRefObject, useContext, useRef } from 'react'; +import { z } from 'zod'; +import useBreweryPostComments from '@/hooks/useBreweryPostComments'; +import LoadingComponent from '../BeerById/LoadingComponent'; +import CommentsComponent from '../ui/CommentsComponent'; + +interface BreweryBeerSectionProps { + breweryPost: z.infer; +} + +const BreweryCommentForm: FC = () => { + return null; +}; + +const BreweryCommentsSection: FC = ({ breweryPost }) => { + const { user } = useContext(UserContext); + + const { id } = breweryPost; + + const PAGE_SIZE = 4; + + const { comments, isLoading, setSize, size, isLoadingMore, isAtEnd } = + useBreweryPostComments({ id, pageSize: PAGE_SIZE }); + + const commentSectionRef: MutableRefObject = useRef(null); + + return ( +
+
+ {user ? ( + + ) : ( +
+
Log in to leave a comment.
+
+ )} +
+ { + /** + * If the comments are loading, show a loading component. Otherwise, show the + * comments. + */ + isLoading ? ( +
+ +
+ ) : ( + + ) + } +
+ ); +}; + +export default BreweryCommentsSection; diff --git a/src/components/BreweryById/BreweryInfoHeader.tsx b/src/components/BreweryById/BreweryInfoHeader.tsx new file mode 100644 index 0000000..1a9e61f --- /dev/null +++ b/src/components/BreweryById/BreweryInfoHeader.tsx @@ -0,0 +1,95 @@ +import UserContext from '@/contexts/userContext'; +import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; +import useTimeDistance from '@/hooks/useTimeDistance'; +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { format } from 'date-fns'; +import { FC, useContext } from 'react'; +import { Link } from 'react-daisyui'; +import { FaRegEdit } from 'react-icons/fa'; +import { z } from 'zod'; +import BreweryPostLikeButton from '../BreweryIndex/BreweryPostLikeButton'; + +interface BreweryInfoHeaderProps { + breweryPost: z.infer; +} +const BreweryInfoHeader: FC = ({ breweryPost }) => { + const createdAt = new Date(breweryPost.createdAt); + const timeDistance = useTimeDistance(createdAt); + + const { user } = useContext(UserContext); + const idMatches = user && breweryPost.postedBy.id === user.id; + const isPostOwner = !!(user && idMatches); + + const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id); + + return ( +
+
+
+
+
+

{breweryPost.name}

+

+ Located in + {` ${breweryPost.location.city}, ${ + breweryPost.location.stateOrProvince || breweryPost.location.country + }`} +

+
+
+

+ {' posted by '} + + {`${breweryPost.postedBy.username} `} + + {timeDistance && ( + {`${timeDistance} ago`} + )} +

+
+
+ {isPostOwner && ( +
+ + + +
+ )} +
+
+

{breweryPost.description}

+
+
+
+ {(!!likeCount || likeCount === 0) && ( + + Liked by {likeCount} user{likeCount !== 1 && 's'} + + )} +
+
+
+ {user && ( + + )} +
+
+
+
+
+ ); +}; + +export default BreweryInfoHeader; diff --git a/src/components/BreweryById/BreweryMap.tsx b/src/components/BreweryById/BreweryMap.tsx new file mode 100644 index 0000000..4d0ecaf --- /dev/null +++ b/src/components/BreweryById/BreweryMap.tsx @@ -0,0 +1,43 @@ +import useMediaQuery from '@/hooks/useMediaQuery'; + +import { FC } from 'react'; +import Map, { Marker } from 'react-map-gl'; + +interface BreweryMapProps { + latitude: number; + longitude: number; +} +const BreweryMap: FC = ({ latitude, longitude }) => { + const isDesktop = useMediaQuery('(min-width: 1024px)'); + const theme = + typeof window !== 'undefined' ? window.localStorage.getItem('theme') : 'dark'; + + const mapStyle = + theme === 'dark' + ? 'mapbox://styles/mapbox/dark-v11' + : 'mapbox://styles/mapbox/light-v10'; + return ( +
+
+ + + +
+
+ ); +}; + +export default BreweryMap; diff --git a/src/components/ui/CommentsComponent.tsx b/src/components/ui/CommentsComponent.tsx new file mode 100644 index 0000000..01501c9 --- /dev/null +++ b/src/components/ui/CommentsComponent.tsx @@ -0,0 +1,114 @@ +import { FC, MutableRefObject } from 'react'; +import { FaArrowUp } from 'react-icons/fa'; +import { mutate } from 'swr'; +import { useInView } from 'react-intersection-observer'; + +import useBeerPostComments from '@/hooks/useBeerPostComments'; +import useBreweryPostComments from '@/hooks/useBreweryPostComments'; +import NoCommentsCard from '../BeerById/NoCommentsCard'; +import LoadingComponent from '../BeerById/LoadingComponent'; +import CommentCardBody from '../BeerById/CommentCardBody'; + +interface CommentsComponentProps { + commentSectionRef: MutableRefObject; + pageSize: number; + size: ReturnType['size']; + setSize: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['setSize']; + comments: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['comments']; + isAtEnd: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['isAtEnd']; + isLoadingMore: ReturnType< + typeof useBeerPostComments | typeof useBreweryPostComments + >['isLoadingMore']; +} + +const CommentsComponent: FC = ({ + commentSectionRef, + comments, + isAtEnd, + isLoadingMore, + pageSize, + setSize, + size, +}) => { + const { ref: lastCommentRef } = useInView({ + /** + * When the last comment comes into view, call setSize from useBeerPostComments to + * load more comments. + */ + onChange: (visible) => { + if (!visible || isAtEnd) return; + setSize(size + 1); + }, + }); + + return ( + <> + {!!comments.length && ( +
+ {comments.map((comment, index) => { + const isPenulitmateComment = index === comments.length - 2; + + /** + * Attach a ref to the last comment in the list. When it comes into view, the + * component will call setSize to load more comments. + */ + return ( +
+ +
+ ); + })} + + { + /** + * If there are more comments to load, show a loading component with a + * skeleton loader and a loading spinner. + */ + !!isLoadingMore && + } + + { + /** + * If the user has scrolled to the end of the comments, show a button that + * will scroll them back to the top of the comments section. + */ + !!isAtEnd && ( +
+
+ +
+
+ ) + } +
+ )} + + {!comments.length && } + + ); +}; + +export default CommentsComponent; diff --git a/src/hooks/useBeerPostComments.ts b/src/hooks/useBeerPostComments.ts index 3a75e56..126ac8c 100644 --- a/src/hooks/useBeerPostComments.ts +++ b/src/hooks/useBeerPostComments.ts @@ -1,4 +1,4 @@ -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; import useSWRInfinite from 'swr/infinite'; @@ -30,7 +30,7 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => { if (!parsed.success) { throw new Error(parsed.error.message); } - const parsedPayload = z.array(BeerCommentQueryResult).safeParse(parsed.data.payload); + const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload); if (!parsedPayload.success) { throw new Error(parsedPayload.error.message); diff --git a/src/hooks/useBreweryPostComments.ts b/src/hooks/useBreweryPostComments.ts new file mode 100644 index 0000000..bb87859 --- /dev/null +++ b/src/hooks/useBreweryPostComments.ts @@ -0,0 +1,71 @@ +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; +import useSWRInfinite from 'swr/infinite'; + +interface UseBreweryPostCommentsProps { + id: string; + pageSize: number; +} + +/** + * A custom React hook that fetches comments for a specific brewery post. + * + * @param props - The props object. + * @param props.pageNum - The page number of the comments to fetch. + * @param props.id - The ID of the brewery post to fetch comments for. + * @param props.pageSize - The number of comments to fetch per page. + * @returns An object containing the fetched comments, the total number of comment pages, + * a boolean indicating if the request is currently loading, and a function to mutate + * the data. + */ +const useBreweryPostComments = ({ id, pageSize }: UseBreweryPostCommentsProps) => { + const fetcher = async (url: string) => { + const response = await fetch(url); + const json = await response.json(); + const count = response.headers.get('X-Total-Count'); + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error(parsed.error.message); + } + const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error(parsedPayload.error.message); + } + + const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize); + + return { comments: parsedPayload.data, pageCount }; + }; + + const { data, error, isLoading, mutate, size, setSize } = useSWRInfinite( + (index) => + `/api/breweries/${id}/comments?page_num=${index + 1}&page_size=${pageSize}`, + fetcher, + { parallel: true }, + ); + + const comments = data?.flatMap((d) => d.comments) ?? []; + const pageCount = data?.[0].pageCount ?? 0; + + const isLoadingMore = + isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined'); + + const isAtEnd = !(size < data?.[0].pageCount!); + + return { + comments, + isLoading, + error: error as undefined, + mutate, + size, + setSize, + isLoadingMore, + isAtEnd, + pageCount, + }; +}; + +export default useBreweryPostComments; diff --git a/src/pages/api/beer-comments/[id].ts b/src/pages/api/beer-comments/[id].ts index 124b6e2..bbd9178 100644 --- a/src/pages/api/beer-comments/[id].ts +++ b/src/pages/api/beer-comments/[id].ts @@ -4,7 +4,8 @@ import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import ServerError from '@/config/util/ServerError'; import DBClient from '@/prisma/DBClient'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter, NextHandler } from 'next-connect'; @@ -16,7 +17,7 @@ interface DeleteCommentRequest extends UserExtendedNextApiRequest { interface EditCommentRequest extends UserExtendedNextApiRequest { query: { id: string }; - body: z.infer; + body: z.infer; } const checkIfCommentOwner = async ( @@ -96,7 +97,7 @@ router .put( validateRequest({ querySchema: z.object({ id: z.string().uuid() }), - bodySchema: BeerCommentValidationSchema, + bodySchema: CreateCommentValidationSchema, }), getCurrentUser, checkIfCommentOwner, diff --git a/src/pages/api/beers/[id]/comments/index.ts b/src/pages/api/beers/[id]/comments/index.ts index 2e20d8b..a385c40 100644 --- a/src/pages/api/beers/[id]/comments/index.ts +++ b/src/pages/api/beers/[id]/comments/index.ts @@ -6,16 +6,15 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; - import { createRouter } from 'next-connect'; import { z } from 'zod'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import { NextApiResponse } from 'next'; -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; interface CreateCommentRequest extends UserExtendedNextApiRequest { - body: z.infer; + body: z.infer; query: { id: string }; } @@ -31,13 +30,12 @@ const createComment = async ( const beerPostId = req.query.id; - const newBeerComment: z.infer = - await createNewBeerComment({ - content, - rating, - beerPostId, - userId: req.user!.id, - }); + const newBeerComment: z.infer = await createNewBeerComment({ + content, + rating, + beerPostId, + userId: req.user!.id, + }); res.status(201).json({ message: 'Beer comment created successfully', @@ -80,7 +78,7 @@ const router = createRouter< router.post( validateRequest({ - bodySchema: BeerCommentValidationSchema, + bodySchema: CreateCommentValidationSchema, querySchema: z.object({ id: z.string().uuid() }), }), getCurrentUser, diff --git a/src/pages/api/breweries/[id]/comments/index.ts b/src/pages/api/breweries/[id]/comments/index.ts new file mode 100644 index 0000000..f3cc827 --- /dev/null +++ b/src/pages/api/breweries/[id]/comments/index.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import DBClient from '@/prisma/DBClient'; + +import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; + +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; + +import { createRouter } from 'next-connect'; +import { z } from 'zod'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +import { NextApiResponse } from 'next'; + +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + +interface CreateCommentRequest extends UserExtendedNextApiRequest { + body: z.infer; + query: { id: string }; +} + +interface GetAllCommentsRequest extends UserExtendedNextApiRequest { + query: { id: string; page_size: string; page_num: string }; +} + +// const createComment = async ( +// req: CreateCommentRequest, +// res: NextApiResponse>, +// ) => { +// const { content, rating } = req.body; + +// const beerPostId = req.query.id; + +// const newBeerComment: z.infer = +// await createNewBeerComment({ +// content, +// rating, +// beerPostId, +// userId: req.user!.id, +// }); + +// res.status(201).json({ +// message: 'Beer comment created successfully', +// statusCode: 201, +// payload: newBeerComment, +// success: true, +// }); +// }; + +const getAll = async ( + req: GetAllCommentsRequest, + res: NextApiResponse>, +) => { + const breweryPostId = req.query.id; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num } = req.query; + + const comments = await getAllBreweryComments( + { id: breweryPostId }, + { pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) }, + ); + + const pageCount = await DBClient.instance.breweryComment.count({ + where: { breweryPostId }, + }); + + res.setHeader('X-Total-Count', pageCount); + + res.status(200).json({ + message: 'Beer comments fetched successfully', + statusCode: 200, + payload: comments, + success: true, + }); +}; + +const router = createRouter< + // I don't want to use any, but I can't figure out how to get the types to work + any, + NextApiResponse> +>(); + +// router.post( +// validateRequest({ +// bodySchema: CreateBeerCommentValidationSchema, +// querySchema: z.object({ id: z.string().uuid() }), +// }), +// getCurrentUser, +// createComment, +// ); + +router.get( + validateRequest({ + querySchema: z.object({ + id: z.string().uuid(), + page_size: z.coerce.number().int().positive(), + page_num: z.coerce.number().int().positive(), + }), + }), + getAll, +); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/breweries/[id].tsx b/src/pages/breweries/[id].tsx index 354db49..2c309b8 100644 --- a/src/pages/breweries/[id].tsx +++ b/src/pages/breweries/[id].tsx @@ -2,135 +2,26 @@ import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { GetServerSideProps, NextPage } from 'next'; import 'mapbox-gl/dist/mapbox-gl.css'; -import MapGL, { Marker } from 'react-map-gl'; + import { z } from 'zod'; -import { FC, useContext } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader import { Carousel } from 'react-responsive-carousel'; -import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount'; -import useTimeDistance from '@/hooks/useTimeDistance'; -import UserContext from '@/contexts/userContext'; -import Link from 'next/link'; -import { FaRegEdit } from 'react-icons/fa'; -import format from 'date-fns/format'; -import BreweryPostLikeButton from '@/components/BreweryIndex/BreweryPostLikeButton'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { Tab } from '@headlessui/react'; +import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader'; +import BreweryMap from '@/components/BreweryById/BreweryMap'; +import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx'; +import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection'; interface BreweryPageProps { breweryPost: z.infer; } -interface BreweryInfoHeaderProps { - breweryPost: z.infer; -} -const BreweryInfoHeader: FC = ({ breweryPost }) => { - const createdAt = new Date(breweryPost.createdAt); - const timeDistance = useTimeDistance(createdAt); - - const { user } = useContext(UserContext); - const idMatches = user && breweryPost.postedBy.id === user.id; - const isPostOwner = !!(user && idMatches); - - const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id); - - return ( -
-
-
-
-
-

{breweryPost.name}

-

- Located in - {` ${breweryPost.location.city}, ${ - breweryPost.location.stateOrProvince || breweryPost.location.country - }`} -

-
-
-

- {' posted by '} - - {`${breweryPost.postedBy.username} `} - - {timeDistance && ( - {`${timeDistance} ago`} - )} -

-
-
- {isPostOwner && ( -
- - - -
- )} -
-
-

{breweryPost.description}

-
-
-
- {(!!likeCount || likeCount === 0) && ( - - Liked by {likeCount} user{likeCount !== 1 && 's'} - - )} -
-
-
- {user && ( - - )} -
-
-
-
-
- ); -}; - -interface BreweryMapProps { - latitude: number; - longitude: number; -} -const BreweryMap: FC = ({ latitude, longitude }) => { - return ( - - - - ); -}; - const BreweryByIdPage: NextPage = ({ breweryPost }) => { const [longitude, latitude] = breweryPost.location.coordinates; + const isDesktop = useMediaQuery('(min-width: 1024px)'); return ( <> @@ -166,8 +57,39 @@ const BreweryByIdPage: NextPage = ({ breweryPost }) => {
- - + {isDesktop ? ( +
+
+ +
+
+ + +
+
+ ) : ( + <> + + + + + Comments + + + Beers + + + + + + + + + + + + + )}
diff --git a/src/prisma/seed/create/createNewBreweryPostComments.ts b/src/prisma/seed/create/createNewBreweryPostComments.ts index 46ad24b..be49ae5 100644 --- a/src/prisma/seed/create/createNewBreweryPostComments.ts +++ b/src/prisma/seed/create/createNewBreweryPostComments.ts @@ -30,7 +30,7 @@ const createNewBreweryPostComments = async ({ const rating = Math.floor(Math.random() * 5) + 1; // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfComments; i++) { - const content = faker.lorem.lines(5); + const content = faker.lorem.lines(3).replace(/\n/g, ' '); const user = users[Math.floor(Math.random() * users.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; diff --git a/src/prisma/seed/create/createNewUsers.ts b/src/prisma/seed/create/createNewUsers.ts index 8eba539..aa55174 100644 --- a/src/prisma/seed/create/createNewUsers.ts +++ b/src/prisma/seed/create/createNewUsers.ts @@ -1,8 +1,8 @@ -import argon2 from 'argon2'; // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; import crypto from 'crypto'; import DBClient from '../../DBClient'; +import { hashPassword } from '../../../config/auth/passwordFns'; interface CreateNewUsersArgs { numberOfUsers: number; @@ -21,24 +21,40 @@ interface UserData { const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const prisma = DBClient.instance; - const hashedPasswords = await Promise.all( - Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())), - ); - + const password = 'passwoRd!3'; + const hash = await hashPassword(password); const data: UserData[] = []; + const takenUsernames: string[] = []; + const takenEmails: string[] = []; + // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfUsers; i++) { - const randomValue = crypto.randomBytes(4).toString('hex'); + const randomValue = crypto.randomBytes(1).toString('hex'); const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); - const username = `${firstName[0]}.${lastName}.${randomValue}`; - const email = faker.internet.email(firstName, randomValue, 'example.com'); - const hash = hashedPasswords[i]; + const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase(); + const email = faker.internet + .email(firstName, randomValue, 'example.com') + .toLowerCase(); + + const userAvailable = + !takenUsernames.includes(username) && !takenEmails.includes(email); + + if (!userAvailable) { + i -= 1; + + // eslint-disable-next-line no-continue + continue; + } + takenUsernames.push(username); + takenEmails.push(email); + const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const createdAt = faker.date.past(1); const user = { firstName, lastName, email, username, dateOfBirth, createdAt, hash }; + data.push(user); } diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index c0a23d2..3386e30 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -28,30 +28,32 @@ import logger from '../../config/pino/logger'; logger.info('Users created successfully.'); const locations = await createNewLocations({ - numberOfLocations: 150, + numberOfLocations: 1600, joinData: { users }, }); + logger.info('Locations created successfully.'); const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 130, joinData: { users, locations } }), + createNewBreweryPosts({ numberOfPosts: 1500, joinData: { users, locations } }), createNewBeerTypes({ joinData: { users } }), ]); logger.info('Brewery posts and beer types created successfully.'); const beerPosts = await createNewBeerPosts({ - numberOfPosts: 200, + numberOfPosts: 3000, joinData: { breweryPosts, beerTypes, users }, }); + logger.info('Beer posts created successfully.'); const [beerPostComments, breweryPostComments] = await Promise.all([ createNewBeerPostComments({ - numberOfComments: 45000, + numberOfComments: 100000, joinData: { beerPosts, users }, }), createNewBreweryPostComments({ - numberOfComments: 45000, + numberOfComments: 100000, joinData: { breweryPosts, users }, }), ]); @@ -59,11 +61,11 @@ import logger from '../../config/pino/logger'; const [beerPostLikes, breweryPostLikes] = await Promise.all([ createNewBeerPostLikes({ - numberOfLikes: 10000, + numberOfLikes: 100000, joinData: { beerPosts, users }, }), createNewBreweryPostLikes({ - numberOfLikes: 10000, + numberOfLikes: 100000, joinData: { breweryPosts, users }, }), ]); @@ -71,11 +73,11 @@ import logger from '../../config/pino/logger'; const [beerImages, breweryImages] = await Promise.all([ createNewBeerImages({ - numberOfImages: 100000, + numberOfImages: 20000, joinData: { beerPosts, users }, }), createNewBreweryImages({ - numberOfImages: 100000, + numberOfImages: 20000, joinData: { breweryPosts, users }, }), ]); diff --git a/src/requests/sendCreateBeerCommentRequest.ts b/src/requests/sendCreateBeerCommentRequest.ts index cf6d79f..230d43d 100644 --- a/src/requests/sendCreateBeerCommentRequest.ts +++ b/src/requests/sendCreateBeerCommentRequest.ts @@ -1,9 +1,10 @@ -import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; -import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; -const BeerCommentValidationSchemaWithId = BeerCommentValidationSchema.extend({ +const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ beerPostId: z.string().uuid(), }); @@ -30,7 +31,7 @@ const sendCreateBeerCommentRequest = async ({ throw new Error('Invalid API response'); } - const parsedPayload = BeerCommentQueryResult.safeParse(parsedResponse.data.payload); + const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload); if (!parsedPayload.success) { throw new Error('Invalid API response payload'); diff --git a/src/services/BeerComment/createNewBeerComment.ts b/src/services/BeerComment/createNewBeerComment.ts index 1231a1a..c869b68 100644 --- a/src/services/BeerComment/createNewBeerComment.ts +++ b/src/services/BeerComment/createNewBeerComment.ts @@ -1,8 +1,8 @@ import DBClient from '@/prisma/DBClient'; import { z } from 'zod'; -import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema'; +import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema'; -const CreateNewBeerCommentServiceSchema = BeerCommentValidationSchema.extend({ +const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({ userId: z.string().uuid(), beerPostId: z.string().uuid(), }); diff --git a/src/services/BeerComment/getAllBeerComments.ts b/src/services/BeerComment/getAllBeerComments.ts index ba161bd..a3582ef 100644 --- a/src/services/BeerComment/getAllBeerComments.ts +++ b/src/services/BeerComment/getAllBeerComments.ts @@ -1,14 +1,14 @@ import DBClient from '@/prisma/DBClient'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod'; -import BeerCommentQueryResult from './schema/BeerCommentQueryResult'; +import CommentQueryResult from '../types/CommentSchema/CommentQueryResult'; const getAllBeerComments = async ( { id }: Pick, 'id'>, { pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number }, ) => { const skip = (pageNum - 1) * pageSize; - const beerComments: z.infer[] = + const beerComments: z.infer[] = await DBClient.instance.beerComment.findMany({ skip, take: pageSize, diff --git a/src/services/BreweryComment/getAllBreweryComments.ts b/src/services/BreweryComment/getAllBreweryComments.ts new file mode 100644 index 0000000..175eb5e --- /dev/null +++ b/src/services/BreweryComment/getAllBreweryComments.ts @@ -0,0 +1,28 @@ +import DBClient from '@/prisma/DBClient'; +import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import { z } from 'zod'; +import CommentQueryResult from '../types/CommentSchema/CommentQueryResult'; + +const getAllBreweryComments = async ( + { id }: Pick, 'id'>, + { pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number }, +) => { + const skip = (pageNum - 1) * pageSize; + const breweryComments: z.infer[] = + await DBClient.instance.breweryComment.findMany({ + skip, + take: pageSize, + where: { breweryPostId: id }, + select: { + id: true, + content: true, + rating: true, + createdAt: true, + postedBy: { select: { id: true, username: true, createdAt: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + return breweryComments; +}; + +export default getAllBreweryComments; diff --git a/src/services/BeerComment/schema/BeerCommentQueryResult.ts b/src/services/types/CommentSchema/CommentQueryResult.ts similarity index 76% rename from src/services/BeerComment/schema/BeerCommentQueryResult.ts rename to src/services/types/CommentSchema/CommentQueryResult.ts index 155f4fa..f88ba5b 100644 --- a/src/services/BeerComment/schema/BeerCommentQueryResult.ts +++ b/src/services/types/CommentSchema/CommentQueryResult.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const BeerCommentQueryResult = z.object({ +const CommentQueryResult = z.object({ id: z.string().uuid(), content: z.string().min(1).max(500), rating: z.number().int().min(1).max(5), @@ -11,4 +11,4 @@ const BeerCommentQueryResult = z.object({ }), }); -export default BeerCommentQueryResult; +export default CommentQueryResult; diff --git a/src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts b/src/services/types/CommentSchema/CreateCommentValidationSchema.ts similarity index 78% rename from src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts rename to src/services/types/CommentSchema/CreateCommentValidationSchema.ts index 174b061..816c071 100644 --- a/src/services/BeerComment/schema/CreateBeerCommentValidationSchema.ts +++ b/src/services/types/CommentSchema/CreateCommentValidationSchema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const BeerCommentValidationSchema = z.object({ +const CreateCommentValidationSchema = z.object({ content: z .string() .min(1, { message: 'Comment must not be empty.' }) @@ -12,4 +12,4 @@ const BeerCommentValidationSchema = z.object({ .max(5, { message: 'Rating must be less than 5.' }), }); -export default BeerCommentValidationSchema; +export default CreateCommentValidationSchema;