diff --git a/src/components/BeerStyleById/BeerStyleCommentForm.tsx b/src/components/BeerStyleById/BeerStyleCommentForm.tsx new file mode 100644 index 0000000..1ffe005 --- /dev/null +++ b/src/components/BeerStyleById/BeerStyleCommentForm.tsx @@ -0,0 +1,66 @@ +import { zodResolver } from '@hookform/resolvers/zod'; + +import { FunctionComponent } from 'react'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { z } from 'zod'; + +import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; +import toast from 'react-hot-toast'; +import createErrorToast from '@/util/createErrorToast'; + +import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult'; +import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments'; +import sendCreateBeerStyleCommentRequest from '@/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest'; +import CommentForm from '../ui/CommentForm'; + +interface BeerCommentFormProps { + beerStyle: z.infer; + mutate: ReturnType['mutate']; +} + +const BeerStyleCommentForm: FunctionComponent = ({ + beerStyle, + mutate, +}) => { + const { register, handleSubmit, formState, watch, reset, setValue } = useForm< + z.infer + >({ + defaultValues: { rating: 0 }, + resolver: zodResolver(CreateCommentValidationSchema), + }); + + const onSubmit: SubmitHandler> = async ( + data, + ) => { + const loadingToast = toast.loading('Posting a new comment...'); + try { + await sendCreateBeerStyleCommentRequest({ + content: data.content, + rating: data.rating, + beerStyleId: beerStyle.id, + }); + reset(); + toast.remove(loadingToast); + toast.success('Comment posted successfully.'); + await mutate(); + } catch (error) { + await mutate(); + toast.remove(loadingToast); + createErrorToast(error); + reset(); + } + }; + + return ( + + ); +}; + +export default BeerStyleCommentForm; diff --git a/src/components/BeerStyleById/BeerStyleCommentSection.tsx b/src/components/BeerStyleById/BeerStyleCommentSection.tsx new file mode 100644 index 0000000..96f8554 --- /dev/null +++ b/src/components/BeerStyleById/BeerStyleCommentSection.tsx @@ -0,0 +1,97 @@ +import UserContext from '@/contexts/UserContext'; + +import { FC, MutableRefObject, useContext, useRef } from 'react'; +import { z } from 'zod'; +import { useRouter } from 'next/router'; +import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; + +import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult'; +import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments'; +import LoadingComponent from '../BeerById/LoadingComponent'; +import CommentsComponent from '../ui/CommentsComponent'; +import BeerStyleCommentForm from './BeerStyleCommentForm'; + +interface BeerStyleCommentsSectionProps { + beerStyle: z.infer; +} + +const BeerStyleCommentsSection: FC = ({ beerStyle }) => { + const { user } = useContext(UserContext); + const router = useRouter(); + + const pageNum = parseInt(router.query.comments_page as string, 10) || 1; + const PAGE_SIZE = 15; + + const { comments, isLoading, mutate, setSize, size, isLoadingMore, isAtEnd } = + useBeerStyleComments({ id: beerStyle.id, pageNum, pageSize: PAGE_SIZE }); + + const commentSectionRef: MutableRefObject = useRef(null); + + const handleDeleteRequest = async (id: string) => { + const response = await fetch(`/api/beer-style-comments/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete comment.'); + } + }; + + const handleEditRequest = async ( + id: string, + data: z.infer, + ) => { + const response = await fetch(`/api/beer-style-comments/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: data.content, rating: data.rating }), + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + }; + + return ( +
+
+
+ {user ? ( + + ) : ( +
+ Log in to leave a comment. +
+ )} +
+
+ + { + /** + * If the comments are loading, show a loading component. Otherwise, show the + * comments. + */ + isLoading ? ( +
+ +
+ ) : ( + + ) + } +
+ ); +}; + +export default BeerStyleCommentsSection; diff --git a/src/hooks/data-fetching/beer-style-comments/useBeerStyleComments.ts b/src/hooks/data-fetching/beer-style-comments/useBeerStyleComments.ts new file mode 100644 index 0000000..8e4f4f0 --- /dev/null +++ b/src/hooks/data-fetching/beer-style-comments/useBeerStyleComments.ts @@ -0,0 +1,61 @@ +import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; +import useSWRInfinite from 'swr/infinite'; + +interface UseBeerStyleCommentsProps { + id: string; + pageSize: number; + pageNum: number; +} + +const useBeerStyleComments = ({ id, pageSize }: UseBeerStyleCommentsProps) => { + 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/beers/styles/${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 useBeerStyleComments; diff --git a/src/pages/api/beer-style-comments/[id].ts b/src/pages/api/beer-style-comments/[id].ts new file mode 100644 index 0000000..6e0af5b --- /dev/null +++ b/src/pages/api/beer-style-comments/[id].ts @@ -0,0 +1,106 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; +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 CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { NextApiResponse } from 'next'; +import { createRouter, NextHandler } from 'next-connect'; +import { z } from 'zod'; + +interface DeleteCommentRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} + +interface EditCommentRequest extends UserExtendedNextApiRequest { + query: { id: string }; + body: z.infer; +} + +const checkIfCommentOwner = async ( + req: DeleteCommentRequest | EditCommentRequest, + res: NextApiResponse>, + next: NextHandler, +) => { + const { id } = req.query; + const user = req.user!; + const comment = await DBClient.instance.beerStyleComment.findFirst({ where: { id } }); + + if (!comment) { + throw new ServerError('Comment not found', 404); + } + + if (comment.postedById !== user.id) { + throw new ServerError('You are not authorized to modify this comment', 403); + } + + return next(); +}; + +const editComment = async ( + req: EditCommentRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + const updated = await DBClient.instance.beerStyleComment.update({ + where: { id }, + data: { + content: req.body.content, + rating: req.body.rating, + updatedAt: new Date(), + }, + }); + + return res.status(200).json({ + success: true, + message: 'Comment updated successfully', + statusCode: 200, + payload: updated, + }); +}; + +const deleteComment = async ( + req: DeleteCommentRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + await DBClient.instance.beerStyleComment.delete({ where: { id } }); + + res.status(200).json({ + success: true, + message: 'Comment deleted successfully', + statusCode: 200, + }); +}; + +const router = createRouter< + DeleteCommentRequest, + NextApiResponse> +>(); + +router + .delete( + validateRequest({ + querySchema: z.object({ id: z.string().cuid() }), + }), + getCurrentUser, + checkIfCommentOwner, + deleteComment, + ) + .put( + validateRequest({ + querySchema: z.object({ id: z.string().cuid() }), + bodySchema: CreateCommentValidationSchema, + }), + getCurrentUser, + checkIfCommentOwner, + editComment, + ); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/api/beers/styles/[id]/comments/index.ts b/src/pages/api/beers/styles/[id]/comments/index.ts new file mode 100644 index 0000000..6a1f714 --- /dev/null +++ b/src/pages/api/beers/styles/[id]/comments/index.ts @@ -0,0 +1,103 @@ +import DBClient from '@/prisma/DBClient'; + +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/schema/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; +import createNewBeerStyleComment from '@/services/BeerStyleComment/createNewBeerStyleComment'; +import getAllBeerStyleComments from '@/services/BeerStyleComment/getAllBeerStyleComments'; + +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 newBeerStyleComment: z.infer = + await createNewBeerStyleComment({ + content, + rating, + beerStyleId: req.query.id, + userId: req.user!.id, + }); + + res.status(201).json({ + message: 'Beer comment created successfully', + statusCode: 201, + payload: newBeerStyleComment, + success: true, + }); +}; + +const getAll = async ( + req: GetAllCommentsRequest, + res: NextApiResponse>, +) => { + const beerStyleId = req.query.id; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { page_size, page_num } = req.query; + + const comments = await getAllBeerStyleComments({ + beerStyleId, + pageNum: parseInt(page_num, 10), + pageSize: parseInt(page_size, 10), + }); + + const pageCount = await DBClient.instance.beerStyleComment.count({ + where: { beerStyleId }, + }); + + 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: CreateCommentValidationSchema, + querySchema: z.object({ id: z.string().cuid() }), + }), + getCurrentUser, + createComment, +); + +router.get( + validateRequest({ + querySchema: z.object({ + id: z.string().cuid(), + 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/api/beers/styles/[id].ts b/src/pages/api/beers/styles/[id]/index.ts similarity index 100% rename from src/pages/api/beers/styles/[id].ts rename to src/pages/api/beers/styles/[id]/index.ts diff --git a/src/pages/beers/styles/[id]/index.tsx b/src/pages/beers/styles/[id]/index.tsx index 647467a..6a18f6d 100644 --- a/src/pages/beers/styles/[id]/index.tsx +++ b/src/pages/beers/styles/[id]/index.tsx @@ -8,12 +8,13 @@ import { Tab } from '@headlessui/react'; import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById'; import BeerStyleHeader from '@/components/BeerStyleById/BeerStyleHeader'; import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult'; +import BeerStyleCommentSection from '@/components/BeerStyleById/BeerStyleCommentSection'; interface BeerStylePageProps { beerStyle: z.infer; } -const BeerByIdPage: NextPage = ({ beerStyle }) => { +const BeerStyleByIdPage: NextPage = ({ beerStyle }) => { const isDesktop = useMediaQuery('(min-width: 1024px)'); return ( @@ -29,8 +30,10 @@ const BeerByIdPage: NextPage = ({ beerStyle }) => { {isDesktop ? (
-
{/* Comments go here */}
-
{/* Recommendations go here */}
+
+ +
+
{/* Beers of this style go here */}
) : ( @@ -43,8 +46,10 @@ const BeerByIdPage: NextPage = ({ beerStyle }) => { - {/* Comments go here */} - {/* Recommendations go here */} + + + + {/* Beers of this style go here */} )} @@ -55,7 +60,7 @@ const BeerByIdPage: NextPage = ({ beerStyle }) => { ); }; -export default BeerByIdPage; +export default BeerStyleByIdPage; export const getServerSideProps: GetServerSideProps = async ({ params }) => { const id = params!.id as string; diff --git a/src/prisma/migrations/20231009153905_/migration.sql b/src/prisma/migrations/20231009153905_/migration.sql new file mode 100644 index 0000000..2671535 --- /dev/null +++ b/src/prisma/migrations/20231009153905_/migration.sql @@ -0,0 +1,35 @@ +-- CreateTable +CREATE TABLE "BeerStyleLike" ( + "id" TEXT NOT NULL, + "beerStyleId" TEXT NOT NULL, + "likedById" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3), + + CONSTRAINT "BeerStyleLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BeerStyleComment" ( + "id" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "beerStyleId" TEXT NOT NULL, + "postedById" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3), + + CONSTRAINT "BeerStyleComment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "BeerStyleLike" ADD CONSTRAINT "BeerStyleLike_beerStyleId_fkey" FOREIGN KEY ("beerStyleId") REFERENCES "BeerStyle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BeerStyleLike" ADD CONSTRAINT "BeerStyleLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BeerStyleComment" ADD CONSTRAINT "BeerStyleComment_beerStyleId_fkey" FOREIGN KEY ("beerStyleId") REFERENCES "BeerStyle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BeerStyleComment" ADD CONSTRAINT "BeerStyleComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index d5d660e..781e61b 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -18,17 +18,17 @@ enum Role { } model User { - id String @id @default(cuid()) - username String @unique + id String @id @default(cuid()) + 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) - accountIsVerified Boolean @default(false) + email String @unique + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + accountIsVerified Boolean @default(false) dateOfBirth DateTime - role Role @default(USER) + role Role @default(USER) beerPosts BeerPost[] beerStyles BeerStyle[] breweryPosts BreweryPost[] @@ -40,6 +40,8 @@ model User { BreweryPostLike BreweryPostLike[] Location Location[] Glassware Glassware[] + BeerStyleLike BeerStyleLike[] + BeerStyleComment BeerStyleComment[] } model BeerPost { @@ -94,18 +96,42 @@ model BeerComment { } model BeerStyle { - id String @id @default(cuid()) - name String - description String - createdAt DateTime @default(now()) @db.Timestamptz(3) - updatedAt DateTime? @updatedAt @db.Timestamptz(3) - postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) - glassware Glassware @relation(fields: [glasswareId], references: [id], onDelete: Cascade) - glasswareId String + id String @id @default(cuid()) + name String + description String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) + postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) + glassware Glassware @relation(fields: [glasswareId], references: [id], onDelete: Cascade) + glasswareId String + postedById String + abvRange Float[] + ibuRange Float[] + beerPosts BeerPost[] + BeerStyleLike BeerStyleLike[] + BeerStyleComment BeerStyleComment[] +} + +model BeerStyleLike { + id String @id @default(cuid()) + beerStyle BeerStyle @relation(fields: [beerStyleId], references: [id], onDelete: Cascade) + beerStyleId 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 BeerStyleComment { + id String @id @default(cuid()) + rating Int + beerStyle BeerStyle @relation(fields: [beerStyleId], references: [id], onDelete: Cascade) + beerStyleId String + postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedById String - abvRange Float[] - ibuRange Float[] - beerPosts BeerPost[] + content String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime? @updatedAt @db.Timestamptz(3) } model Glassware { diff --git a/src/prisma/seed/create/createNewBeerStyleComments.ts b/src/prisma/seed/create/createNewBeerStyleComments.ts new file mode 100644 index 0000000..6e71d1c --- /dev/null +++ b/src/prisma/seed/create/createNewBeerStyleComments.ts @@ -0,0 +1,56 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { faker } from '@faker-js/faker'; +import { BeerStyle, User } from '@prisma/client'; + +import DBClient from '../../DBClient'; + +interface CreateNewBeerCommentsArgs { + numberOfComments: number; + joinData: { + beerStyles: BeerStyle[]; + users: User[]; + }; +} + +interface BeerStyleComment { + content: string; + postedById: string; + beerStyleId: string; + rating: number; + createdAt: Date; +} + +const createNewBeerStyleComments = async ({ + numberOfComments, + joinData, +}: CreateNewBeerCommentsArgs) => { + const { beerStyles, users } = joinData; + const prisma = DBClient.instance; + + const beerStyleCommentData: BeerStyleComment[] = []; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < numberOfComments; i++) { + const content = faker.lorem.lines(5); + const user = users[Math.floor(Math.random() * users.length)]; + const beerStyle = beerStyles[Math.floor(Math.random() * beerStyles.length)]; + const createdAt = faker.date.past({ years: 1 }); + const rating = Math.floor(Math.random() * 5) + 1; + + beerStyleCommentData.push({ + content, + postedById: user.id, + beerStyleId: beerStyle.id, + createdAt, + rating, + }); + } + + await prisma.beerStyleComment.createMany({ + data: beerStyleCommentData, + }); + + return prisma.beerStyleComment.findMany(); +}; + +export default createNewBeerStyleComments; diff --git a/src/prisma/seed/create/createNewBeerStyleLikes.ts b/src/prisma/seed/create/createNewBeerStyleLikes.ts new file mode 100644 index 0000000..dc6772b --- /dev/null +++ b/src/prisma/seed/create/createNewBeerStyleLikes.ts @@ -0,0 +1,44 @@ +import type { BeerStyle, User } from '@prisma/client'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { faker } from '@faker-js/faker'; +import DBClient from '../../DBClient'; + +interface BeerPostLikeData { + beerStyleId: string; + likedById: string; + createdAt: Date; +} + +interface CreateNewBeerStyleLikesArgs { + joinData: { + beerStyles: BeerStyle[]; + users: User[]; + }; + numberOfLikes: number; +} + +const createNewBeerStyleLikes = async ({ + joinData: { beerStyles, users }, + numberOfLikes, +}: CreateNewBeerStyleLikesArgs) => { + const beerStyleLikeData: BeerPostLikeData[] = []; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < numberOfLikes; i++) { + const beerStyle = beerStyles[Math.floor(Math.random() * beerStyles.length)]; + const user = users[Math.floor(Math.random() * users.length)]; + const createdAt = faker.date.past({ years: 1 }); + beerStyleLikeData.push({ + beerStyleId: beerStyle.id, + likedById: user.id, + createdAt, + }); + } + + await DBClient.instance.beerStyleLike.createMany({ + data: beerStyleLikeData, + }); + + return DBClient.instance.beerStyleLike.findMany(); +}; + +export default createNewBeerStyleLikes; diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index b0c14b6..4377655 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -16,6 +16,8 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes'; import createNewLocations from './create/createNewLocations'; import logger from '../../config/pino/logger'; import createAdminUser from './create/createAdminUser'; +import createNewBeerStyleComments from './create/createNewBeerStyleComments'; +import createNewBeerStyleLikes from './create/createNewBeerStyleLikes'; (async () => { try { @@ -51,11 +53,15 @@ import createAdminUser from './create/createAdminUser'; logger.info('Beer posts created successfully.'); - const [beerPostComments, breweryPostComments] = await Promise.all([ + const [beerPostComments, beerStyleComments, breweryPostComments] = await Promise.all([ createNewBeerPostComments({ numberOfComments: 100000, joinData: { beerPosts, users }, }), + createNewBeerStyleComments({ + numberOfComments: 5000, + joinData: { beerStyles, users }, + }), createNewBreweryPostComments({ numberOfComments: 50000, joinData: { breweryPosts, users }, @@ -63,11 +69,15 @@ import createAdminUser from './create/createAdminUser'; ]); logger.info('Created beer post comments and brewery post comments.'); - const [beerPostLikes, breweryPostLikes] = await Promise.all([ + const [beerPostLikes, beerStyleLikes, breweryPostLikes] = await Promise.all([ createNewBeerPostLikes({ numberOfLikes: 500000, joinData: { beerPosts, users }, }), + createNewBeerStyleLikes({ + numberOfLikes: 50000, + joinData: { beerStyles, users }, + }), createNewBreweryPostLikes({ numberOfLikes: 100000, joinData: { breweryPosts, users }, @@ -96,6 +106,8 @@ import createAdminUser from './create/createAdminUser'; numberOfBreweryPosts: breweryPosts.length, numberOfBeerPosts: beerPosts.length, numberOfBeerStyles: beerStyles.length, + numberOfBeerStyleLikes: beerStyleLikes.length, + numberOfBeerStyleComments: beerStyleComments.length, numberOfBeerPostLikes: beerPostLikes.length, numberOfBreweryPostLikes: breweryPostLikes.length, numberOfBeerPostComments: beerPostComments.length, diff --git a/src/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest.ts b/src/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest.ts new file mode 100644 index 0000000..89a0cd9 --- /dev/null +++ b/src/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest.ts @@ -0,0 +1,53 @@ +import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema'; + +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +const BeerStyleCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ + beerStyleId: z.string().cuid(), +}); + +/** + * Sends a POST request to the server to create a new beer comment. + * + * @param data The data to be sent to the server. + * @param data.beerPostId The ID of the beer post to comment on. + * @param data.content The content of the comment. + * @param data.rating The rating of the beer. + * @returns A promise that resolves to the created comment. + * @throws An error if the request fails, the API response is invalid, or the API response + * payload is invalid. + */ +const sendCreateBeerStyleCommentRequest = async ({ + beerStyleId, + content, + rating, +}: z.infer) => { + const response = await fetch(`/api/beers/styles/${beerStyleId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ beerStyleId, content, rating }), + }); + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + + const parsedResponse = APIResponseValidationSchema.safeParse(data); + + if (!parsedResponse.success) { + throw new Error('Invalid API response'); + } + + const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload); + + if (!parsedPayload.success) { + throw new Error('Invalid API response payload'); + } + + return parsedPayload.data; +}; + +export default sendCreateBeerStyleCommentRequest; diff --git a/src/services/BeerStyleComment/createNewBeerStyleComment.ts b/src/services/BeerStyleComment/createNewBeerStyleComment.ts new file mode 100644 index 0000000..0d84586 --- /dev/null +++ b/src/services/BeerStyleComment/createNewBeerStyleComment.ts @@ -0,0 +1,37 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema'; +import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult'; + +const CreateNewBeerStyleCommentServiceSchema = CreateCommentValidationSchema.extend({ + userId: z.string().cuid(), + beerStyleId: z.string().cuid(), +}); + +type CreateNewBeerCommentArgs = z.infer; + +const createNewBeerStyleComment = async ({ + content, + rating, + userId, + beerStyleId, +}: CreateNewBeerCommentArgs): Promise> => { + return DBClient.instance.beerStyleComment.create({ + data: { + content, + rating, + beerStyle: { connect: { id: beerStyleId } }, + postedBy: { connect: { id: userId } }, + }, + select: { + id: true, + content: true, + rating: true, + postedBy: { select: { id: true, username: true } }, + createdAt: true, + updatedAt: true, + }, + }); +}; + +export default createNewBeerStyleComment; diff --git a/src/services/BeerStyleComment/getAllBeerStyleComments.ts b/src/services/BeerStyleComment/getAllBeerStyleComments.ts new file mode 100644 index 0000000..e8bdeff --- /dev/null +++ b/src/services/BeerStyleComment/getAllBeerStyleComments.ts @@ -0,0 +1,32 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult'; + +interface GetAllBeerStyleCommentArgs { + beerStyleId: string; + pageNum: number; + pageSize: number; +} + +const getAllBeerStyleComments = async ({ + beerStyleId, + pageNum, + pageSize, +}: GetAllBeerStyleCommentArgs): Promise[]> => { + return DBClient.instance.beerStyleComment.findMany({ + skip: (pageNum - 1) * pageSize, + take: pageSize, + where: { beerStyleId }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + content: true, + rating: true, + createdAt: true, + updatedAt: true, + postedBy: { select: { id: true, username: true, createdAt: true } }, + }, + }); +}; + +export default getAllBeerStyleComments; diff --git a/src/services/BeerStyleComment/getBeerStyleCommentCount.ts b/src/services/BeerStyleComment/getBeerStyleCommentCount.ts new file mode 100644 index 0000000..c52185c --- /dev/null +++ b/src/services/BeerStyleComment/getBeerStyleCommentCount.ts @@ -0,0 +1,15 @@ +import DBClient from '@/prisma/DBClient'; + +interface GetBeerStyleCommentCountArgs { + beerStyleId: string; +} + +const getBeerCommentCount = async ({ + beerStyleId, +}: GetBeerStyleCommentCountArgs): Promise => { + return DBClient.instance.beerStyleComment.count({ + where: { beerStyleId }, + }); +}; + +export default getBeerCommentCount;