From fe277d59641bc6a0b092ed4a67783551e70d5776 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 28 Jan 2023 21:05:20 -0500 Subject: [PATCH] Refactor api requests and components out of pages --- .prettierrc | 3 +- components/BeerById/BeerInfoHeader.tsx | 78 ++ components/BeerById/CommentCard.tsx | 21 + components/BeerForm.tsx | 105 +- components/BeerIndex/BeerCard.tsx | 27 + components/BeerIndex/Pagination.tsx | 37 + components/{ => ui}/Layout.tsx | 0 components/{ => ui}/Navbar.tsx | 0 components/ui/{ => forms}/Button.tsx | 7 +- components/ui/forms/FormError.tsx | 2 - components/ui/forms/FormInfo.tsx | 10 +- components/ui/forms/FormLabel.tsx | 6 +- components/ui/forms/FormSelect.tsx | 25 +- components/ui/forms/FormTextArea.tsx | 26 +- components/ui/forms/FormTextInput.tsx | 23 +- package-lock.json | 1031 +++++++++++++++++ package.json | 19 +- pages/api/beers/create.ts | 37 +- pages/beers/[id].tsx | 106 +- pages/beers/create.tsx | 9 +- pages/beers/index.tsx | 66 +- .../migrations/20230127095307_/migration.sql | 12 + prisma/schema.prisma | 2 + prisma/seed/create/createNewBeerImages.ts | 1 + prisma/seed/create/createNewBreweryImages.ts | 1 + prisma/seed/index.ts | 4 +- requests/sendCreateBeerPostRequest.ts | 40 + services/BeerPost/getAllBeerPosts.ts | 3 +- services/BeerPost/getBeerPostById.ts | 1 + .../BeerPost/types/BeerPostQueryResult.ts | 2 +- validation/APIResponseValidationSchema.ts | 10 + validation/BeerPostValidationSchema.ts | 43 + 32 files changed, 1455 insertions(+), 302 deletions(-) create mode 100644 components/BeerById/BeerInfoHeader.tsx create mode 100644 components/BeerById/CommentCard.tsx create mode 100644 components/BeerIndex/BeerCard.tsx create mode 100644 components/BeerIndex/Pagination.tsx rename components/{ => ui}/Layout.tsx (100%) rename components/{ => ui}/Navbar.tsx (100%) rename components/ui/{ => forms}/Button.tsx (80%) create mode 100644 prisma/migrations/20230127095307_/migration.sql create mode 100644 requests/sendCreateBeerPostRequest.ts create mode 100644 validation/APIResponseValidationSchema.ts create mode 100644 validation/BeerPostValidationSchema.ts diff --git a/.prettierrc b/.prettierrc index 42a08a0..ad4d5eb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "semi": true, "trailingComma": "all", "singleQuote": true, - "printWidth": 90 + "printWidth": 90, + "plugins": ["prettier-plugin-jsdoc"] } diff --git a/components/BeerById/BeerInfoHeader.tsx b/components/BeerById/BeerInfoHeader.tsx new file mode 100644 index 0000000..13603c7 --- /dev/null +++ b/components/BeerById/BeerInfoHeader.tsx @@ -0,0 +1,78 @@ +import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult'; +import Link from 'next/link'; +import formatDistanceStrict from 'date-fns/formatDistanceStrict'; +import { useState } from 'react'; +import { FaRegThumbsUp, FaThumbsUp } from 'react-icons/fa'; + +const BeerInfoHeader: React.FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) => { + const createdAtDate = new Date(beerPost.createdAt); + const timeDistance = formatDistanceStrict(createdAtDate, Date.now()); + + const [isLiked, setIsLiked] = useState(false); + + return ( +
+

{beerPost.name}

+

+ by{' '} + + {beerPost.brewery.name} + +

+ +

+ posted by{' '} + + {beerPost.postedBy.username} + + {` ${timeDistance}`} ago +

+ +

{beerPost.description}

+
+
+
+ + {beerPost.type.name} + +
+
+ {beerPost.abv}% ABV + {beerPost.ibu} IBU +
+
+
+ +
+
+
+ ); +}; + +export default BeerInfoHeader; diff --git a/components/BeerById/CommentCard.tsx b/components/BeerById/CommentCard.tsx new file mode 100644 index 0000000..d4bab83 --- /dev/null +++ b/components/BeerById/CommentCard.tsx @@ -0,0 +1,21 @@ +import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult'; +import formatDistanceStrict from 'date-fns/formatDistanceStrict'; + +const CommentCard: React.FC<{ + comment: BeerPostQueryResult['beerComments'][number]; +}> = ({ comment }) => { + return ( +
+
+

{comment.postedBy.username}

+

{`posted ${formatDistanceStrict( + new Date(comment.createdAt), + new Date(), + )} ago`}

+

{comment.content}

+
+
+ ); +}; + +export default CommentCard; diff --git a/components/BeerForm.tsx b/components/BeerForm.tsx index a720d5e..0700b52 100644 --- a/components/BeerForm.tsx +++ b/components/BeerForm.tsx @@ -1,12 +1,14 @@ -import { NewBeerInfo } from '@/pages/api/beers/create'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import { BeerType } from '@prisma/client'; import { FunctionComponent } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; - -import Button from './ui/Button'; +import { zodResolver } from '@hookform/resolvers/zod'; +import BeerPostValidationSchema from '@/validation/BeerPostValidationSchema'; +import Router from 'next/router'; +import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest'; +import Button from './ui/forms/Button'; import FormError from './ui/forms/FormError'; import FormInfo from './ui/forms/FormInfo'; import FormLabel from './ui/forms/FormLabel'; @@ -15,34 +17,18 @@ import FormSelect from './ui/forms/FormSelect'; import FormTextArea from './ui/forms/FormTextArea'; import FormTextInput from './ui/forms/FormTextInput'; -type IFormInput = z.infer; +type BeerPostT = z.infer; interface BeerFormProps { - type: 'edit' | 'create'; + formType: 'edit' | 'create'; // eslint-disable-next-line react/require-default-props - defaultValues?: IFormInput; + defaultValues?: BeerPostT; breweries?: BreweryPostQueryResult[]; types?: BeerType[]; } -const sendCreateBeerPostRequest = async (data: IFormInput) => { - // const body = JSON.stringify(data); - - const response = await fetch('/api/beers/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - const json = await response.json(); - - console.log(json); -}; - const BeerForm: FunctionComponent = ({ - type, + formType, defaultValues, breweries = [], types = [], @@ -51,7 +37,8 @@ const BeerForm: FunctionComponent = ({ register, handleSubmit, formState: { errors }, - } = useForm({ + } = useForm({ + resolver: zodResolver(BeerPostValidationSchema), defaultValues: { name: defaultValues?.name, description: defaultValues?.description, @@ -60,47 +47,19 @@ const BeerForm: FunctionComponent = ({ }, }); - // const navigate = useNavigate(); - - const nameValidationSchema = register('name', { - required: 'Beer name is required.', - }); - const breweryValidationSchema = register('breweryId', { - required: 'Brewery name is required.', - }); - const abvValidationSchema = register('abv', { - required: 'ABV is required.', - valueAsNumber: true, - max: { value: 50, message: 'ABV must be less than 50%.' }, - min: { - value: 0.1, - message: 'ABV must be greater than 0.1%', - }, - validate: (abv) => !Number.isNaN(abv) || 'ABV is invalid.', - }); - const ibuValidationSchema = register('ibu', { - required: 'IBU is required.', - min: { - value: 2, - message: 'IBU must be greater than 2.', - }, - valueAsNumber: true, - validate: (ibu) => !Number.isNaN(ibu) || 'IBU is invalid.', - }); - - const descriptionValidationSchema = register('description', { - required: 'Description is required.', - }); - - const typeIdValidationSchema = register('typeId', { - required: 'Type is required.', - }); - - const onSubmit: SubmitHandler = async (data) => { - switch (type) { - case 'create': - await sendCreateBeerPostRequest(data); - break; + const onSubmit: SubmitHandler = async (data) => { + switch (formType) { + case 'create': { + try { + const response = await sendCreateBeerPostRequest(data); + Router.push(`/beers/${response.id}`); + break; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + break; + } + } case 'edit': break; default: @@ -117,13 +76,13 @@ const BeerForm: FunctionComponent = ({ - {type === 'create' && breweries.length && ( + {formType === 'create' && breweries.length && ( <> Brewery @@ -131,7 +90,7 @@ const BeerForm: FunctionComponent = ({ ({ @@ -153,7 +112,7 @@ const BeerForm: FunctionComponent = ({ = ({ = ({ @@ -194,7 +153,7 @@ const BeerForm: FunctionComponent = ({ ({ @@ -207,7 +166,7 @@ const BeerForm: FunctionComponent = ({ diff --git a/components/BeerIndex/BeerCard.tsx b/components/BeerIndex/BeerCard.tsx new file mode 100644 index 0000000..74c073e --- /dev/null +++ b/components/BeerIndex/BeerCard.tsx @@ -0,0 +1,27 @@ +import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult'; +import Link from 'next/link'; +import { FC } from 'react'; +import Image from 'next/image'; + +const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => { + return ( +
+
+ {post.beerImages.length > 0 && ( + {post.name} + )} +
+ +
+
+

+ {post.name} +

+

{post.brewery.name}

+
+
+
+ ); +}; + +export default BeerCard; diff --git a/components/BeerIndex/Pagination.tsx b/components/BeerIndex/Pagination.tsx new file mode 100644 index 0000000..dc18fb8 --- /dev/null +++ b/components/BeerIndex/Pagination.tsx @@ -0,0 +1,37 @@ +import { useRouter } from 'next/router'; +import { FC } from 'react'; + +interface PaginationProps { + pageNum: number; + pageCount: number; +} + +const Pagination: FC = ({ pageCount, pageNum }) => { + const router = useRouter(); + + return ( +
+ + + +
+ ); +}; + +export default Pagination; diff --git a/components/Layout.tsx b/components/ui/Layout.tsx similarity index 100% rename from components/Layout.tsx rename to components/ui/Layout.tsx diff --git a/components/Navbar.tsx b/components/ui/Navbar.tsx similarity index 100% rename from components/Navbar.tsx rename to components/ui/Navbar.tsx diff --git a/components/ui/Button.tsx b/components/ui/forms/Button.tsx similarity index 80% rename from components/ui/Button.tsx rename to components/ui/forms/Button.tsx index 0f1db4e..6b32a73 100644 --- a/components/ui/Button.tsx +++ b/components/ui/forms/Button.tsx @@ -3,18 +3,13 @@ import { FunctionComponent } from 'react'; interface FormButtonProps { children: string; type: 'button' | 'submit' | 'reset'; - className?: string; } -const Button: FunctionComponent = ({ children, type, className }) => ( +const Button: FunctionComponent = ({ children, type }) => ( // eslint-disable-next-line react/button-has-type ); -Button.defaultProps = { - className: '', -}; - export default Button; diff --git a/components/ui/forms/FormError.tsx b/components/ui/forms/FormError.tsx index 5d20030..8992c60 100644 --- a/components/ui/forms/FormError.tsx +++ b/components/ui/forms/FormError.tsx @@ -3,8 +3,6 @@ import { FunctionComponent } from 'react'; // import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; /** - * Component for a styled form error message. - * * @example * Something went wrong!; */ diff --git a/components/ui/forms/FormInfo.tsx b/components/ui/forms/FormInfo.tsx index 5baeaa3..388dc2d 100644 --- a/components/ui/forms/FormInfo.tsx +++ b/components/ui/forms/FormInfo.tsx @@ -1,10 +1,16 @@ import { FunctionComponent, ReactNode } from 'react'; -/** A container for both the form error and form label. */ interface FormInfoProps { - children: Array | ReactNode; + children: [ReactNode, ReactNode]; } +/** + * @example + * + * Name + * {errors.name?.message} + * ; + */ const FormInfo: FunctionComponent = ({ children }) => (
{children}
); diff --git a/components/ui/forms/FormLabel.tsx b/components/ui/forms/FormLabel.tsx index 977a38e..ea72095 100644 --- a/components/ui/forms/FormLabel.tsx +++ b/components/ui/forms/FormLabel.tsx @@ -5,9 +5,13 @@ interface FormLabelProps { children: string; } +/** + * @example + * Name; + */ const FormLabel: FunctionComponent = ({ htmlFor, children }) => (