Work on brewery page, refactors

Refactor query types to explicitly use z.infer
This commit is contained in:
Aaron William Po
2023-03-31 21:13:35 -04:00
parent d8a8dad37f
commit b69dbc95b4
33 changed files with 308 additions and 243 deletions

View File

@@ -1,6 +1,6 @@
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest'; import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; 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 { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { FunctionComponent, useState, useEffect } from 'react'; import { FunctionComponent, useState, useEffect } from 'react';
@@ -16,7 +16,7 @@ import FormSegment from '../ui/forms/FormSegment';
import FormTextArea from '../ui/forms/FormTextArea'; import FormTextArea from '../ui/forms/FormTextArea';
interface BeerCommentFormProps { interface BeerCommentFormProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
} }
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => { const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => {

View File

@@ -2,33 +2,24 @@ import Link from 'next/link';
import formatDistanceStrict from 'date-fns/formatDistanceStrict'; import formatDistanceStrict from 'date-fns/formatDistanceStrict';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { FC, useContext, useEffect, useState } from 'react'; import { FC, useContext, useEffect, useState } from 'react';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { FaRegEdit } from 'react-icons/fa'; import { FaRegEdit } from 'react-icons/fa';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
import BeerPostLikeButton from './BeerPostLikeButton'; import BeerPostLikeButton from './BeerPostLikeButton';
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({ const BeerInfoHeader: FC<{
beerPost, beerPost: z.infer<typeof beerPostQueryResult>;
initialLikeCount, initialLikeCount: number;
}) => { }> = ({ beerPost, initialLikeCount }) => {
const createdAtDate = new Date(beerPost.createdAt); const createdAtDate = new Date(beerPost.createdAt);
const [timeDistance, setTimeDistance] = useState(''); const [timeDistance, setTimeDistance] = useState('');
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const [likeCount, setLikeCount] = useState(initialLikeCount); const [likeCount, setLikeCount] = useState(initialLikeCount);
const [isPostOwner, setIsPostOwner] = useState(false); const idMatches = user && beerPost.postedBy.id === user.id;
const isPostOwner = !!(user && idMatches);
useEffect(() => {
const idMatches = user && beerPost.postedBy.id === user.id;
if (!(user && idMatches)) {
setIsPostOwner(false);
return;
}
setIsPostOwner(true);
}, [user, beerPost]);
useEffect(() => { useEffect(() => {
setLikeCount(initialLikeCount); setLikeCount(initialLikeCount);
@@ -67,15 +58,15 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
</div> </div>
<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>
<span <span
className="tooltip tooltip-bottom" className="tooltip tooltip-right"
data-tip={format(createdAtDate, 'MM/dd/yyyy')} data-tip={format(createdAtDate, 'MM/dd/yyyy')}
> >
{` ${timeDistance}`} ago {`${timeDistance} ago`}
</span> </span>
</h3> </h3>

View File

@@ -1,11 +1,12 @@
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { FC } from 'react'; import { FC } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
interface BeerCommentsPaginationBarProps { interface BeerCommentsPaginationBarProps {
commentsPageNum: number; commentsPageNum: number;
commentsPageCount: number; commentsPageCount: number;
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
} }
const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({

View File

@@ -1,15 +1,17 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { FC, useContext } from 'react'; import { FC, useContext } from 'react';
import { z } from 'zod';
import BeerCommentForm from './BeerCommentForm'; import BeerCommentForm from './BeerCommentForm';
import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar'; import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar';
import CommentCard from './CommentCard'; import CommentCard from './CommentCard';
interface BeerPostCommentsSectionProps { interface BeerPostCommentsSectionProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
comments: BeerCommentQueryResultArrayT; comments: z.infer<typeof BeerCommentQueryResult>[];
commentsPageCount: number; commentsPageCount: number;
} }

View File

@@ -1,5 +1,5 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { format, formatDistanceStrict } from 'date-fns'; import { format, formatDistanceStrict } from 'date-fns';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -7,9 +7,10 @@ import { useContext, useEffect, useState } from 'react';
import { Rating } from 'react-daisyui'; import { Rating } from 'react-daisyui';
import { FaEllipsisH } from 'react-icons/fa'; import { FaEllipsisH } from 'react-icons/fa';
import { z } from 'zod';
const CommentCardDropdown: React.FC<{ const CommentCardDropdown: React.FC<{
comment: BeerCommentQueryResultT; comment: z.infer<typeof BeerCommentQueryResult>;
beerPostId: string; beerPostId: string;
}> = ({ comment, beerPostId }) => { }> = ({ comment, beerPostId }) => {
const router = useRouter(); const router = useRouter();
@@ -52,7 +53,7 @@ const CommentCardDropdown: React.FC<{
}; };
const CommentCard: React.FC<{ const CommentCard: React.FC<{
comment: BeerCommentQueryResultT; comment: z.infer<typeof BeerCommentQueryResult>;
beerPostId: string; beerPostId: string;
}> = ({ comment, beerPostId }) => { }> = ({ comment, beerPostId }) => {
const [timeDistance, setTimeDistance] = useState(''); const [timeDistance, setTimeDistance] = useState('');

View File

@@ -1,9 +1,10 @@
import Link from 'next/link'; import Link from 'next/link';
import { FC } from 'react'; import { FC } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => { const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) => {
return ( return (
<div className="card bg-base-300" key={post.id}> <div className="card bg-base-300" key={post.id}>
<figure className="card-image h-96"> <figure className="card-image h-96">

View File

@@ -1,12 +1,12 @@
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest'; import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
import CreateBeerPostValidationSchema 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 { zodResolver } from '@hookform/resolvers/zod';
import { BeerType } from '@prisma/client'; import { BeerType } from '@prisma/client';
import router from 'next/router'; import router from 'next/router';
import { FunctionComponent, useState } from 'react'; import { FunctionComponent, useState } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import ErrorAlert from './ui/alerts/ErrorAlert'; import ErrorAlert from './ui/alerts/ErrorAlert';
import Button from './ui/forms/Button'; import Button from './ui/forms/Button';
import FormError from './ui/forms/FormError'; import FormError from './ui/forms/FormError';
@@ -20,7 +20,7 @@ import FormTextInput from './ui/forms/FormTextInput';
type CreateBeerPostSchema = z.infer<typeof CreateBeerPostValidationSchema>; type CreateBeerPostSchema = z.infer<typeof CreateBeerPostValidationSchema>;
interface BeerFormProps { interface BeerFormProps {
breweries: BreweryPostQueryResult[]; breweries: z.infer<typeof BreweryPostQueryResult>[];
types: BeerType[]; types: BeerType[];
} }

View File

@@ -133,7 +133,7 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
{isSubmitting ? 'Submitting...' : 'Submit'} {isSubmitting ? 'Submitting...' : 'Submit'}
</Button> </Button>
<button <button
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`} className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
type="button" type="button"
onClick={onDelete} onClick={onDelete}
> >

View File

@@ -44,7 +44,7 @@ const Navbar = () => {
return ( return (
<nav className="navbar bg-primary text-primary-content"> <nav className="navbar bg-primary text-primary-content">
<div className="flex-1"> <div className="flex-1">
<Link className="btn-ghost btn text-3xl normal-case" href="/"> <Link className="btn btn-ghost text-3xl normal-case" href="/">
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span> <span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
</Link> </Link>
</div> </div>
@@ -68,8 +68,8 @@ const Navbar = () => {
</ul> </ul>
</div> </div>
<div className="flex-none lg:hidden"> <div className="flex-none lg:hidden">
<div className="dropdown-end dropdown"> <div className="dropdown dropdown-end">
<label tabIndex={0} className="btn-ghost btn-circle btn"> <label tabIndex={0} className="btn btn-ghost btn-circle">
<span 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"

View File

@@ -22,7 +22,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
<div className="align-center my-20 flex h-fit flex-col items-center justify-center"> <div className="align-center my-20 flex h-fit flex-col items-center justify-center">
<div className="w-8/12"> <div className="w-8/12">
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}> <div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
<Link href={backLink} className="btn-ghost btn-sm btn p-0"> <Link href={backLink} className="btn btn-ghost btn-sm p-0">
<BiArrowBack className="text-xl" /> <BiArrowBack className="text-xl" />
</Link> </Link>
</div> </div>

View File

@@ -1,5 +1,6 @@
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import useSWR from 'swr'; import useSWR from 'swr';
import { beerPostQueryResultArraySchema } from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod';
const useBeerPostSearch = (query: string | undefined) => { const useBeerPostSearch = (query: string | undefined) => {
const { data, isLoading, error } = useSWR( const { data, isLoading, error } = useSWR(
@@ -13,7 +14,7 @@ const useBeerPostSearch = (query: string | undefined) => {
} }
const json = await response.json(); const json = await response.json();
const result = beerPostQueryResultArraySchema.parse(json); const result = z.array(beerPostQueryResult).parse(json);
return result; return result;
}, },

View File

@@ -1,17 +1,15 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useContext, useEffect } from 'react'; import { useContext } from 'react';
const useRedirectWhenLoggedIn = () => { const useRedirectWhenLoggedIn = () => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const router = useRouter(); const router = useRouter();
useEffect(() => { if (!user) {
if (!user) { return;
return; }
} router.push('/');
router.push('/');
}, [user, router]);
}; };
export default useRedirectWhenLoggedIn; export default useRedirectWhenLoggedIn;

View File

@@ -1,21 +1,28 @@
import DBClient from '@/prisma/DBClient';
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types'; import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment'; import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { createRouter } from 'next-connect'; import { createRouter } from 'next-connect';
import { z } from 'zod'; import { z } from 'zod';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
interface CreateCommentRequest extends UserExtendedNextApiRequest { interface CreateCommentRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof BeerCommentValidationSchema>; body: z.infer<typeof BeerCommentValidationSchema>;
query: { id: string }; query: { id: string };
} }
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string };
}
const createComment = async ( const createComment = async (
req: CreateCommentRequest, req: CreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>, res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
@@ -24,12 +31,13 @@ const createComment = async (
const beerPostId = req.query.id; const beerPostId = req.query.id;
const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({ const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
content, await createNewBeerComment({
rating, content,
beerPostId, rating,
userId: req.user!.id, beerPostId,
}); userId: req.user!.id,
});
res.status(201).json({ res.status(201).json({
message: 'Beer comment created successfully', message: 'Beer comment created successfully',
@@ -39,8 +47,34 @@ const createComment = async (
}); });
}; };
const getAll = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const beerPostId = req.query.id;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;
const comments = await getAllBeerComments(
{ id: beerPostId },
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
);
const pageCount = await DBClient.instance.beerComment.count({ where: { beerPostId } });
res.setHeader('X-Total-Count', pageCount);
res.status(200).json({
message: 'Beer comments fetched successfully',
statusCode: 200,
payload: comments,
success: true,
});
};
const router = createRouter< const router = createRouter<
CreateCommentRequest, // I don't want to use any, but I can't figure out how to get the types to work
any,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>> NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>(); >();
@@ -53,5 +87,16 @@ router.post(
createComment, createComment,
); );
router.get(
validateRequest({
querySchema: z.object({
id: z.string().uuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
}),
getAll,
);
const handler = router.handler(NextConnectOptions); const handler = router.handler(NextConnectOptions);
export default handler; export default handler;

View File

@@ -5,7 +5,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect'; import { createRouter } from 'next-connect';
import { z } from 'zod'; import { z } from 'zod';
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
const SearchSchema = z.object({ const SearchSchema = z.object({
search: z.string().min(1), search: z.string().min(1),
@@ -18,29 +18,30 @@ interface SearchAPIRequest extends NextApiRequest {
const search = async (req: SearchAPIRequest, res: NextApiResponse) => { const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
const { search: query } = req.query; const { search: query } = req.query;
const beers: BeerPostQueryResult[] = await DBClient.instance.beerPost.findMany({ const beers: z.infer<typeof beerPostQueryResult>[] =
select: { await DBClient.instance.beerPost.findMany({
id: true, select: {
name: true, id: true,
ibu: true, name: true,
abv: true, ibu: true,
createdAt: true, abv: true,
description: true, createdAt: true,
postedBy: { select: { username: true, id: true } }, description: true,
brewery: { select: { name: true, id: true } }, postedBy: { select: { username: true, id: true } },
type: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, type: { select: { name: true, id: true } },
}, beerImages: { select: { alt: true, path: true, caption: true, id: true } },
where: { },
OR: [ where: {
{ name: { contains: query, mode: 'insensitive' } }, OR: [
{ description: { contains: query, mode: 'insensitive' } }, { name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ brewery: { name: { contains: query, mode: 'insensitive' } } }, { brewery: { name: { contains: query, mode: 'insensitive' } } },
{ type: { name: { contains: query, mode: 'insensitive' } } }, { type: { name: { contains: query, mode: 'insensitive' } } },
], ],
}, },
}); });
res.status(200).json(beers); res.status(200).json(beers);
}; };

View File

@@ -5,13 +5,14 @@ import React from 'react';
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired'; import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
import getBeerPostById from '@/services/BeerPost/getBeerPostById'; import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import EditBeerPostForm from '@/components/EditBeerPostForm'; import EditBeerPostForm from '@/components/EditBeerPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout'; import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import { BiBeer } from 'react-icons/bi'; import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
interface EditPageProps { interface EditPageProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
} }
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => { const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {

View File

@@ -11,19 +11,20 @@ import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
import getBeerPostById from '@/services/BeerPost/getBeerPostById'; import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations'; import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPost } from '@prisma/client'; import { BeerPost } from '@prisma/client';
import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount'; import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount'; import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount';
import { z } from 'zod';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
interface BeerPageProps { interface BeerPageProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
beerRecommendations: (BeerPost & { beerRecommendations: (BeerPost & {
brewery: { id: string; name: string }; brewery: { id: string; name: string };
beerImages: { id: string; alt: string; url: string }[]; beerImages: { id: string; alt: string; url: string }[];
})[]; })[];
beerComments: BeerCommentQueryResultArrayT; beerComments: z.infer<typeof BeerCommentQueryResult>[];
commentsPageCount: number; commentsPageCount: number;
likeCount: number; likeCount: number;
} }

View File

@@ -8,9 +8,10 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
import { BeerType } from '@prisma/client'; import { BeerType } from '@prisma/client';
import { NextPage } from 'next'; import { NextPage } from 'next';
import { BiBeer } from 'react-icons/bi'; import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
interface CreateBeerPageProps { interface CreateBeerPageProps {
breweries: BreweryPostQueryResult[]; breweries: z.infer<typeof BreweryPostQueryResult>[];
types: BeerType[]; types: BeerType[];
} }

View File

@@ -6,11 +6,12 @@ import DBClient from '@/prisma/DBClient';
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar'; import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
import BeerCard from '@/components/BeerIndex/BeerCard'; import BeerCard from '@/components/BeerIndex/BeerCard';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import Head from 'next/head'; import Head from 'next/head';
import { z } from 'zod';
interface BeerPageProps { interface BeerPageProps {
initialBeerPosts: BeerPostQueryResult[]; initialBeerPosts: z.infer<typeof beerPostQueryResult>[];
pageCount: number; pageCount: number;
} }

View File

@@ -14,8 +14,8 @@ const DEBOUNCE_DELAY = 300;
const SearchPage: NextPage = () => { const SearchPage: NextPage = () => {
const router = useRouter(); const router = useRouter();
const querySearch = router.query.search as string | undefined; const querySearch = (router.query.search as string) || '';
const [searchValue, setSearchValue] = useState(querySearch || ''); const [searchValue, setSearchValue] = useState(querySearch);
const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue); const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue);
const debounceSearch = debounce((value: string) => { const debounceSearch = debounce((value: string) => {
@@ -36,7 +36,7 @@ const SearchPage: NextPage = () => {
if (!querySearch || searchValue) { if (!querySearch || searchValue) {
return; return;
} }
setSearchValue(searchValue); setSearchValue(querySearch);
}, DEBOUNCE_DELAY)(); }, DEBOUNCE_DELAY)();
}, [querySearch, searchValue]); }, [querySearch, searchValue]);

View File

@@ -1,10 +1,12 @@
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { GetServerSideProps, NextPage } from 'next'; import { GetServerSideProps, NextPage } from 'next';
import { z } from 'zod';
interface BreweryPageProps { interface BreweryPageProps {
breweryPost: BeerPostQueryResult; breweryPost: z.infer<typeof BreweryPostQueryResult>;
} }
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => { const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {

View File

@@ -4,24 +4,58 @@ import Link from 'next/link';
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts'; import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import { FC } from 'react';
import Image from 'next/image';
import { z } from 'zod';
interface BreweryPageProps { interface BreweryPageProps {
breweryPosts: BreweryPostQueryResult[]; breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
} }
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
brewery,
}) => {
return (
<div className="card bg-base-300" key={brewery.id}>
<figure className="card-image h-96">
{brewery.breweryImages.length > 0 && (
<Image
src={brewery.breweryImages[0].path}
alt={brewery.name}
width="1029"
height="110"
/>
)}
</figure>
<div className="card-body space-y-3">
<div>
<h2 className="text-3xl font-bold">
<Link href={`/breweries/${brewery.id}`}>{brewery.name}</Link>
</h2>
<h3 className="text-xl font-semibold">{brewery.location}</h3>
</div>
</div>
</div>
);
};
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => { const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
return ( return (
<Layout> <Layout>
<h1 className="text-3xl font-bold underline">Brewery Posts</h1> <div className="flex items-center justify-center bg-base-100">
{breweryPosts.map((post) => { <div className="my-10 flex w-10/12 flex-col space-y-4">
return ( <header className="my-10">
<div key={post.id}> <div className="space-y-2">
<h2> <h1 className="text-6xl font-bold">Breweries</h1>
<Link href={`/breweries/${post.id}`}>{post.name}</Link> </div>
</h2> </header>
<div className="grid gap-5 md:grid-cols-1 xl:grid-cols-2">
{breweryPosts.map((brewery) => {
return <BreweryCard brewery={brewery} key={brewery.id} />;
})}
</div> </div>
); </div>
})} </div>
</Layout> </Layout>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { BeerCommentQueryResult } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod'; import { z } from 'zod';
@@ -14,14 +14,8 @@ const sendCreateBeerCommentRequest = async ({
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => { }: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/beers/${beerPostId}/comments`, { const response = await fetch(`/api/beers/${beerPostId}/comments`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body: JSON.stringify({ beerPostId, content, rating }),
},
body: JSON.stringify({
beerPostId,
content,
rating,
}),
}); });
const data = await response.json(); const data = await response.json();

View File

@@ -1,4 +1,4 @@
import { beerPostQueryResultSchema } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema'; import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod'; import { z } from 'zod';
@@ -24,7 +24,7 @@ const sendCreateBeerPostRequest = async (
throw new Error(message); throw new Error(message);
} }
const parsedPayload = beerPostQueryResultSchema.safeParse(payload); const parsedPayload = beerPostQueryResult.safeParse(payload);
if (!parsedPayload.success) { if (!parsedPayload.success) {
throw new Error('Invalid API response payload'); throw new Error('Invalid API response payload');

View File

@@ -1,13 +1,14 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerCommentQueryResultArrayT } from './schema/BeerCommentQueryResult'; import { z } from 'zod';
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
const getAllBeerComments = async ( const getAllBeerComments = async (
{ id }: Pick<BeerPostQueryResult, 'id'>, { id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number }, { pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
) => { ) => {
const skip = (pageNum - 1) * pageSize; const skip = (pageNum - 1) * pageSize;
const beerComments: BeerCommentQueryResultArrayT = const beerComments: z.infer<typeof BeerCommentQueryResult>[] =
await DBClient.instance.beerComment.findMany({ await DBClient.instance.beerComment.findMany({
skip, skip,
take: pageSize, take: pageSize,

View File

@@ -1,8 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
export const BeerCommentQueryResult = z.object({ const BeerCommentQueryResult = z.object({
id: z.string().uuid(), id: z.string().uuid(),
content: z.string().min(1).max(300), content: z.string().min(1).max(500),
rating: z.number().int().min(1).max(5), rating: z.number().int().min(1).max(5),
createdAt: z.coerce.date(), createdAt: z.coerce.date(),
postedBy: z.object({ postedBy: z.object({
@@ -10,6 +10,5 @@ export const BeerCommentQueryResult = z.object({
username: z.string().min(1).max(50), username: z.string().min(1).max(50),
}), }),
}); });
export const BeerCommentQueryResultArray = z.array(BeerCommentQueryResult);
export type BeerCommentQueryResultT = z.infer<typeof BeerCommentQueryResult>; export default BeerCommentQueryResult;
export type BeerCommentQueryResultArrayT = z.infer<typeof BeerCommentQueryResultArray>;

View File

@@ -1,6 +1,6 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { z } from 'zod'; import { z } from 'zod';
import { BeerPostQueryResult } from './schema/BeerPostQueryResult'; import beerPostQueryResult from './schema/BeerPostQueryResult';
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema'; import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({ const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
@@ -16,29 +16,30 @@ const createNewBeerPost = async ({
breweryId, breweryId,
userId, userId,
}: z.infer<typeof CreateBeerPostWithUserSchema>) => { }: z.infer<typeof CreateBeerPostWithUserSchema>) => {
const newBeerPost: BeerPostQueryResult = await DBClient.instance.beerPost.create({ const newBeerPost: z.infer<typeof beerPostQueryResult> =
data: { await DBClient.instance.beerPost.create({
name, data: {
description, name,
abv, description,
ibu, abv,
type: { connect: { id: typeId } }, ibu,
postedBy: { connect: { id: userId } }, type: { connect: { id: typeId } },
brewery: { connect: { id: breweryId } }, postedBy: { connect: { id: userId } },
}, brewery: { connect: { id: breweryId } },
select: { },
id: true, select: {
name: true, id: true,
description: true, name: true,
abv: true, description: true,
ibu: true, abv: true,
createdAt: true, ibu: true,
beerImages: { select: { id: true, path: true, caption: true, alt: true } }, createdAt: true,
brewery: { select: { id: true, name: true } }, beerImages: { select: { id: true, path: true, caption: true, alt: true } },
type: { select: { id: true, name: true } }, brewery: { select: { id: true, name: true } },
postedBy: { select: { id: true, username: true } }, type: { select: { id: true, name: true } },
}, postedBy: { select: { id: true, username: true } },
}); },
});
return newBeerPost; return newBeerPost;
}; };

View File

@@ -1,27 +1,30 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance; const prisma = DBClient.instance;
const getAllBeerPosts = async (pageNum: number, pageSize: number) => { const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
const skip = (pageNum - 1) * pageSize; const skip = (pageNum - 1) * pageSize;
const beerPosts: BeerPostQueryResult[] = await prisma.beerPost.findMany({ const beerPosts: z.infer<typeof beerPostQueryResult>[] = await prisma.beerPost.findMany(
select: { {
id: true, select: {
name: true, id: true,
ibu: true, name: true,
abv: true, ibu: true,
description: true, abv: true,
createdAt: true, description: true,
type: { select: { name: true, id: true } }, createdAt: true,
brewery: { select: { name: true, id: true } }, type: { select: { name: true, id: true } },
postedBy: { select: { id: true, username: true } }, brewery: { select: { name: true, id: true } },
beerImages: { select: { path: true, caption: true, id: true, alt: true } }, postedBy: { select: { id: true, username: true } },
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
},
take: pageSize,
skip,
}, },
take: pageSize, );
skip,
});
return beerPosts; return beerPosts;
}; };

View File

@@ -1,24 +1,26 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance; const prisma = DBClient.instance;
const getBeerPostById = async (id: string) => { const getBeerPostById = async (id: string) => {
const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({ const beerPost: z.infer<typeof beerPostQueryResult> | null =
select: { await prisma.beerPost.findFirst({
id: true, select: {
name: true, id: true,
ibu: true, name: true,
abv: true, ibu: true,
createdAt: true, abv: true,
description: true, createdAt: true,
postedBy: { select: { username: true, id: true } }, description: true,
brewery: { select: { name: true, id: true } }, postedBy: { select: { username: true, id: true } },
type: { select: { name: true, id: true } }, brewery: { select: { name: true, id: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } }, type: { select: { name: true, id: true } },
}, beerImages: { select: { alt: true, path: true, caption: true, id: true } },
where: { id }, },
}); where: { id },
});
return beerPost; return beerPost;
}; };

View File

@@ -1,8 +1,9 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
const getBeerRecommendations = async ( const getBeerRecommendations = async (
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery' | 'id'>, beerPost: Pick<z.infer<typeof beerPostQueryResult>, 'type' | 'brewery' | 'id'>,
) => { ) => {
const beerRecommendations = await DBClient.instance.beerPost.findMany({ const beerRecommendations = await DBClient.instance.beerPost.findMany({
where: { where: {

View File

@@ -1,36 +1,18 @@
import { z } from 'zod'; import { z } from 'zod';
export const beerPostQueryResultSchema = z.object({ const beerPostQueryResult = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
brewery: z.object({ brewery: z.object({ id: z.string(), name: z.string() }),
id: z.string(),
name: z.string(),
}),
description: z.string(), description: z.string(),
beerImages: z.array( beerImages: z.array(
z.object({ z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
path: z.string(),
caption: z.string(),
id: z.string(),
alt: z.string(),
}),
), ),
ibu: z.number(), ibu: z.number(),
abv: z.number(), abv: z.number(),
type: z.object({ type: z.object({ id: z.string(), name: z.string() }),
id: z.string(), postedBy: z.object({ id: z.string(), username: z.string() }),
name: z.string(),
}),
postedBy: z.object({
id: z.string(),
username: z.string(),
}),
createdAt: z.coerce.date(), createdAt: z.coerce.date(),
}); });
export const beerPostQueryResultArraySchema = z.array(beerPostQueryResultSchema); export default beerPostQueryResult;
export type BeerPostQueryResult = z.infer<typeof beerPostQueryResultSchema>;
export type BeerPostQueryResultArray = z.infer<typeof beerPostQueryResultArraySchema>;

View File

@@ -1,17 +1,20 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from './types/BreweryPostQueryResult'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance; const prisma = DBClient.instance;
const getAllBreweryPosts = async () => { const getAllBreweryPosts = async () => {
const breweryPosts: BreweryPostQueryResult[] = await prisma.breweryPost.findMany({ const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
select: { await prisma.breweryPost.findMany({
id: true, select: {
location: true, id: true,
name: true, location: true,
postedBy: { select: { firstName: true, lastName: true, id: true } }, name: true,
}, postedBy: { select: { username: true, id: true } },
}); breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
},
});
return breweryPosts; return breweryPosts;
}; };

View File

@@ -1,26 +1,21 @@
import DBClient from '@/prisma/DBClient'; import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from './types/BreweryPostQueryResult'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { z } from 'zod';
const prisma = DBClient.instance; const prisma = DBClient.instance;
const getBreweryPostById = async (id: string) => { const getBreweryPostById = async (id: string) => {
const breweryPost: BreweryPostQueryResult | null = await prisma.breweryPost.findFirst({ const breweryPost: z.infer<typeof BreweryPostQueryResult> | null =
select: { await prisma.breweryPost.findFirst({
id: true, select: {
location: true, id: true,
name: true, location: true,
postedBy: { name: true,
select: { breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
firstName: true, postedBy: { select: { username: true, id: true } },
lastName: true,
id: true,
},
}, },
}, where: { id },
where: { });
id,
},
});
return breweryPost; return breweryPost;
}; };

View File

@@ -1,10 +1,13 @@
export default interface BreweryPostQueryResult { import { z } from 'zod';
id: string;
location: string; const BreweryPostQueryResult = z.object({
name: string; id: z.string(),
postedBy: { location: z.string(),
id: string; name: z.string(),
firstName: string; postedBy: z.object({ id: z.string(), username: z.string() }),
lastName: string; breweryImages: z.array(
}; z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
} ),
});
export default BreweryPostQueryResult;