Did more work to beer post page, seed

Worked on comments and beer recs features. Fine tuning database seed amounts.
This commit is contained in:
Aaron William Po
2023-01-29 21:53:05 -05:00
parent fe277d5964
commit 0b96c8f1f5
38 changed files with 833 additions and 221 deletions

View File

@@ -0,0 +1,85 @@
import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult';
import { Dispatch, FunctionComponent, SetStateAction } from 'react';
import { z } from 'zod';
import FormLabel from '@/components/ui/forms/FormLabel';
import FormError from '@/components/ui/forms/FormError';
import FormTextArea from '@/components/ui/forms/FormTextArea';
import { SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import Button from '@/components/ui/forms/Button';
import FormInfo from '@/components/ui/forms/FormInfo';
// @ts-expect-error
import ReactStars from 'react-rating-stars-component';
import FormSegment from '@/components/ui/forms/FormSegment';
import BeerCommentQueryResult from '@/services/BeerPost/types/BeerCommentQueryResult';
import BeerCommentValidationSchema from '@/validation/CreateBeerCommentValidationSchema';
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
interface BeerCommentFormProps {
beerPost: BeerPostQueryResult;
setComments: Dispatch<SetStateAction<BeerCommentQueryResult[]>>;
}
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
beerPost,
setComments,
}) => {
const {
register,
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm<z.infer<typeof BeerCommentValidationSchema>>({
defaultValues: {
beerPostId: beerPost.id,
rating: 0,
},
resolver: zodResolver(BeerCommentValidationSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
data,
) => {
setValue('rating', 0);
await sendCreateBeerCommentRequest(data);
setComments((prev) => prev);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormInfo>
<FormLabel htmlFor="content">Leave a comment</FormLabel>
<FormError>{errors.content?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextArea
id="content"
formValidationSchema={register('content')}
placeholder="Comment"
rows={5}
error={!!errors.content?.message}
/>
</FormSegment>
<FormInfo>
<FormLabel htmlFor="rating">Rating</FormLabel>
<FormError>{errors.rating?.message}</FormError>
</FormInfo>
<ReactStars
id="rating"
count={5}
size={34}
activeColor="#ffd700"
edit={true}
value={0}
onChange={(value: 1 | 2 | 3 | 4 | 5) => setValue('rating', value)}
/>
<Button type="submit">Submit</Button>
</form>
);
};
export default BeerCommentForm;

View File

@@ -1,6 +1,7 @@
import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult'; import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult';
import Link from 'next/link'; import Link from 'next/link';
import formatDistanceStrict from 'date-fns/formatDistanceStrict'; import formatDistanceStrict from 'date-fns/formatDistanceStrict';
import format from 'date-fns/format';
import { useState } from 'react'; import { useState } from 'react';
import { FaRegThumbsUp, FaThumbsUp } from 'react-icons/fa'; import { FaRegThumbsUp, FaThumbsUp } from 'react-icons/fa';
@@ -11,7 +12,8 @@ const BeerInfoHeader: React.FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost
const [isLiked, setIsLiked] = useState(false); const [isLiked, setIsLiked] = useState(false);
return ( return (
<div className="flex h-full flex-col justify-center bg-base-300 p-10"> <div className="card flex flex-col justify-center bg-base-300">
<div className="card-body">
<h1 className="text-4xl font-bold">{beerPost.name}</h1> <h1 className="text-4xl font-bold">{beerPost.name}</h1>
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">
by{' '} by{' '}
@@ -26,17 +28,22 @@ const BeerInfoHeader: React.FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost
<h3 className="italic"> <h3 className="italic">
posted by{' '} posted by{' '}
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link"> <Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
{beerPost.postedBy.username} {beerPost.postedBy.username}{' '}
</Link> </Link>
{` ${timeDistance}`} ago <span
className="tooltip tooltip-bottom"
data-tip={format(createdAtDate, 'MM/dd/yyyy')}
>
{timeDistance} ago
</span>
</h3> </h3>
<p>{beerPost.description}</p> <p>{beerPost.description}</p>
<div className="flex justify-between"> <div className="mt-5 flex justify-between">
<div> <div>
<div className="mb-1"> <div className="mb-1">
<Link <Link
className="text-lg font-medium" className="link-hover link text-lg font-bold"
href={`/beers/types/${beerPost.type.id}`} href={`/beers/types/${beerPost.type.id}`}
> >
{beerPost.type.name} {beerPost.type.name}
@@ -72,6 +79,7 @@ const BeerInfoHeader: React.FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -0,0 +1,34 @@
import { FunctionComponent } from 'react';
import Link from 'next/link';
import BeerRecommendationQueryResult from '@/services/BeerPost/types/BeerReccomendationQueryResult';
interface BeerRecommendationsProps {
beerRecommendations: BeerRecommendationQueryResult[];
}
const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
beerRecommendations,
}) => {
return (
<div className="card sticky top-2 h-full overflow-y-scroll bg-base-300">
<div className="card-body">
{beerRecommendations.map((beerPost) => (
<div key={beerPost.id} className="w-full">
<div>
<Link href={`/beers/${beerPost.id}`} className="link-hover">
<h2 className="text-2xl font-bold">{beerPost.name}</h2>
</Link>
<Link href={`/breweries/${beerPost.brewery.id}`} className="link-hover">
<p className="text-lg font-semibold">{beerPost.brewery.name}</p>
</Link>
</div>
<p>{beerPost.abv}% ABV</p>
<p>{beerPost.ibu} IBU</p>
</div>
))}
</div>
</div>
);
};
export default BeerRecommendations;

View File

@@ -1,19 +1,28 @@
import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult'; import BeerCommentQueryResult from '@/services/BeerPost/types/BeerCommentQueryResult';
import formatDistanceStrict from 'date-fns/formatDistanceStrict'; import formatDistanceStrict from 'date-fns/formatDistanceStrict';
// @ts-expect-error
import ReactStars from 'react-rating-stars-component';
const CommentCard: React.FC<{ const CommentCard: React.FC<{
comment: BeerPostQueryResult['beerComments'][number]; comment: BeerCommentQueryResult;
}> = ({ comment }) => { }> = ({ comment }) => {
const timeDistance = formatDistanceStrict(new Date(comment.createdAt), new Date());
return ( return (
<div className="card bg-base-300"> <div className="card-body h-56">
<div className="card-body"> <div className="flex justify-between">
<div>
<h3 className="text-2xl font-semibold">{comment.postedBy.username}</h3> <h3 className="text-2xl font-semibold">{comment.postedBy.username}</h3>
<h4 className="italic">{`posted ${formatDistanceStrict( <h4 className="italic">posted {timeDistance} ago</h4>
new Date(comment.createdAt),
new Date(),
)} ago`}</h4>
<p>{comment.content}</p>
</div> </div>
<ReactStars
count={5}
size={24}
activeColor="#ffd700"
edit={false}
value={comment.rating}
/>
</div>
<p>{comment.content}</p>
</div> </div>
); );
}; };

View File

@@ -5,7 +5,7 @@ import { FunctionComponent } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import BeerPostValidationSchema from '@/validation/BeerPostValidationSchema'; import BeerPostValidationSchema from '@/validation/CreateBeerPostValidationSchema';
import Router from 'next/router'; import Router from 'next/router';
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest'; import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
import Button from './ui/forms/Button'; import Button from './ui/forms/Button';

View File

@@ -52,7 +52,7 @@ const Navbar = () => {
<div className="flex-none lg:hidden"> <div className="flex-none lg:hidden">
<div className="dropdown-end dropdown"> <div className="dropdown-end dropdown">
<label tabIndex={0} className="btn-ghost btn-circle btn"> <label tabIndex={0} className="btn-ghost btn-circle btn">
<div className="w-10 rounded-full"> <span className="w-10 rounded-full">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@@ -66,7 +66,7 @@ const Navbar = () => {
d="M4 6h16M4 12h16M4 18h16" d="M4 6h16M4 12h16M4 18h16"
/> />
</svg> </svg>
</div> </span>
</label> </label>
<ul <ul
tabIndex={0} tabIndex={0}

View File

@@ -0,0 +1,8 @@
class ServerError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = 'ServerError';
}
}
export default ServerError;

361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.42.1", "react-hook-form": "^7.42.1",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",
"react-rating-stars-component": "^2.2.0",
"react": "18.2.0", "react": "18.2.0",
"typescript": "4.9.4", "typescript": "4.9.4",
"zod": "^3.20.2" "zod": "^3.20.2"

17
pages/404.tsx Normal file
View File

@@ -0,0 +1,17 @@
// create a 404 next js page using tailwind
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
const NotFound: NextPage = () => {
return (
<Layout>
<div className="flex h-full flex-col items-center justify-center space-y-4">
<h1 className="text-7xl font-bold">Error: 404</h1>
<h2 className="text-xl font-bold">Page Not Found</h2>
</div>
</Layout>
);
};
export default NotFound;

View File

@@ -0,0 +1,74 @@
import DBClient from '@/prisma/DBClient';
import { NextApiHandler } from 'next';
import { z } from 'zod';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import ServerError from '@/config/util/ServerError';
import BeerCommentValidationSchema from '@/validation/CreateBeerCommentValidationSchema';
const handler: NextApiHandler<z.infer<typeof APIResponseValidationSchema>> = async (
req,
res,
) => {
try {
const { method } = req;
if (method !== 'POST') {
throw new ServerError('Method not allowed', 405);
}
const cleanedReqBody = BeerCommentValidationSchema.safeParse(req.body);
if (!cleanedReqBody.success) {
throw new ServerError('Invalid request body', 400);
}
const user = await DBClient.instance.user.findFirstOrThrow();
const { content, rating, beerPostId } = cleanedReqBody.data;
const newBeerComment = await DBClient.instance.beerComment.create({
data: {
content,
rating,
beerPost: { connect: { id: beerPostId } },
postedBy: { connect: { id: user.id } },
},
select: {
id: true,
content: true,
rating: true,
postedBy: {
select: {
id: true,
username: true,
},
},
createdAt: true,
},
});
res.status(201).json({
message: 'Beer comment created successfully',
statusCode: 201,
payload: newBeerComment.id,
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;

View File

@@ -1,16 +1,10 @@
import BeerPostValidationSchema from '@/validation/BeerPostValidationSchema'; import BeerPostValidationSchema from '@/validation/CreateBeerPostValidationSchema';
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { NextApiHandler } from 'next'; import { NextApiHandler } from 'next';
import { z } from 'zod'; import { z } from 'zod';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import ServerError from '@/config/util/ServerError';
class ServerError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = 'ServerError';
}
}
const handler: NextApiHandler<z.infer<typeof APIResponseValidationSchema>> = async ( const handler: NextApiHandler<z.infer<typeof APIResponseValidationSchema>> = async (
req, req,

View File

@@ -7,12 +7,37 @@ import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader'; import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
import CommentCard from '@/components/BeerById/CommentCard'; import CommentCard from '@/components/BeerById/CommentCard';
import { useState } from 'react';
import { BeerPost } from '@prisma/client';
import BeerCommentQueryResult from '@/services/BeerPost/types/BeerCommentQueryResult';
import BeerCommentForm from '../../components/BeerById/BeerCommentForm';
import BeerRecommendations from '../../components/BeerById/BeerRecommendations';
import getBeerRecommendations from '../../services/BeerPost/getBeerRecommendations';
import getAllBeerComments from '../../services/BeerPost/getAllBeerComments';
interface BeerPageProps { interface BeerPageProps {
beerPost: BeerPostQueryResult; beerPost: BeerPostQueryResult;
beerRecommendations: (BeerPost & {
brewery: {
id: string;
name: string;
};
beerImages: {
id: string;
alt: string;
url: string;
}[];
})[];
beerComments: BeerCommentQueryResult[];
} }
const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => { const BeerByIdPage: NextPage<BeerPageProps> = ({
beerPost,
beerRecommendations,
beerComments,
}) => {
const [comments, setComments] = useState(beerComments);
return ( return (
<Layout> <Layout>
<Head> <Head>
@@ -24,6 +49,8 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
<Image <Image
alt={beerPost.beerImages[0].alt} alt={beerPost.beerImages[0].alt}
src={beerPost.beerImages[0].url} src={beerPost.beerImages[0].url}
height={1080}
width={1920}
className="h-[42rem] w-full object-cover" className="h-[42rem] w-full object-cover"
/> />
)} )}
@@ -33,15 +60,19 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
<BeerInfoHeader beerPost={beerPost} /> <BeerInfoHeader beerPost={beerPost} />
<div className="mt-4 flex space-x-3"> <div className="mt-4 flex space-x-3">
<div className="w-[60%] space-y-3"> <div className="w-[60%] space-y-3">
<div className="card h-[22rem] bg-base-300"></div> <div className="card h-96 bg-base-300">
<div className="card h-[44rem] overflow-y-auto bg-base-300"> <div className="card-body">
{beerPost.beerComments.map((comment) => ( <BeerCommentForm beerPost={beerPost} setComments={setComments} />
</div>
</div>
<div className="card bg-base-300">
{comments.map((comment) => (
<CommentCard key={comment.id} comment={comment} /> <CommentCard key={comment.id} comment={comment} />
))} ))}
</div> </div>
</div> </div>
<div className="w-[40%]"> <div className="w-[40%]">
<div className="card h-full bg-base-300"></div> <BeerRecommendations beerRecommendations={beerRecommendations} />
</div> </div>
</div> </div>
</div> </div>
@@ -53,9 +84,22 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => { export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const beerPost = await getBeerPostById(context.params!.id! as string); const beerPost = await getBeerPostById(context.params!.id! as string);
return !beerPost
? { notFound: true } if (!beerPost) {
: { props: { beerPost: JSON.parse(JSON.stringify(beerPost)) } }; return { notFound: true };
}
const { type, brewery, id } = beerPost;
const beerComments = await getAllBeerComments({ id }, { pageSize: 3, pageNum: 1 });
const beerRecommendations = await getBeerRecommendations({ type, brewery });
const props = {
beerPost: JSON.parse(JSON.stringify(beerPost)),
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
beerComments: JSON.parse(JSON.stringify(beerComments)),
};
return { props };
}; };
export default BeerByIdPage; export default BeerByIdPage;

View File

@@ -21,13 +21,15 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
return ( return (
<Layout> <Layout>
<div className="flex items-center justify-center bg-base-100"> <div className="flex items-center justify-center bg-base-100">
<main className="mt-10 flex w-10/12 flex-col space-y-4"> <main className="my-10 flex w-10/12 flex-col space-y-4">
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{initialBeerPosts.map((post) => { {initialBeerPosts.map((post) => {
return <BeerCard post={post} key={post.id} />; return <BeerCard post={post} key={post.id} />;
})} })}
</div> </div>
<div className="flex justify-center">
<Pagination pageNum={pageNum} pageCount={pageCount} /> <Pagination pageNum={pageNum} pageCount={pageCount} />
</div>
</main> </main>
</div> </div>
</Layout> </Layout>

View File

@@ -1,7 +1,12 @@
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next'; import { NextPage } from 'next';
const Home: NextPage = () => { const Home: NextPage = () => {
return <h1 className="text-3xl font-bold underline">Hello world!</h1>; return (
<Layout>
<div></div>
</Layout>
);
}; };
export default Home; export default Home;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `rating` to the `BeerComment` table without a default value. This is not possible if the table is not empty.
- Added the required column `rating` to the `BreweryComment` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "BeerComment" ADD COLUMN "rating" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "BreweryComment" ADD COLUMN "rating" INTEGER NOT NULL;

View File

@@ -46,6 +46,7 @@ model BeerPost {
model BeerComment { model BeerComment {
id String @id @default(uuid()) id String @id @default(uuid())
rating Int
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade) beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
beerPostId String beerPostId String
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
@@ -81,6 +82,7 @@ model BreweryPost {
model BreweryComment { model BreweryComment {
id String @id @default(uuid()) id String @id @default(uuid())
rating Int
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade) breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
breweryPostId String breweryPostId String
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)

View File

@@ -1,3 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import { BeerPost, BeerImage } from '@prisma/client'; import { BeerPost, BeerImage } from '@prisma/client';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
@@ -10,7 +12,7 @@ const createNewBeerImages = async ({
beerPosts, beerPosts,
}: CreateNewBeerImagesArgs) => { }: CreateNewBeerImagesArgs) => {
const prisma = DBClient.instance; const prisma = DBClient.instance;
const createdAt = faker.date.past(1);
const beerImagesPromises: Promise<BeerImage>[] = []; const beerImagesPromises: Promise<BeerImage>[] = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
@@ -22,6 +24,7 @@ const createNewBeerImages = async ({
url: 'https://picsum.photos/900/1600', url: 'https://picsum.photos/900/1600',
alt: 'Placeholder beer image.', alt: 'Placeholder beer image.',
beerPost: { connect: { id: beerPost.id } }, beerPost: { connect: { id: beerPost.id } },
createdAt,
}, },
}), }),
); );

View File

@@ -23,12 +23,15 @@ const createNewBeerComments = async ({
const content = faker.lorem.lines(5); const content = faker.lorem.lines(5);
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
const createdAt = faker.date.past(1);
beerCommentPromises.push( beerCommentPromises.push(
prisma.beerComment.create({ prisma.beerComment.create({
data: { data: {
content, content,
postedBy: { connect: { id: user.id } }, postedBy: { connect: { id: user.id } },
beerPost: { connect: { id: beerPost.id } }, beerPost: { connect: { id: beerPost.id } },
rating: Math.floor(Math.random() * 5) + 1,
createdAt,
}, },
}), }),
); );

View File

@@ -25,17 +25,18 @@ const createNewBeerPosts = async ({
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)]; const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)];
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
const createdAt = faker.date.past(1);
beerPostPromises.push( beerPostPromises.push(
prisma.beerPost.create({ prisma.beerPost.create({
data: { data: {
abv: 10, abv: Math.floor(Math.random() * (12 - 4) + 4),
ibu: 10, ibu: Math.floor(Math.random() * (60 - 10) + 10),
name: `${faker.commerce.productName()} ${beerType.name}`, name: faker.commerce.productName(),
description: faker.lorem.lines(24), description: faker.lorem.lines(24),
brewery: { connect: { id: breweryPost.id } }, brewery: { connect: { id: breweryPost.id } },
postedBy: { connect: { id: user.id } }, postedBy: { connect: { id: user.id } },
type: { connect: { id: beerType.id } }, type: { connect: { id: beerType.id } },
createdAt,
}, },
}), }),
); );

View File

@@ -1,3 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import { User, BeerType } from '@prisma/client'; import { User, BeerType } from '@prisma/client';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
@@ -36,9 +38,10 @@ const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
types.forEach((type) => { types.forEach((type) => {
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past(1);
beerTypePromises.push( beerTypePromises.push(
prisma.beerType.create({ prisma.beerType.create({
data: { name: type, postedBy: { connect: { id: user.id } } }, data: { name: type, postedBy: { connect: { id: user.id } }, createdAt },
}), }),
); );
}); });

View File

@@ -1,3 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import { BreweryPost, BreweryImage } from '@prisma/client'; import { BreweryPost, BreweryImage } from '@prisma/client';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
@@ -10,7 +12,7 @@ const createNewBreweryImages = async ({
breweryPosts, breweryPosts,
}: CreateBreweryImagesArgs) => { }: CreateBreweryImagesArgs) => {
const prisma = DBClient.instance; const prisma = DBClient.instance;
const createdAt = faker.date.past(1);
const breweryImagesPromises: Promise<BreweryImage>[] = []; const breweryImagesPromises: Promise<BreweryImage>[] = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
@@ -23,6 +25,7 @@ const createNewBreweryImages = async ({
url: 'https://picsum.photos/900/1600', url: 'https://picsum.photos/900/1600',
alt: 'Placeholder brewery image.', alt: 'Placeholder brewery image.',
breweryPost: { connect: { id: breweryPost.id } }, breweryPost: { connect: { id: breweryPost.id } },
createdAt,
}, },
}), }),
); );

View File

@@ -18,6 +18,7 @@ const createNewBreweryPostComments = async ({
const { breweryPosts, users } = joinData; const { breweryPosts, users } = joinData;
const prisma = DBClient.instance; const prisma = DBClient.instance;
const breweryCommentPromises: Promise<BreweryComment>[] = []; const breweryCommentPromises: Promise<BreweryComment>[] = [];
const createdAt = faker.date.past(1);
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfComments; i++) { for (let i = 0; i < numberOfComments; i++) {
const content = faker.lorem.lines(5); const content = faker.lorem.lines(5);
@@ -29,6 +30,8 @@ const createNewBreweryPostComments = async ({
content, content,
postedBy: { connect: { id: user.id } }, postedBy: { connect: { id: user.id } },
breweryPost: { connect: { id: breweryPost.id } }, breweryPost: { connect: { id: breweryPost.id } },
rating: Math.floor(Math.random() * 5) + 1,
createdAt,
}, },
}), }),
); );

View File

@@ -23,10 +23,16 @@ const createNewBreweryPosts = async ({
const location = faker.address.cityName(); const location = faker.address.cityName();
const description = faker.lorem.lines(5); const description = faker.lorem.lines(5);
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past(1);
breweryPromises.push( breweryPromises.push(
prisma.breweryPost.create({ prisma.breweryPost.create({
data: { name, location, description, postedBy: { connect: { id: user.id } } }, data: {
name,
location,
description,
postedBy: { connect: { id: user.id } },
createdAt,
},
}), }),
); );
} }

View File

@@ -16,7 +16,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
const username = `${firstName[0]}.${lastName}`; const username = `${firstName[0]}.${lastName}`;
const email = faker.internet.email(firstName, lastName, 'example.com'); const email = faker.internet.email(firstName, lastName, 'example.com');
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
const createdAt = faker.date.past(1);
userPromises.push( userPromises.push(
prisma.user.create({ prisma.user.create({
data: { data: {
@@ -25,6 +25,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
email, email,
username, username,
dateOfBirth, dateOfBirth,
createdAt,
}, },
}), }),
); );

View File

@@ -28,16 +28,16 @@ import createNewUsers from './create/createNewUsers';
createNewBeerTypes({ joinData: { users } }), createNewBeerTypes({ joinData: { users } }),
]); ]);
const beerPosts = await createNewBeerPosts({ const beerPosts = await createNewBeerPosts({
numberOfPosts: 100, numberOfPosts: 48,
joinData: { breweryPosts, beerTypes, users }, joinData: { breweryPosts, beerTypes, users },
}); });
const [beerPostComments, breweryPostComments] = await Promise.all([ const [beerPostComments, breweryPostComments] = await Promise.all([
createNewBeerPostComments({ createNewBeerPostComments({
numberOfComments: 500, numberOfComments: 1000,
joinData: { beerPosts, users }, joinData: { beerPosts, users },
}), }),
createNewBreweryPostComments({ createNewBreweryPostComments({
numberOfComments: 500, numberOfComments: 1000,
joinData: { breweryPosts, users }, joinData: { breweryPosts, users },
}), }),
]); ]);

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
import BeerCommentValidationSchema from '../validation/CreateBeerCommentValidationSchema';
const sendCreateBeerCommentRequest = async ({
beerPostId,
content,
rating,
}: z.infer<typeof BeerCommentValidationSchema>) => {
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
beerPostId,
content,
rating,
}),
});
const data = await response.json();
console.log(data);
};
export default sendCreateBeerCommentRequest;

View File

@@ -1,4 +1,4 @@
import BeerPostValidationSchema from '@/validation/BeerPostValidationSchema'; import BeerPostValidationSchema from '@/validation/CreateBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod'; import { z } from 'zod';

View File

@@ -0,0 +1,37 @@
import BeerCommentQueryResult from '@/services/BeerPost/types/BeerCommentQueryResult';
import DBClient from '@/prisma/DBClient';
import BeerPostQueryResult from './types/BeerPostQueryResult';
const getAllBeerComments = async (
{ id }: Pick<BeerPostQueryResult, 'id'>,
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
) => {
const skip = (pageNum - 1) * pageSize;
const beerComments: BeerCommentQueryResult[] =
await DBClient.instance.beerComment.findMany({
where: {
beerPostId: id,
},
select: {
id: true,
content: true,
rating: true,
createdAt: true,
postedBy: {
select: {
id: true,
username: true,
createdAt: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
skip,
take: pageSize,
});
return beerComments;
};
export default getAllBeerComments;

View File

@@ -32,19 +32,6 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
username: true, username: true,
}, },
}, },
beerComments: {
select: {
id: true,
content: true,
createdAt: true,
postedBy: {
select: {
username: true,
id: true,
},
},
},
},
beerImages: { beerImages: {
select: { select: {
url: true, url: true,

View File

@@ -17,6 +17,7 @@ const getBeerPostById = async (id: string) => {
id: true, id: true,
}, },
}, },
rating: true,
}, },
}, },
id: true, id: true,

View File

@@ -0,0 +1,31 @@
import BeerPostQueryResult from '@/services/BeerPost/types/BeerPostQueryResult';
import DBClient from '@/prisma/DBClient';
const getBeerRecommendations = async (
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery'>,
) => {
const beerRecommendations = await DBClient.instance.beerPost.findMany({
where: {
OR: [
{
typeId: beerPost.type.id,
},
{
breweryId: beerPost.brewery.id,
},
],
},
include: {
beerImages: {
select: { id: true, url: true, alt: true },
},
brewery: {
select: { id: true, name: true },
},
},
});
return beerRecommendations;
};
export default getBeerRecommendations;

View File

@@ -0,0 +1,13 @@
interface BeerCommentQueryResult {
id: string;
content: string;
rating: number;
createdAt: Date;
postedBy: {
id: string;
createdAt: Date;
username: string;
};
}
export default BeerCommentQueryResult;

View File

@@ -22,15 +22,6 @@ export default interface BeerPostQueryResult {
id: string; id: string;
username: string; username: string;
}; };
beerComments: {
id: string;
content: string;
createdAt: Date;
postedBy: {
id: string;
username: string;
};
}[];
createdAt: Date; createdAt: Date;
} }

View File

@@ -0,0 +1,15 @@
import { BeerPost } from '@prisma/client';
type BeerRecommendationQueryResult = BeerPost & {
brewery: {
id: string;
name: string;
};
beerImages: {
id: string;
alt: string;
url: string;
}[];
};
export default BeerRecommendationQueryResult;

View File

@@ -7,6 +7,6 @@ module.exports = {
plugins: [require('daisyui')], plugins: [require('daisyui')],
daisyui: { daisyui: {
logs: false, logs: false,
themes: ['lemonade'], themes: ['dracula'],
}, },
}; };

View File

@@ -0,0 +1,22 @@
import { z } from 'zod';
const BeerCommentValidationSchema = z.object({
content: z
.string()
.min(1, {
message: 'Comment must not be empty.',
})
.max(300, {
message: 'Comment must be less than 300 characters.',
}),
rating: z
.number()
.int()
.min(1, { message: 'Rating must be greater than 1.' })
.max(5, { message: 'Rating must be less than 5.' }),
beerPostId: z.string().uuid({
message: 'Beer post ID must be a valid UUID.',
}),
});
export default BeerCommentValidationSchema;