From 070d537a6a740e89270639c24de0cad5d46d65ac Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Wed, 19 Apr 2023 23:14:43 -0400 Subject: [PATCH] Get basic editing functionality for beer comments --- src/components/BeerById/CommentCardBody.tsx | 227 ++++++++++++++++++-- src/pages/api/beer-comments/[id].ts | 107 +++++++++ src/pages/api/beer-comments/[id].tsx | 60 ------ 3 files changed, 313 insertions(+), 81 deletions(-) create mode 100644 src/pages/api/beer-comments/[id].ts delete mode 100644 src/pages/api/beer-comments/[id].tsx diff --git a/src/components/BeerById/CommentCardBody.tsx b/src/components/BeerById/CommentCardBody.tsx index f8102f6..6cc4142 100644 --- a/src/components/BeerById/CommentCardBody.tsx +++ b/src/components/BeerById/CommentCardBody.tsx @@ -2,14 +2,22 @@ import UserContext from '@/contexts/userContext'; import useBeerPostComments from '@/hooks/useBeerPostComments'; import useTimeDistance from '@/hooks/useTimeDistance'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; import format from 'date-fns/format'; import Link from 'next/link'; -import { FC, useContext } from 'react'; +import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react'; import { Rating } from 'react-daisyui'; +import { SubmitHandler, useForm } from 'react-hook-form'; import { FaEllipsisH } from 'react-icons/fa'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; +import FormError from '../ui/forms/FormError'; +import FormInfo from '../ui/forms/FormInfo'; +import FormLabel from '../ui/forms/FormLabel'; +import FormSegment from '../ui/forms/FormSegment'; +import FormTextArea from '../ui/forms/FormTextArea'; interface CommentCardProps { comment: z.infer; @@ -17,25 +25,21 @@ interface CommentCardProps { ref?: ReturnType['ref']; } -const CommentCardDropdown: FC = ({ comment, mutate }) => { +interface CommentCardDropdownProps extends CommentCardProps { + inEditMode: boolean; + setInEditMode: Dispatch>; +} + +const CommentCardDropdown: FC = ({ + comment, + setInEditMode, +}) => { const { user } = useContext(UserContext); const isCommentOwner = user?.id === comment.postedBy.id; - const handleDelete = async () => { - const response = await fetch(`/api/beer-comments/${comment.id}`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error('Failed to delete comment'); - } - - await mutate(); - }; - return ( -
+
@@ -46,9 +50,13 @@ const CommentCardDropdown: FC = ({ comment, mutate }) => {
  • {isCommentOwner ? ( <> - - ) : ( @@ -60,9 +68,157 @@ const CommentCardDropdown: FC = ({ comment, mutate }) => { ); }; -const CommentCardBody: FC = ({ comment, mutate, ref }) => { - const { user } = useContext(UserContext); +const EditCommentBody: FC = ({ + comment, + setInEditMode, + ref, + mutate, +}) => { + const { register, handleSubmit, formState, setValue, watch } = useForm< + z.infer + >({ + defaultValues: { + content: comment.content, + rating: comment.rating, + }, + resolver: zodResolver(BeerCommentValidationSchema), + }); + const { errors } = formState; + + const [isDeleting, setIsDeleting] = useState(false); + + useEffect(() => { + return () => { + setIsDeleting(false); + }; + }, []); + + const handleDelete = async () => { + setIsDeleting(true); + const response = await fetch(`/api/beer-comments/${comment.id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete comment'); + } + + await mutate(); + }; + + const onSubmit: SubmitHandler> = async ( + data, + ) => { + const response = await fetch(`/api/beer-comments/${comment.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: data.content, + rating: data.rating, + }), + }); + + if (!response.ok) { + throw new Error('Failed to update comment'); + } + + await mutate(); + setInEditMode(false); + }; + return ( +
    +
    +
    + + Edit your comment + {errors.content?.message} + + + + +
    +
    + + Change your rating + {errors.rating?.message} + + { + setValue('rating', value); + }} + > + {Array.from({ length: 5 }).map((val, index) => ( + + ))} + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    + ); +}; + +const CommentContentBody: FC = ({ + comment, + ref, + mutate, + inEditMode, + setInEditMode, +}) => { + const { user } = useContext(UserContext); const timeDistance = useTimeDistance(new Date(comment.createdAt)); return ( @@ -86,7 +242,14 @@ const CommentCardBody: FC = ({ comment, mutate, ref }) => {
  • - {user && } + {user && ( + + )}
    @@ -107,4 +270,26 @@ const CommentCardBody: FC = ({ comment, mutate, ref }) => { ); }; +const CommentCardBody: FC = ({ comment, mutate, ref }) => { + const [inEditMode, setInEditMode] = useState(false); + + return !inEditMode ? ( + + ) : ( + + ); +}; + export default CommentCardBody; diff --git a/src/pages/api/beer-comments/[id].ts b/src/pages/api/beer-comments/[id].ts new file mode 100644 index 0000000..124b6e2 --- /dev/null +++ b/src/pages/api/beer-comments/[id].ts @@ -0,0 +1,107 @@ +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 BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +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.beerComment.findUnique({ + 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); + } + + await next(); +}; + +const editComment = async ( + req: EditCommentRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + const updated = await DBClient.instance.beerComment.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.beerComment.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().uuid() }) }), + getCurrentUser, + checkIfCommentOwner, + deleteComment, + ) + .put( + validateRequest({ + querySchema: z.object({ id: z.string().uuid() }), + bodySchema: BeerCommentValidationSchema, + }), + getCurrentUser, + checkIfCommentOwner, + editComment, + ); + +const handler = router.handler(NextConnectOptions); +export default handler; diff --git a/src/pages/api/beer-comments/[id].tsx b/src/pages/api/beer-comments/[id].tsx deleted file mode 100644 index 8a2803d..0000000 --- a/src/pages/api/beer-comments/[id].tsx +++ /dev/null @@ -1,60 +0,0 @@ -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 APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { NextApiResponse } from 'next'; -import { createRouter } from 'next-connect'; -import { z } from 'zod'; - -interface DeleteCommentRequest extends UserExtendedNextApiRequest { - query: { id: string }; -} - -const deleteComment = async ( - req: DeleteCommentRequest, - res: NextApiResponse>, -) => { - const { id } = req.query; - const user = req.user!; - - const comment = await DBClient.instance.beerComment.findUnique({ - where: { id }, - }); - - if (!comment) { - throw new ServerError('Comment not found', 404); - } - - if (comment.postedById !== user.id) { - throw new ServerError('You are not authorized to delete this comment', 403); - } - - await DBClient.instance.beerComment.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().uuid() }), - }), - getCurrentUser, - deleteComment, -); - -const handler = router.handler(NextConnectOptions); -export default handler;