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/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 f8102f6..95becc6 100644 --- a/src/components/BeerById/CommentCardBody.tsx +++ b/src/components/BeerById/CommentCardBody.tsx @@ -1,15 +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 format from 'date-fns/format'; -import Link from 'next/link'; -import { FC, useContext } from 'react'; -import { Rating } from 'react-daisyui'; - -import { FaEllipsisH } from 'react-icons/fa'; +import { FC, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { z } from 'zod'; +import CommentContentBody from './CommentContentBody'; +import EditCommentBody from './EditCommentBody'; interface CommentCardProps { comment: z.infer; @@ -17,93 +12,18 @@ interface CommentCardProps { ref?: ReturnType['ref']; } -const CommentCardDropdown: FC = ({ comment, mutate }) => { - 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 ( -
- -
    -
  • - {isCommentOwner ? ( - <> - - - - ) : ( - - )} -
  • -
-
- ); -}; - const CommentCardBody: FC = ({ comment, mutate, ref }) => { - const { user } = useContext(UserContext); + const [inEditMode, setInEditMode] = useState(false); - const timeDistance = useTimeDistance(new Date(comment.createdAt)); - - return ( -
-
-
-

- - {comment.postedBy.username} - -

-

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

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

{comment.content}

-
-
+ 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 2f63e03..324d972 100644 --- a/src/components/ui/Navbar.tsx +++ b/src/components/ui/Navbar.tsx @@ -2,8 +2,9 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import useNavbar from '@/hooks/useNavbar'; import Link from 'next/link'; import { FC } from 'react'; - +import { MdDarkMode, MdLightMode } from 'react-icons/md'; import { GiHamburgerMenu } from 'react-icons/gi'; +import useTheme from '@/hooks/useTheme'; const DesktopLinks: FC = () => { const { pages, currentURL } = useNavbar(); @@ -17,7 +18,7 @@ const DesktopLinks: FC = () => { {page.name} @@ -59,6 +60,8 @@ const MobileLinks: FC = () => { const Navbar = () => { const isDesktopView = useMediaQuery('(min-width: 1024px)'); + const { theme, setTheme } = useTheme(); + return ( ); 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 }) => (