mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
feat: add beer style comments
This commit is contained in:
66
src/components/BeerStyleById/BeerStyleCommentForm.tsx
Normal file
66
src/components/BeerStyleById/BeerStyleCommentForm.tsx
Normal 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;
|
||||||
97
src/components/BeerStyleById/BeerStyleCommentSection.tsx
Normal file
97
src/components/BeerStyleById/BeerStyleCommentSection.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
106
src/pages/api/beer-style-comments/[id].ts
Normal file
106
src/pages/api/beer-style-comments/[id].ts
Normal 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;
|
||||||
103
src/pages/api/beers/styles/[id]/comments/index.ts
Normal file
103
src/pages/api/beers/styles/[id]/comments/index.ts
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
35
src/prisma/migrations/20231009153905_/migration.sql
Normal file
35
src/prisma/migrations/20231009153905_/migration.sql
Normal 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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
56
src/prisma/seed/create/createNewBeerStyleComments.ts
Normal file
56
src/prisma/seed/create/createNewBeerStyleComments.ts
Normal 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;
|
||||||
44
src/prisma/seed/create/createNewBeerStyleLikes.ts
Normal file
44
src/prisma/seed/create/createNewBeerStyleLikes.ts
Normal 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;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
37
src/services/BeerStyleComment/createNewBeerStyleComment.ts
Normal file
37
src/services/BeerStyleComment/createNewBeerStyleComment.ts
Normal 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;
|
||||||
32
src/services/BeerStyleComment/getAllBeerStyleComments.ts
Normal file
32
src/services/BeerStyleComment/getAllBeerStyleComments.ts
Normal 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;
|
||||||
15
src/services/BeerStyleComment/getBeerStyleCommentCount.ts
Normal file
15
src/services/BeerStyleComment/getBeerStyleCommentCount.ts
Normal 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;
|
||||||
Reference in New Issue
Block a user