From 070d537a6a740e89270639c24de0cad5d46d65ac Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Wed, 19 Apr 2023 23:14:43 -0400 Subject: [PATCH 1/5] 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; From 2dfb080d0cd878815d37de846f805178be0dbb43 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Fri, 21 Apr 2023 23:31:09 -0400 Subject: [PATCH 2/5] Implement theming --- package-lock.json | 23 +++++++++++++----- package.json | 3 ++- src/components/ui/Navbar.tsx | 47 +++++++++++++++++++++++++++++++++--- src/styles/globals.css | 4 +-- tailwind.config.js | 24 +++++++++--------- 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa6ef09..a8d255e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "react-responsive-carousel": "^3.2.23", "sparkpost": "^2.1.4", "swr": "^2.1.2", + "theme-change": "^2.5.0", "zod": "^3.21.4" }, "devDependencies": { @@ -9643,6 +9644,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/theme-change": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz", + "integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ==" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -10213,9 +10219,9 @@ } }, "node_modules/vm2": { - "version": "3.9.16", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz", - "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==", + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==", "optional": true, "dependencies": { "acorn": "^8.7.0", @@ -17148,6 +17154,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "theme-change": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz", + "integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ==" + }, "thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -17582,9 +17593,9 @@ } }, "vm2": { - "version": "3.9.16", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz", - "integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==", + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==", "optional": true, "requires": { "acorn": "^8.7.0", diff --git a/package.json b/package.json index 2282de2..3386faa 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "react-responsive-carousel": "^3.2.23", "sparkpost": "^2.1.4", "swr": "^2.1.2", + "theme-change": "^2.5.0", "zod": "^3.21.4" }, "devDependencies": { @@ -77,7 +78,7 @@ "tailwindcss-animate": "^1.0.5", "ts-node": "^10.9.1", "typescript": "^5.0.3" - }, + }, "prisma": { "schema": "./src/prisma/schema.prisma" } diff --git a/src/components/ui/Navbar.tsx b/src/components/ui/Navbar.tsx index 2f63e03..0b7bcd3 100644 --- a/src/components/ui/Navbar.tsx +++ b/src/components/ui/Navbar.tsx @@ -1,8 +1,8 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import useNavbar from '@/hooks/useNavbar'; import Link from 'next/link'; -import { FC } from 'react'; - +import { FC, useEffect, useState } from 'react'; +import { MdDarkMode, MdLightMode } from 'react-icons/md'; import { GiHamburgerMenu } from 'react-icons/gi'; const DesktopLinks: FC = () => { @@ -56,9 +56,29 @@ const MobileLinks: FC = () => { ); }; +const useTheme = () => { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + useEffect(() => { + const savedTheme = localStorage.getItem('theme'); + if (prefersDarkMode && !savedTheme) { + setTheme('dark'); + localStorage.setItem('theme', 'dark'); + return; + } + setTheme(savedTheme as 'light' | 'dark'); + }, [prefersDarkMode, theme]); + + return { theme, setTheme }; +}; + const Navbar = () => { const isDesktopView = useMediaQuery('(min-width: 1024px)'); + const { theme, setTheme } = useTheme(); + return ( ); }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 4ccbf08..b02a8f5 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -3,5 +3,5 @@ @tailwind utilities; .card { - @apply shadow-md card-compact bg-base-300 -} \ No newline at end of file + @apply card-compact bg-base-300; +} diff --git a/tailwind.config.js b/tailwind.config.js index 0d20479..7822c55 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,7 +1,7 @@ //themes -const darkTheme = { - default: { +const myThemes = { + dark: { primary: 'hsl(227, 23%, 20%)', secondary: 'hsl(255, 9%, 69%)', error: 'hsl(9, 52%, 57%)', @@ -12,14 +12,11 @@ const darkTheme = { warning: 'hsl(50, 98%, 50%)', 'primary-content': 'hsl(0, 0%, 98%)', 'error-content': 'hsl(0, 0%, 98%)', - 'base-100': 'hsl(190, 4%, 11%)', - 'base-200': 'hsl(190, 4%, 8%)', - 'base-300': 'hsl(190, 4%, 5%)', + 'base-100': 'hsl(227, 20%, 11%)', + 'base-200': 'hsl(227, 20%, 8%)', + 'base-300': 'hsl(227, 20%, 5%)', }, -}; - -const pastelTheme = { - default: { + light: { primary: 'hsl(180, 15%, 60%)', secondary: 'hsl(21, 54%, 83%)', error: 'hsl(4, 87%, 74%)', @@ -30,9 +27,9 @@ const pastelTheme = { warning: 'hsl(40, 76%, 73%)', 'primary-content': 'hsl(0, 0%, 0%)', 'error-content': 'hsl(0, 0%, 0%)', - 'base-100': 'hsl(0, 0%, 94%)', - 'base-200': 'hsl(0, 0%, 90%)', - 'base-300': 'hsl(0, 0%, 85%)', + 'base-100': 'hsl(180, 8%, 94%)', + 'base-200': 'hsl(180, 8%, 92%)', + 'base-300': 'hsl(180, 8%, 88%)', }, }; @@ -50,10 +47,11 @@ module.exports = { require('@headlessui/tailwindcss'), require('daisyui'), require('tailwindcss-animate'), + require('autoprefixer'), ], daisyui: { logs: false, - themes: [darkTheme, pastelTheme], + themes: [myThemes], }, }; From 6a00532f7544256e3745e8a986f79ddcce88c11e Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Fri, 21 Apr 2023 23:32:18 -0400 Subject: [PATCH 3/5] Style updates --- public/favicon/site.webmanifest | 12 +- .../BeerById/BeerRecommendations.tsx | 2 +- src/components/BeerById/CommentCardBody.tsx | 275 +----------------- .../BeerById/CommentCardDropdown.tsx | 47 +++ .../BeerById/CommentContentBody.tsx | 67 +++++ src/components/BeerById/EditCommentBody.tsx | 158 ++++++++++ src/components/ui/Navbar.tsx | 48 +-- src/components/ui/alerts/ErrorAlert.tsx | 4 +- src/components/ui/forms/FormLabel.tsx | 2 +- src/components/ui/forms/FormPageLayout.tsx | 4 +- src/components/ui/forms/FormTextArea.tsx | 2 +- src/components/ui/forms/FormTextInput.tsx | 2 +- src/pages/404.tsx | 8 +- src/pages/500.tsx | 8 +- src/pages/_app.tsx | 5 + src/pages/_document.tsx | 4 + src/pages/beers/[id]/edit.tsx | 2 +- src/pages/beers/[id]/index.tsx | 6 +- src/pages/beers/index.tsx | 4 +- src/pages/breweries/index.tsx | 2 +- src/pages/index.tsx | 5 +- src/prisma/seed/clean/index.ts | 7 + 22 files changed, 360 insertions(+), 314 deletions(-) create mode 100644 src/components/BeerById/CommentCardDropdown.tsx create mode 100644 src/components/BeerById/CommentContentBody.tsx create mode 100644 src/components/BeerById/EditCommentBody.tsx create mode 100644 src/prisma/seed/clean/index.ts diff --git a/public/favicon/site.webmanifest b/public/favicon/site.webmanifest index 45dc8a2..0b08af1 100644 --- a/public/favicon/site.webmanifest +++ b/public/favicon/site.webmanifest @@ -1 +1,11 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "", + "short_name": "", + "icons": [ + { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/components/BeerById/BeerRecommendations.tsx b/src/components/BeerById/BeerRecommendations.tsx index 57b43a1..1e3b3bb 100644 --- a/src/components/BeerById/BeerRecommendations.tsx +++ b/src/components/BeerById/BeerRecommendations.tsx @@ -9,7 +9,7 @@ const BeerRecommendations: FunctionComponent = ({ beerRecommendations, }) => { return ( -
    +
    {beerRecommendations.map((beerPost) => (
    diff --git a/src/components/BeerById/CommentCardBody.tsx b/src/components/BeerById/CommentCardBody.tsx index 6cc4142..95becc6 100644 --- a/src/components/BeerById/CommentCardBody.tsx +++ b/src/components/BeerById/CommentCardBody.tsx @@ -1,23 +1,10 @@ -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 { 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 { FC, useState } from 'react'; 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'; +import CommentContentBody from './CommentContentBody'; +import EditCommentBody from './EditCommentBody'; interface CommentCardProps { comment: z.infer; @@ -25,269 +12,17 @@ interface CommentCardProps { ref?: ReturnType['ref']; } -interface CommentCardDropdownProps extends CommentCardProps { - inEditMode: boolean; - setInEditMode: Dispatch>; -} - -const CommentCardDropdown: FC = ({ - comment, - setInEditMode, -}) => { - const { user } = useContext(UserContext); - - const isCommentOwner = user?.id === comment.postedBy.id; - - return ( -
    - -
      -
    • - {isCommentOwner ? ( - <> - - - ) : ( - - )} -
    • -
    -
    - ); -}; - -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 ( -
    -
    -
    -

    - - {comment.postedBy.username} - -

    -

    - posted{' '} - {' '} - ago -

    -
    - - {user && ( - - )} -
    - -
    - - {Array.from({ length: 5 }).map((val, index) => ( - - ))} - -

    {comment.content}

    -
    -
    - ); -}; - const CommentCardBody: FC = ({ comment, mutate, ref }) => { const [inEditMode, setInEditMode] = useState(false); return !inEditMode ? ( - + ) : ( ); }; diff --git a/src/components/BeerById/CommentCardDropdown.tsx b/src/components/BeerById/CommentCardDropdown.tsx new file mode 100644 index 0000000..69e103b --- /dev/null +++ b/src/components/BeerById/CommentCardDropdown.tsx @@ -0,0 +1,47 @@ +import UserContext from '@/contexts/userContext'; +import { Dispatch, SetStateAction, FC, useContext } from 'react'; +import { FaEllipsisH } from 'react-icons/fa'; +import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import { z } from 'zod'; + +interface CommentCardDropdownProps { + comment: z.infer; + setInEditMode: Dispatch>; +} + +const CommentCardDropdown: FC = ({ + comment, + setInEditMode, +}) => { + const { user } = useContext(UserContext); + const isCommentOwner = user?.id === comment.postedBy.id; + + return ( +
    + +
      +
    • + {isCommentOwner ? ( + + ) : ( + + )} +
    • +
    +
    + ); +}; + +export default CommentCardDropdown; diff --git a/src/components/BeerById/CommentContentBody.tsx b/src/components/BeerById/CommentContentBody.tsx new file mode 100644 index 0000000..c9b3416 --- /dev/null +++ b/src/components/BeerById/CommentContentBody.tsx @@ -0,0 +1,67 @@ +import UserContext from '@/contexts/userContext'; +import useTimeDistance from '@/hooks/useTimeDistance'; +import { format } from 'date-fns'; +import { Dispatch, FC, SetStateAction, useContext } from 'react'; +import { Link, Rating } from 'react-daisyui'; +import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import { useInView } from 'react-intersection-observer'; +import { z } from 'zod'; +import CommentCardDropdown from './CommentCardDropdown'; + +interface CommentContentBodyProps { + comment: z.infer; + ref: ReturnType['ref'] | undefined; + setInEditMode: Dispatch>; +} + +const CommentContentBody: FC = ({ + comment, + ref, + setInEditMode, +}) => { + const { user } = useContext(UserContext); + const timeDistance = useTimeDistance(new Date(comment.createdAt)); + + return ( +
    +
    +
    +

    + + {comment.postedBy.username} + +

    +

    + posted{' '} + {' '} + ago +

    +
    + + {user && } +
    + +
    + + {Array.from({ length: 5 }).map((val, index) => ( + + ))} + +

    {comment.content}

    +
    +
    + ); +}; + +export default CommentContentBody; diff --git a/src/components/BeerById/EditCommentBody.tsx b/src/components/BeerById/EditCommentBody.tsx new file mode 100644 index 0000000..8ac8636 --- /dev/null +++ b/src/components/BeerById/EditCommentBody.tsx @@ -0,0 +1,158 @@ +import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { Rating } from 'react-daisyui'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { z } from 'zod'; +import useBeerPostComments from '@/hooks/useBeerPostComments'; +import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; +import { useInView } from 'react-intersection-observer'; +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 CommentCardDropdownProps { + comment: z.infer; + setInEditMode: Dispatch>; + ref: ReturnType['ref'] | undefined; + mutate: ReturnType['mutate']; +} + +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) => ( + + ))} + +
    +
    + + + +
    +
    +
    +
    +
    + ); +}; + +export default EditCommentBody; diff --git a/src/components/ui/Navbar.tsx b/src/components/ui/Navbar.tsx index 0b7bcd3..400bd6f 100644 --- a/src/components/ui/Navbar.tsx +++ b/src/components/ui/Navbar.tsx @@ -86,28 +86,34 @@ const Navbar = () => { The Biergarten App
    -
    -
    {isDesktopView ? : }
    {' '} - {theme === 'light' ? ( - - ) : ( - - )} + +
    +
    + {theme === 'light' ? ( + + ) : ( + + )} +
    +
    {isDesktopView ? : }
    ); }; diff --git a/src/components/ui/alerts/ErrorAlert.tsx b/src/components/ui/alerts/ErrorAlert.tsx index 47049d2..b19b922 100644 --- a/src/components/ui/alerts/ErrorAlert.tsx +++ b/src/components/ui/alerts/ErrorAlert.tsx @@ -8,8 +8,8 @@ interface ErrorAlertProps { const ErrorAlert: FC = ({ error, setError }) => { return ( -
    -
    +
    +
    {error}
    diff --git a/src/components/ui/forms/FormLabel.tsx b/src/components/ui/forms/FormLabel.tsx index ea72095..fb13dad 100644 --- a/src/components/ui/forms/FormLabel.tsx +++ b/src/components/ui/forms/FormLabel.tsx @@ -11,7 +11,7 @@ interface FormLabelProps { */ const FormLabel: FunctionComponent = ({ htmlFor, children }) => (