diff --git a/.gitignore b/.gitignore index a9fd027..26a5e38 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ yarn-error.log* next-env.d.ts # http requests -*.http \ No newline at end of file +*.http + +# uploaded images +public/uploads diff --git a/components/BeerById/BeerCommentForm.tsx b/components/BeerById/BeerCommentForm.tsx index e9a27f6..3cd4099 100644 --- a/components/BeerById/BeerCommentForm.tsx +++ b/components/BeerById/BeerCommentForm.tsx @@ -1,7 +1,7 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest'; import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; -import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRouter } from 'next/router'; import { Dispatch, SetStateAction, FunctionComponent, useState, useEffect } from 'react'; @@ -26,7 +26,6 @@ const BeerCommentForm: FunctionComponent = ({ beerPost }) z.infer >({ defaultValues: { - beerPostId: beerPost.id, rating: 0, }, resolver: zodResolver(BeerCommentValidationSchema), @@ -35,8 +34,8 @@ const BeerCommentForm: FunctionComponent = ({ beerPost }) const [rating, setRating] = useState(0); useEffect(() => { setRating(0); - reset({ beerPostId: beerPost.id, rating: 0, content: '' }); - }, [beerPost.id, reset]); + reset({ rating: 0, content: '' }); + }, [reset]); const router = useRouter(); const onSubmit: SubmitHandler> = async ( @@ -44,7 +43,11 @@ const BeerCommentForm: FunctionComponent = ({ beerPost }) ) => { setValue('rating', 0); setRating(0); - await sendCreateBeerCommentRequest(data); + await sendCreateBeerCommentRequest({ + content: data.content, + rating: data.rating, + beerPostId: beerPost.id, + }); reset(); router.replace(router.asPath, undefined, { scroll: false }); }; diff --git a/components/BeerById/BeerInfoHeader.tsx b/components/BeerById/BeerInfoHeader.tsx index 7d8b7e9..49f970b 100644 --- a/components/BeerById/BeerInfoHeader.tsx +++ b/components/BeerById/BeerInfoHeader.tsx @@ -2,50 +2,29 @@ import Link from 'next/link'; import formatDistanceStrict from 'date-fns/formatDistanceStrict'; import format from 'date-fns/format'; import { FC, useContext, useEffect, useState } from 'react'; -import { FaRegThumbsUp, FaThumbsUp } from 'react-icons/fa'; -import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import UserContext from '@/contexts/userContext'; -import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest'; -import sendLikeRequest from '../../requests/sendLikeRequest'; +import BeerPostLikeButton from './BeerPostLikeButton'; -const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) => { +const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({ + beerPost, + initialLikeCount, +}) => { const createdAtDate = new Date(beerPost.createdAt); const [timeDistance, setTimeDistance] = useState(''); const { user } = useContext(UserContext); - const [loading, setLoading] = useState(true); - const [isLiked, setIsLiked] = useState(false); + const [likeCount, setLikeCount] = useState(initialLikeCount); useEffect(() => { - if (!user) { - setLoading(false); - return; - } - sendCheckIfUserLikesBeerPostRequest(beerPost.id) - .then((currentLikeStatus) => { - setIsLiked(currentLikeStatus); - setLoading(false); - }) - .catch((e) => { - console.error(e); - setLoading(false); - }); - }, [user, beerPost.id]); + setLikeCount(initialLikeCount); + }, [initialLikeCount]); useEffect(() => { setTimeDistance(formatDistanceStrict(new Date(beerPost.createdAt), new Date())); }, [beerPost.createdAt]); - const handleLike = async () => { - try { - await sendLikeRequest(beerPost); - setIsLiked(!isLiked); - } catch (e) { - console.error(e); - } - }; - return (
@@ -75,8 +54,8 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) =>

{beerPost.description}

-
-
+
+
= ({ beerPost }) => {beerPost.abv}% ABV {beerPost.ibu} IBU
+
+ + Liked by {likeCount} user{likeCount !== 1 && 's'} + +
-
+
{user && ( - + )}
diff --git a/components/BeerById/BeerPostLikeButton.tsx b/components/BeerById/BeerPostLikeButton.tsx new file mode 100644 index 0000000..84b6ae9 --- /dev/null +++ b/components/BeerById/BeerPostLikeButton.tsx @@ -0,0 +1,69 @@ +import UserContext from '@/contexts/userContext'; +import sendLikeRequest from '@/requests/sendLikeRequest'; +import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react'; +import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; +import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest'; + +const BeerPostLikeButton: FC<{ + beerPostId: string; + setLikeCount: Dispatch>; +}> = ({ beerPostId, setLikeCount }) => { + const [loading, setLoading] = useState(true); + const [isLiked, setIsLiked] = useState(false); + + const { user } = useContext(UserContext); + + useEffect(() => { + if (!user) { + setLoading(false); + return; + } + sendCheckIfUserLikesBeerPostRequest(beerPostId) + .then((currentLikeStatus) => { + setIsLiked(currentLikeStatus); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + }, [user, beerPostId]); + + const handleLike = async () => { + try { + setLoading(true); + await sendLikeRequest(beerPostId); + setIsLiked(!isLiked); + setLikeCount((prevCount) => prevCount + (isLiked ? -1 : 1)); + setLoading(false); + } catch (error) { + setLoading(false); + } + }; + + return ( + + ); +}; + +export default BeerPostLikeButton; diff --git a/components/BeerById/CommentCard.tsx b/components/BeerById/CommentCard.tsx index 5af3e49..7d5712b 100644 --- a/components/BeerById/CommentCard.tsx +++ b/components/BeerById/CommentCard.tsx @@ -13,7 +13,7 @@ const CommentCard: React.FC<{ }, [comment.createdAt]); return ( -
+

{comment.postedBy.username}

diff --git a/components/BeerForm.tsx b/components/BeerForm.tsx index fd57e3c..950805d 100644 --- a/components/BeerForm.tsx +++ b/components/BeerForm.tsx @@ -1,12 +1,13 @@ import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest'; -import BeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema'; +import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { zodResolver } from '@hookform/resolvers/zod'; import { BeerType } from '@prisma/client'; import router from 'next/router'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; +import ErrorAlert from './ui/alerts/ErrorAlert'; import Button from './ui/forms/Button'; import FormError from './ui/forms/FormError'; import FormInfo from './ui/forms/FormInfo'; @@ -16,7 +17,7 @@ import FormSelect from './ui/forms/FormSelect'; import FormTextArea from './ui/forms/FormTextArea'; import FormTextInput from './ui/forms/FormTextInput'; -type BeerPostT = z.infer; +type BeerPostT = z.infer; interface BeerFormProps { formType: 'edit' | 'create'; @@ -37,7 +38,7 @@ const BeerForm: FunctionComponent = ({ handleSubmit, formState: { errors }, } = useForm({ - resolver: zodResolver(BeerPostValidationSchema), + resolver: zodResolver(CreateBeerPostValidationSchema), defaultValues: { name: defaultValues?.name, description: defaultValues?.description, @@ -46,7 +47,12 @@ const BeerForm: FunctionComponent = ({ }, }); + const [error, setError] = useState(''); + + const [isSubmitting, setIsSubmitting] = useState(false); + const onSubmit: SubmitHandler = async (data) => { + setIsSubmitting(true); switch (formType) { case 'create': { try { @@ -54,8 +60,9 @@ const BeerForm: FunctionComponent = ({ router.push(`/beers/${response.id}`); break; } catch (e) { - // eslint-disable-next-line no-console - console.error(e); + if (e instanceof Error) { + setError(e.message); + } break; } } @@ -68,6 +75,9 @@ const BeerForm: FunctionComponent = ({ return (
+
+ {error && } +
Name {errors.name?.message} @@ -79,6 +89,7 @@ const BeerForm: FunctionComponent = ({ error={!!errors.name} type="text" id="name" + disabled={isSubmitting} /> {formType === 'create' && breweries.length && ( @@ -89,6 +100,7 @@ const BeerForm: FunctionComponent = ({ = ({ {errors.abv?.message} = ({ {errors.ibu?.message} = ({ = ({ = ({ /> - + {!isSubmitting && ( + + )} + + {isSubmitting && ( + + )} ); }; diff --git a/components/BeerIndex/BeerCard.tsx b/components/BeerIndex/BeerCard.tsx index 9a68635..86341d5 100644 --- a/components/BeerIndex/BeerCard.tsx +++ b/components/BeerIndex/BeerCard.tsx @@ -1,14 +1,19 @@ import Link from 'next/link'; import { FC } from 'react'; import Image from 'next/image'; -import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => { return (
{post.beerImages.length > 0 && ( - {post.name} + {post.name} )}
diff --git a/components/Login/LoginForm.tsx b/components/Login/LoginForm.tsx index 08288df..722bc35 100644 --- a/components/Login/LoginForm.tsx +++ b/components/Login/LoginForm.tsx @@ -2,8 +2,10 @@ import sendLoginUserRequest from '@/requests/sendLoginUserRequest'; import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRouter } from 'next/router'; +import { useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; +import ErrorAlert from '../ui/alerts/ErrorAlert'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; import FormLabel from '../ui/forms/FormLabel'; @@ -13,7 +15,7 @@ import FormTextInput from '../ui/forms/FormTextInput'; type LoginT = z.infer; const LoginForm = () => { const router = useRouter(); - const { register, handleSubmit, formState } = useForm({ + const { register, handleSubmit, formState, reset } = useForm({ resolver: zodResolver(LoginValidationSchema), defaultValues: { username: '', @@ -23,18 +25,23 @@ const LoginForm = () => { const { errors } = formState; + const [responseError, setResponseError] = useState(''); + const onSubmit: SubmitHandler = async (data) => { try { const response = await sendLoginUserRequest(data); router.push(`/users/${response.id}`); } catch (error) { - console.error(error); + if (error instanceof Error) { + setResponseError(error.message); + reset(); + } } }; return ( -
+
username @@ -65,8 +72,9 @@ const LoginForm = () => {
+ {responseError && }
-
diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index c712b61..a329b6b 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -2,9 +2,10 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ /* eslint-disable jsx-a11y/label-has-for */ +import UserContext from '@/contexts/userContext'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; interface Page { slug: string; @@ -14,19 +15,36 @@ const Navbar = () => { const router = useRouter(); const [currentURL, setCurrentURL] = useState('/'); + const { user } = useContext(UserContext); + useEffect(() => { setCurrentURL(router.asPath); }, [router.asPath]); - const pages: Page[] = [ + const authenticatedPages: readonly Page[] = [ + { slug: '/account', name: 'Account' }, + { slug: '/api/users/logout', name: 'Logout' }, + ]; + + const unauthenticatedPages: readonly Page[] = [ + { slug: '/login', name: 'Login' }, + { slug: '/register', name: 'Register' }, + ]; + + const otherPages: readonly Page[] = [ { slug: '/beers', name: 'Beers' }, { slug: '/breweries', name: 'Breweries' }, ]; + const pages: readonly Page[] = [ + ...otherPages, + ...(user ? authenticatedPages : unauthenticatedPages), + ]; + return ( -
-