feat: add beer style comments

This commit is contained in:
Aaron William Po
2023-10-15 20:24:40 -04:00
parent 27af922a91
commit c8e8207e30
16 changed files with 774 additions and 26 deletions

View File

@@ -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<typeof BeerStyleQueryResult>;
mutate: ReturnType<typeof useBeerStyleComments>['mutate'];
}
const BeerStyleCommentForm: FunctionComponent<BeerCommentFormProps> = ({
beerStyle,
mutate,
}) => {
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
z.infer<typeof CreateCommentValidationSchema>
>({
defaultValues: { rating: 0 },
resolver: zodResolver(CreateCommentValidationSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = 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 (
<CommentForm
handleSubmit={handleSubmit}
onSubmit={onSubmit}
watch={watch}
setValue={setValue}
formState={formState}
register={register}
/>
);
};
export default BeerStyleCommentForm;

View File

@@ -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<typeof BeerStyleQueryResult>;
}
const BeerStyleCommentsSection: FC<BeerStyleCommentsSectionProps> = ({ 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<HTMLDivElement | null> = 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<typeof CreateCommentValidationSchema>,
) => {
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 (
<div className="w-full space-y-3" ref={commentSectionRef}>
<div className="card bg-base-300">
<div className="card-body h-full">
{user ? (
<BeerStyleCommentForm beerStyle={beerStyle} mutate={mutate} />
) : (
<div className="flex h-52 flex-col items-center justify-center">
<span className="text-lg font-bold">Log in to leave a comment.</span>
</div>
)}
</div>
</div>
{
/**
* If the comments are loading, show a loading component. Otherwise, show the
* comments.
*/
isLoading ? (
<div className="card bg-base-300 pb-6">
<LoadingComponent length={PAGE_SIZE} />
</div>
) : (
<CommentsComponent
commentSectionRef={commentSectionRef}
comments={comments}
isLoadingMore={isLoadingMore}
isAtEnd={isAtEnd}
pageSize={PAGE_SIZE}
setSize={setSize}
size={size}
mutate={mutate}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
/>
)
}
</div>
);
};
export default BeerStyleCommentsSection;

View File

@@ -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;

View File

@@ -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<typeof CreateCommentValidationSchema>;
}
const checkIfCommentOwner = async (
req: DeleteCommentRequest | EditCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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<z.infer<typeof APIResponseValidationSchema>>
>();
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;

View File

@@ -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<typeof CreateCommentValidationSchema>;
query: { id: string };
}
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string };
}
const createComment = async (
req: CreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { content, rating } = req.body;
const newBeerStyleComment: z.infer<typeof CommentQueryResult> =
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<z.infer<typeof APIResponseValidationSchema>>,
) => {
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<z.infer<typeof APIResponseValidationSchema>>
>();
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;

View File

@@ -8,12 +8,13 @@ import { Tab } from '@headlessui/react';
import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById'; import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById';
import BeerStyleHeader from '@/components/BeerStyleById/BeerStyleHeader'; import BeerStyleHeader from '@/components/BeerStyleById/BeerStyleHeader';
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult'; import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
import BeerStyleCommentSection from '@/components/BeerStyleById/BeerStyleCommentSection';
interface BeerStylePageProps { interface BeerStylePageProps {
beerStyle: z.infer<typeof BeerStyleQueryResult>; beerStyle: z.infer<typeof BeerStyleQueryResult>;
} }
const BeerByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => { const BeerStyleByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
const isDesktop = useMediaQuery('(min-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1024px)');
return ( return (
@@ -29,8 +30,10 @@ const BeerByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
{isDesktop ? ( {isDesktop ? (
<div className="mt-4 flex flex-row space-x-3 space-y-0"> <div className="mt-4 flex flex-row space-x-3 space-y-0">
<div className="w-[60%]">{/* Comments go here */}</div> <div className="w-[60%]">
<div className="w-[40%]">{/* Recommendations go here */}</div> <BeerStyleCommentSection beerStyle={beerStyle} />
</div>
<div className="w-[40%]">{/* Beers of this style go here */}</div>
</div> </div>
) : ( ) : (
<Tab.Group> <Tab.Group>
@@ -43,8 +46,10 @@ const BeerByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
</Tab> </Tab>
</Tab.List> </Tab.List>
<Tab.Panels className="mt-2"> <Tab.Panels className="mt-2">
<Tab.Panel>{/* Comments go here */}</Tab.Panel> <Tab.Panel>
<Tab.Panel>{/* Recommendations go here */}</Tab.Panel> <BeerStyleCommentSection beerStyle={beerStyle} />
</Tab.Panel>
<Tab.Panel>{/* Beers of this style go here */}</Tab.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
)} )}
@@ -55,7 +60,7 @@ const BeerByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
); );
}; };
export default BeerByIdPage; export default BeerStyleByIdPage;
export const getServerSideProps: GetServerSideProps = async ({ params }) => { export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const id = params!.id as string; const id = params!.id as string;

View File

@@ -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;

View File

@@ -40,6 +40,8 @@ model User {
BreweryPostLike BreweryPostLike[] BreweryPostLike BreweryPostLike[]
Location Location[] Location Location[]
Glassware Glassware[] Glassware Glassware[]
BeerStyleLike BeerStyleLike[]
BeerStyleComment BeerStyleComment[]
} }
model BeerPost { model BeerPost {
@@ -106,6 +108,30 @@ model BeerStyle {
abvRange Float[] abvRange Float[]
ibuRange Float[] ibuRange Float[]
beerPosts BeerPost[] 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
content String
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
} }
model Glassware { model Glassware {

View File

@@ -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;

View File

@@ -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;

View File

@@ -16,6 +16,8 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
import createNewLocations from './create/createNewLocations'; import createNewLocations from './create/createNewLocations';
import logger from '../../config/pino/logger'; import logger from '../../config/pino/logger';
import createAdminUser from './create/createAdminUser'; import createAdminUser from './create/createAdminUser';
import createNewBeerStyleComments from './create/createNewBeerStyleComments';
import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
(async () => { (async () => {
try { try {
@@ -51,11 +53,15 @@ import createAdminUser from './create/createAdminUser';
logger.info('Beer posts created successfully.'); logger.info('Beer posts created successfully.');
const [beerPostComments, breweryPostComments] = await Promise.all([ const [beerPostComments, beerStyleComments, breweryPostComments] = await Promise.all([
createNewBeerPostComments({ createNewBeerPostComments({
numberOfComments: 100000, numberOfComments: 100000,
joinData: { beerPosts, users }, joinData: { beerPosts, users },
}), }),
createNewBeerStyleComments({
numberOfComments: 5000,
joinData: { beerStyles, users },
}),
createNewBreweryPostComments({ createNewBreweryPostComments({
numberOfComments: 50000, numberOfComments: 50000,
joinData: { breweryPosts, users }, joinData: { breweryPosts, users },
@@ -63,11 +69,15 @@ import createAdminUser from './create/createAdminUser';
]); ]);
logger.info('Created beer post comments and brewery post comments.'); logger.info('Created beer post comments and brewery post comments.');
const [beerPostLikes, breweryPostLikes] = await Promise.all([ const [beerPostLikes, beerStyleLikes, breweryPostLikes] = await Promise.all([
createNewBeerPostLikes({ createNewBeerPostLikes({
numberOfLikes: 500000, numberOfLikes: 500000,
joinData: { beerPosts, users }, joinData: { beerPosts, users },
}), }),
createNewBeerStyleLikes({
numberOfLikes: 50000,
joinData: { beerStyles, users },
}),
createNewBreweryPostLikes({ createNewBreweryPostLikes({
numberOfLikes: 100000, numberOfLikes: 100000,
joinData: { breweryPosts, users }, joinData: { breweryPosts, users },
@@ -96,6 +106,8 @@ import createAdminUser from './create/createAdminUser';
numberOfBreweryPosts: breweryPosts.length, numberOfBreweryPosts: breweryPosts.length,
numberOfBeerPosts: beerPosts.length, numberOfBeerPosts: beerPosts.length,
numberOfBeerStyles: beerStyles.length, numberOfBeerStyles: beerStyles.length,
numberOfBeerStyleLikes: beerStyleLikes.length,
numberOfBeerStyleComments: beerStyleComments.length,
numberOfBeerPostLikes: beerPostLikes.length, numberOfBeerPostLikes: beerPostLikes.length,
numberOfBreweryPostLikes: breweryPostLikes.length, numberOfBreweryPostLikes: breweryPostLikes.length,
numberOfBeerPostComments: beerPostComments.length, numberOfBeerPostComments: beerPostComments.length,

View File

@@ -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<typeof BeerStyleCommentValidationSchemaWithId>) => {
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;

View File

@@ -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<typeof CreateNewBeerStyleCommentServiceSchema>;
const createNewBeerStyleComment = async ({
content,
rating,
userId,
beerStyleId,
}: CreateNewBeerCommentArgs): Promise<z.infer<typeof CommentQueryResult>> => {
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;

View File

@@ -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<z.infer<typeof CommentQueryResult>[]> => {
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;

View File

@@ -0,0 +1,15 @@
import DBClient from '@/prisma/DBClient';
interface GetBeerStyleCommentCountArgs {
beerStyleId: string;
}
const getBeerCommentCount = async ({
beerStyleId,
}: GetBeerStyleCommentCountArgs): Promise<number> => {
return DBClient.instance.beerStyleComment.count({
where: { beerStyleId },
});
};
export default getBeerCommentCount;