mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #52 from aaronpo97/dev
Style updates, beer style features
This commit is contained in:
6814
package-lock.json
generated
6814
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -14,8 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/iron": "^7.0.1",
|
"@hapi/iron": "^7.0.1",
|
||||||
"@headlessui/react": "^1.7.15",
|
"@headlessui/react": "^1.7.15",
|
||||||
"@headlessui/tailwindcss": "^0.1.3",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@hookform/resolvers": "^3.1.1",
|
"@hookform/resolvers": "^3.3.1",
|
||||||
"@mapbox/mapbox-sdk": "^0.15.2",
|
"@mapbox/mapbox-sdk": "^0.15.2",
|
||||||
"@mapbox/search-js-core": "^1.0.0-beta.17",
|
"@mapbox/search-js-core": "^1.0.0-beta.17",
|
||||||
"@mapbox/search-js-react": "^1.0.0-beta.17",
|
"@mapbox/search-js-react": "^1.0.0-beta.17",
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"@react-email/render": "^0.0.7",
|
"@react-email/render": "^0.0.7",
|
||||||
"@react-email/tailwind": "^0.0.8",
|
"@react-email/tailwind": "^0.0.8",
|
||||||
"@vercel/analytics": "^1.0.1",
|
"@vercel/analytics": "^1.1.0",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.31.1",
|
||||||
"cloudinary": "^1.37.3",
|
"cloudinary": "^1.41.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"mapbox-gl": "^2.15.0",
|
"mapbox-gl": "^2.15.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "^13.4.10",
|
"next": "^13.5.4",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -70,14 +70,14 @@
|
|||||||
"@types/sparkpost": "^2.1.5",
|
"@types/sparkpost": "^2.1.5",
|
||||||
"@vercel/fetch": "^7.0.0",
|
"@vercel/fetch": "^7.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"daisyui": "^3.2.1",
|
"daisyui": "^3.9.2",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.2.1",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.51.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.1.0",
|
"eslint-config-airbnb-typescript": "17.1.0",
|
||||||
"eslint-config-next": "^13.4.10",
|
"eslint-config-next": "^13.5.4",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"onchange": "^7.1.0",
|
"onchange": "^7.1.0",
|
||||||
"postcss": "^8.4.26",
|
"postcss": "^8.4.26",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "./src/prisma/schema.prisma",
|
"schema": "./src/prisma/schema.prisma",
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const BeerInfoHeader: FC<BeerInfoHeaderProps> = ({ beerPost }) => {
|
|||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
className="link-hover link text-lg font-bold"
|
className="link-hover link text-lg font-bold"
|
||||||
href={`/beers/types/${beerPost.style.id}`}
|
href={`/beers/styles/${beerPost.style.id}`}
|
||||||
>
|
>
|
||||||
{beerPost.style.name}
|
{beerPost.style.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const BeerRecommendationsSection: FC<{
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Link className="link-hover link" href={`/beers/${post.id}`}>
|
<Link className="link-hover link" href={`/beers/${post.id}`}>
|
||||||
<span className="text-xl font-semibold">{post.name}</span>
|
<span className="text-xl font-bold">{post.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@@ -70,7 +70,12 @@ const BeerRecommendationsSection: FC<{
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg font-medium">{post.style.name}</span>
|
<Link
|
||||||
|
className="link link-hover"
|
||||||
|
href={`/beers/styles/${post.style.id}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{post.style.name}</span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<span>{post.abv.toFixed(1)}% ABV</span>
|
<span>{post.abv.toFixed(1)}% ABV</span>
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md lg:text-xl">{post.style.name}</p>
|
<Link
|
||||||
|
className="text-md lg:text-xl hover:underline"
|
||||||
|
href={`/beers/styles/${post.style.id}`}
|
||||||
|
>
|
||||||
|
{post.style.name}
|
||||||
|
</Link>
|
||||||
<div className="space-x-3">
|
<div className="space-x-3">
|
||||||
<span className="text-sm lg:text-lg">{post.abv.toFixed(1)}% ABV</span>
|
<span className="text-sm lg:text-lg">{post.abv.toFixed(1)}% ABV</span>
|
||||||
<span className="text-sm lg:text-lg">{post.ibu.toFixed(1)} IBU</span>
|
<span className="text-sm lg:text-lg">{post.ibu.toFixed(1)} IBU</span>
|
||||||
|
|||||||
66
src/components/BeerStyleById/BeerStyleCommentForm.tsx
Normal file
66
src/components/BeerStyleById/BeerStyleCommentForm.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
|
|
||||||
|
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
||||||
|
import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments';
|
||||||
|
import sendCreateBeerStyleCommentRequest from '@/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest';
|
||||||
|
import CommentForm from '../ui/CommentForm';
|
||||||
|
|
||||||
|
interface BeerCommentFormProps {
|
||||||
|
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
||||||
|
mutate: ReturnType<typeof useBeerStyleComments>['mutate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerStyleCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||||
|
beerStyle,
|
||||||
|
mutate,
|
||||||
|
}) => {
|
||||||
|
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
|
||||||
|
z.infer<typeof CreateCommentValidationSchema>
|
||||||
|
>({
|
||||||
|
defaultValues: { rating: 0 },
|
||||||
|
resolver: zodResolver(CreateCommentValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||||
|
data,
|
||||||
|
) => {
|
||||||
|
const loadingToast = toast.loading('Posting a new comment...');
|
||||||
|
try {
|
||||||
|
await sendCreateBeerStyleCommentRequest({
|
||||||
|
content: data.content,
|
||||||
|
rating: data.rating,
|
||||||
|
beerStyleId: beerStyle.id,
|
||||||
|
});
|
||||||
|
reset();
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
toast.success('Comment posted successfully.');
|
||||||
|
await mutate();
|
||||||
|
} catch (error) {
|
||||||
|
await mutate();
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
createErrorToast(error);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommentForm
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
watch={watch}
|
||||||
|
setValue={setValue}
|
||||||
|
formState={formState}
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerStyleCommentForm;
|
||||||
97
src/components/BeerStyleById/BeerStyleCommentSection.tsx
Normal file
97
src/components/BeerStyleById/BeerStyleCommentSection.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
|
||||||
|
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
|
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
||||||
|
import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments';
|
||||||
|
import LoadingComponent from '../BeerById/LoadingComponent';
|
||||||
|
import CommentsComponent from '../ui/CommentsComponent';
|
||||||
|
import BeerStyleCommentForm from './BeerStyleCommentForm';
|
||||||
|
|
||||||
|
interface BeerStyleCommentsSectionProps {
|
||||||
|
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerStyleCommentsSection: FC<BeerStyleCommentsSectionProps> = ({ beerStyle }) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const pageNum = parseInt(router.query.comments_page as string, 10) || 1;
|
||||||
|
const PAGE_SIZE = 15;
|
||||||
|
|
||||||
|
const { comments, isLoading, mutate, setSize, size, isLoadingMore, isAtEnd } =
|
||||||
|
useBeerStyleComments({ id: beerStyle.id, pageNum, pageSize: PAGE_SIZE });
|
||||||
|
|
||||||
|
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
|
||||||
|
const handleDeleteRequest = async (id: string) => {
|
||||||
|
const response = await fetch(`/api/beer-style-comments/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete comment.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditRequest = async (
|
||||||
|
id: string,
|
||||||
|
data: z.infer<typeof CreateCommentValidationSchema>,
|
||||||
|
) => {
|
||||||
|
const response = await fetch(`/api/beer-style-comments/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content: data.content, rating: data.rating }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-3" ref={commentSectionRef}>
|
||||||
|
<div className="card bg-base-300">
|
||||||
|
<div className="card-body h-full">
|
||||||
|
{user ? (
|
||||||
|
<BeerStyleCommentForm beerStyle={beerStyle} mutate={mutate} />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-52 flex-col items-center justify-center">
|
||||||
|
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If the comments are loading, show a loading component. Otherwise, show the
|
||||||
|
* comments.
|
||||||
|
*/
|
||||||
|
isLoading ? (
|
||||||
|
<div className="card bg-base-300 pb-6">
|
||||||
|
<LoadingComponent length={PAGE_SIZE} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommentsComponent
|
||||||
|
commentSectionRef={commentSectionRef}
|
||||||
|
comments={comments}
|
||||||
|
isLoadingMore={isLoadingMore}
|
||||||
|
isAtEnd={isAtEnd}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
setSize={setSize}
|
||||||
|
size={size}
|
||||||
|
mutate={mutate}
|
||||||
|
handleDeleteRequest={handleDeleteRequest}
|
||||||
|
handleEditRequest={handleEditRequest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerStyleCommentsSection;
|
||||||
112
src/components/BeerStyleById/BeerStyleHeader.tsx
Normal file
112
src/components/BeerStyleById/BeerStyleHeader.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { FC, useContext } from 'react';
|
||||||
|
|
||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import { FaRegEdit } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||||
|
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
||||||
|
import useBeerStyleLikeCount from '@/hooks/data-fetching/beer-style-likes/useBeerStyleLikeCount';
|
||||||
|
import BeerStyleLikeButton from './BeerStyleLikeButton';
|
||||||
|
|
||||||
|
interface BeerInfoHeaderProps {
|
||||||
|
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerStyleHeader: FC<BeerInfoHeaderProps> = ({ beerStyle }) => {
|
||||||
|
const createdAt = new Date(beerStyle.createdAt);
|
||||||
|
const timeDistance = useTimeDistance(createdAt);
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const idMatches = user && beerStyle.postedBy.id === user.id;
|
||||||
|
const isPostOwner = !!(user && idMatches);
|
||||||
|
|
||||||
|
const { likeCount, mutate } = useBeerStyleLikeCount(beerStyle.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="card flex flex-col justify-center bg-base-300">
|
||||||
|
<div className="card-body">
|
||||||
|
<header className="flex justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold lg:text-4xl">{beerStyle.name}</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="italic">
|
||||||
|
{' posted by '}
|
||||||
|
<Link
|
||||||
|
href={`/users/${beerStyle.postedBy.id}`}
|
||||||
|
className="link-hover link"
|
||||||
|
>
|
||||||
|
{`${beerStyle.postedBy.username} `}
|
||||||
|
</Link>
|
||||||
|
{timeDistance && (
|
||||||
|
<span
|
||||||
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||||
|
>
|
||||||
|
{`${timeDistance} ago`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPostOwner && (
|
||||||
|
<div className="tooltip tooltip-left" data-tip={`Edit '${beerStyle.name}'`}>
|
||||||
|
<Link href={`/beers/${beerStyle.id}/edit`} className="btn-ghost btn-xs btn">
|
||||||
|
<FaRegEdit className="text-xl" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<p>{beerStyle.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex w-25 space-x-3 flex-row">
|
||||||
|
<div className="text-sm font-bold">
|
||||||
|
ABV Range:{' '}
|
||||||
|
<span>
|
||||||
|
{beerStyle.abvRange[0].toFixed(1)}% - {beerStyle.abvRange[0].toFixed(1)}
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-bold">
|
||||||
|
IBU Range:{' '}
|
||||||
|
<span>
|
||||||
|
{beerStyle.ibuRange[0].toFixed(1)} - {beerStyle.ibuRange[1].toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold">
|
||||||
|
Recommended Glassware:{' '}
|
||||||
|
<span className="text-sm font-bold italic">{beerStyle.glassware.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
{(!!likeCount || likeCount === 0) && (
|
||||||
|
<span>
|
||||||
|
Liked by {likeCount}
|
||||||
|
{likeCount !== 1 ? ' users' : ' user'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions items-end">
|
||||||
|
{user && (
|
||||||
|
<BeerStyleLikeButton beerStyleId={beerStyle.id} mutateCount={mutate} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerStyleHeader;
|
||||||
34
src/components/BeerStyleById/BeerStyleLikeButton.tsx
Normal file
34
src/components/BeerStyleById/BeerStyleLikeButton.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
||||||
|
import useCheckIfUserLikesBeerStyle from '@/hooks/data-fetching/beer-style-likes/useCheckIfUserLikesBeerPost';
|
||||||
|
import sendBeerStyleLikeRequest from '@/requests/BeerStyleLike/sendBeerStyleLikeRequest';
|
||||||
|
import LikeButton from '../ui/LikeButton';
|
||||||
|
|
||||||
|
const BeerStyleLikeButton: FC<{
|
||||||
|
beerStyleId: string;
|
||||||
|
mutateCount: ReturnType<typeof useGetBeerPostLikeCount>['mutate'];
|
||||||
|
}> = ({ beerStyleId, mutateCount }) => {
|
||||||
|
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerStyle(beerStyleId);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [isLiked]);
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await sendBeerStyleLikeRequest(beerStyleId);
|
||||||
|
|
||||||
|
await Promise.all([mutateCount(), mutateLikeStatus()]);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <LikeButton isLiked={!!isLiked} handleLike={handleLike} loading={loading} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerStyleLikeButton;
|
||||||
@@ -11,7 +11,7 @@ const BeerStyleCard: FC<{ beerStyle: z.infer<typeof BeerStyleQueryResult> }> = (
|
|||||||
<div className="card card-compact bg-base-300">
|
<div className="card card-compact bg-base-300">
|
||||||
<div className="card-body justify-between">
|
<div className="card-body justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Link href={`/beers/types/${beerStyle.id}`}>
|
<Link href={`/beers/styles/${beerStyle.id}`}>
|
||||||
<h3 className="link-hover link overflow-hidden whitespace-normal text-2xl font-bold lg:truncate lg:text-3xl">
|
<h3 className="link-hover link overflow-hidden whitespace-normal text-2xl font-bold lg:truncate lg:text-3xl">
|
||||||
{beerStyle.name}
|
{beerStyle.name}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -20,7 +20,7 @@ const BeerStyleCard: FC<{ beerStyle: z.infer<typeof BeerStyleQueryResult> }> = (
|
|||||||
<div className="text-sm font-bold">
|
<div className="text-sm font-bold">
|
||||||
ABV Range:{' '}
|
ABV Range:{' '}
|
||||||
<span>
|
<span>
|
||||||
{beerStyle.abvRange[0].toFixed(1)}% - {beerStyle.abvRange[1].toFixed(1)}%
|
{beerStyle.abvRange[0].toFixed(1)}% - {beerStyle.abvRange[0].toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold">
|
<div className="text-sm font-bold">
|
||||||
@@ -31,8 +31,8 @@ const BeerStyleCard: FC<{ beerStyle: z.infer<typeof BeerStyleQueryResult> }> = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="h-20">
|
||||||
<p>{beerStyle.description}</p>
|
<p className="overflow-ellipsis line-clamp-3">{beerStyle.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="font-semibold">
|
<div className="font-semibold">
|
||||||
@@ -76,7 +76,12 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg font-medium">{beerPost.style.name}</span>
|
<Link
|
||||||
|
className="text-lg font-medium link link-hover"
|
||||||
|
href={`/beers/styles/${beerPost.style.id}`}
|
||||||
|
>
|
||||||
|
{beerPost.style.name}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<span>{beerPost.abv}% ABV</span>
|
<span>{beerPost.abv}% ABV</span>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
|
||||||
|
interface UseBeerStyleCommentsProps {
|
||||||
|
id: string;
|
||||||
|
pageSize: number;
|
||||||
|
pageNum: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBeerStyleComments = ({ id, pageSize }: UseBeerStyleCommentsProps) => {
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const count = response.headers.get('X-Total-Count');
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(parsed.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload);
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error(parsedPayload.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||||
|
return { comments: parsedPayload.data, pageCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate, size, setSize } = useSWRInfinite(
|
||||||
|
(index) =>
|
||||||
|
`/api/beers/styles/${id}/comments?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
|
fetcher,
|
||||||
|
{ parallel: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const comments = data?.flatMap((d) => d.comments) ?? [];
|
||||||
|
const pageCount = data?.[0].pageCount ?? 0;
|
||||||
|
|
||||||
|
const isLoadingMore =
|
||||||
|
isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
|
||||||
|
|
||||||
|
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||||
|
|
||||||
|
return {
|
||||||
|
comments,
|
||||||
|
isLoading,
|
||||||
|
error: error as undefined,
|
||||||
|
mutate,
|
||||||
|
size,
|
||||||
|
setSize,
|
||||||
|
isLoadingMore,
|
||||||
|
isAtEnd,
|
||||||
|
pageCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBeerStyleComments;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to fetch the like count for a beer style from the server.
|
||||||
|
*
|
||||||
|
* @param beerStyleId - The ID of the beer style to fetch the like count for.
|
||||||
|
* @returns An object with the following properties:
|
||||||
|
*
|
||||||
|
* - `error`: The error that occurred while fetching the like count.
|
||||||
|
* - `isLoading`: A boolean indicating whether the like count is being fetched.
|
||||||
|
* - `mutate`: A function to mutate the like count.
|
||||||
|
* - `likeCount`: The like count for the beer style.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useGetBeerStyleLikeCount = (beerStyleId: string) => {
|
||||||
|
const { error, mutate, data, isLoading } = useSWR(
|
||||||
|
`/api/beers/styles/${beerStyleId}/like`,
|
||||||
|
async (url) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('Failed to parse API response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = z
|
||||||
|
.object({
|
||||||
|
likeCount: z.number(),
|
||||||
|
})
|
||||||
|
.safeParse(parsed.data.payload);
|
||||||
|
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('Failed to parse API response payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload.data.likeCount;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: error as unknown,
|
||||||
|
isLoading,
|
||||||
|
mutate,
|
||||||
|
likeCount: data as number | undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetBeerStyleLikeCount;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that checks if the current user has liked a beer style by fetching
|
||||||
|
* data from the server.
|
||||||
|
*
|
||||||
|
* @param beerStyleId The ID of the beer style to check for likes.
|
||||||
|
* @returns An object with the following properties:
|
||||||
|
*
|
||||||
|
* - `error`: The error that occurred while fetching the data.
|
||||||
|
* - `isLoading`: A boolean indicating whether the data is being fetched.
|
||||||
|
* - `mutate`: A function to mutate the data.
|
||||||
|
* - `isLiked`: A boolean indicating whether the current user has liked the beer style.
|
||||||
|
*/
|
||||||
|
const useCheckIfUserLikesBeerStyle = (beerStyleId: string) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
`/api/beers/styles/${beerStyleId}/like/is-liked`,
|
||||||
|
async (url) => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User is not logged in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const json = await response.json();
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('Invalid API response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { payload } = parsed.data;
|
||||||
|
const parsedPayload = z.object({ isLiked: z.boolean() }).safeParse(payload);
|
||||||
|
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('Invalid API response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isLiked } = parsedPayload.data;
|
||||||
|
|
||||||
|
return isLiked;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLiked: data,
|
||||||
|
error: error as unknown,
|
||||||
|
isLoading,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCheckIfUserLikesBeerStyle;
|
||||||
@@ -4,15 +4,14 @@ import '@/styles/globals.css';
|
|||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { themeChange } from 'theme-change';
|
import { themeChange } from 'theme-change';
|
||||||
|
|
||||||
import { Analytics } from '@vercel/analytics/react';
|
import { Analytics } from '@vercel/analytics/react';
|
||||||
|
|
||||||
import { Space_Grotesk } from 'next/font/google';
|
import { Nunito_Sans } from 'next/font/google';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import CustomToast from '@/components/ui/CustomToast';
|
import CustomToast from '@/components/ui/CustomToast';
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({ subsets: ['latin'] });
|
const font = Nunito_Sans({ subsets: ['latin-ext'] });
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -24,7 +23,7 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<style jsx global>
|
<style jsx global>
|
||||||
{`
|
{`
|
||||||
html {
|
html {
|
||||||
font-family: ${spaceGrotesk.style.fontFamily};
|
font-family: ${font.style.fontFamily};
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
106
src/pages/api/beer-style-comments/[id].ts
Normal file
106
src/pages/api/beer-style-comments/[id].ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { createRouter, NextHandler } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface DeleteCommentRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditCommentRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
body: z.infer<typeof CreateCommentValidationSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfCommentOwner = async (
|
||||||
|
req: DeleteCommentRequest | EditCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
next: NextHandler,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
const user = req.user!;
|
||||||
|
const comment = await DBClient.instance.beerStyleComment.findFirst({ where: { id } });
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
throw new ServerError('Comment not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.postedById !== user.id) {
|
||||||
|
throw new ServerError('You are not authorized to modify this comment', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const editComment = async (
|
||||||
|
req: EditCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
const updated = await DBClient.instance.beerStyleComment.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
content: req.body.content,
|
||||||
|
rating: req.body.rating,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Comment updated successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: updated,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteComment = async (
|
||||||
|
req: DeleteCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
await DBClient.instance.beerStyleComment.delete({ where: { id } });
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Comment deleted successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
DeleteCommentRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router
|
||||||
|
.delete(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
|
getCurrentUser,
|
||||||
|
checkIfCommentOwner,
|
||||||
|
deleteComment,
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
|
bodySchema: CreateCommentValidationSchema,
|
||||||
|
}),
|
||||||
|
getCurrentUser,
|
||||||
|
checkIfCommentOwner,
|
||||||
|
editComment,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
@@ -8,9 +8,9 @@ import { NextApiResponse } from 'next';
|
|||||||
import { createRouter, NextHandler } from 'next-connect';
|
import { createRouter, NextHandler } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import DBClient from '@/prisma/DBClient';
|
|
||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import deleteBeerPostById from '@/services/BeerPost/deleteBeerPostById';
|
||||||
|
|
||||||
interface BeerPostRequest extends UserExtendedNextApiRequest {
|
interface BeerPostRequest extends UserExtendedNextApiRequest {
|
||||||
query: { id: string };
|
query: { id: string };
|
||||||
@@ -45,12 +45,7 @@ const editBeerPost = async (
|
|||||||
req: EditBeerPostRequest,
|
req: EditBeerPostRequest,
|
||||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
await editBeerPostById({ id: req.query.id, data: req.body });
|
||||||
body,
|
|
||||||
query: { id },
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
await editBeerPostById(id, body);
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'Beer post updated successfully',
|
message: 'Beer post updated successfully',
|
||||||
@@ -60,14 +55,9 @@ const editBeerPost = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
||||||
const {
|
const { id } = req.query;
|
||||||
query: { id },
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
const deleted = await DBClient.instance.beerPost.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const deleted = deleteBeerPostById({ beerPostId: id });
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
throw new ServerError('Beer post not found', 404);
|
throw new ServerError('Beer post not found', 404);
|
||||||
}
|
}
|
||||||
@@ -78,12 +68,14 @@ const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = createRouter<
|
const router = createRouter<
|
||||||
EditBeerPostRequest,
|
EditBeerPostRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
router.put(
|
router
|
||||||
|
.put(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
bodySchema: EditBeerPostValidationSchema,
|
bodySchema: EditBeerPostValidationSchema,
|
||||||
querySchema: z.object({ id: z.string() }),
|
querySchema: z.object({ id: z.string() }),
|
||||||
@@ -91,13 +83,13 @@ router.put(
|
|||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfBeerPostOwner,
|
checkIfBeerPostOwner,
|
||||||
editBeerPost,
|
editBeerPost,
|
||||||
);
|
)
|
||||||
router.delete(
|
.delete(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string() }) }),
|
validateRequest({ querySchema: z.object({ id: z.string() }) }),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfBeerPostOwner,
|
checkIfBeerPostOwner,
|
||||||
deleteBeerPost,
|
deleteBeerPost,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handler = router.handler(NextConnectOptions);
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const getBeerPosts = async (
|
|||||||
const pageNum = parseInt(req.query.page_num, 10);
|
const pageNum = parseInt(req.query.page_num, 10);
|
||||||
const pageSize = parseInt(req.query.page_size, 10);
|
const pageSize = parseInt(req.query.page_size, 10);
|
||||||
|
|
||||||
const beerPosts = await getAllBeerPosts(pageNum, pageSize);
|
const beerPosts = await getAllBeerPosts({ pageNum, pageSize });
|
||||||
const beerPostCount = await DBClient.instance.beerPost.count();
|
const beerPostCount = await DBClient.instance.beerPost.count();
|
||||||
|
|
||||||
res.setHeader('X-Total-Count', beerPostCount);
|
res.setHeader('X-Total-Count', beerPostCount);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
|
|||||||
ibu: true,
|
ibu: true,
|
||||||
abv: true,
|
abv: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
description: true,
|
description: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
brewery: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
|
|||||||
103
src/pages/api/beers/styles/[id]/comments/index.ts
Normal file
103
src/pages/api/beers/styles/[id]/comments/index.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
|
||||||
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import createNewBeerStyleComment from '@/services/BeerStyleComment/createNewBeerStyleComment';
|
||||||
|
import getAllBeerStyleComments from '@/services/BeerStyleComment/getAllBeerStyleComments';
|
||||||
|
|
||||||
|
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||||
|
body: z.infer<typeof CreateCommentValidationSchema>;
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string; page_size: string; page_num: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const createComment = async (
|
||||||
|
req: CreateCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { content, rating } = req.body;
|
||||||
|
|
||||||
|
const newBeerStyleComment: z.infer<typeof CommentQueryResult> =
|
||||||
|
await createNewBeerStyleComment({
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
beerStyleId: req.query.id,
|
||||||
|
userId: req.user!.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Beer comment created successfully',
|
||||||
|
statusCode: 201,
|
||||||
|
payload: newBeerStyleComment,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAll = async (
|
||||||
|
req: GetAllCommentsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const beerStyleId = req.query.id;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { page_size, page_num } = req.query;
|
||||||
|
|
||||||
|
const comments = await getAllBeerStyleComments({
|
||||||
|
beerStyleId,
|
||||||
|
pageNum: parseInt(page_num, 10),
|
||||||
|
pageSize: parseInt(page_size, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageCount = await DBClient.instance.beerStyleComment.count({
|
||||||
|
where: { beerStyleId },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', pageCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Beer comments fetched successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: comments,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
// 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>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
validateRequest({
|
||||||
|
bodySchema: CreateCommentValidationSchema,
|
||||||
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
|
getCurrentUser,
|
||||||
|
createComment,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
id: z.string().cuid(),
|
||||||
|
page_size: z.coerce.number().int().positive(),
|
||||||
|
page_num: z.coerce.number().int().positive(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
41
src/pages/api/beers/styles/[id]/index.ts
Normal file
41
src/pages/api/beers/styles/[id]/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface GetBeerStyleByIdRequest extends NextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBeerStyle = async (
|
||||||
|
req: GetBeerStyleByIdRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
const beerStyle = await getBeerStyleById(id);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Beer types retrieved successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: beerStyle,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetBeerStyleByIdRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
|
getBeerStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler();
|
||||||
|
|
||||||
|
export default handler;
|
||||||
86
src/pages/api/beers/styles/[id]/like/index.ts
Normal file
86
src/pages/api/beers/styles/[id]/like/index.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
|
||||||
|
import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById';
|
||||||
|
import findBeerStyleLikeById from '@/services/BeerStyleLike/findBeerStyleLikeById';
|
||||||
|
import getBeerStyleLikeCount from '@/services/BeerStyleLike/getBeerStyleLikeCount';
|
||||||
|
import createBeerStyleLike from '@/services/BeerStyleLike/createBeerStyleLike';
|
||||||
|
import removeBeerStyleLikeById from '@/services/BeerStyleLike/removeBeerStyleLikeById';
|
||||||
|
|
||||||
|
const sendLikeRequest = async (
|
||||||
|
req: UserExtendedNextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const user = req.user!;
|
||||||
|
const id = req.query.id as string;
|
||||||
|
|
||||||
|
const beerStyle = await getBeerStyleById(id);
|
||||||
|
if (!beerStyle) {
|
||||||
|
throw new ServerError('Could not find a beer style with that id', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyLiked = await findBeerStyleLikeById({
|
||||||
|
beerStyleId: beerStyle.id,
|
||||||
|
likedById: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const jsonResponse = {
|
||||||
|
success: true as const,
|
||||||
|
message: '',
|
||||||
|
statusCode: 200 as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (alreadyLiked) {
|
||||||
|
await removeBeerStyleLikeById({ beerStyleLikeId: alreadyLiked.id });
|
||||||
|
jsonResponse.message = 'Successfully unliked beer style.';
|
||||||
|
} else {
|
||||||
|
await createBeerStyleLike({ beerStyleId: beerStyle.id, user });
|
||||||
|
jsonResponse.message = 'Successfully liked beer style.';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(jsonResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLikeCount = async (
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const id = req.query.id as string;
|
||||||
|
|
||||||
|
const likeCount = await getBeerStyleLikeCount({ beerStyleId: id });
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Successfully retrieved like count.',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: { likeCount },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
UserExtendedNextApiRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
getCurrentUser,
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
|
sendLikeRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
|
getLikeCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
export default handler;
|
||||||
53
src/pages/api/beers/styles/[id]/like/is-liked.ts
Normal file
53
src/pages/api/beers/styles/[id]/like/is-liked.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface FindBeerStyleLikeByIdArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
likedById: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findBeerStyleLikeById = async ({
|
||||||
|
beerStyleId,
|
||||||
|
likedById,
|
||||||
|
}: FindBeerStyleLikeByIdArgs) => {
|
||||||
|
return DBClient.instance.beerStyleLike.findFirst({
|
||||||
|
where: { beerStyleId, likedById },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfLiked = async (
|
||||||
|
req: UserExtendedNextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const user = req.user!;
|
||||||
|
const beerStyleId = req.query.id as string;
|
||||||
|
|
||||||
|
const alreadyLiked = await findBeerStyleLikeById({ beerStyleId, likedById: user.id });
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: alreadyLiked ? 'Beer style is liked.' : 'Beer style is not liked.',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: { isLiked: !!alreadyLiked },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
UserExtendedNextApiRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
getCurrentUser,
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||||
|
checkIfLiked,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
@@ -29,6 +29,7 @@ const getAllBeersByBrewery = async (
|
|||||||
ibu: true,
|
ibu: true,
|
||||||
abv: true,
|
abv: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
description: true,
|
description: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
brewery: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
|
|||||||
@@ -61,10 +61,11 @@ const getAll = async (
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
const { page_size, page_num } = req.query;
|
const { page_size, page_num } = req.query;
|
||||||
|
|
||||||
const comments = await getAllBreweryComments(
|
const comments = await getAllBreweryComments({
|
||||||
{ id: breweryPostId },
|
id: breweryPostId,
|
||||||
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
|
pageNum: parseInt(page_num, 10),
|
||||||
);
|
pageSize: parseInt(page_size, 10),
|
||||||
|
});
|
||||||
|
|
||||||
const pageCount = await DBClient.instance.breweryComment.count({
|
const pageCount = await DBClient.instance.breweryComment.count({
|
||||||
where: { breweryPostId },
|
where: { breweryPostId },
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const getBreweryPosts = async (
|
|||||||
const pageNum = parseInt(req.query.page_num, 10);
|
const pageNum = parseInt(req.query.page_num, 10);
|
||||||
const pageSize = parseInt(req.query.page_size, 10);
|
const pageSize = parseInt(req.query.page_size, 10);
|
||||||
|
|
||||||
const breweryPosts = await getAllBreweryPosts(pageNum, pageSize);
|
const breweryPosts = await getAllBreweryPosts({ pageNum, pageSize });
|
||||||
const breweryPostCount = await DBClient.instance.breweryPost.count();
|
const breweryPostCount = await DBClient.instance.breweryPost.count();
|
||||||
|
|
||||||
res.setHeader('X-Total-Count', breweryPostCount);
|
res.setHeader('X-Total-Count', breweryPostCount);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
|||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import getBreweryCommentById from '@/services/BreweryComment/getBreweryCommentById';
|
||||||
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
@@ -27,9 +28,7 @@ const checkIfCommentOwner = async (
|
|||||||
) => {
|
) => {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
const user = req.user!;
|
const user = req.user!;
|
||||||
const comment = await DBClient.instance.breweryComment.findUnique({
|
const comment = await getBreweryCommentById(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
throw new ServerError('Comment not found', 404);
|
throw new ServerError('Comment not found', 404);
|
||||||
@@ -87,7 +86,9 @@ const router = createRouter<
|
|||||||
|
|
||||||
router
|
router
|
||||||
.delete(
|
.delete(
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
validateRequest({
|
||||||
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfCommentOwner,
|
checkIfCommentOwner,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
|
|||||||
70
src/pages/beers/styles/[id]/index.tsx
Normal file
70
src/pages/beers/styles/[id]/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { NextPage, GetServerSideProps } from 'next';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import getBeerStyleById from '@/services/BeerStyles/getBeerStyleById';
|
||||||
|
import BeerStyleHeader from '@/components/BeerStyleById/BeerStyleHeader';
|
||||||
|
import BeerStyleQueryResult from '@/services/BeerStyles/schema/BeerStyleQueryResult';
|
||||||
|
import BeerStyleCommentSection from '@/components/BeerStyleById/BeerStyleCommentSection';
|
||||||
|
|
||||||
|
interface BeerStylePageProps {
|
||||||
|
beerStyle: z.infer<typeof BeerStyleQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerStyleByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
|
||||||
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{beerStyle.name}</title>
|
||||||
|
<meta name="description" content={beerStyle.description} />
|
||||||
|
</Head>
|
||||||
|
<>
|
||||||
|
<main className="mb-12 mt-10 flex w-full items-center justify-center">
|
||||||
|
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||||
|
<BeerStyleHeader beerStyle={beerStyle} />
|
||||||
|
|
||||||
|
{isDesktop ? (
|
||||||
|
<div className="mt-4 flex flex-row space-x-3 space-y-0">
|
||||||
|
<div className="w-[60%]">
|
||||||
|
<BeerStyleCommentSection beerStyle={beerStyle} />
|
||||||
|
</div>
|
||||||
|
<div className="w-[40%]">{/* Beers of this style go here */}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
||||||
|
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||||
|
Comments
|
||||||
|
</Tab>
|
||||||
|
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||||
|
Beers in this Style
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels className="mt-2">
|
||||||
|
<Tab.Panel>
|
||||||
|
<BeerStyleCommentSection beerStyle={beerStyle} />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>{/* Beers of this style go here */}</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerStyleByIdPage;
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||||
|
const id = params!.id as string;
|
||||||
|
const beerStyle = await getBeerStyleById(id);
|
||||||
|
|
||||||
|
return { props: { beerStyle: JSON.parse(JSON.stringify(beerStyle)) } };
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import { MutableRefObject, useRef } from 'react';
|
|||||||
import { FaArrowUp } from 'react-icons/fa';
|
import { FaArrowUp } from 'react-icons/fa';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
|
||||||
import BeerStyleCard from '@/components/BeerStyle/BeerStyleCard';
|
import BeerStyleCard from '@/components/BeerStyleIndex/BeerStyleCard';
|
||||||
import SmLoadingCard from '@/components/ui/SmLoadingCard';
|
import SmLoadingCard from '@/components/ui/SmLoadingCard';
|
||||||
import Spinner from '@/components/ui/Spinner';
|
import Spinner from '@/components/ui/Spinner';
|
||||||
import useBeerStyles from '@/hooks/data-fetching/beer-styles/useBeerStyles';
|
import useBeerStyles from '@/hooks/data-fetching/beer-styles/useBeerStyles';
|
||||||
@@ -38,7 +38,7 @@ const BeerStylePage: NextPage = () => {
|
|||||||
<header className="my-10 flex justify-between lg:flex-row">
|
<header className="my-10 flex justify-between lg:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten App</h1>
|
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten App</h1>
|
||||||
<h2 className="text-2xl font-bold lg:text-4xl">Types</h2>
|
<h2 className="text-2xl font-bold lg:text-4xl">Styles</h2>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="grid gap-6 xl:grid-cols-2">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
|
|||||||
35
src/prisma/migrations/20231009153905_/migration.sql
Normal file
35
src/prisma/migrations/20231009153905_/migration.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerStyleLike" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"beerStyleId" TEXT NOT NULL,
|
||||||
|
"likedById" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BeerStyleLike_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerStyleComment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"rating" INTEGER NOT NULL,
|
||||||
|
"beerStyleId" TEXT NOT NULL,
|
||||||
|
"postedById" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BeerStyleComment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerStyleLike" ADD CONSTRAINT "BeerStyleLike_beerStyleId_fkey" FOREIGN KEY ("beerStyleId") REFERENCES "BeerStyle"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerStyleLike" ADD CONSTRAINT "BeerStyleLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerStyleComment" ADD CONSTRAINT "BeerStyleComment_beerStyleId_fkey" FOREIGN KEY ("beerStyleId") REFERENCES "BeerStyle"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerStyleComment" ADD CONSTRAINT "BeerStyleComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -40,6 +40,8 @@ model User {
|
|||||||
BreweryPostLike BreweryPostLike[]
|
BreweryPostLike BreweryPostLike[]
|
||||||
Location Location[]
|
Location Location[]
|
||||||
Glassware Glassware[]
|
Glassware Glassware[]
|
||||||
|
BeerStyleLike BeerStyleLike[]
|
||||||
|
BeerStyleComment BeerStyleComment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model BeerPost {
|
model BeerPost {
|
||||||
@@ -106,6 +108,30 @@ model BeerStyle {
|
|||||||
abvRange Float[]
|
abvRange Float[]
|
||||||
ibuRange Float[]
|
ibuRange Float[]
|
||||||
beerPosts BeerPost[]
|
beerPosts BeerPost[]
|
||||||
|
BeerStyleLike BeerStyleLike[]
|
||||||
|
BeerStyleComment BeerStyleComment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model BeerStyleLike {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
beerStyle BeerStyle @relation(fields: [beerStyleId], references: [id], onDelete: Cascade)
|
||||||
|
beerStyleId String
|
||||||
|
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
||||||
|
likedById String
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
model BeerStyleComment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
rating Int
|
||||||
|
beerStyle BeerStyle @relation(fields: [beerStyleId], references: [id], onDelete: Cascade)
|
||||||
|
beerStyleId String
|
||||||
|
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||||
|
postedById String
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Glassware {
|
model Glassware {
|
||||||
|
|||||||
56
src/prisma/seed/create/createNewBeerStyleComments.ts
Normal file
56
src/prisma/seed/create/createNewBeerStyleComments.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { BeerStyle, User } from '@prisma/client';
|
||||||
|
|
||||||
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
|
interface CreateNewBeerCommentsArgs {
|
||||||
|
numberOfComments: number;
|
||||||
|
joinData: {
|
||||||
|
beerStyles: BeerStyle[];
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeerStyleComment {
|
||||||
|
content: string;
|
||||||
|
postedById: string;
|
||||||
|
beerStyleId: string;
|
||||||
|
rating: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewBeerStyleComments = async ({
|
||||||
|
numberOfComments,
|
||||||
|
joinData,
|
||||||
|
}: CreateNewBeerCommentsArgs) => {
|
||||||
|
const { beerStyles, users } = joinData;
|
||||||
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
|
const beerStyleCommentData: BeerStyleComment[] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0; i < numberOfComments; i++) {
|
||||||
|
const content = faker.lorem.lines(5);
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
const beerStyle = beerStyles[Math.floor(Math.random() * beerStyles.length)];
|
||||||
|
const createdAt = faker.date.past({ years: 1 });
|
||||||
|
const rating = Math.floor(Math.random() * 5) + 1;
|
||||||
|
|
||||||
|
beerStyleCommentData.push({
|
||||||
|
content,
|
||||||
|
postedById: user.id,
|
||||||
|
beerStyleId: beerStyle.id,
|
||||||
|
createdAt,
|
||||||
|
rating,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.beerStyleComment.createMany({
|
||||||
|
data: beerStyleCommentData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return prisma.beerStyleComment.findMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewBeerStyleComments;
|
||||||
44
src/prisma/seed/create/createNewBeerStyleLikes.ts
Normal file
44
src/prisma/seed/create/createNewBeerStyleLikes.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { BeerStyle, User } from '@prisma/client';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
|
interface BeerPostLikeData {
|
||||||
|
beerStyleId: string;
|
||||||
|
likedById: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateNewBeerStyleLikesArgs {
|
||||||
|
joinData: {
|
||||||
|
beerStyles: BeerStyle[];
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
|
numberOfLikes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewBeerStyleLikes = async ({
|
||||||
|
joinData: { beerStyles, users },
|
||||||
|
numberOfLikes,
|
||||||
|
}: CreateNewBeerStyleLikesArgs) => {
|
||||||
|
const beerStyleLikeData: BeerPostLikeData[] = [];
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0; i < numberOfLikes; i++) {
|
||||||
|
const beerStyle = beerStyles[Math.floor(Math.random() * beerStyles.length)];
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
const createdAt = faker.date.past({ years: 1 });
|
||||||
|
beerStyleLikeData.push({
|
||||||
|
beerStyleId: beerStyle.id,
|
||||||
|
likedById: user.id,
|
||||||
|
createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await DBClient.instance.beerStyleLike.createMany({
|
||||||
|
data: beerStyleLikeData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return DBClient.instance.beerStyleLike.findMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewBeerStyleLikes;
|
||||||
@@ -16,6 +16,8 @@ import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
|
|||||||
import createNewLocations from './create/createNewLocations';
|
import createNewLocations from './create/createNewLocations';
|
||||||
import logger from '../../config/pino/logger';
|
import logger from '../../config/pino/logger';
|
||||||
import createAdminUser from './create/createAdminUser';
|
import createAdminUser from './create/createAdminUser';
|
||||||
|
import createNewBeerStyleComments from './create/createNewBeerStyleComments';
|
||||||
|
import createNewBeerStyleLikes from './create/createNewBeerStyleLikes';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -51,11 +53,15 @@ import createAdminUser from './create/createAdminUser';
|
|||||||
|
|
||||||
logger.info('Beer posts created successfully.');
|
logger.info('Beer posts created successfully.');
|
||||||
|
|
||||||
const [beerPostComments, breweryPostComments] = await Promise.all([
|
const [beerPostComments, beerStyleComments, breweryPostComments] = await Promise.all([
|
||||||
createNewBeerPostComments({
|
createNewBeerPostComments({
|
||||||
numberOfComments: 100000,
|
numberOfComments: 100000,
|
||||||
joinData: { beerPosts, users },
|
joinData: { beerPosts, users },
|
||||||
}),
|
}),
|
||||||
|
createNewBeerStyleComments({
|
||||||
|
numberOfComments: 5000,
|
||||||
|
joinData: { beerStyles, users },
|
||||||
|
}),
|
||||||
createNewBreweryPostComments({
|
createNewBreweryPostComments({
|
||||||
numberOfComments: 50000,
|
numberOfComments: 50000,
|
||||||
joinData: { breweryPosts, users },
|
joinData: { breweryPosts, users },
|
||||||
@@ -63,11 +69,15 @@ import createAdminUser from './create/createAdminUser';
|
|||||||
]);
|
]);
|
||||||
logger.info('Created beer post comments and brewery post comments.');
|
logger.info('Created beer post comments and brewery post comments.');
|
||||||
|
|
||||||
const [beerPostLikes, breweryPostLikes] = await Promise.all([
|
const [beerPostLikes, beerStyleLikes, breweryPostLikes] = await Promise.all([
|
||||||
createNewBeerPostLikes({
|
createNewBeerPostLikes({
|
||||||
numberOfLikes: 500000,
|
numberOfLikes: 500000,
|
||||||
joinData: { beerPosts, users },
|
joinData: { beerPosts, users },
|
||||||
}),
|
}),
|
||||||
|
createNewBeerStyleLikes({
|
||||||
|
numberOfLikes: 50000,
|
||||||
|
joinData: { beerStyles, users },
|
||||||
|
}),
|
||||||
createNewBreweryPostLikes({
|
createNewBreweryPostLikes({
|
||||||
numberOfLikes: 100000,
|
numberOfLikes: 100000,
|
||||||
joinData: { breweryPosts, users },
|
joinData: { breweryPosts, users },
|
||||||
@@ -96,6 +106,8 @@ import createAdminUser from './create/createAdminUser';
|
|||||||
numberOfBreweryPosts: breweryPosts.length,
|
numberOfBreweryPosts: breweryPosts.length,
|
||||||
numberOfBeerPosts: beerPosts.length,
|
numberOfBeerPosts: beerPosts.length,
|
||||||
numberOfBeerStyles: beerStyles.length,
|
numberOfBeerStyles: beerStyles.length,
|
||||||
|
numberOfBeerStyleLikes: beerStyleLikes.length,
|
||||||
|
numberOfBeerStyleComments: beerStyleComments.length,
|
||||||
numberOfBeerPostLikes: beerPostLikes.length,
|
numberOfBeerPostLikes: beerPostLikes.length,
|
||||||
numberOfBreweryPostLikes: breweryPostLikes.length,
|
numberOfBreweryPostLikes: breweryPostLikes.length,
|
||||||
numberOfBeerPostComments: beerPostComments.length,
|
numberOfBeerPostComments: beerPostComments.length,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
interface IBeerStyle {
|
interface BeerStyle {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
glassware: string;
|
glassware: string;
|
||||||
@@ -6,7 +6,7 @@ interface IBeerStyle {
|
|||||||
ibuRange: [number, number];
|
ibuRange: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const beerStyles: IBeerStyle[] = [
|
const beerStyles: BeerStyle[] = [
|
||||||
{
|
{
|
||||||
name: 'Bock',
|
name: 'Bock',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
export default [
|
interface City {
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canadianCities: City[] = [
|
||||||
{
|
{
|
||||||
city: 'Toronto',
|
city: 'Toronto',
|
||||||
province: 'Ontario',
|
province: 'Ontario',
|
||||||
@@ -10423,3 +10430,5 @@ export default [
|
|||||||
longitude: -110.4739,
|
longitude: -110.4739,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default canadianCities;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
|
||||||
|
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const BeerStyleCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
|
||||||
|
beerStyleId: z.string().cuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a POST request to the server to create a new beer comment.
|
||||||
|
*
|
||||||
|
* @param data The data to be sent to the server.
|
||||||
|
* @param data.beerPostId The ID of the beer post to comment on.
|
||||||
|
* @param data.content The content of the comment.
|
||||||
|
* @param data.rating The rating of the beer.
|
||||||
|
* @returns A promise that resolves to the created comment.
|
||||||
|
* @throws An error if the request fails, the API response is invalid, or the API response
|
||||||
|
* payload is invalid.
|
||||||
|
*/
|
||||||
|
const sendCreateBeerStyleCommentRequest = async ({
|
||||||
|
beerStyleId,
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
}: z.infer<typeof BeerStyleCommentValidationSchemaWithId>) => {
|
||||||
|
const response = await fetch(`/api/beers/styles/${beerStyleId}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ beerStyleId, content, rating }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (!parsedResponse.success) {
|
||||||
|
throw new Error('Invalid API response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
|
||||||
|
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('Invalid API response payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendCreateBeerStyleCommentRequest;
|
||||||
31
src/requests/BeerStyleLike/sendBeerStyleLikeRequest.ts
Normal file
31
src/requests/BeerStyleLike/sendBeerStyleLikeRequest.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a POST request to the server to like or unlike a beer post.
|
||||||
|
*
|
||||||
|
* @param beerStyleId The ID of the beer post to like or unlike.
|
||||||
|
* @returns An object containing a success boolean and a message string.
|
||||||
|
* @throws An error if the response is not ok or if the API response is invalid.
|
||||||
|
*/
|
||||||
|
const sendBeerStyleLikeRequest = async (beerStyleId: string) => {
|
||||||
|
const response = await fetch(`/api/beers/styles/${beerStyleId}/like`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Something went wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('Invalid API response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, message } = parsed.data;
|
||||||
|
|
||||||
|
return { success, message };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendBeerStyleLikeRequest;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema';
|
import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({
|
const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
userId: z.string().cuid(),
|
userId: z.string().cuid(),
|
||||||
@@ -15,7 +15,7 @@ const createNewBeerComment = async ({
|
|||||||
rating,
|
rating,
|
||||||
beerPostId,
|
beerPostId,
|
||||||
userId,
|
userId,
|
||||||
}: CreateNewBeerCommentArgs): Promise<z.infer<typeof BeerCommentQueryResult>> => {
|
}: CreateNewBeerCommentArgs): Promise<z.infer<typeof CommentQueryResult>> => {
|
||||||
return DBClient.instance.beerComment.create({
|
return DBClient.instance.beerComment.create({
|
||||||
data: {
|
data: {
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
interface EditBeerCommentByIdArgs {
|
interface EditBeerCommentByIdArgs {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,7 +12,7 @@ const editBeerCommentById = async ({
|
|||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
rating,
|
rating,
|
||||||
}: EditBeerCommentByIdArgs): Promise<z.infer<typeof BeerCommentQueryResult>> => {
|
}: EditBeerCommentByIdArgs): Promise<z.infer<typeof CommentQueryResult>> => {
|
||||||
return DBClient.instance.beerComment.update({
|
return DBClient.instance.beerComment.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { content, rating, updatedAt: new Date() },
|
data: { content, rating, updatedAt: new Date() },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
interface FindBeerCommentArgs {
|
interface FindBeerCommentArgs {
|
||||||
beerCommentId: string;
|
beerCommentId: string;
|
||||||
@@ -8,7 +8,7 @@ interface FindBeerCommentArgs {
|
|||||||
|
|
||||||
const findBeerCommentById = async ({
|
const findBeerCommentById = async ({
|
||||||
beerCommentId,
|
beerCommentId,
|
||||||
}: FindBeerCommentArgs): Promise<z.infer<typeof BeerCommentQueryResult> | null> => {
|
}: FindBeerCommentArgs): Promise<z.infer<typeof CommentQueryResult> | null> => {
|
||||||
return DBClient.instance.beerComment.findUnique({
|
return DBClient.instance.beerComment.findUnique({
|
||||||
where: { id: beerCommentId },
|
where: { id: beerCommentId },
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const getAllBeerComments = async ({
|
|||||||
content: true,
|
content: true,
|
||||||
rating: true,
|
rating: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const BeerCommentQueryResult = z.object({
|
|
||||||
id: z.string().cuid(),
|
|
||||||
content: z.string(),
|
|
||||||
rating: z.number(),
|
|
||||||
postedBy: z.object({
|
|
||||||
id: z.string().cuid(),
|
|
||||||
username: z.string(),
|
|
||||||
}),
|
|
||||||
createdAt: z.coerce.date(),
|
|
||||||
updatedAt: z.coerce.date().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default BeerCommentQueryResult;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const CreateCommentValidationSchema = z.object({
|
|
||||||
userId: z.string().uuid(),
|
|
||||||
beerPostId: z.string().uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default CreateCommentValidationSchema;
|
|
||||||
@@ -7,7 +7,7 @@ const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
|||||||
userId: z.string().cuid(),
|
userId: z.string().cuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewBeerPost = async ({
|
const createNewBeerPost = ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
abv,
|
abv,
|
||||||
@@ -15,9 +15,10 @@ const createNewBeerPost = async ({
|
|||||||
styleId,
|
styleId,
|
||||||
breweryId,
|
breweryId,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
}: z.infer<typeof CreateBeerPostWithUserSchema>): Promise<
|
||||||
const newBeerPost: z.infer<typeof BeerPostQueryResult> =
|
z.infer<typeof BeerPostQueryResult>
|
||||||
await DBClient.instance.beerPost.create({
|
> => {
|
||||||
|
return DBClient.instance.beerPost.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -34,13 +35,13 @@ const createNewBeerPost = async ({
|
|||||||
abv: true,
|
abv: true,
|
||||||
ibu: true,
|
ibu: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
brewery: { select: { id: true, name: true } },
|
brewery: { select: { id: true, name: true } },
|
||||||
style: { select: { id: true, name: true, description: true } },
|
style: { select: { id: true, name: true, description: true } },
|
||||||
postedBy: { select: { id: true, username: true } },
|
postedBy: { select: { id: true, username: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return newBeerPost;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerPost;
|
export default createNewBeerPost;
|
||||||
|
|||||||
30
src/services/BeerPost/deleteBeerPostById.ts
Normal file
30
src/services/BeerPost/deleteBeerPostById.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
||||||
|
|
||||||
|
interface DeleteBeerPostByIdArgs {
|
||||||
|
beerPostId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBeerPostById = ({
|
||||||
|
beerPostId,
|
||||||
|
}: DeleteBeerPostByIdArgs): Promise<z.infer<typeof BeerPostQueryResult> | null> => {
|
||||||
|
return DBClient.instance.beerPost.delete({
|
||||||
|
where: { id: beerPostId },
|
||||||
|
select: {
|
||||||
|
abv: true,
|
||||||
|
createdAt: true,
|
||||||
|
description: true,
|
||||||
|
ibu: true,
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
updatedAt: true,
|
||||||
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
|
style: { select: { id: true, name: true, description: true } },
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
brewery: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteBeerPostById;
|
||||||
@@ -1,10 +1,36 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import EditBeerPostValidationSchema from './schema/EditBeerPostValidationSchema';
|
import EditBeerPostValidationSchema from './schema/EditBeerPostValidationSchema';
|
||||||
|
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const schema = EditBeerPostValidationSchema.omit({ id: true });
|
const schema = EditBeerPostValidationSchema.omit({ id: true, styleId: true });
|
||||||
|
|
||||||
export default async function editBeerPostById(id: string, data: z.infer<typeof schema>) {
|
interface EditBeerPostByIdArgs {
|
||||||
const beerPost = await DBClient.instance.beerPost.update({ where: { id }, data });
|
id: string;
|
||||||
return beerPost;
|
data: z.infer<typeof schema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editBeerPostById = ({
|
||||||
|
id,
|
||||||
|
data: { abv, ibu, name, description },
|
||||||
|
}: EditBeerPostByIdArgs): Promise<z.infer<typeof BeerPostQueryResult>> => {
|
||||||
|
return DBClient.instance.beerPost.update({
|
||||||
|
where: { id },
|
||||||
|
data: { abv, ibu, name, description },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
abv: true,
|
||||||
|
ibu: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
|
brewery: { select: { id: true, name: true } },
|
||||||
|
style: { select: { id: true, name: true, description: true } },
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default editBeerPostById;
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
interface GetAllBeerPostsArgs {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
const beerPosts: z.infer<typeof BeerPostQueryResult>[] = await prisma.beerPost.findMany(
|
const getAllBeerPosts = ({
|
||||||
{
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
}: GetAllBeerPostsArgs): Promise<z.infer<typeof BeerPostQueryResult>[]> => {
|
||||||
|
return prisma.beerPost.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -16,18 +21,16 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
|||||||
abv: true,
|
abv: true,
|
||||||
description: true,
|
description: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
style: { select: { name: true, id: true, description: true } },
|
style: { select: { name: true, id: true, description: true } },
|
||||||
brewery: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
postedBy: { select: { id: true, username: true } },
|
postedBy: { select: { id: true, username: true } },
|
||||||
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
},
|
},
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip,
|
skip: (pageNum - 1) * pageSize,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return beerPosts;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getAllBeerPosts;
|
export default getAllBeerPosts;
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getBeerPostById = async (id: string) => {
|
const getBeerPostById = async (
|
||||||
const beerPost: z.infer<typeof BeerPostQueryResult> | null =
|
id: string,
|
||||||
await prisma.beerPost.findFirst({
|
): Promise<z.infer<typeof BeerPostQueryResult> | null> => {
|
||||||
|
return prisma.beerPost.findFirst({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
ibu: true,
|
ibu: true,
|
||||||
abv: true,
|
abv: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
description: true,
|
description: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
brewery: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
@@ -21,8 +23,6 @@ const getBeerPostById = async (id: string) => {
|
|||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
return beerPost;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getBeerPostById;
|
export default getBeerPostById;
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const getBeerRecommendations = async ({
|
|||||||
beerPost,
|
beerPost,
|
||||||
pageNum,
|
pageNum,
|
||||||
pageSize,
|
pageSize,
|
||||||
}: GetBeerRecommendationsArgs) => {
|
}: GetBeerRecommendationsArgs): Promise<{
|
||||||
|
beerRecommendations: z.infer<typeof BeerPostQueryResult>[];
|
||||||
|
count: number;
|
||||||
|
}> => {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
const take = pageSize;
|
const take = pageSize;
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ const getBeerRecommendations = async ({
|
|||||||
abv: true,
|
abv: true,
|
||||||
description: true,
|
description: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
style: { select: { name: true, id: true, description: true } },
|
style: { select: { name: true, id: true, description: true } },
|
||||||
brewery: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
postedBy: { select: { id: true, username: true } },
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const BeerPostQueryResult = z.object({
|
|||||||
style: z.object({ id: z.string(), name: z.string(), description: z.string() }),
|
style: z.object({ id: z.string(), name: z.string(), description: z.string() }),
|
||||||
postedBy: z.object({ id: z.string(), username: z.string() }),
|
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.coerce.date().optional(),
|
updatedAt: z.coerce.date().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BeerPostQueryResult;
|
export default BeerPostQueryResult;
|
||||||
|
|||||||
37
src/services/BeerStyleComment/createNewBeerStyleComment.ts
Normal file
37
src/services/BeerStyleComment/createNewBeerStyleComment.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
|
const CreateNewBeerStyleCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
|
userId: z.string().cuid(),
|
||||||
|
beerStyleId: z.string().cuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type CreateNewBeerCommentArgs = z.infer<typeof CreateNewBeerStyleCommentServiceSchema>;
|
||||||
|
|
||||||
|
const createNewBeerStyleComment = async ({
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
userId,
|
||||||
|
beerStyleId,
|
||||||
|
}: CreateNewBeerCommentArgs): Promise<z.infer<typeof CommentQueryResult>> => {
|
||||||
|
return DBClient.instance.beerStyleComment.create({
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
beerStyle: { connect: { id: beerStyleId } },
|
||||||
|
postedBy: { connect: { id: userId } },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
rating: true,
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewBeerStyleComment;
|
||||||
32
src/services/BeerStyleComment/getAllBeerStyleComments.ts
Normal file
32
src/services/BeerStyleComment/getAllBeerStyleComments.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
|
interface GetAllBeerStyleCommentArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllBeerStyleComments = async ({
|
||||||
|
beerStyleId,
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
}: GetAllBeerStyleCommentArgs): Promise<z.infer<typeof CommentQueryResult>[]> => {
|
||||||
|
return DBClient.instance.beerStyleComment.findMany({
|
||||||
|
skip: (pageNum - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
where: { beerStyleId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
rating: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllBeerStyleComments;
|
||||||
15
src/services/BeerStyleComment/getBeerStyleCommentCount.ts
Normal file
15
src/services/BeerStyleComment/getBeerStyleCommentCount.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface GetBeerStyleCommentCountArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBeerCommentCount = async ({
|
||||||
|
beerStyleId,
|
||||||
|
}: GetBeerStyleCommentCountArgs): Promise<number> => {
|
||||||
|
return DBClient.instance.beerStyleComment.count({
|
||||||
|
where: { beerStyleId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBeerCommentCount;
|
||||||
18
src/services/BeerStyleLike/createBeerStyleLike.ts
Normal file
18
src/services/BeerStyleLike/createBeerStyleLike.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||||
|
|
||||||
|
interface CreateBeerStyleLikeArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
user: z.infer<typeof GetUserSchema>;
|
||||||
|
}
|
||||||
|
const createBeerStyleLike = async ({ beerStyleId, user }: CreateBeerStyleLikeArgs) => {
|
||||||
|
return DBClient.instance.beerStyleLike.create({
|
||||||
|
data: {
|
||||||
|
beerStyleId,
|
||||||
|
likedById: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createBeerStyleLike;
|
||||||
16
src/services/BeerStyleLike/findBeerStyleLikeById.ts
Normal file
16
src/services/BeerStyleLike/findBeerStyleLikeById.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface FindBeerStyleLikeByIdArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
likedById: string;
|
||||||
|
}
|
||||||
|
const findBeerStyleLikeById = async ({
|
||||||
|
beerStyleId,
|
||||||
|
likedById,
|
||||||
|
}: FindBeerStyleLikeByIdArgs) => {
|
||||||
|
return DBClient.instance.beerStyleLike.findFirst({
|
||||||
|
where: { beerStyleId, likedById },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default findBeerStyleLikeById;
|
||||||
10
src/services/BeerStyleLike/getBeerStyleLikeCount.ts
Normal file
10
src/services/BeerStyleLike/getBeerStyleLikeCount.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface GetBeerStyleLikeCountArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
}
|
||||||
|
const getBeerStyleLikeCount = async ({ beerStyleId }: GetBeerStyleLikeCountArgs) => {
|
||||||
|
return DBClient.instance.beerStyleLike.count({ where: { beerStyleId } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBeerStyleLikeCount;
|
||||||
12
src/services/BeerStyleLike/removeBeerStyleLikeById.ts
Normal file
12
src/services/BeerStyleLike/removeBeerStyleLikeById.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface RemoveBeerStyleLikeByIdArgs {
|
||||||
|
beerStyleLikeId: string;
|
||||||
|
}
|
||||||
|
const removeBeerStyleLikeById = async ({
|
||||||
|
beerStyleLikeId,
|
||||||
|
}: RemoveBeerStyleLikeByIdArgs) => {
|
||||||
|
return DBClient.instance.beerStyleLike.delete({ where: { id: beerStyleLikeId } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default removeBeerStyleLikeById;
|
||||||
34
src/services/BeerStyles/deleteBeerStyleById.ts
Normal file
34
src/services/BeerStyles/deleteBeerStyleById.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import BeerStyleQueryResult from './schema/BeerStyleQueryResult';
|
||||||
|
|
||||||
|
interface DeleteBeerStyleByIdArgs {
|
||||||
|
beerStyleId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBeerStyleById = async ({
|
||||||
|
beerStyleId,
|
||||||
|
}: DeleteBeerStyleByIdArgs): Promise<z.infer<typeof BeerStyleQueryResult> | null> => {
|
||||||
|
const deleted = await DBClient.instance.beerStyle.delete({
|
||||||
|
where: { id: beerStyleId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
abvRange: true,
|
||||||
|
ibuRange: true,
|
||||||
|
description: true,
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
glassware: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the ibuRange and abvRange
|
||||||
|
* fields to [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return deleted as Awaited<ReturnType<typeof deleteBeerStyleById>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteBeerStyleById;
|
||||||
30
src/services/BeerStyles/editBeerStyleById.ts
Normal file
30
src/services/BeerStyles/editBeerStyleById.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerStyleQueryResult from './schema/BeerStyleQueryResult';
|
||||||
|
|
||||||
|
const editBeerStyleById = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<z.infer<typeof BeerStyleQueryResult> | null> => {
|
||||||
|
const beerStyle = await DBClient.instance.beerStyle.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
abvRange: true,
|
||||||
|
ibuRange: true,
|
||||||
|
description: true,
|
||||||
|
glassware: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the ibuRange and abvRange
|
||||||
|
* fields to [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return beerStyle as Awaited<ReturnType<typeof editBeerStyleById>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default editBeerStyleById;
|
||||||
@@ -2,14 +2,16 @@ import DBClient from '@/prisma/DBClient';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerStyleQueryResult from './schema/BeerStyleQueryResult';
|
import BeerStyleQueryResult from './schema/BeerStyleQueryResult';
|
||||||
|
|
||||||
|
interface GetAllBeerStylesArgs {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
const getAllBeerStyles = async ({
|
const getAllBeerStyles = async ({
|
||||||
pageNum,
|
pageNum,
|
||||||
pageSize,
|
pageSize,
|
||||||
}: {
|
}: GetAllBeerStylesArgs): Promise<z.infer<typeof BeerStyleQueryResult>[]> => {
|
||||||
pageNum: number;
|
const beerStyles = await DBClient.instance.beerStyle.findMany({
|
||||||
pageSize: number;
|
|
||||||
}): Promise<z.infer<typeof BeerStyleQueryResult>[]> =>
|
|
||||||
DBClient.instance.beerStyle.findMany({
|
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip: (pageNum - 1) * pageSize,
|
skip: (pageNum - 1) * pageSize,
|
||||||
select: {
|
select: {
|
||||||
@@ -23,6 +25,13 @@ const getAllBeerStyles = async ({
|
|||||||
description: true,
|
description: true,
|
||||||
glassware: { select: { id: true, name: true } },
|
glassware: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
}) as ReturnType<typeof getAllBeerStyles>;
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the ibuRange and abvRange
|
||||||
|
* fields to [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return beerStyles as Awaited<ReturnType<typeof getAllBeerStyles>>;
|
||||||
|
};
|
||||||
|
|
||||||
export default getAllBeerStyles;
|
export default getAllBeerStyles;
|
||||||
|
|||||||
30
src/services/BeerStyles/getBeerStyleById.ts
Normal file
30
src/services/BeerStyles/getBeerStyleById.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerStyleQueryResult from './schema/BeerStyleQueryResult';
|
||||||
|
|
||||||
|
const getBeerStyleById = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<z.infer<typeof BeerStyleQueryResult> | null> => {
|
||||||
|
const beerStyle = await DBClient.instance.beerStyle.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
abvRange: true,
|
||||||
|
ibuRange: true,
|
||||||
|
description: true,
|
||||||
|
glassware: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the ibuRange and abvRange
|
||||||
|
* fields to [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return beerStyle as Awaited<ReturnType<typeof getBeerStyleById>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBeerStyleById;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const CreateBeerStyleValidationSchema = z.object({
|
||||||
|
glasswareId: z
|
||||||
|
.string()
|
||||||
|
.cuid({
|
||||||
|
message: 'Glassware ID must be a valid CUID.',
|
||||||
|
})
|
||||||
|
.min(1, { message: 'Glassware ID is required.' }),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Description is required.' })
|
||||||
|
.max(500, { message: 'Description must be less than or equal to 500 characters.' }),
|
||||||
|
ibuRange: z
|
||||||
|
.tuple([
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'IBU range minimum must be greater than or equal to 0.' })
|
||||||
|
.max(100, { message: 'IBU range minimum must be less than or equal to 100.' }),
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'IBU range maximum must be greater than or equal to 0.' })
|
||||||
|
.max(100, { message: 'IBU range maximum must be less than or equal to 100.' }),
|
||||||
|
])
|
||||||
|
.refine((ibuRange) => ibuRange[0] <= ibuRange[1], {
|
||||||
|
message: 'IBU range minimum must be less than or equal to maximum.',
|
||||||
|
}),
|
||||||
|
abvRange: z
|
||||||
|
.tuple([
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'ABV range minimum must be greater than or equal to 0.' }),
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'ABV range maximum must be greater than or equal to 0.' }),
|
||||||
|
])
|
||||||
|
.refine((abvRange) => abvRange[0] <= abvRange[1], {
|
||||||
|
message: 'ABV range minimum must be less than or equal to maximum.',
|
||||||
|
}),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Name is required.' })
|
||||||
|
.max(100, { message: 'Name must be less than or equal to 100 characters.' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CreateBeerStyleValidationSchema;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema';
|
import CreateCommentValidationSchema from '../schema/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
userId: z.string().cuid(),
|
userId: z.string().cuid(),
|
||||||
@@ -12,7 +13,9 @@ const createNewBreweryComment = async ({
|
|||||||
rating,
|
rating,
|
||||||
breweryPostId,
|
breweryPostId,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateNewBreweryCommentServiceSchema>) => {
|
}: z.infer<typeof CreateNewBreweryCommentServiceSchema>): Promise<
|
||||||
|
z.infer<typeof CommentQueryResult>
|
||||||
|
> => {
|
||||||
return DBClient.instance.breweryComment.create({
|
return DBClient.instance.breweryComment.create({
|
||||||
data: {
|
data: {
|
||||||
content,
|
content,
|
||||||
@@ -26,6 +29,7 @@ const createNewBreweryComment = async ({
|
|||||||
rating: true,
|
rating: true,
|
||||||
postedBy: { select: { id: true, username: true } },
|
postedBy: { select: { id: true, username: true } },
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
import CommentQueryResult from '../schema/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
const getAllBreweryComments = async (
|
const getAllBreweryComments = async ({
|
||||||
{ id }: Pick<z.infer<typeof BeerPostQueryResult>, 'id'>,
|
id,
|
||||||
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
pageNum,
|
||||||
) => {
|
pageSize,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) => {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
const breweryComments: z.infer<typeof CommentQueryResult>[] =
|
const breweryComments: z.infer<typeof CommentQueryResult>[] =
|
||||||
await DBClient.instance.breweryComment.findMany({
|
await DBClient.instance.breweryComment.findMany({
|
||||||
@@ -18,6 +22,7 @@ const getAllBreweryComments = async (
|
|||||||
content: true,
|
content: true,
|
||||||
rating: true,
|
rating: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
postedBy: { select: { id: true, username: true, createdAt: true } },
|
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
|
|||||||
9
src/services/BreweryComment/getBreweryCommentById.ts
Normal file
9
src/services/BreweryComment/getBreweryCommentById.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
const getBreweryCommentById = async (id: string) => {
|
||||||
|
return DBClient.instance.breweryComment.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBreweryCommentById;
|
||||||
@@ -19,9 +19,10 @@ const createNewBreweryPost = async ({
|
|||||||
locationId,
|
locationId,
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateNewBreweryPostWithUserAndLocationSchema>) => {
|
}: z.infer<typeof CreateNewBreweryPostWithUserAndLocationSchema>): Promise<
|
||||||
const breweryPost: z.infer<typeof BreweryPostQueryResult> =
|
z.infer<typeof BreweryPostQueryResult>
|
||||||
(await DBClient.instance.breweryPost.create({
|
> => {
|
||||||
|
const post = (await DBClient.instance.breweryPost.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -47,9 +48,8 @@ const createNewBreweryPost = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})) as z.infer<typeof BreweryPostQueryResult>;
|
})) as Awaited<ReturnType<typeof createNewBreweryPost>>;
|
||||||
|
|
||||||
return breweryPost;
|
return post;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBreweryPost;
|
export default createNewBreweryPost;
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => {
|
const getAllBreweryPosts = async ({
|
||||||
const skip = pageNum && pageSize ? (pageNum - 1) * pageSize : undefined;
|
pageNum,
|
||||||
const take = pageNum && pageSize ? pageSize : undefined;
|
pageSize,
|
||||||
|
}: {
|
||||||
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
|
pageNum: number;
|
||||||
(await prisma.breweryPost.findMany({
|
pageSize: number;
|
||||||
skip,
|
}): Promise<z.infer<typeof BreweryPostQueryResult>[]> => {
|
||||||
take,
|
const breweryPosts = await prisma.breweryPost.findMany({
|
||||||
|
take: pageSize,
|
||||||
|
skip: (pageNum - 1) * pageSize,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
location: {
|
location: {
|
||||||
@@ -32,9 +34,13 @@ const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => {
|
|||||||
dateEstablished: true,
|
dateEstablished: true,
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
})) as z.infer<typeof BreweryPostQueryResult>[];
|
});
|
||||||
|
|
||||||
return breweryPosts;
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the coordinates field to
|
||||||
|
* [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return breweryPosts as Awaited<ReturnType<typeof getAllBreweryPosts>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getAllBreweryPosts;
|
export default getAllBreweryPosts;
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { z } from 'zod';
|
|||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getBreweryPostById = async (id: string) => {
|
const getBreweryPostById = async (id: string) => {
|
||||||
const breweryPost: z.infer<typeof BreweryPostQueryResult> | null =
|
const breweryPost = await prisma.breweryPost.findFirst({
|
||||||
(await prisma.breweryPost.findFirst({
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
location: {
|
location: {
|
||||||
@@ -26,9 +25,13 @@ const getBreweryPostById = async (id: string) => {
|
|||||||
dateEstablished: true,
|
dateEstablished: true,
|
||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
})) as z.infer<typeof BreweryPostQueryResult> | null;
|
});
|
||||||
|
|
||||||
return breweryPost;
|
/**
|
||||||
|
* Prisma does not support tuples, so we have to typecast the coordinates field to
|
||||||
|
* [number, number] in order to satisfy the zod schema.
|
||||||
|
*/
|
||||||
|
return breweryPost as z.infer<typeof BreweryPostQueryResult> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getBreweryPostById;
|
export default getBreweryPostById;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const CommentQueryResult = z.object({
|
|||||||
id: z.string().cuid(),
|
id: z.string().cuid(),
|
||||||
username: z.string().min(1).max(50),
|
username: z.string().min(1).max(50),
|
||||||
}),
|
}),
|
||||||
|
updatedAt: z.coerce.date().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default CommentQueryResult;
|
export default CommentQueryResult;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const myThemes = {
|
const myThemes = {
|
||||||
dark: {
|
dark: {
|
||||||
primary: 'hsl(227, 25%, 30%)',
|
primary: 'hsl(227, 25%, 14%)',
|
||||||
secondary: 'hsl(255, 9%, 69%)',
|
secondary: 'hsl(255, 9%, 69%)',
|
||||||
error: 'hsl(9, 52%, 57%)',
|
error: 'hsl(9, 52%, 57%)',
|
||||||
accent: 'hsl(316, 96%, 60%)',
|
accent: 'hsl(316, 96%, 60%)',
|
||||||
@@ -12,9 +12,9 @@ const myThemes = {
|
|||||||
warning: 'hsl(50, 98%, 50%)',
|
warning: 'hsl(50, 98%, 50%)',
|
||||||
'primary-content': 'hsl(0, 0%, 98%)',
|
'primary-content': 'hsl(0, 0%, 98%)',
|
||||||
'error-content': 'hsl(0, 0%, 98%)',
|
'error-content': 'hsl(0, 0%, 98%)',
|
||||||
'base-100': 'hsl(227, 20%, 15%)',
|
'base-100': 'hsl(227, 20%, 10%)',
|
||||||
'base-200': 'hsl(227, 20%, 11%)',
|
'base-200': 'hsl(227, 20%, 8%)',
|
||||||
'base-300': 'hsl(227, 20%, 5%)',
|
'base-300': 'hsl(227, 20%, 1%)',
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
primary: 'hsl(180, 15%, 60%)',
|
primary: 'hsl(180, 15%, 60%)',
|
||||||
|
|||||||
Reference in New Issue
Block a user