From d0bced13769bbcf271f4b724b34521a77d0f449c Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Tue, 24 Jan 2023 21:03:31 -0500 Subject: [PATCH] Add create beer, beer post page --- components/BeerForm.tsx | 82 +++++++++++------ package-lock.json | 31 ++++++- package.json | 4 +- pages/api/beers/create.ts | 92 +++++++++++++++++++ pages/beers/[id].tsx | 26 +++++- pages/beers/create.tsx | 21 ++++- pages/beers/index.tsx | 2 +- prisma/schema.prisma | 6 +- prisma/seed/index.ts | 2 +- services/BeerPost/getAllBeerPosts.ts | 15 +++ services/BeerPost/getBeerPostById.ts | 13 +++ .../BeerPost/types/BeerPostQueryResult.ts | 10 ++ 12 files changed, 264 insertions(+), 40 deletions(-) create mode 100644 pages/api/beers/create.ts diff --git a/components/BeerForm.tsx b/components/BeerForm.tsx index aec4bcc..a720d5e 100644 --- a/components/BeerForm.tsx +++ b/components/BeerForm.tsx @@ -1,13 +1,11 @@ +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 { useNavigate } from 'react-router-dom'; -// import createBeerPost from '../api/beerPostRoutes/createBeerPost'; -// import getAllBreweryPosts from '../api/breweryPostRoutes/getAllBreweryPosts'; +import { z } from 'zod'; -// import BreweryPostI from '../types/BreweryPostI'; -// import isValidUuid from '../util/isValidUuid'; import Button from './ui/Button'; import FormError from './ui/forms/FormError'; import FormInfo from './ui/forms/FormInfo'; @@ -17,25 +15,37 @@ import FormSelect from './ui/forms/FormSelect'; import FormTextArea from './ui/forms/FormTextArea'; import FormTextInput from './ui/forms/FormTextInput'; -interface IFormInput { - name: string; - description: string; - type: string; - abv: number; - ibu: number; - breweryId: string; -} +type IFormInput = z.infer; interface BeerFormProps { type: 'edit' | 'create'; // eslint-disable-next-line react/require-default-props defaultValues?: IFormInput; 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, defaultValues, breweries = [], + types = [], }) => { const { register, @@ -78,8 +88,24 @@ const BeerForm: FunctionComponent = ({ validate: (ibu) => !Number.isNaN(ibu) || 'IBU is invalid.', }); - const onSubmit: SubmitHandler = (data) => { - console.log(data); + 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; + case 'edit': + break; + default: + break; + } }; return ( @@ -156,27 +182,27 @@ const BeerForm: FunctionComponent = ({ - Type - {errors.type?.message} + Type + {errors.typeId?.message} - ({ + value: beerType.id, + text: beerType.name, + }))} + placeholder="Beer type" + message="Pick a beer type" /> diff --git a/package-lock.json b/package-lock.json index 59b2f60..72596ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "my-project", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^2.9.10", "@next/font": "13.1.2", "@prisma/client": "^4.8.1", "@types/node": "18.11.18", @@ -24,7 +25,8 @@ "react-dom": "18.2.0", "react-hook-form": "^7.42.1", "react-icons": "^4.7.1", - "typescript": "4.9.4" + "typescript": "4.9.4", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^7.6.0", @@ -101,6 +103,14 @@ "npm": ">=6.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz", + "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -5216,6 +5226,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -5260,6 +5278,12 @@ "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", "dev": true }, + "@hookform/resolvers": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.10.tgz", + "integrity": "sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -8771,6 +8795,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==" } } } diff --git a/package.json b/package.json index 710d67f..ce59314 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "seed": "npx ts-node ./prisma/seed/index.ts" }, "dependencies": { + "@hookform/resolvers": "^2.9.10", "@next/font": "13.1.2", "@prisma/client": "^4.8.1", "@types/node": "18.11.18", @@ -29,7 +30,8 @@ "react-dom": "18.2.0", "react-hook-form": "^7.42.1", "react-icons": "^4.7.1", - "typescript": "4.9.4" + "typescript": "4.9.4", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^7.6.0", diff --git a/pages/api/beers/create.ts b/pages/api/beers/create.ts new file mode 100644 index 0000000..686d78d --- /dev/null +++ b/pages/api/beers/create.ts @@ -0,0 +1,92 @@ +import DBClient from '@/prisma/DBClient'; +import { NextApiHandler } from 'next'; +import { z } from 'zod'; + +class ServerError extends Error { + constructor(message: string, public statusCode: number) { + super(message); + this.name = 'ServerError'; + } +} + +export const NewBeerInfo = z.object({ + name: z.string().min(1).max(100), + description: z.string().min(1).max(1000), + typeId: z.string().uuid(), + abv: z.number().min(1).max(50), + ibu: z.number().min(2), + breweryId: z.string().uuid(), +}); + +export const ResponseBody = z.object({ + message: z.string(), + statusCode: z.number(), + success: z.boolean(), + payload: z.unknown(), +}); + +const handler: NextApiHandler> = async (req, res) => { + try { + const { method } = req; + + if (method !== 'POST') { + throw new ServerError('Method not allowed', 405); + } + + const cleanedReqBody = NewBeerInfo.safeParse(req.body); + + if (!cleanedReqBody.success) { + throw new ServerError('Invalid request body', 400); + } + + const { name, description, typeId, abv, ibu, breweryId } = cleanedReqBody.data; + + const user = await DBClient.instance.user.findFirstOrThrow(); + + const newBeerPost = await DBClient.instance.beerPost.create({ + data: { + name, + description, + abv, + ibu, + type: { + connect: { + id: typeId, + }, + }, + postedBy: { + connect: { + id: user.id, + }, + }, + brewery: { + connect: { + id: breweryId, + }, + }, + }, + }); + + res + .status(200) + .json({ message: 'Success', statusCode: 200, payload: newBeerPost, success: true }); + } catch (error) { + if (error instanceof ServerError) { + res.status(error.statusCode).json({ + message: error.message, + statusCode: error.statusCode, + payload: null, + success: false, + }); + } else { + res.status(500).json({ + message: 'Internal server error', + statusCode: 500, + payload: null, + success: false, + }); + } + } +}; + +export default handler; diff --git a/pages/beers/[id].tsx b/pages/beers/[id].tsx index 109d88c..9a5fdae 100644 --- a/pages/beers/[id].tsx +++ b/pages/beers/[id].tsx @@ -86,7 +86,25 @@ const BeerInfoHeader: React.FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost ); }; +const CommentCard: React.FC<{ comment: BeerPostQueryResult['beerComments'][number] }> = ({ + comment, +}) => { + return ( +
+
+

{comment.postedBy.username}

+

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

+

{comment.content}

+
+
+ ); +}; + const BeerByIdPage: NextPage = ({ beerPost }) => { + console.log(beerPost.beerComments); return ( @@ -108,7 +126,13 @@ const BeerByIdPage: NextPage = ({ beerPost }) => {
-
+
+ {/* for each comment make a card */} + + {beerPost.beerComments.map((comment) => ( + + ))} +
diff --git a/pages/beers/create.tsx b/pages/beers/create.tsx index 574083c..772402e 100644 --- a/pages/beers/create.tsx +++ b/pages/beers/create.tsx @@ -1,19 +1,29 @@ import BeerForm from '@/components/BeerForm'; import Layout from '@/components/Layout'; +import DBClient from '@/prisma/DBClient'; import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { BeerType } from '@prisma/client'; import { NextPage } from 'next'; +import { BiBeer } from 'react-icons/bi'; + interface CreateBeerPageProps { breweries: BreweryPostQueryResult[]; + types: BeerType[]; } -const Create: NextPage = ({ breweries }) => { +const Create: NextPage = ({ breweries, types }) => { return ( -
+
- +
+ +

Create a New Beer

+
+ +
@@ -22,9 +32,12 @@ const Create: NextPage = ({ breweries }) => { export const getServerSideProps = async () => { const breweryPosts = await getAllBreweryPosts(); + + const beerTypes = await DBClient.instance.beerType.findMany(); return { props: { - breweries: breweryPosts, + breweries: JSON.parse(JSON.stringify(breweryPosts)), + types: JSON.parse(JSON.stringify(beerTypes)), }, }; }; diff --git a/pages/beers/index.tsx b/pages/beers/index.tsx index bbf9786..f909a44 100644 --- a/pages/beers/index.tsx +++ b/pages/beers/index.tsx @@ -96,7 +96,7 @@ export const getServerSideProps: GetServerSideProps = async (cont const pageNumber = parseInt(query.page_num as string, 10) || 1; - const pageSize = 24; + const pageSize = 12; const numberOfPosts = await DBClient.instance.beerPost.count(); const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d07d9a2..ac2a4b5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ datasource db { } model User { - id String @id @default(cuid()) + id String @id @default(uuid()) username String firstName String lastName String @@ -56,7 +56,7 @@ model BeerComment { } model BeerType { - id String @id @default(cuid()) + id String @id @default(uuid()) name String createdAt DateTime @default(now()) @db.Timestamptz(3) updatedAt DateTime? @updatedAt @db.Timestamptz(3) @@ -66,7 +66,7 @@ model BeerType { } model BreweryPost { - id String @id @default(cuid()) + id String @id @default(uuid()) name String location String beers BeerPost[] diff --git a/prisma/seed/index.ts b/prisma/seed/index.ts index cc7c57d..b03501a 100644 --- a/prisma/seed/index.ts +++ b/prisma/seed/index.ts @@ -24,7 +24,7 @@ import createNewUsers from './create/createNewUsers'; const users = await createNewUsers({ numberOfUsers: 1000 }); const [breweryPosts, beerTypes] = await Promise.all([ - createNewBreweryPosts({ numberOfPosts: 10000, joinData: { users } }), + createNewBreweryPosts({ numberOfPosts: 10, joinData: { users } }), createNewBeerTypes({ joinData: { users } }), ]); const beerPosts = await createNewBeerPosts({ diff --git a/services/BeerPost/getAllBeerPosts.ts b/services/BeerPost/getAllBeerPosts.ts index 2a0ed83..fa590f5 100644 --- a/services/BeerPost/getAllBeerPosts.ts +++ b/services/BeerPost/getAllBeerPosts.ts @@ -32,6 +32,21 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => { username: true, }, }, + + beerComments: { + select: { + id: true, + content: true, + createdAt: true, + postedBy: { + select: { + username: true, + id: true, + }, + }, + }, + }, + beerImages: { select: { url: true, diff --git a/services/BeerPost/getBeerPostById.ts b/services/BeerPost/getBeerPostById.ts index 1e698c9..34c48d3 100644 --- a/services/BeerPost/getBeerPostById.ts +++ b/services/BeerPost/getBeerPostById.ts @@ -6,6 +6,19 @@ const prisma = DBClient.instance; const getBeerPostById = async (id: string) => { const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({ select: { + beerComments: { + select: { + id: true, + content: true, + createdAt: true, + postedBy: { + select: { + username: true, + id: true, + }, + }, + }, + }, id: true, name: true, brewery: { diff --git a/services/BeerPost/types/BeerPostQueryResult.ts b/services/BeerPost/types/BeerPostQueryResult.ts index d9d27ff..7620ba1 100644 --- a/services/BeerPost/types/BeerPostQueryResult.ts +++ b/services/BeerPost/types/BeerPostQueryResult.ts @@ -22,5 +22,15 @@ export default interface BeerPostQueryResult { username: string; }; + beerComments: { + id: string; + content: string; + createdAt: Date; + postedBy: { + id: string; + username: string; + }; + }[]; + createdAt: Date; }