mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +00:00
Merge pull request #30 from aaronpo97/brewery-page
Feat: Implement brewery page component and update database with location table and date established field
This commit is contained in:
1549
package-lock.json
generated
1549
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -8,17 +8,19 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"seed": "npx ts-node ./src/prisma/seed/index.ts"
|
"seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/iron": "^7.0.1",
|
"@hapi/iron": "^7.0.1",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@headlessui/tailwindcss": "^0.1.2",
|
"@headlessui/tailwindcss": "^0.1.2",
|
||||||
"@hookform/resolvers": "^3.0.0",
|
"@hookform/resolvers": "^3.0.0",
|
||||||
"@prisma/client": "^4.12.0",
|
"@mapbox/mapbox-sdk": "^0.15.0",
|
||||||
|
"@prisma/client": "^4.13.0",
|
||||||
"@react-email/components": "^0.0.4",
|
"@react-email/components": "^0.0.4",
|
||||||
"@react-email/render": "^0.0.6",
|
"@react-email/render": "^0.0.6",
|
||||||
"@react-email/tailwind": "^0.0.7",
|
"@react-email/tailwind": "^0.0.7",
|
||||||
|
"@vercel/analytics": "^1.0.0",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"cloudinary": "^1.35.0",
|
"cloudinary": "^1.35.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mapbox-gl": "^2.14.1",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "^13.2.4",
|
"next": "^13.2.4",
|
||||||
@@ -41,6 +44,7 @@
|
|||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-intersection-observer": "^9.4.3",
|
"react-intersection-observer": "^9.4.3",
|
||||||
|
"react-map-gl": "^7.0.23",
|
||||||
"react-responsive-carousel": "^3.2.23",
|
"react-responsive-carousel": "^3.2.23",
|
||||||
"sparkpost": "^2.1.4",
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.1.2",
|
"swr": "^2.1.2",
|
||||||
@@ -53,6 +57,7 @@
|
|||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/lodash": "^4.14.192",
|
"@types/lodash": "^4.14.192",
|
||||||
|
"@types/mapbox__mapbox-sdk": "^0.13.4",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
@@ -73,7 +78,7 @@
|
|||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.6",
|
"prettier-plugin-tailwindcss": "^0.2.6",
|
||||||
"prisma": "^4.12.0",
|
"prisma": "^4.13.0",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
|||||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
|
||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
import { FunctionComponent, useState, useEffect } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
import { Rating } from 'react-daisyui';
|
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
import Button from '../ui/forms/Button';
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
import FormError from '../ui/forms/FormError';
|
import CommentForm from '../ui/CommentForm';
|
||||||
import FormInfo from '../ui/forms/FormInfo';
|
|
||||||
import FormLabel from '../ui/forms/FormLabel';
|
|
||||||
import FormSegment from '../ui/forms/FormSegment';
|
|
||||||
import FormTextArea from '../ui/forms/FormTextArea';
|
|
||||||
|
|
||||||
interface BeerCommentFormProps {
|
interface BeerCommentFormProps {
|
||||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
@@ -25,26 +20,16 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
|||||||
beerPost,
|
beerPost,
|
||||||
mutate,
|
mutate,
|
||||||
}) => {
|
}) => {
|
||||||
const { register, handleSubmit, formState, reset, setValue } = useForm<
|
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
|
||||||
z.infer<typeof BeerCommentValidationSchema>
|
z.infer<typeof CreateCommentValidationSchema>
|
||||||
>({
|
>({
|
||||||
defaultValues: {
|
defaultValues: { rating: 0 },
|
||||||
rating: 0,
|
resolver: zodResolver(CreateCommentValidationSchema),
|
||||||
},
|
|
||||||
resolver: zodResolver(BeerCommentValidationSchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [rating, setRating] = useState(0);
|
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||||
useEffect(() => {
|
|
||||||
setRating(0);
|
|
||||||
reset({ rating: 0, content: '' });
|
|
||||||
}, [reset]);
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
|
|
||||||
data,
|
data,
|
||||||
) => {
|
) => {
|
||||||
setValue('rating', 0);
|
|
||||||
setRating(0);
|
|
||||||
await sendCreateBeerCommentRequest({
|
await sendCreateBeerCommentRequest({
|
||||||
content: data.content,
|
content: data.content,
|
||||||
rating: data.rating,
|
rating: data.rating,
|
||||||
@@ -54,50 +39,15 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
|||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { errors } = formState;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
<CommentForm
|
||||||
<div>
|
handleSubmit={handleSubmit}
|
||||||
<FormInfo>
|
onSubmit={onSubmit}
|
||||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
watch={watch}
|
||||||
<FormError>{errors.content?.message}</FormError>
|
setValue={setValue}
|
||||||
</FormInfo>
|
formState={formState}
|
||||||
<FormSegment>
|
register={register}
|
||||||
<FormTextArea
|
|
||||||
id="content"
|
|
||||||
formValidationSchema={register('content')}
|
|
||||||
placeholder="Comment"
|
|
||||||
rows={5}
|
|
||||||
error={!!errors.content?.message}
|
|
||||||
disabled={formState.isSubmitting}
|
|
||||||
/>
|
/>
|
||||||
</FormSegment>
|
|
||||||
<FormInfo>
|
|
||||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
|
||||||
<FormError>{errors.rating?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<Rating
|
|
||||||
value={rating}
|
|
||||||
onChange={(value) => {
|
|
||||||
setRating(value);
|
|
||||||
setValue('rating', value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
|
||||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
|
||||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
|
||||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
|
||||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
|
||||||
</Rating>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import UserContext from '@/contexts/userContext';
|
|||||||
import { FaRegEdit } from 'react-icons/fa';
|
import { FaRegEdit } from 'react-icons/fa';
|
||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import useGetLikeCount from '@/hooks/useGetLikeCount';
|
import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount';
|
||||||
import useTimeDistance from '@/hooks/useTimeDistance';
|
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||||
import BeerPostLikeButton from './BeerPostLikeButton';
|
import BeerPostLikeButton from './BeerPostLikeButton';
|
||||||
|
|
||||||
const BeerInfoHeader: FC<{
|
interface BeerInfoHeaderProps {
|
||||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
}> = ({ beerPost }) => {
|
}
|
||||||
|
|
||||||
|
const BeerInfoHeader: FC<BeerInfoHeaderProps> = ({ beerPost }) => {
|
||||||
const createdAt = new Date(beerPost.createdAt);
|
const createdAt = new Date(beerPost.createdAt);
|
||||||
const timeDistance = useTimeDistance(createdAt);
|
const timeDistance = useTimeDistance(createdAt);
|
||||||
|
|
||||||
@@ -20,13 +22,14 @@ const BeerInfoHeader: FC<{
|
|||||||
const idMatches = user && beerPost.postedBy.id === user.id;
|
const idMatches = user && beerPost.postedBy.id === user.id;
|
||||||
const isPostOwner = !!(user && idMatches);
|
const isPostOwner = !!(user && idMatches);
|
||||||
|
|
||||||
const { likeCount, mutate } = useGetLikeCount(beerPost.id);
|
const { likeCount, mutate } = useGetBeerPostLikeCount(beerPost.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="card flex flex-col justify-center bg-base-300">
|
<article className="card flex flex-col justify-center bg-base-300">
|
||||||
<article className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex justify-between">
|
<header className="flex justify-between">
|
||||||
<header>
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
<h1 className="text-2xl font-bold lg:text-4xl">{beerPost.name}</h1>
|
<h1 className="text-2xl font-bold lg:text-4xl">{beerPost.name}</h1>
|
||||||
<h2 className="text-lg font-semibold lg:text-2xl">
|
<h2 className="text-lg font-semibold lg:text-2xl">
|
||||||
by{' '}
|
by{' '}
|
||||||
@@ -37,16 +40,8 @@ const BeerInfoHeader: FC<{
|
|||||||
{beerPost.brewery.name}
|
{beerPost.brewery.name}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
|
||||||
{isPostOwner && (
|
|
||||||
<div className="tooltip tooltip-left" data-tip={`Edit '${beerPost.name}'`}>
|
|
||||||
<Link href={`/beers/${beerPost.id}/edit`} className="btn-ghost btn-xs btn">
|
|
||||||
<FaRegEdit className="text-xl" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="italic">
|
<h3 className="italic">
|
||||||
{' posted by '}
|
{' posted by '}
|
||||||
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
||||||
@@ -54,14 +49,25 @@ const BeerInfoHeader: FC<{
|
|||||||
</Link>
|
</Link>
|
||||||
{timeDistance && (
|
{timeDistance && (
|
||||||
<span
|
<span
|
||||||
className="tooltip tooltip-right"
|
className="tooltip tooltip-bottom"
|
||||||
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||||
>
|
>
|
||||||
{`${timeDistance} ago`}
|
{`${timeDistance} ago`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPostOwner && (
|
||||||
|
<div className="tooltip tooltip-left" data-tip={`Edit '${beerPost.name}'`}>
|
||||||
|
<Link href={`/beers/${beerPost.id}/edit`} className="btn-ghost btn-xs btn">
|
||||||
|
<FaRegEdit className="text-xl" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
<p>{beerPost.description}</p>
|
<p>{beerPost.description}</p>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -86,11 +92,14 @@ const BeerInfoHeader: FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-actions items-end">
|
<div className="card-actions items-end">
|
||||||
{user && <BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />}
|
{user && (
|
||||||
|
<BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
|
|
||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
@@ -7,13 +6,10 @@ import { FC, MutableRefObject, useContext, useRef } from 'react';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
import { FaArrowUp } from 'react-icons/fa';
|
|
||||||
import BeerCommentForm from './BeerCommentForm';
|
import BeerCommentForm from './BeerCommentForm';
|
||||||
|
|
||||||
import CommentCardBody from './CommentCardBody';
|
|
||||||
import NoCommentsCard from './NoCommentsCard';
|
|
||||||
import LoadingComponent from './LoadingComponent';
|
import LoadingComponent from './LoadingComponent';
|
||||||
|
import CommentsComponent from '../ui/CommentsComponent';
|
||||||
|
|
||||||
interface BeerPostCommentsSectionProps {
|
interface BeerPostCommentsSectionProps {
|
||||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
@@ -33,20 +29,9 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
|
|||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { ref: lastCommentRef } = useInView({
|
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
/**
|
|
||||||
* When the last comment comes into view, call setSize from useBeerPostComments to
|
|
||||||
* load more comments.
|
|
||||||
*/
|
|
||||||
onChange: (visible) => {
|
|
||||||
if (!visible || isAtEnd) return;
|
|
||||||
setSize(size + 1);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const sectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-3" ref={sectionRef}>
|
<div className="w-full space-y-3" ref={commentSectionRef}>
|
||||||
<div className="card bg-base-300">
|
<div className="card bg-base-300">
|
||||||
<div className="card-body h-full">
|
<div className="card-body h-full">
|
||||||
{user ? (
|
{user ? (
|
||||||
@@ -69,66 +54,15 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
|
|||||||
<LoadingComponent length={PAGE_SIZE} />
|
<LoadingComponent length={PAGE_SIZE} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<CommentsComponent
|
||||||
{!!comments.length && (
|
commentSectionRef={commentSectionRef}
|
||||||
<div className="card bg-base-300 pb-6">
|
comments={comments}
|
||||||
{comments.map((comment, index) => {
|
isLoadingMore={isLoadingMore}
|
||||||
const isPenulitmateComment = index === comments.length - 2;
|
isAtEnd={isAtEnd}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
/**
|
setSize={setSize}
|
||||||
* Attach a ref to the last comment in the list. When it comes into
|
size={size}
|
||||||
* view, the component will call setSize to load more comments.
|
/>
|
||||||
*/
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={isPenulitmateComment ? lastCommentRef : undefined}
|
|
||||||
key={comment.id}
|
|
||||||
>
|
|
||||||
<CommentCardBody comment={comment} mutate={mutate} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* If there are more comments to load, show a loading component with a
|
|
||||||
* skeleton loader and a loading spinner.
|
|
||||||
*/
|
|
||||||
!!isLoadingMore && <LoadingComponent length={PAGE_SIZE} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* If the user has scrolled to the end of the comments, show a button
|
|
||||||
* that will scroll them back to the top of the comments section.
|
|
||||||
*/
|
|
||||||
!!isAtEnd && (
|
|
||||||
<div className="flex h-20 items-center justify-center text-center">
|
|
||||||
<div
|
|
||||||
className="tooltip tooltip-bottom"
|
|
||||||
data-tip="Scroll back to top of comments."
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-ghost btn-sm btn"
|
|
||||||
aria-label="Scroll back to top of comments"
|
|
||||||
onClick={() => {
|
|
||||||
sectionRef.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaArrowUp />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!comments.length && <NoCommentsCard />}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
|
import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
|
||||||
import sendLikeRequest from '@/requests/sendLikeRequest';
|
import sendBeerPostLikeRequest from '@/requests/sendBeerPostLikeRequest';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
|
|
||||||
|
|
||||||
import useGetLikeCount from '@/hooks/useGetLikeCount';
|
import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount';
|
||||||
|
import LikeButton from '../ui/LikeButton';
|
||||||
|
|
||||||
const BeerPostLikeButton: FC<{
|
const BeerPostLikeButton: FC<{
|
||||||
beerPostId: string;
|
beerPostId: string;
|
||||||
mutateCount: ReturnType<typeof useGetLikeCount>['mutate'];
|
mutateCount: ReturnType<typeof useGetBeerPostLikeCount>['mutate'];
|
||||||
}> = ({ beerPostId, mutateCount }) => {
|
}> = ({ beerPostId, mutateCount }) => {
|
||||||
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
|
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -19,7 +19,7 @@ const BeerPostLikeButton: FC<{
|
|||||||
const handleLike = async () => {
|
const handleLike = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await sendLikeRequest(beerPostId);
|
await sendBeerPostLikeRequest(beerPostId);
|
||||||
|
|
||||||
await Promise.all([mutateCount(), mutateLikeStatus()]);
|
await Promise.all([mutateCount(), mutateLikeStatus()]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -28,30 +28,7 @@ const BeerPostLikeButton: FC<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <LikeButton isLiked={!!isLiked} handleLike={handleLike} loading={loading} />;
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn-sm btn gap-2 rounded-2xl lg:btn-md ${
|
|
||||||
!isLiked ? 'btn-ghost outline' : 'btn-primary'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
handleLike();
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{isLiked ? (
|
|
||||||
<>
|
|
||||||
<FaThumbsUp className="lg:text-2xl" />
|
|
||||||
Liked
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaRegThumbsUp className="lg:text-2xl" />
|
|
||||||
Like
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BeerPostLikeButton;
|
export default BeerPostLikeButton;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
|
interface BeerRecommendationLoadingComponentProps {
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeerRecommendationLoadingComponent: FC<BeerRecommendationLoadingComponentProps> = ({
|
||||||
|
length,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length }).map((_, i) => (
|
||||||
|
<div className="animate my-3 fade-in-10" key={i}>
|
||||||
|
<div className="flex animate-pulse space-x-4">
|
||||||
|
<div className="flex-1 space-y-4 py-1">
|
||||||
|
<div className="h-4 w-3/4 rounded bg-base-100" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 rounded bg-base-100" />
|
||||||
|
<div className="h-4 w-11/12 rounded bg-base-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="p-1">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerRecommendationLoadingComponent;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -7,7 +7,7 @@ import CommentContentBody from './CommentContentBody';
|
|||||||
import EditCommentBody from './EditCommentBody';
|
import EditCommentBody from './EditCommentBody';
|
||||||
|
|
||||||
interface CommentCardProps {
|
interface CommentCardProps {
|
||||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
comment: z.infer<typeof CommentQueryResult>;
|
||||||
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
||||||
ref?: ReturnType<typeof useInView>['ref'];
|
ref?: ReturnType<typeof useInView>['ref'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import { Dispatch, SetStateAction, FC, useContext } from 'react';
|
import { Dispatch, SetStateAction, FC, useContext } from 'react';
|
||||||
import { FaEllipsisH } from 'react-icons/fa';
|
import { FaEllipsisH } from 'react-icons/fa';
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface CommentCardDropdownProps {
|
interface CommentCardDropdownProps {
|
||||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
comment: z.infer<typeof CommentQueryResult>;
|
||||||
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import useTimeDistance from '@/hooks/useTimeDistance';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Dispatch, FC, SetStateAction, useContext } from 'react';
|
import { Dispatch, FC, SetStateAction, useContext } from 'react';
|
||||||
import { Link, Rating } from 'react-daisyui';
|
import { Link, Rating } from 'react-daisyui';
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CommentCardDropdown from './CommentCardDropdown';
|
import CommentCardDropdown from './CommentCardDropdown';
|
||||||
|
|
||||||
interface CommentContentBodyProps {
|
interface CommentContentBodyProps {
|
||||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
comment: z.infer<typeof CommentQueryResult>;
|
||||||
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
||||||
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react';
|
import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react';
|
||||||
import { Rating } from 'react-daisyui';
|
import { Rating } from 'react-daisyui';
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
import FormError from '../ui/forms/FormError';
|
import FormError from '../ui/forms/FormError';
|
||||||
import FormInfo from '../ui/forms/FormInfo';
|
import FormInfo from '../ui/forms/FormInfo';
|
||||||
import FormLabel from '../ui/forms/FormLabel';
|
import FormLabel from '../ui/forms/FormLabel';
|
||||||
@@ -14,7 +14,7 @@ import FormSegment from '../ui/forms/FormSegment';
|
|||||||
import FormTextArea from '../ui/forms/FormTextArea';
|
import FormTextArea from '../ui/forms/FormTextArea';
|
||||||
|
|
||||||
interface CommentCardDropdownProps {
|
interface CommentCardDropdownProps {
|
||||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
comment: z.infer<typeof CommentQueryResult>;
|
||||||
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
||||||
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
||||||
@@ -27,13 +27,13 @@ const EditCommentBody: FC<CommentCardDropdownProps> = ({
|
|||||||
mutate,
|
mutate,
|
||||||
}) => {
|
}) => {
|
||||||
const { register, handleSubmit, formState, setValue, watch } = useForm<
|
const { register, handleSubmit, formState, setValue, watch } = useForm<
|
||||||
z.infer<typeof BeerCommentValidationSchema>
|
z.infer<typeof CreateCommentValidationSchema>
|
||||||
>({
|
>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
content: comment.content,
|
content: comment.content,
|
||||||
rating: comment.rating,
|
rating: comment.rating,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BeerCommentValidationSchema),
|
resolver: zodResolver(CreateCommentValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
@@ -59,7 +59,7 @@ const EditCommentBody: FC<CommentCardDropdownProps> = ({
|
|||||||
await mutate();
|
await mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
|
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||||
data,
|
data,
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import Image from 'next/image';
|
|||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import useGetLikeCount from '@/hooks/useGetLikeCount';
|
import useGetBeerPostLikeCount from '@/hooks/useBeerPostLikeCount';
|
||||||
import BeerPostLikeButton from '../BeerById/BeerPostLikeButton';
|
import BeerPostLikeButton from '../BeerById/BeerPostLikeButton';
|
||||||
|
|
||||||
const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) => {
|
const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
const { mutate, likeCount } = useGetLikeCount(post.id);
|
const { mutate, likeCount } = useGetBeerPostLikeCount(post.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card card-compact bg-base-300" key={post.id}>
|
<div className="card card-compact bg-base-300" key={post.id}>
|
||||||
@@ -27,26 +27,28 @@ const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) =
|
|||||||
<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/${post.id}`}>
|
<Link href={`/beers/${post.id}`}>
|
||||||
<h2 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">
|
||||||
{post.name}
|
{post.name}
|
||||||
</h2>
|
|
||||||
</Link>
|
|
||||||
<Link href={`/breweries/${post.brewery.id}`}>
|
|
||||||
<h3 className="text-md link-hover link whitespace-normal lg:truncate lg:text-xl">
|
|
||||||
{post.brewery.name}
|
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href={`/breweries/${post.brewery.id}`}>
|
||||||
|
<h4 className="text-md link-hover link whitespace-normal lg:truncate lg:text-xl">
|
||||||
|
{post.brewery.name}
|
||||||
|
</h4>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md lg:text-xl">{post.type.name}</p>
|
<p className="text-md lg:text-xl">{post.type.name}</p>
|
||||||
<div className="space-x-3">
|
<div className="space-x-3">
|
||||||
<span className="text-sm lg:text-lg">{post.abv}% ABV</span>
|
<span className="text-sm lg:text-lg">{post.abv}% ABV</span>
|
||||||
<span className="text-sm lg:text-lg">{post.ibu} IBU</span>
|
<span className="text-sm lg:text-lg">{post.ibu} IBU</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span>
|
||||||
|
liked by {likeCount} user{likeCount === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div>
|
||||||
liked by {likeCount} users
|
|
||||||
{user && <BeerPostLikeButton beerPostId={post.id} mutateCount={mutate} />}
|
{user && <BeerPostLikeButton beerPostId={post.id} mutateCount={mutate} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
src/components/BreweryById/BreweryBeerSection.tsx
Normal file
84
src/components/BreweryById/BreweryBeerSection.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import UseBeerPostsByBrewery from '@/hooks/useBeerPostsByBrewery';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BeerRecommendationLoadingComponent from '../BeerById/BeerRecommendationLoadingComponent';
|
||||||
|
|
||||||
|
interface BreweryCommentsSectionProps {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) => {
|
||||||
|
const PAGE_SIZE = 2;
|
||||||
|
const { beerPosts, isAtEnd, isLoadingMore, setSize, size } = UseBeerPostsByBrewery({
|
||||||
|
breweryId: breweryPost.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const { ref: penultimateBeerPostRef } = useInView({
|
||||||
|
/**
|
||||||
|
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||||
|
* load more beer posts.
|
||||||
|
*/
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body">
|
||||||
|
<>
|
||||||
|
<h3 className="text-2xl font-bold">Brews</h3>
|
||||||
|
{!!beerPosts.length && (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{beerPosts.map((beerPost, index) => {
|
||||||
|
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a ref to the second last beer post in the list. When it comes
|
||||||
|
* into view, the component will call setSize to load more beer posts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isPenultimateBeerPost ? penultimateBeerPostRef : undefined}
|
||||||
|
key={beerPost.id}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Link className="link-hover link" href={`/beers/${beerPost.id}`}>
|
||||||
|
<span className="text-xl font-semibold">{beerPost.name}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-lg font-medium">{beerPost.type.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<span>{beerPost.abv}% ABV</span>
|
||||||
|
<span>{beerPost.ibu} IBU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If there are more beer posts to load, show a loading component with a
|
||||||
|
* skeleton loader and a loading spinner.
|
||||||
|
*/
|
||||||
|
!!isLoadingMore && !isAtEnd && (
|
||||||
|
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryBeersSection;
|
||||||
144
src/components/BreweryById/BreweryCommentsSection.tsx
Normal file
144
src/components/BreweryById/BreweryCommentsSection.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useBreweryPostComments from '@/hooks/useBreweryPostComments';
|
||||||
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
|
import LoadingComponent from '../BeerById/LoadingComponent';
|
||||||
|
import CommentsComponent from '../ui/CommentsComponent';
|
||||||
|
import CommentForm from '../ui/CommentForm';
|
||||||
|
|
||||||
|
interface BreweryBeerSectionProps {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreweryCommentFormProps {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
mutate: ReturnType<typeof useBreweryPostComments>['mutate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
|
||||||
|
breweryPostId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendCreateBreweryCommentRequest = async ({
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
breweryPostId,
|
||||||
|
}: z.infer<typeof BreweryCommentValidationSchemaWithId>) => {
|
||||||
|
const response = await fetch(`/api/breweries/${breweryPostId}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BreweryCommentForm: FC<BreweryCommentFormProps> = ({ breweryPost, 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,
|
||||||
|
) => {
|
||||||
|
await sendCreateBreweryCommentRequest({
|
||||||
|
content: data.content,
|
||||||
|
rating: data.rating,
|
||||||
|
breweryPostId: breweryPost.id,
|
||||||
|
});
|
||||||
|
await mutate();
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommentForm
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
watch={watch}
|
||||||
|
setValue={setValue}
|
||||||
|
formState={formState}
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const PAGE_SIZE = 4;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
setSize,
|
||||||
|
size,
|
||||||
|
isLoadingMore,
|
||||||
|
isAtEnd,
|
||||||
|
mutate,
|
||||||
|
comments: breweryComments,
|
||||||
|
} = useBreweryPostComments({ id: breweryPost.id, pageSize: PAGE_SIZE });
|
||||||
|
|
||||||
|
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-3" ref={commentSectionRef}>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body h-full">
|
||||||
|
{user ? (
|
||||||
|
<BreweryCommentForm breweryPost={breweryPost} mutate={mutate} />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-52 flex-col items-center justify-center">
|
||||||
|
<div className="text-lg font-bold">Log in to leave a comment.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If the comments are loading, show a loading component. Otherwise, show the
|
||||||
|
* comments.
|
||||||
|
*/
|
||||||
|
isLoading ? (
|
||||||
|
<div className="card pb-6">
|
||||||
|
<LoadingComponent length={PAGE_SIZE} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CommentsComponent
|
||||||
|
comments={breweryComments}
|
||||||
|
isLoadingMore={isLoadingMore}
|
||||||
|
isAtEnd={isAtEnd}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
setSize={setSize}
|
||||||
|
size={size}
|
||||||
|
commentSectionRef={commentSectionRef}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryCommentsSection;
|
||||||
95
src/components/BreweryById/BreweryInfoHeader.tsx
Normal file
95
src/components/BreweryById/BreweryInfoHeader.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount';
|
||||||
|
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { FC, useContext } from 'react';
|
||||||
|
import { Link } from 'react-daisyui';
|
||||||
|
import { FaRegEdit } from 'react-icons/fa';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import BreweryPostLikeButton from '../BreweryIndex/BreweryPostLikeButton';
|
||||||
|
|
||||||
|
interface BreweryInfoHeaderProps {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
}
|
||||||
|
const BreweryInfoHeader: FC<BreweryInfoHeaderProps> = ({ breweryPost }) => {
|
||||||
|
const createdAt = new Date(breweryPost.createdAt);
|
||||||
|
const timeDistance = useTimeDistance(createdAt);
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const idMatches = user && breweryPost.postedBy.id === user.id;
|
||||||
|
const isPostOwner = !!(user && idMatches);
|
||||||
|
|
||||||
|
const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.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">{breweryPost.name}</h1>
|
||||||
|
<h2 className="text-lg font-semibold lg:text-2xl">
|
||||||
|
Located in
|
||||||
|
{` ${breweryPost.location.city}, ${
|
||||||
|
breweryPost.location.stateOrProvince || breweryPost.location.country
|
||||||
|
}`}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="italic">
|
||||||
|
{' posted by '}
|
||||||
|
<Link
|
||||||
|
href={`/users/${breweryPost.postedBy.id}`}
|
||||||
|
className="link-hover link"
|
||||||
|
>
|
||||||
|
{`${breweryPost.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 '${breweryPost.name}'`}>
|
||||||
|
<Link
|
||||||
|
href={`/breweries/${breweryPost.id}/edit`}
|
||||||
|
className="btn-ghost btn-xs btn"
|
||||||
|
>
|
||||||
|
<FaRegEdit className="text-xl" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{breweryPost.description}</p>
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>
|
||||||
|
{(!!likeCount || likeCount === 0) && (
|
||||||
|
<span>
|
||||||
|
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions">
|
||||||
|
{user && (
|
||||||
|
<BreweryPostLikeButton
|
||||||
|
breweryPostId={breweryPost.id}
|
||||||
|
mutateCount={mutate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryInfoHeader;
|
||||||
55
src/components/BreweryById/BreweryPostMap.tsx
Normal file
55
src/components/BreweryById/BreweryPostMap.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import useMediaQuery from '@/hooks/useMediaQuery';
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import { FC, useMemo } from 'react';
|
||||||
|
import Map, { Marker } from 'react-map-gl';
|
||||||
|
|
||||||
|
import LocationMarker from '../ui/LocationMarker';
|
||||||
|
|
||||||
|
interface BreweryMapProps {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
||||||
|
|
||||||
|
const BreweryPostMap: FC<BreweryMapProps> = ({ latitude, longitude }) => {
|
||||||
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||||
|
|
||||||
|
const windowIsDefined = typeof window !== 'undefined';
|
||||||
|
const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme');
|
||||||
|
|
||||||
|
const theme = (
|
||||||
|
windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light'
|
||||||
|
) as 'light' | 'dark';
|
||||||
|
|
||||||
|
const pin = useMemo(
|
||||||
|
() => (
|
||||||
|
<Marker latitude={latitude} longitude={longitude}>
|
||||||
|
<LocationMarker />
|
||||||
|
</Marker>
|
||||||
|
),
|
||||||
|
[latitude, longitude],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapStyles: MapStyles = {
|
||||||
|
light: 'mapbox://styles/mapbox/light-v10',
|
||||||
|
dark: 'mapbox://styles/mapbox/dark-v11',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body">
|
||||||
|
<Map
|
||||||
|
initialViewState={{ latitude, longitude, zoom: 17 }}
|
||||||
|
style={{ width: '100%', height: isDesktop ? 480 : 240 }}
|
||||||
|
mapStyle={mapStyles[theme]}
|
||||||
|
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string}
|
||||||
|
scrollZoom
|
||||||
|
>
|
||||||
|
{pin}
|
||||||
|
</Map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryPostMap;
|
||||||
53
src/components/BreweryIndex/BreweryCard.tsx
Normal file
53
src/components/BreweryIndex/BreweryCard.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { FC, useContext } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import BreweryPostLikeButton from './BreweryPostLikeButton';
|
||||||
|
|
||||||
|
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
||||||
|
brewery,
|
||||||
|
}) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const { likeCount, mutate } = useGetBreweryPostLikeCount(brewery.id);
|
||||||
|
return (
|
||||||
|
<div className="card" key={brewery.id}>
|
||||||
|
<figure className="card-image h-96">
|
||||||
|
{brewery.breweryImages.length > 0 && (
|
||||||
|
<Image
|
||||||
|
src={brewery.breweryImages[0].path}
|
||||||
|
alt={brewery.name}
|
||||||
|
width="1029"
|
||||||
|
height="110"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
<div className="card-body">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-2 text-2xl font-bold lg:text-3xl">
|
||||||
|
<Link href={`/breweries/${brewery.id}`} className="link-hover link">
|
||||||
|
{brewery.name}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<h3 className="text-xl font-normal lg:text-2xl">
|
||||||
|
located in {brewery.location.city},{' '}
|
||||||
|
{brewery.location.stateOrProvince || brewery.location.country}
|
||||||
|
</h3>
|
||||||
|
<h4 className="text-lg lg:text-xl">
|
||||||
|
est. {brewery.dateEstablished.getFullYear()}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>liked by {likeCount} users</span>
|
||||||
|
{user && (
|
||||||
|
<BreweryPostLikeButton breweryPostId={brewery.id} mutateCount={mutate} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryCard;
|
||||||
30
src/components/BreweryIndex/BreweryPostLikeButton.tsx
Normal file
30
src/components/BreweryIndex/BreweryPostLikeButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import useCheckIfUserLikesBreweryPost from '@/hooks/useCheckIfUserLikesBreweryPost';
|
||||||
|
import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount';
|
||||||
|
import sendBreweryPostLikeRequest from '@/requests/sendBreweryPostLikeRequest';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
import LikeButton from '../ui/LikeButton';
|
||||||
|
|
||||||
|
const BreweryPostLikeButton: FC<{
|
||||||
|
breweryPostId: string;
|
||||||
|
mutateCount: ReturnType<typeof useGetBreweryPostLikeCount>['mutate'];
|
||||||
|
}> = ({ breweryPostId, mutateCount }) => {
|
||||||
|
const { isLiked, mutate: mutateLikeStatus } =
|
||||||
|
useCheckIfUserLikesBreweryPost(breweryPostId);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await sendBreweryPostLikeRequest(breweryPostId);
|
||||||
|
await Promise.all([mutateCount(), mutateLikeStatus()]);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <LikeButton isLiked={!!isLiked} handleLike={handleLike} loading={isLoading} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryPostLikeButton;
|
||||||
85
src/components/ui/CommentForm.tsx
Normal file
85
src/components/ui/CommentForm.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Rating } from 'react-daisyui';
|
||||||
|
import type {
|
||||||
|
FormState,
|
||||||
|
SubmitHandler,
|
||||||
|
UseFormHandleSubmit,
|
||||||
|
UseFormRegister,
|
||||||
|
UseFormSetValue,
|
||||||
|
UseFormWatch,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import FormError from './forms/FormError';
|
||||||
|
import FormInfo from './forms/FormInfo';
|
||||||
|
import FormLabel from './forms/FormLabel';
|
||||||
|
import FormSegment from './forms/FormSegment';
|
||||||
|
import FormTextArea from './forms/FormTextArea';
|
||||||
|
import Button from './forms/Button';
|
||||||
|
|
||||||
|
interface Comment {
|
||||||
|
content: string;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentFormProps {
|
||||||
|
handleSubmit: UseFormHandleSubmit<Comment>;
|
||||||
|
onSubmit: SubmitHandler<Comment>;
|
||||||
|
watch: UseFormWatch<Comment>;
|
||||||
|
setValue: UseFormSetValue<Comment>;
|
||||||
|
formState: FormState<Comment>;
|
||||||
|
register: UseFormRegister<Comment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentForm: FC<CommentFormProps> = ({
|
||||||
|
handleSubmit,
|
||||||
|
onSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState,
|
||||||
|
register,
|
||||||
|
}) => {
|
||||||
|
const { errors } = formState;
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||||
|
<FormError>{errors.content?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextArea
|
||||||
|
id="content"
|
||||||
|
formValidationSchema={register('content')}
|
||||||
|
placeholder="Comment"
|
||||||
|
rows={5}
|
||||||
|
error={!!errors.content?.message}
|
||||||
|
disabled={formState.isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||||
|
<FormError>{errors.rating?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<Rating
|
||||||
|
value={watch('rating')}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValue('rating', value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
</Rating>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentForm;
|
||||||
114
src/components/ui/CommentsComponent.tsx
Normal file
114
src/components/ui/CommentsComponent.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { FC, MutableRefObject } from 'react';
|
||||||
|
import { FaArrowUp } from 'react-icons/fa';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
|
||||||
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
|
import useBreweryPostComments from '@/hooks/useBreweryPostComments';
|
||||||
|
import NoCommentsCard from '../BeerById/NoCommentsCard';
|
||||||
|
import LoadingComponent from '../BeerById/LoadingComponent';
|
||||||
|
import CommentCardBody from '../BeerById/CommentCardBody';
|
||||||
|
|
||||||
|
interface CommentsComponentProps {
|
||||||
|
commentSectionRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
pageSize: number;
|
||||||
|
size: ReturnType<typeof useBeerPostComments | typeof useBreweryPostComments>['size'];
|
||||||
|
setSize: ReturnType<
|
||||||
|
typeof useBeerPostComments | typeof useBreweryPostComments
|
||||||
|
>['setSize'];
|
||||||
|
comments: ReturnType<
|
||||||
|
typeof useBeerPostComments | typeof useBreweryPostComments
|
||||||
|
>['comments'];
|
||||||
|
isAtEnd: ReturnType<
|
||||||
|
typeof useBeerPostComments | typeof useBreweryPostComments
|
||||||
|
>['isAtEnd'];
|
||||||
|
isLoadingMore: ReturnType<
|
||||||
|
typeof useBeerPostComments | typeof useBreweryPostComments
|
||||||
|
>['isLoadingMore'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentsComponent: FC<CommentsComponentProps> = ({
|
||||||
|
commentSectionRef,
|
||||||
|
comments,
|
||||||
|
isAtEnd,
|
||||||
|
isLoadingMore,
|
||||||
|
pageSize,
|
||||||
|
setSize,
|
||||||
|
size,
|
||||||
|
}) => {
|
||||||
|
const { ref: penultimateCommentRef } = useInView({
|
||||||
|
/**
|
||||||
|
* When the second last comment comes into view, call setSize from useBeerPostComments
|
||||||
|
* to load more comments.
|
||||||
|
*/
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!comments.length && (
|
||||||
|
<div className="card bg-base-300 pb-6">
|
||||||
|
{comments.map((comment, index) => {
|
||||||
|
const isPenultimateComment = index === comments.length - 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a ref to the last comment in the list. When it comes into view, the
|
||||||
|
* component will call setSize to load more comments.
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isPenultimateComment ? penultimateCommentRef : undefined}
|
||||||
|
key={comment.id}
|
||||||
|
>
|
||||||
|
<CommentCardBody comment={comment} mutate={mutate} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If there are more comments to load, show a loading component with a
|
||||||
|
* skeleton loader and a loading spinner.
|
||||||
|
*/
|
||||||
|
!!isLoadingMore && <LoadingComponent length={pageSize} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* If the user has scrolled to the end of the comments, show a button that
|
||||||
|
* will scroll them back to the top of the comments section.
|
||||||
|
*/
|
||||||
|
!!isAtEnd && (
|
||||||
|
<div className="flex h-20 items-center justify-center text-center">
|
||||||
|
<div
|
||||||
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip="Scroll back to top of comments."
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
aria-label="Scroll back to top of comments"
|
||||||
|
onClick={() => {
|
||||||
|
commentSectionRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!comments.length && <NoCommentsCard />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentsComponent;
|
||||||
37
src/components/ui/LikeButton.tsx
Normal file
37
src/components/ui/LikeButton.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface LikeButtonProps {
|
||||||
|
isLiked: boolean;
|
||||||
|
handleLike: () => Promise<void>;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LikeButton: FC<LikeButtonProps> = ({ isLiked, handleLike, loading }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn-sm btn gap-2 rounded-2xl lg:btn-md ${
|
||||||
|
!isLiked ? 'btn-ghost outline' : 'btn-primary'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
handleLike();
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{isLiked ? (
|
||||||
|
<>
|
||||||
|
<FaThumbsUp className="lg:text-2xl" />
|
||||||
|
Liked
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaRegThumbsUp className="lg:text-2xl" />
|
||||||
|
Like
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LikeButton;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
const BeerPostLoadingCard: FC = () => {
|
const LoadingCard: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-300">
|
<div className="card bg-base-300">
|
||||||
<figure className="h-96 border-8 border-base-300 bg-base-300">
|
<figure className="h-96 border-8 border-base-300 bg-base-300">
|
||||||
@@ -23,4 +23,4 @@ const BeerPostLoadingCard: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BeerPostLoadingCard;
|
export default LoadingCard;
|
||||||
20
src/components/ui/LocationMarker.tsx
Normal file
20
src/components/ui/LocationMarker.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { HiLocationMarker } from 'react-icons/hi';
|
||||||
|
|
||||||
|
interface LocationMarkerProps {
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
color?: 'blue' | 'red' | 'green' | 'yellow';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses: Record<NonNullable<LocationMarkerProps['size']>, `text-${string}`> = {
|
||||||
|
sm: 'text-2xl',
|
||||||
|
md: 'text-3xl',
|
||||||
|
lg: 'text-4xl',
|
||||||
|
xl: 'text-5xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocationMarker: FC<LocationMarkerProps> = ({ size = 'md', color = 'blue' }) => {
|
||||||
|
return <HiLocationMarker className={`${sizeClasses[size]} text-${color}-400`} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LocationMarker);
|
||||||
12
src/config/env/index.ts
vendored
12
src/config/env/index.ts
vendored
@@ -22,6 +22,7 @@ const envSchema = z.object({
|
|||||||
NODE_ENV: z.enum(['development', 'production', 'test']),
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||||
SPARKPOST_API_KEY: z.string(),
|
SPARKPOST_API_KEY: z.string(),
|
||||||
SPARKPOST_SENDER_ADDRESS: z.string().email(),
|
SPARKPOST_SENDER_ADDRESS: z.string().email(),
|
||||||
|
MAPBOX_ACCESS_TOKEN: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = envSchema.safeParse(env);
|
const parsed = envSchema.safeParse(env);
|
||||||
@@ -145,3 +146,14 @@ export const SPARKPOST_API_KEY = parsed.data.SPARKPOST_API_KEY;
|
|||||||
* @see https://app.sparkpost.com/domains/list/sending
|
* @see https://app.sparkpost.com/domains/list/sending
|
||||||
*/
|
*/
|
||||||
export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS;
|
export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Your Mapbox access token.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 'pk.abcdefghijklmnopqrstuvwxyz123456';
|
||||||
|
*
|
||||||
|
* @see https://docs.mapbox.com/help/how-mapbox-works/access-tokens/
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MAPBOX_ACCESS_TOKEN = parsed.data.MAPBOX_ACCESS_TOKEN;
|
||||||
12
src/config/mapbox/geocoder.ts
Normal file
12
src/config/mapbox/geocoder.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import mbxGeocoding from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
|
|
||||||
|
import { MAPBOX_ACCESS_TOKEN } from '../env';
|
||||||
|
|
||||||
|
const geocoder = mbxGeocoding({ accessToken: MAPBOX_ACCESS_TOKEN });
|
||||||
|
|
||||||
|
const geocode = async (query: string) => {
|
||||||
|
const geoData = await geocoder.forwardGeocode({ query, limit: 1 }).send();
|
||||||
|
return geoData.body.features[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default geocode;
|
||||||
@@ -27,8 +27,8 @@ export type ExtendedGetServerSideProps<
|
|||||||
) => Promise<GetServerSidePropsResult<P>>;
|
) => Promise<GetServerSidePropsResult<P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Higher Order Function that adds authentication requirement to a Next.js server-side
|
* A Higher Order Function that adds an authentication requirement to a Next.js
|
||||||
* page component.
|
* server-side page component.
|
||||||
*
|
*
|
||||||
* @param fn An async function that receives the GetServerSidePropsContext and
|
* @param fn An async function that receives the GetServerSidePropsContext and
|
||||||
* authenticated session as arguments and returns a GetServerSidePropsResult with props
|
* authenticated session as arguments and returns a GetServerSidePropsResult with props
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import useSWRInfinite from 'swr/infinite';
|
import useSWRInfinite from 'swr/infinite';
|
||||||
@@ -30,7 +30,7 @@ const useBeerPostComments = ({ id, pageSize }: UseBeerPostCommentsProps) => {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(parsed.error.message);
|
throw new Error(parsed.error.message);
|
||||||
}
|
}
|
||||||
const parsedPayload = z.array(BeerCommentQueryResult).safeParse(parsed.data.payload);
|
const parsedPayload = z.array(CommentQueryResult).safeParse(parsed.data.payload);
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error(parsedPayload.error.message);
|
throw new Error(parsedPayload.error.message);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import useSWR from 'swr';
|
|||||||
* state of the request.
|
* state of the request.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const useGetLikeCount = (beerPostId: string) => {
|
const useGetBeerPostLikeCount = (beerPostId: string) => {
|
||||||
const { error, mutate, data, isLoading } = useSWR(
|
const { error, mutate, data, isLoading } = useSWR(
|
||||||
`/api/beers/${beerPostId}/like`,
|
`/api/beers/${beerPostId}/like`,
|
||||||
async (url) => {
|
async (url) => {
|
||||||
@@ -45,4 +45,4 @@ const useGetLikeCount = (beerPostId: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useGetLikeCount;
|
export default useGetBeerPostLikeCount;
|
||||||
69
src/hooks/useBeerPostsByBrewery.ts
Normal file
69
src/hooks/useBeerPostsByBrewery.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface UseBeerPostsByBreweryParams {
|
||||||
|
pageSize: number;
|
||||||
|
breweryId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook using SWR to fetch beer posts from the API.
|
||||||
|
*
|
||||||
|
* @param options The options to use when fetching beer posts.
|
||||||
|
* @param options.pageSize The number of beer posts to fetch per page.
|
||||||
|
* @param options.breweryId The ID of the brewery to fetch beer posts for.
|
||||||
|
* @returns An object containing the beer posts, page count, and loading state.
|
||||||
|
*/
|
||||||
|
const UseBeerPostsByBrewery = ({ pageSize, breweryId }: UseBeerPostsByBreweryParams) => {
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const count = response.headers.get('X-Total-Count');
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = z.array(beerPostQueryResult).safeParse(parsed.data.payload);
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||||
|
return {
|
||||||
|
beerPosts: parsedPayload.data,
|
||||||
|
pageCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||||
|
(index) =>
|
||||||
|
`/api/breweries/${breweryId}/beers?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
const beerPosts = data?.flatMap((d) => d.beerPosts) ?? [];
|
||||||
|
const pageCount = data?.[0].pageCount ?? 0;
|
||||||
|
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||||
|
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||||
|
|
||||||
|
return {
|
||||||
|
beerPosts,
|
||||||
|
pageCount,
|
||||||
|
size,
|
||||||
|
setSize,
|
||||||
|
isLoading,
|
||||||
|
isLoadingMore,
|
||||||
|
isAtEnd,
|
||||||
|
error: error as unknown,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UseBeerPostsByBrewery;
|
||||||
71
src/hooks/useBreweryPostComments.ts
Normal file
71
src/hooks/useBreweryPostComments.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
|
||||||
|
interface UseBreweryPostCommentsProps {
|
||||||
|
id: string;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that fetches comments for a specific brewery post.
|
||||||
|
*
|
||||||
|
* @param props - The props object.
|
||||||
|
* @param props.pageNum - The page number of the comments to fetch.
|
||||||
|
* @param props.id - The ID of the brewery post to fetch comments for.
|
||||||
|
* @param props.pageSize - The number of comments to fetch per page.
|
||||||
|
* @returns An object containing the fetched comments, the total number of comment pages,
|
||||||
|
* a boolean indicating if the request is currently loading, and a function to mutate
|
||||||
|
* the data.
|
||||||
|
*/
|
||||||
|
const useBreweryPostComments = ({ id, pageSize }: UseBreweryPostCommentsProps) => {
|
||||||
|
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/breweries/${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 useBreweryPostComments;
|
||||||
62
src/hooks/useBreweryPosts.ts
Normal file
62
src/hooks/useBreweryPosts.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook using SWR to fetch brewery posts from the API.
|
||||||
|
*
|
||||||
|
* @param options The options to use when fetching brewery posts.
|
||||||
|
* @param options.pageSize The number of brewery posts to fetch per page.
|
||||||
|
* @returns An object containing the brewery posts, page count, and loading state.
|
||||||
|
*/
|
||||||
|
const useBreweryPosts = ({ pageSize }: { pageSize: number }) => {
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const count = response.headers.get('X-Total-Count');
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = z.array(BreweryPostQueryResult).safeParse(parsed.data.payload);
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('API response validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
|
||||||
|
return {
|
||||||
|
breweryPosts: parsedPayload.data,
|
||||||
|
pageCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||||
|
(index) => `/api/breweries?pageNum=${index + 1}&pageSize=${pageSize}`,
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
const breweryPosts = data?.flatMap((d) => d.breweryPosts) ?? [];
|
||||||
|
const pageCount = data?.[0].pageCount ?? 0;
|
||||||
|
const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
|
||||||
|
const isAtEnd = !(size < data?.[0].pageCount!);
|
||||||
|
|
||||||
|
return {
|
||||||
|
breweryPosts,
|
||||||
|
pageCount,
|
||||||
|
size,
|
||||||
|
setSize,
|
||||||
|
isLoading,
|
||||||
|
isLoadingMore,
|
||||||
|
isAtEnd,
|
||||||
|
error: error as unknown,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBreweryPosts;
|
||||||
45
src/hooks/useCheckIfUserLikesBreweryPost.ts
Normal file
45
src/hooks/useCheckIfUserLikesBreweryPost.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const useCheckIfUserLikesBreweryPost = (breweryPostId: string) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
`/api/breweries/${breweryPostId}/like/is-liked`,
|
||||||
|
async () => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User is not logged in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/breweries/${breweryPostId}/like/is-liked`);
|
||||||
|
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 useCheckIfUserLikesBreweryPost;
|
||||||
66
src/hooks/useGeolocation.ts
Normal file
66
src/hooks/useGeolocation.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React Hook that retrieves and monitors the user's geolocation using the
|
||||||
|
* browser's built-in `navigator.geolocation` API.
|
||||||
|
*
|
||||||
|
* @returns An object containing the user's geolocation information and any errors that
|
||||||
|
* might occur. The object has the following properties:
|
||||||
|
*
|
||||||
|
* - `coords` - The user's current geolocation coordinates, or null if the geolocation could
|
||||||
|
* not be retrieved.
|
||||||
|
* - `timestamp` - The timestamp when the user's geolocation was last updated, or null if
|
||||||
|
* the geolocation could not be retrieved.
|
||||||
|
* - `error` - Any error that might occur while retrieving or monitoring the user's
|
||||||
|
* geolocation, or null if there are no errors.
|
||||||
|
*/
|
||||||
|
const useGeolocation = () => {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
coords: GeolocationCoordinates | null;
|
||||||
|
timestamp: number | null;
|
||||||
|
}>({
|
||||||
|
coords: null,
|
||||||
|
timestamp: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<GeolocationPositionError | null>(null);
|
||||||
|
|
||||||
|
// Set up the event listeners for the geolocation updates
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Callback function for successful geolocation update.
|
||||||
|
*
|
||||||
|
* @param position - The geolocation position object.
|
||||||
|
*/
|
||||||
|
const onEvent = (position: GeolocationPosition) => {
|
||||||
|
const { coords, timestamp } = position;
|
||||||
|
setError(null);
|
||||||
|
setState({ coords, timestamp });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for geolocation error.
|
||||||
|
*
|
||||||
|
* @param geoError - The geolocation error object.
|
||||||
|
*/
|
||||||
|
const onError = (geoError: GeolocationPositionError) => {
|
||||||
|
setError(geoError);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the current geolocation
|
||||||
|
navigator.geolocation.getCurrentPosition(onEvent, onError);
|
||||||
|
|
||||||
|
// Monitor any changes in the geolocation
|
||||||
|
const watchId = navigator.geolocation.watchPosition(onEvent, onError);
|
||||||
|
|
||||||
|
// Clean up the event listeners when the component unmounts
|
||||||
|
return () => {
|
||||||
|
navigator.geolocation.clearWatch(watchId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Return the geolocation information and any errors as an object
|
||||||
|
return { coords: state.coords, timestamp: state.timestamp, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGeolocation;
|
||||||
40
src/hooks/useGetBreweryPostLikeCount.ts
Normal file
40
src/hooks/useGetBreweryPostLikeCount.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const useGetBreweryPostLikeCount = (breweryPostId: string) => {
|
||||||
|
const { error, mutate, data, isLoading } = useSWR(
|
||||||
|
`/api/breweries/${breweryPostId}/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 useGetBreweryPostLikeCount;
|
||||||
@@ -5,6 +5,8 @@ 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 { Space_Grotesk } from 'next/font/google';
|
import { Space_Grotesk } 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';
|
||||||
@@ -39,6 +41,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
|
<Analytics />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ 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 BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { createRouter, NextHandler } from 'next-connect';
|
import { createRouter, NextHandler } from 'next-connect';
|
||||||
@@ -16,7 +17,7 @@ interface DeleteCommentRequest extends UserExtendedNextApiRequest {
|
|||||||
|
|
||||||
interface EditCommentRequest extends UserExtendedNextApiRequest {
|
interface EditCommentRequest extends UserExtendedNextApiRequest {
|
||||||
query: { id: string };
|
query: { id: string };
|
||||||
body: z.infer<typeof BeerCommentValidationSchema>;
|
body: z.infer<typeof CreateCommentValidationSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIfCommentOwner = async (
|
const checkIfCommentOwner = async (
|
||||||
@@ -96,7 +97,7 @@ router
|
|||||||
.put(
|
.put(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().uuid() }),
|
||||||
bodySchema: BeerCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
checkIfCommentOwner,
|
checkIfCommentOwner,
|
||||||
|
|||||||
@@ -6,16 +6,15 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
|||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
||||||
|
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
|
||||||
|
|
||||||
import { createRouter } from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||||
body: z.infer<typeof BeerCommentValidationSchema>;
|
body: z.infer<typeof CreateCommentValidationSchema>;
|
||||||
query: { id: string };
|
query: { id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +30,7 @@ const createComment = async (
|
|||||||
|
|
||||||
const beerPostId = req.query.id;
|
const beerPostId = req.query.id;
|
||||||
|
|
||||||
const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
|
const newBeerComment: z.infer<typeof CommentQueryResult> = await createNewBeerComment({
|
||||||
await createNewBeerComment({
|
|
||||||
content,
|
content,
|
||||||
rating,
|
rating,
|
||||||
beerPostId,
|
beerPostId,
|
||||||
@@ -80,7 +78,7 @@ const router = createRouter<
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
validateRequest({
|
validateRequest({
|
||||||
bodySchema: BeerCommentValidationSchema,
|
bodySchema: CreateCommentValidationSchema,
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
querySchema: z.object({ id: z.string().uuid() }),
|
||||||
}),
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const getLikeCount = async (
|
|||||||
) => {
|
) => {
|
||||||
const id = req.query.id as string;
|
const id = req.query.id as string;
|
||||||
|
|
||||||
const likes = await DBClient.instance.beerPostLike.count({
|
const likeCount = await DBClient.instance.beerPostLike.count({
|
||||||
where: { beerPostId: id },
|
where: { beerPostId: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ const getLikeCount = async (
|
|||||||
success: true,
|
success: true,
|
||||||
message: 'Successfully retrieved like count.',
|
message: 'Successfully retrieved like count.',
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
payload: { likeCount: likes },
|
payload: { likeCount },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
72
src/pages/api/breweries/[id]/beers/index.ts
Normal file
72
src/pages/api/breweries/[id]/beers/index.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface GetAllBeersByBreweryRequest extends NextApiRequest {
|
||||||
|
query: { page_size: string; page_num: string; id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllBeersByBrewery = async (
|
||||||
|
req: GetAllBeersByBreweryRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { page_size, page_num, id } = req.query;
|
||||||
|
|
||||||
|
const beers: z.infer<typeof beerPostQueryResult>[] =
|
||||||
|
await DBClient.instance.beerPost.findMany({
|
||||||
|
where: { breweryId: id },
|
||||||
|
take: parseInt(page_size, 10),
|
||||||
|
skip: parseInt(page_num, 10) * parseInt(page_size, 10),
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
ibu: true,
|
||||||
|
abv: true,
|
||||||
|
createdAt: true,
|
||||||
|
description: true,
|
||||||
|
postedBy: { select: { username: true, id: true } },
|
||||||
|
brewery: { select: { name: true, id: true } },
|
||||||
|
type: { select: { name: true, id: true } },
|
||||||
|
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageCount = await DBClient.instance.beerPost.count({
|
||||||
|
where: { breweryId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', pageCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Beers fetched successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: beers,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetAllBeersByBreweryRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
page_size: z.string().nonempty(),
|
||||||
|
page_num: z.string().nonempty(),
|
||||||
|
id: z.string().nonempty(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getAllBeersByBrewery,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
export default handler;
|
||||||
110
src/pages/api/breweries/[id]/comments/index.ts
Normal file
110
src/pages/api/breweries/[id]/comments/index.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
||||||
|
|
||||||
|
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/types/CommentSchema/CommentQueryResult';
|
||||||
|
import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments';
|
||||||
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
import createNewBreweryComment from '@/services/BreweryComment/createNewBreweryComment';
|
||||||
|
|
||||||
|
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 breweryPostId = req.query.id;
|
||||||
|
|
||||||
|
const user = req.user!;
|
||||||
|
|
||||||
|
const newBreweryComment: z.infer<typeof CommentQueryResult> =
|
||||||
|
await createNewBreweryComment({
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
breweryPostId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Beer comment created successfully',
|
||||||
|
statusCode: 201,
|
||||||
|
payload: newBreweryComment,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAll = async (
|
||||||
|
req: GetAllCommentsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const breweryPostId = req.query.id;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { page_size, page_num } = req.query;
|
||||||
|
|
||||||
|
const comments = await getAllBreweryComments(
|
||||||
|
{ id: breweryPostId },
|
||||||
|
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageCount = await DBClient.instance.breweryComment.count({
|
||||||
|
where: { breweryPostId },
|
||||||
|
});
|
||||||
|
|
||||||
|
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().uuid() }),
|
||||||
|
}),
|
||||||
|
getCurrentUser,
|
||||||
|
createComment,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
page_size: z.coerce.number().int().positive(),
|
||||||
|
page_num: z.coerce.number().int().positive(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
97
src/pages/api/breweries/[id]/like/index.ts
Normal file
97
src/pages/api/breweries/[id]/like/index.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const sendLikeRequest = async (
|
||||||
|
req: UserExtendedNextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const id = req.query.id! as string;
|
||||||
|
const user = req.user!;
|
||||||
|
|
||||||
|
const breweryPost = await DBClient.instance.breweryPost.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!breweryPost) {
|
||||||
|
throw new ServerError('Could not find a brewery post with that id', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({
|
||||||
|
where: { breweryPostId: breweryPost.id, likedById: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const jsonResponse = {
|
||||||
|
success: true as const,
|
||||||
|
message: '',
|
||||||
|
statusCode: 200 as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (alreadyLiked) {
|
||||||
|
await DBClient.instance.breweryPostLike.delete({
|
||||||
|
where: { id: alreadyLiked.id },
|
||||||
|
});
|
||||||
|
jsonResponse.message = 'Successfully unliked brewery post';
|
||||||
|
} else {
|
||||||
|
await DBClient.instance.breweryPostLike.create({
|
||||||
|
data: { breweryPostId: breweryPost.id, likedById: user.id },
|
||||||
|
});
|
||||||
|
jsonResponse.message = 'Successfully liked brewery post';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(jsonResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLikeCount = async (
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const id = req.query.id! as string;
|
||||||
|
|
||||||
|
const breweryPost = await DBClient.instance.breweryPost.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!breweryPost) {
|
||||||
|
throw new ServerError('Could not find a brewery post with that id', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const likeCount = await DBClient.instance.breweryPostLike.count({
|
||||||
|
where: { breweryPostId: breweryPost.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().uuid() }) }),
|
||||||
|
sendLikeRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
||||||
|
getLikeCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
export default handler;
|
||||||
49
src/pages/api/breweries/[id]/like/is-liked.ts
Normal file
49
src/pages/api/breweries/[id]/like/is-liked.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const checkIfLiked = async (
|
||||||
|
req: UserExtendedNextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const user = req.user!;
|
||||||
|
const id = req.query.id as string;
|
||||||
|
|
||||||
|
const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({
|
||||||
|
where: {
|
||||||
|
breweryPostId: id,
|
||||||
|
likedById: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: alreadyLiked ? 'Brewery post is liked.' : 'Brewery post 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().uuid(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
checkIfLiked,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
54
src/pages/api/breweries/index.ts
Normal file
54
src/pages/api/breweries/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface GetBreweryPostsRequest extends NextApiRequest {
|
||||||
|
query: {
|
||||||
|
pageNum: string;
|
||||||
|
pageSize: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBreweryPosts = async (
|
||||||
|
req: GetBreweryPostsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const pageNum = parseInt(req.query.pageNum, 10);
|
||||||
|
const pageSize = parseInt(req.query.pageSize, 10);
|
||||||
|
|
||||||
|
const breweryPosts = await getAllBreweryPosts(pageNum, pageSize);
|
||||||
|
const breweryPostCount = await DBClient.instance.breweryPost.count();
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', breweryPostCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Brewery posts retrieved successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: breweryPosts,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
GetBreweryPostsRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
pageNum: z.string().regex(/^\d+$/),
|
||||||
|
pageSize: z.string().regex(/^\d+$/),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getBreweryPosts,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler();
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -14,7 +14,7 @@ import { BeerPost } from '@prisma/client';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
import 'react-responsive-carousel/lib/styles/carousel.min.css';
|
||||||
import { Carousel } from 'react-responsive-carousel';
|
import { Carousel } from 'react-responsive-carousel';
|
||||||
import useMediaQuery from '@/hooks/useMediaQuery';
|
import useMediaQuery from '@/hooks/useMediaQuery';
|
||||||
import { Tab } from '@headlessui/react';
|
import { Tab } from '@headlessui/react';
|
||||||
@@ -37,7 +37,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
<meta name="description" content={beerPost.description} />
|
<meta name="description" content={beerPost.description} />
|
||||||
</Head>
|
</Head>
|
||||||
<>
|
<>
|
||||||
<div>
|
|
||||||
<Carousel
|
<Carousel
|
||||||
className="w-full"
|
className="w-full"
|
||||||
useKeyboardArrows
|
useKeyboardArrows
|
||||||
@@ -63,7 +62,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
|
||||||
<div className="mb-12 mt-10 flex w-full items-center justify-center ">
|
<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">
|
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||||
<BeerInfoHeader beerPost={beerPost} />
|
<BeerInfoHeader beerPost={beerPost} />
|
||||||
|
|
||||||
@@ -97,8 +96,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface CreateBeerPageProps {
|
|||||||
types: BeerType[];
|
types: BeerType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
|
const CreateBeerPost: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
|
||||||
return (
|
return (
|
||||||
<FormPageLayout
|
<FormPageLayout
|
||||||
headingText="Create a new beer"
|
headingText="Create a new beer"
|
||||||
@@ -40,4 +40,4 @@ export const getServerSideProps = withPageAuthRequired<CreateBeerPageProps>(asyn
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Create;
|
export default CreateBeerPost;
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { useInView } from 'react-intersection-observer';
|
|||||||
import Spinner from '@/components/ui/Spinner';
|
import Spinner from '@/components/ui/Spinner';
|
||||||
|
|
||||||
import useBeerPosts from '@/hooks/useBeerPosts';
|
import useBeerPosts from '@/hooks/useBeerPosts';
|
||||||
import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard';
|
|
||||||
import { FaArrowUp, FaPlus } from 'react-icons/fa';
|
import { FaArrowUp, FaPlus } from 'react-icons/fa';
|
||||||
|
import LoadingCard from '@/components/ui/LoadingCard';
|
||||||
|
|
||||||
const BeerPage: NextPage = () => {
|
const BeerPage: NextPage = () => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
@@ -34,13 +34,19 @@ const BeerPage: NextPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Beer</title>
|
<title>Beers | The Biergarten App</title>
|
||||||
<meta name="description" content="Beer posts" />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Find beers made by breweries near you and around the world."
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
|
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
|
||||||
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
|
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
|
||||||
<header className="my-10 flex justify-between lg:flex-row">
|
<header className="my-10 flex justify-between lg:flex-row">
|
||||||
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten App</h1>
|
||||||
|
<h2 className="text-2xl font-bold lg:text-4xl">Beers</h2>
|
||||||
|
</div>
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<div
|
<div
|
||||||
className="tooltip tooltip-left h-full"
|
className="tooltip tooltip-left h-full"
|
||||||
@@ -70,7 +76,7 @@ const BeerPage: NextPage = () => {
|
|||||||
{(isLoading || isLoadingMore) && (
|
{(isLoading || isLoadingMore) && (
|
||||||
<>
|
<>
|
||||||
{Array.from({ length: PAGE_SIZE }, (_, i) => (
|
{Array.from({ length: PAGE_SIZE }, (_, i) => (
|
||||||
<BeerPostLoadingCard key={i} />
|
<LoadingCard key={i} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,97 @@
|
|||||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
||||||
|
import { Carousel } from 'react-responsive-carousel';
|
||||||
|
import useMediaQuery from '@/hooks/useMediaQuery';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader';
|
||||||
|
import BreweryPostMap from '@/components/BreweryById/BreweryPostMap';
|
||||||
|
import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection';
|
||||||
|
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
|
||||||
|
|
||||||
interface BreweryPageProps {
|
interface BreweryPageProps {
|
||||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
||||||
|
const [longitude, latitude] = breweryPost.location.coordinates;
|
||||||
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-3xl font-bold underline">{breweryPost.name}</h1>
|
<Head>
|
||||||
|
<title>{breweryPost.name}</title>
|
||||||
|
<meta name="description" content={breweryPost.description} />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Carousel
|
||||||
|
className="w-full"
|
||||||
|
useKeyboardArrows
|
||||||
|
autoPlay
|
||||||
|
interval={10000}
|
||||||
|
infiniteLoop
|
||||||
|
showThumbs={false}
|
||||||
|
>
|
||||||
|
{breweryPost.breweryImages.length
|
||||||
|
? breweryPost.breweryImages.map((image, index) => (
|
||||||
|
<div key={image.id} id={`image-${index}}`} className="w-full">
|
||||||
|
<Image
|
||||||
|
alt={image.alt}
|
||||||
|
src={image.path}
|
||||||
|
height={1080}
|
||||||
|
width={1920}
|
||||||
|
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: Array.from({ length: 1 }).map((_, i) => (
|
||||||
|
<div className="h-96 lg:h-[42rem]" key={i} />
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
<div 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">
|
||||||
|
<BreweryInfoHeader breweryPost={breweryPost} />
|
||||||
|
{isDesktop ? (
|
||||||
|
<div className="mt-4 flex flex-row space-x-3 space-y-0">
|
||||||
|
<div className="w-[60%]">
|
||||||
|
<BreweryCommentsSection breweryPost={breweryPost} />
|
||||||
|
</div>
|
||||||
|
<div className="w-[40%] space-y-3">
|
||||||
|
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
||||||
|
<BreweryBeersSection breweryPost={breweryPost} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
||||||
|
<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
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels className="mt-2">
|
||||||
|
<Tab.Panel>
|
||||||
|
<BreweryCommentsSection breweryPost={breweryPost} />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BreweryBeersSection breweryPost={breweryPost} />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,70 +1,135 @@
|
|||||||
import { GetServerSideProps, NextPage } from 'next';
|
import BreweryCard from '@/components/BreweryIndex/BreweryCard';
|
||||||
|
import LoadingCard from '@/components/ui/LoadingCard';
|
||||||
import Link from 'next/link';
|
import Spinner from '@/components/ui/Spinner';
|
||||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import useBreweryPosts from '@/hooks/useBreweryPosts';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { NextPage } from 'next';
|
||||||
import { FC } from 'react';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import { useContext, MutableRefObject, useRef } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { FaPlus, FaArrowUp } from 'react-icons/fa';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface BreweryPageProps {
|
interface BreweryPageProps {
|
||||||
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
|
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
const BreweryPage: NextPage<BreweryPageProps> = () => {
|
||||||
brewery,
|
const PAGE_SIZE = 6;
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="card" key={brewery.id}>
|
|
||||||
<figure className="card-image h-96">
|
|
||||||
{brewery.breweryImages.length > 0 && (
|
|
||||||
<Image
|
|
||||||
src={brewery.breweryImages[0].path}
|
|
||||||
alt={brewery.name}
|
|
||||||
width="1029"
|
|
||||||
height="110"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</figure>
|
|
||||||
<div className="card-body space-y-3">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-3xl font-bold">
|
|
||||||
<Link href={`/breweries/${brewery.id}`}>{brewery.name}</Link>
|
|
||||||
</h2>
|
|
||||||
<h3 className="text-xl font-semibold">{brewery.location}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
const { breweryPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } =
|
||||||
|
useBreweryPosts({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ref: lastBreweryPostRef } = useInView({
|
||||||
|
onChange: (visible) => {
|
||||||
|
if (!visible || isAtEnd) return;
|
||||||
|
setSize(size + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-center bg-base-100">
|
<Head>
|
||||||
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
<title>Breweries</title>
|
||||||
<header className="my-10">
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Find breweries near you and around the world."
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
|
||||||
|
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
|
||||||
|
<header className="my-10 flex justify-between lg:flex-row">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-6xl font-bold">Breweries</h1>
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten App</h1>
|
||||||
|
<h2 className="text-2xl font-bold lg:text-4xl">Breweries</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
className="link-hover link text-xl font-bold lg:text-2xl"
|
||||||
|
href="/breweries/map"
|
||||||
|
>
|
||||||
|
View map
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!user && (
|
||||||
|
<div
|
||||||
|
className="tooltip tooltip-left h-full"
|
||||||
|
data-tip="Create a new brewery post"
|
||||||
|
>
|
||||||
|
<Link href="/breweries/create" className="btn-ghost btn-sm btn">
|
||||||
|
<FaPlus />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
<div className="grid gap-5 md:grid-cols-1 xl:grid-cols-2">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
{breweryPosts.map((brewery) => {
|
{!!breweryPosts.length && !isLoading && (
|
||||||
return <BreweryCard brewery={brewery} key={brewery.id} />;
|
<>
|
||||||
})}
|
{breweryPosts.map((breweryPost) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={breweryPost.id}
|
||||||
|
ref={
|
||||||
|
breweryPosts[breweryPosts.length - 1] === breweryPost
|
||||||
|
? lastBreweryPostRef
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BreweryCard brewery={breweryPost} />
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(isLoading || isLoadingMore) && (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: PAGE_SIZE }, (_, i) => (
|
||||||
|
<LoadingCard key={i} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(isLoading || isLoadingMore) && (
|
||||||
|
<div className="flex h-32 w-full items-center justify-center">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isAtEnd && !isLoading && (
|
||||||
|
<div className="flex h-20 items-center justify-center text-center">
|
||||||
|
<div
|
||||||
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip="Scroll back to top of page."
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
aria-label="Scroll back to top of page."
|
||||||
|
onClick={() => {
|
||||||
|
pageRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<BreweryPageProps> = async () => {
|
|
||||||
const breweryPosts = await getAllBreweryPosts();
|
|
||||||
return {
|
|
||||||
props: { breweryPosts: JSON.parse(JSON.stringify(breweryPosts)) },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BreweryPage;
|
export default BreweryPage;
|
||||||
|
|||||||
152
src/pages/breweries/map.tsx
Normal file
152
src/pages/breweries/map.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import Map, {
|
||||||
|
FullscreenControl,
|
||||||
|
Marker,
|
||||||
|
NavigationControl,
|
||||||
|
Popup,
|
||||||
|
ScaleControl,
|
||||||
|
} from 'react-map-gl';
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
import LocationMarker from '@/components/ui/LocationMarker';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import useGeolocation from '@/hooks/useGeolocation';
|
||||||
|
|
||||||
|
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
||||||
|
|
||||||
|
interface BreweryMapPageProps {
|
||||||
|
breweries: {
|
||||||
|
location: {
|
||||||
|
city: string;
|
||||||
|
stateOrProvince: string | null;
|
||||||
|
country: string | null;
|
||||||
|
coordinates: number[];
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ breweries }) => {
|
||||||
|
const windowIsDefined = typeof window !== 'undefined';
|
||||||
|
const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme');
|
||||||
|
|
||||||
|
const [popupInfo, setPopupInfo] = useState<BreweryMapPageProps['breweries'][0] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = (
|
||||||
|
windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light'
|
||||||
|
) as 'light' | 'dark';
|
||||||
|
|
||||||
|
const mapStyles: MapStyles = {
|
||||||
|
light: 'mapbox://styles/mapbox/light-v10',
|
||||||
|
dark: 'mapbox://styles/mapbox/dark-v11',
|
||||||
|
};
|
||||||
|
|
||||||
|
const pins = useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
{breweries.map((brewery) => {
|
||||||
|
const [longitude, latitude] = brewery.location.coordinates;
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
latitude={latitude}
|
||||||
|
longitude={longitude}
|
||||||
|
key={brewery.id}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.originalEvent.stopPropagation();
|
||||||
|
setPopupInfo(brewery);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LocationMarker size="md" color="blue" />
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[breweries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { coords, error } = useGeolocation();
|
||||||
|
|
||||||
|
const userLocationPin = useMemo(
|
||||||
|
() =>
|
||||||
|
coords && !error ? (
|
||||||
|
<Marker latitude={coords.latitude} longitude={coords.longitude}>
|
||||||
|
<LocationMarker size="lg" color="red" />
|
||||||
|
</Marker>
|
||||||
|
) : null,
|
||||||
|
[coords, error],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Brewery Map | The Biergarten App</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Find breweries near you and around the world."
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<div className="h-full">
|
||||||
|
<Map
|
||||||
|
initialViewState={{ zoom: 2 }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
mapStyle={mapStyles[theme]}
|
||||||
|
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}
|
||||||
|
scrollZoom
|
||||||
|
>
|
||||||
|
<FullscreenControl position="top-left" />
|
||||||
|
<NavigationControl position="top-left" />
|
||||||
|
<ScaleControl />
|
||||||
|
{pins}
|
||||||
|
{userLocationPin}
|
||||||
|
{popupInfo && (
|
||||||
|
<Popup
|
||||||
|
anchor="bottom"
|
||||||
|
longitude={popupInfo.location.coordinates[0]}
|
||||||
|
latitude={popupInfo.location.coordinates[1]}
|
||||||
|
onClose={() => setPopupInfo(null)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col text-black ">
|
||||||
|
<Link
|
||||||
|
className="link-hover link text-base font-bold"
|
||||||
|
href={`/breweries/${popupInfo.id}`}
|
||||||
|
>
|
||||||
|
{popupInfo.name}
|
||||||
|
</Link>
|
||||||
|
<p className="text-base">
|
||||||
|
{popupInfo.location.city}
|
||||||
|
{popupInfo.location.stateOrProvince
|
||||||
|
? `, ${popupInfo.location.stateOrProvince}`
|
||||||
|
: ''}
|
||||||
|
{popupInfo.location.country ? `, ${popupInfo.location.country}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreweryMapPage;
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<BreweryMapPageProps> = async () => {
|
||||||
|
const breweries = await DBClient.instance.breweryPost.findMany({
|
||||||
|
select: {
|
||||||
|
location: {
|
||||||
|
select: { coordinates: true, city: true, country: true, stateOrProvince: true },
|
||||||
|
},
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { props: { breweries } };
|
||||||
|
};
|
||||||
16
src/prisma/migrations/20230423163714_/migration.sql
Normal file
16
src/prisma/migrations/20230423163714_/migration.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BreweryPostLike" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"breweryPostId" STRING NOT NULL,
|
||||||
|
"likedById" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BreweryPostLike_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryPostLike" ADD CONSTRAINT "BreweryPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
2
src/prisma/migrations/20230423235322_/migration.sql
Normal file
2
src/prisma/migrations/20230423235322_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "dateEstablished" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
15
src/prisma/migrations/20230424192859_/migration.sql
Normal file
15
src/prisma/migrations/20230424192859_/migration.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `location` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `address` to the `BreweryPost` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `city` to the `BreweryPost` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "location";
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "address" STRING NOT NULL;
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "city" STRING NOT NULL;
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "coordinates" FLOAT8[];
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "country" STRING;
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "stateOrProvince" STRING;
|
||||||
41
src/prisma/migrations/20230426013222_/migration.sql
Normal file
41
src/prisma/migrations/20230426013222_/migration.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `address` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `city` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `coordinates` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `country` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `stateOrProvince` on the `BreweryPost` table. All the data in the column will be lost.
|
||||||
|
- A unique constraint covering the columns `[locationId]` on the table `BreweryPost` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `locationId` to the `BreweryPost` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "address";
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "city";
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "coordinates";
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "country";
|
||||||
|
ALTER TABLE "BreweryPost" DROP COLUMN "stateOrProvince";
|
||||||
|
ALTER TABLE "BreweryPost" ADD COLUMN "locationId" STRING NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Location" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"city" STRING NOT NULL,
|
||||||
|
"stateOrProvince" STRING,
|
||||||
|
"country" STRING,
|
||||||
|
"coordinates" FLOAT8[],
|
||||||
|
"address" STRING NOT NULL,
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "BreweryPost_locationId_key" ON "BreweryPost"("locationId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Location" ADD CONSTRAINT "Location_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryPost" ADD CONSTRAINT "BreweryPost_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -29,6 +29,8 @@ model User {
|
|||||||
BeerPostLikes BeerPostLike[]
|
BeerPostLikes BeerPostLike[]
|
||||||
BeerImage BeerImage[]
|
BeerImage BeerImage[]
|
||||||
BreweryImage BreweryImage[]
|
BreweryImage BreweryImage[]
|
||||||
|
BreweryPostLike BreweryPostLike[]
|
||||||
|
Location Location[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model BeerPost {
|
model BeerPost {
|
||||||
@@ -60,6 +62,16 @@ model BeerPostLike {
|
|||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model BreweryPostLike {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
||||||
|
breweryPostId 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 BeerComment {
|
model BeerComment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
rating Int
|
rating Int
|
||||||
@@ -82,10 +94,23 @@ model BeerType {
|
|||||||
beerPosts BeerPost[]
|
beerPosts BeerPost[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Location {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
city String
|
||||||
|
stateOrProvince String?
|
||||||
|
country String?
|
||||||
|
coordinates Float[]
|
||||||
|
address String
|
||||||
|
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||||
|
postedById String
|
||||||
|
BreweryPost BreweryPost?
|
||||||
|
}
|
||||||
|
|
||||||
model BreweryPost {
|
model BreweryPost {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
location String
|
location Location @relation(fields: [locationId], references: [id])
|
||||||
|
locationId String @unique
|
||||||
beers BeerPost[]
|
beers BeerPost[]
|
||||||
description String
|
description String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
@@ -94,6 +119,8 @@ model BreweryPost {
|
|||||||
postedById String
|
postedById String
|
||||||
breweryComments BreweryComment[]
|
breweryComments BreweryComment[]
|
||||||
breweryImages BreweryImage[]
|
breweryImages BreweryImage[]
|
||||||
|
breweryPostLike BreweryPostLike[]
|
||||||
|
dateEstablished DateTime @default(now()) @db.Timestamptz(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model BreweryComment {
|
model BreweryComment {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import logger from '@/config/pino/logger';
|
import logger from '../../../config/pino/logger';
|
||||||
import cleanDatabase from './cleanDatabase';
|
import cleanDatabase from './cleanDatabase';
|
||||||
|
|
||||||
cleanDatabase().then(() => {
|
cleanDatabase().then(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BeerPost, BeerImage, User } from '@prisma/client';
|
import { BeerPost, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateNewBeerImagesArgs {
|
interface CreateNewBeerImagesArgs {
|
||||||
@@ -8,13 +8,22 @@ interface CreateNewBeerImagesArgs {
|
|||||||
joinData: { beerPosts: BeerPost[]; users: User[] };
|
joinData: { beerPosts: BeerPost[]; users: User[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BeerImageData {
|
||||||
|
path: string;
|
||||||
|
alt: string;
|
||||||
|
caption: string;
|
||||||
|
beerPostId: string;
|
||||||
|
postedById: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
const createNewBeerImages = async ({
|
const createNewBeerImages = async ({
|
||||||
numberOfImages,
|
numberOfImages,
|
||||||
joinData: { beerPosts, users },
|
joinData: { beerPosts, users },
|
||||||
}: CreateNewBeerImagesArgs) => {
|
}: CreateNewBeerImagesArgs) => {
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
const beerImagesPromises: Promise<BeerImage>[] = [];
|
|
||||||
|
const beerImageData: BeerImageData[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfImages; i++) {
|
for (let i = 0; i < numberOfImages; i++) {
|
||||||
@@ -23,21 +32,18 @@ const createNewBeerImages = async ({
|
|||||||
const caption = faker.lorem.sentence();
|
const caption = faker.lorem.sentence();
|
||||||
const alt = faker.lorem.sentence();
|
const alt = faker.lorem.sentence();
|
||||||
|
|
||||||
beerImagesPromises.push(
|
beerImageData.push({
|
||||||
prisma.beerImage.create({
|
|
||||||
data: {
|
|
||||||
path: 'https://picsum.photos/5000/5000',
|
path: 'https://picsum.photos/5000/5000',
|
||||||
alt,
|
alt,
|
||||||
caption,
|
caption,
|
||||||
beerPost: { connect: { id: beerPost.id } },
|
beerPostId: beerPost.id,
|
||||||
postedBy: { connect: { id: user.id } },
|
postedById: user.id,
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(beerImagesPromises);
|
await prisma.beerImage.createMany({ data: beerImageData });
|
||||||
|
return prisma.beerImage.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerImages;
|
export default createNewBeerImages;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BeerComment, BeerPost, User } from '@prisma/client';
|
import { BeerPost, User } from '@prisma/client';
|
||||||
|
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
@@ -11,32 +11,46 @@ interface CreateNewBeerCommentsArgs {
|
|||||||
users: User[];
|
users: User[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BeerCommentData {
|
||||||
|
content: string;
|
||||||
|
postedById: string;
|
||||||
|
beerPostId: string;
|
||||||
|
rating: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBeerComments = async ({
|
const createNewBeerComments = async ({
|
||||||
numberOfComments,
|
numberOfComments,
|
||||||
joinData,
|
joinData,
|
||||||
}: CreateNewBeerCommentsArgs) => {
|
}: CreateNewBeerCommentsArgs) => {
|
||||||
const { beerPosts, users } = joinData;
|
const { beerPosts, users } = joinData;
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const beerCommentPromises: Promise<BeerComment>[] = [];
|
|
||||||
|
const beerCommentData: BeerCommentData[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfComments; i++) {
|
for (let i = 0; i < numberOfComments; i++) {
|
||||||
const content = faker.lorem.lines(5);
|
const content = faker.lorem.lines(5);
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
beerCommentPromises.push(
|
const rating = Math.floor(Math.random() * 5) + 1;
|
||||||
prisma.beerComment.create({
|
|
||||||
data: {
|
beerCommentData.push({
|
||||||
content,
|
content,
|
||||||
postedBy: { connect: { id: user.id } },
|
postedById: user.id,
|
||||||
beerPost: { connect: { id: beerPost.id } },
|
beerPostId: beerPost.id,
|
||||||
rating: Math.floor(Math.random() * 5) + 1,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
rating,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Promise.all(beerCommentPromises);
|
|
||||||
|
await prisma.beerComment.createMany({
|
||||||
|
data: beerCommentData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return prisma.beerComment.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerComments;
|
export default createNewBeerComments;
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
import type { BeerPost, BeerPostLike, User } from '@prisma/client';
|
import type { BeerPost, User } from '@prisma/client';
|
||||||
|
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
|
interface BeerPostLikeData {
|
||||||
|
beerPostId: string;
|
||||||
|
likedById: string;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBeerPostLikes = async ({
|
const createNewBeerPostLikes = async ({
|
||||||
joinData: { beerPosts, users },
|
joinData: { beerPosts, users },
|
||||||
numberOfLikes,
|
numberOfLikes,
|
||||||
}: {
|
}: {
|
||||||
joinData: {
|
joinData: { beerPosts: BeerPost[]; users: User[] };
|
||||||
beerPosts: BeerPost[];
|
|
||||||
users: User[];
|
|
||||||
};
|
|
||||||
numberOfLikes: number;
|
numberOfLikes: number;
|
||||||
}) => {
|
}) => {
|
||||||
const beerPostLikePromises: Promise<BeerPostLike>[] = [];
|
const beerPostLikeData: BeerPostLikeData[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfLikes; i++) {
|
for (let i = 0; i < numberOfLikes; i++) {
|
||||||
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
|
||||||
beerPostLikePromises.push(
|
beerPostLikeData.push({
|
||||||
DBClient.instance.beerPostLike.create({
|
beerPostId: beerPost.id,
|
||||||
data: {
|
likedById: user.id,
|
||||||
beerPost: { connect: { id: beerPost.id } },
|
});
|
||||||
likedBy: { connect: { id: user.id } },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(beerPostLikePromises);
|
await DBClient.instance.beerPostLike.createMany({
|
||||||
|
data: beerPostLikeData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return DBClient.instance.beerPostLike.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerPostLikes;
|
export default createNewBeerPostLikes;
|
||||||
|
|||||||
@@ -13,35 +13,50 @@ interface CreateNewBeerPostsArgs {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BeerPostData {
|
||||||
|
abv: number;
|
||||||
|
ibu: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: Date;
|
||||||
|
breweryId: string;
|
||||||
|
postedById: string;
|
||||||
|
typeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBeerPosts = async ({
|
const createNewBeerPosts = async ({
|
||||||
numberOfPosts,
|
numberOfPosts,
|
||||||
joinData,
|
joinData,
|
||||||
}: CreateNewBeerPostsArgs) => {
|
}: CreateNewBeerPostsArgs) => {
|
||||||
const { users, breweryPosts, beerTypes } = joinData;
|
const { users, breweryPosts, beerTypes } = joinData;
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const beerPostPromises = [];
|
const beerPostData: BeerPostData[] = [];
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfPosts; i++) {
|
for (let i = 0; i < numberOfPosts; i++) {
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)];
|
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)];
|
||||||
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
beerPostPromises.push(
|
|
||||||
prisma.beerPost.create({
|
const abv = Math.floor(Math.random() * (12 - 4) + 4);
|
||||||
data: {
|
const ibu = Math.floor(Math.random() * (60 - 10) + 10);
|
||||||
abv: Math.floor(Math.random() * (12 - 4) + 4),
|
const name = faker.commerce.productName();
|
||||||
ibu: Math.floor(Math.random() * (60 - 10) + 10),
|
const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
|
||||||
name: faker.commerce.productName(),
|
|
||||||
description: faker.lorem.lines(12).replace(/(\r\n|\n|\r)/gm, ' '),
|
beerPostData.push({
|
||||||
brewery: { connect: { id: breweryPost.id } },
|
postedById: user.id,
|
||||||
postedBy: { connect: { id: user.id } },
|
typeId: beerType.id,
|
||||||
type: { connect: { id: beerType.id } },
|
breweryId: breweryPost.id,
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
abv,
|
||||||
}),
|
ibu,
|
||||||
);
|
name,
|
||||||
|
description,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Promise.all(beerPostPromises);
|
|
||||||
|
await prisma.beerPost.createMany({ data: beerPostData });
|
||||||
|
return prisma.beerPost.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerPosts;
|
export default createNewBeerPosts;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { User, BeerType } from '@prisma/client';
|
import { User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateNewBeerTypesArgs {
|
interface CreateNewBeerTypesArgs {
|
||||||
@@ -9,10 +9,17 @@ interface CreateNewBeerTypesArgs {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BeerTypeData {
|
||||||
|
name: string;
|
||||||
|
postedById: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
|
const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
|
||||||
const { users } = joinData;
|
const { users } = joinData;
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const beerTypePromises: Promise<BeerType>[] = [];
|
|
||||||
|
const beerTypeData: BeerTypeData[] = [];
|
||||||
|
|
||||||
const types = [
|
const types = [
|
||||||
'IPA',
|
'IPA',
|
||||||
@@ -39,14 +46,16 @@ const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
|
|||||||
types.forEach((type) => {
|
types.forEach((type) => {
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
beerTypePromises.push(
|
|
||||||
prisma.beerType.create({
|
beerTypeData.push({
|
||||||
data: { name: type, postedBy: { connect: { id: user.id } }, createdAt },
|
name: type,
|
||||||
}),
|
postedById: user.id,
|
||||||
);
|
createdAt,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(beerTypePromises);
|
await prisma.beerType.createMany({ data: beerTypeData, skipDuplicates: true });
|
||||||
|
return prisma.beerType.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBeerTypes;
|
export default createNewBeerTypes;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BreweryPost, BreweryImage, User } from '@prisma/client';
|
import { BreweryPost, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateBreweryImagesArgs {
|
interface CreateBreweryImagesArgs {
|
||||||
@@ -11,34 +11,42 @@ interface CreateBreweryImagesArgs {
|
|||||||
users: User[];
|
users: User[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface BreweryImageData {
|
||||||
|
path: string;
|
||||||
|
alt: string;
|
||||||
|
caption: string;
|
||||||
|
breweryPostId: string;
|
||||||
|
postedById: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBreweryImages = async ({
|
const createNewBreweryImages = async ({
|
||||||
numberOfImages,
|
numberOfImages,
|
||||||
joinData: { breweryPosts, users },
|
joinData: { breweryPosts, users },
|
||||||
}: CreateBreweryImagesArgs) => {
|
}: CreateBreweryImagesArgs) => {
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
const breweryImagesPromises: Promise<BreweryImage>[] = [];
|
const breweryImageData: BreweryImageData[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfImages; i++) {
|
for (let i = 0; i < numberOfImages; i++) {
|
||||||
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
|
||||||
breweryImagesPromises.push(
|
breweryImageData.push({
|
||||||
prisma.breweryImage.create({
|
|
||||||
data: {
|
|
||||||
path: 'https://picsum.photos/5000/5000',
|
path: 'https://picsum.photos/5000/5000',
|
||||||
alt: 'Placeholder brewery image.',
|
alt: 'Placeholder brewery image.',
|
||||||
caption: 'Placeholder brewery image caption.',
|
caption: 'Placeholder brewery image caption.',
|
||||||
breweryPost: { connect: { id: breweryPost.id } },
|
breweryPostId: breweryPost.id,
|
||||||
postedBy: { connect: { id: user.id } },
|
postedById: user.id,
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(breweryImagesPromises);
|
await prisma.breweryImage.createMany({
|
||||||
};
|
data: breweryImageData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return prisma.breweryImage.findMany();
|
||||||
|
};
|
||||||
export default createNewBreweryImages;
|
export default createNewBreweryImages;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BreweryComment, BreweryPost, User } from '@prisma/client';
|
import { BreweryPost, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateNewBreweryPostCommentsArgs {
|
interface CreateNewBreweryPostCommentsArgs {
|
||||||
@@ -11,32 +11,40 @@ interface CreateNewBreweryPostCommentsArgs {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BreweryPostCommentData {
|
||||||
|
content: string;
|
||||||
|
postedById: string;
|
||||||
|
breweryPostId: string;
|
||||||
|
rating: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBreweryPostComments = async ({
|
const createNewBreweryPostComments = async ({
|
||||||
numberOfComments,
|
numberOfComments,
|
||||||
joinData,
|
joinData,
|
||||||
}: CreateNewBreweryPostCommentsArgs) => {
|
}: CreateNewBreweryPostCommentsArgs) => {
|
||||||
const { breweryPosts, users } = joinData;
|
const { breweryPosts, users } = joinData;
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const breweryCommentPromises: Promise<BreweryComment>[] = [];
|
const breweryPostCommentData: BreweryPostCommentData[] = [];
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
|
const rating = Math.floor(Math.random() * 5) + 1;
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfComments; i++) {
|
for (let i = 0; i < numberOfComments; i++) {
|
||||||
const content = faker.lorem.lines(5);
|
const content = faker.lorem.lines(3).replace(/\n/g, ' ');
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||||
breweryCommentPromises.push(
|
|
||||||
prisma.breweryComment.create({
|
breweryPostCommentData.push({
|
||||||
data: {
|
|
||||||
content,
|
content,
|
||||||
postedBy: { connect: { id: user.id } },
|
|
||||||
breweryPost: { connect: { id: breweryPost.id } },
|
|
||||||
rating: Math.floor(Math.random() * 5) + 1,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
rating,
|
||||||
}),
|
postedById: user.id,
|
||||||
);
|
breweryPostId: breweryPost.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Promise.all(breweryCommentPromises);
|
await prisma.breweryComment.createMany({ data: breweryPostCommentData });
|
||||||
|
|
||||||
|
return prisma.breweryComment.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBreweryPostComments;
|
export default createNewBreweryPostComments;
|
||||||
|
|||||||
37
src/prisma/seed/create/createNewBreweryPostLikes.ts
Normal file
37
src/prisma/seed/create/createNewBreweryPostLikes.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { BreweryPost, User } from '@prisma/client';
|
||||||
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
|
interface BreweryPostLikeData {
|
||||||
|
breweryPostId: string;
|
||||||
|
likedById: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewBreweryPostLikes = async ({
|
||||||
|
joinData: { breweryPosts, users },
|
||||||
|
numberOfLikes,
|
||||||
|
}: {
|
||||||
|
joinData: {
|
||||||
|
breweryPosts: BreweryPost[];
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
|
numberOfLikes: number;
|
||||||
|
}) => {
|
||||||
|
const breweryPostLikeData: BreweryPostLikeData[] = [];
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0; i < numberOfLikes; i++) {
|
||||||
|
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
|
||||||
|
breweryPostLikeData.push({
|
||||||
|
breweryPostId: breweryPost.id,
|
||||||
|
likedById: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await DBClient.instance.breweryPostLike.createMany({
|
||||||
|
data: breweryPostLikeData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return DBClient.instance.breweryPostLike.findMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewBreweryPostLikes;
|
||||||
@@ -1,42 +1,56 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { User } from '@prisma/client';
|
import { Location, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateNewBreweryPostsArgs {
|
interface CreateNewBreweryPostsArgs {
|
||||||
numberOfPosts: number;
|
numberOfPosts: number;
|
||||||
joinData: {
|
joinData: {
|
||||||
users: User[];
|
users: User[];
|
||||||
|
locations: Location[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BreweryData {
|
||||||
|
name: string;
|
||||||
|
locationId: string;
|
||||||
|
description: string;
|
||||||
|
postedById: string;
|
||||||
|
createdAt: Date;
|
||||||
|
dateEstablished: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewBreweryPosts = async ({
|
const createNewBreweryPosts = async ({
|
||||||
numberOfPosts,
|
numberOfPosts,
|
||||||
joinData,
|
joinData,
|
||||||
}: CreateNewBreweryPostsArgs) => {
|
}: CreateNewBreweryPostsArgs) => {
|
||||||
const { users } = joinData;
|
const { users, locations } = joinData;
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const breweryPromises = [];
|
const breweryData: BreweryData[] = [];
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfPosts; i++) {
|
for (let i = 0; i < numberOfPosts; i++) {
|
||||||
const name = `${faker.commerce.productName()} Brewing Company`;
|
const name = `${faker.commerce.productName()} Brewing Company`;
|
||||||
const location = faker.address.cityName();
|
const locationIndex = Math.floor(Math.random() * locations.length);
|
||||||
const description = faker.lorem.lines(5);
|
const location = locations[locationIndex];
|
||||||
|
locations.splice(locationIndex, 1); // Remove the location from the array
|
||||||
|
const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
|
||||||
const user = users[Math.floor(Math.random() * users.length)];
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
breweryPromises.push(
|
const dateEstablished = faker.date.past(40);
|
||||||
prisma.breweryPost.create({
|
|
||||||
data: {
|
breweryData.push({
|
||||||
name,
|
name,
|
||||||
location,
|
locationId: location.id,
|
||||||
description,
|
description,
|
||||||
postedBy: { connect: { id: user.id } },
|
postedById: user.id,
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
dateEstablished,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Promise.all(breweryPromises);
|
await prisma.breweryPost.createMany({ data: breweryData, skipDuplicates: true });
|
||||||
|
|
||||||
|
return prisma.breweryPost.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewBreweryPosts;
|
export default createNewBreweryPosts;
|
||||||
|
|||||||
72
src/prisma/seed/create/createNewLocations.ts
Normal file
72
src/prisma/seed/create/createNewLocations.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { User } from '@prisma/client';
|
||||||
|
import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
|
import DBClient from '../../DBClient';
|
||||||
|
import geocode from '../../../config/mapbox/geocoder';
|
||||||
|
|
||||||
|
interface CreateNewLocationsArgs {
|
||||||
|
numberOfLocations: number;
|
||||||
|
joinData: {
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocationData {
|
||||||
|
city: string;
|
||||||
|
stateOrProvince?: string;
|
||||||
|
country?: string;
|
||||||
|
coordinates: number[];
|
||||||
|
address: string;
|
||||||
|
postedById: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewLocations = async ({
|
||||||
|
numberOfLocations,
|
||||||
|
joinData,
|
||||||
|
}: CreateNewLocationsArgs) => {
|
||||||
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
|
const locationNames: string[] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0; i < numberOfLocations; i++) {
|
||||||
|
locationNames.push(faker.address.cityName());
|
||||||
|
}
|
||||||
|
|
||||||
|
const geocodePromises: Promise<GeocodeFeature>[] = [];
|
||||||
|
|
||||||
|
locationNames.forEach((locationName) => {
|
||||||
|
geocodePromises.push(geocode(locationName));
|
||||||
|
});
|
||||||
|
|
||||||
|
const geocodedLocations = await Promise.all(geocodePromises);
|
||||||
|
|
||||||
|
const locationData: LocationData[] = [];
|
||||||
|
|
||||||
|
geocodedLocations.forEach((geodata) => {
|
||||||
|
const randomUser = joinData.users[Math.floor(Math.random() * joinData.users.length)];
|
||||||
|
|
||||||
|
const city = geodata.text;
|
||||||
|
const postedById = randomUser.id;
|
||||||
|
const stateOrProvince = geodata.context?.find((c) => c.id.startsWith('region'))?.text;
|
||||||
|
const country = geodata.context?.find((c) => c.id.startsWith('country'))?.text;
|
||||||
|
const coordinates = geodata.center;
|
||||||
|
const address = geodata.place_name;
|
||||||
|
|
||||||
|
locationData.push({
|
||||||
|
city,
|
||||||
|
stateOrProvince,
|
||||||
|
country,
|
||||||
|
coordinates,
|
||||||
|
address,
|
||||||
|
postedById,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.location.createMany({ data: locationData, skipDuplicates: true });
|
||||||
|
|
||||||
|
return prisma.location.findMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewLocations;
|
||||||
@@ -1,47 +1,65 @@
|
|||||||
import argon2 from 'argon2';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
import { hashPassword } from '../../../config/auth/passwordFns';
|
||||||
|
|
||||||
interface CreateNewUsersArgs {
|
interface CreateNewUsersArgs {
|
||||||
numberOfUsers: number;
|
numberOfUsers: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
dateOfBirth: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const userPromises = [];
|
|
||||||
|
|
||||||
const hashedPasswords = await Promise.all(
|
const password = 'passwoRd!3';
|
||||||
Array.from({ length: numberOfUsers }, () => argon2.hash(faker.internet.password())),
|
const hash = await hashPassword(password);
|
||||||
);
|
const data: UserData[] = [];
|
||||||
|
|
||||||
|
const takenUsernames: string[] = [];
|
||||||
|
const takenEmails: string[] = [];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfUsers; i++) {
|
for (let i = 0; i < numberOfUsers; i++) {
|
||||||
const randomValue = crypto.randomBytes(10).toString('hex');
|
const randomValue = crypto.randomBytes(1).toString('hex');
|
||||||
const firstName = faker.name.firstName();
|
const firstName = faker.name.firstName();
|
||||||
const lastName = faker.name.lastName();
|
const lastName = faker.name.lastName();
|
||||||
const username = `${firstName[0]}.${lastName}.${randomValue}`;
|
const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase();
|
||||||
const email = faker.internet.email(firstName, randomValue, 'example.com');
|
const email = faker.internet
|
||||||
|
.email(firstName, randomValue, 'example.com')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
const userAvailable =
|
||||||
|
!takenUsernames.includes(username) && !takenEmails.includes(email);
|
||||||
|
|
||||||
|
if (!userAvailable) {
|
||||||
|
i -= 1;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
takenUsernames.push(username);
|
||||||
|
takenEmails.push(email);
|
||||||
|
|
||||||
const hash = hashedPasswords[i];
|
|
||||||
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
userPromises.push(
|
|
||||||
prisma.user.create({
|
const user = { firstName, lastName, email, username, dateOfBirth, createdAt, hash };
|
||||||
data: {
|
|
||||||
firstName,
|
data.push(user);
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
username,
|
|
||||||
dateOfBirth,
|
|
||||||
createdAt,
|
|
||||||
hash,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Promise.all(userPromises);
|
|
||||||
|
await prisma.user.createMany({ data, skipDuplicates: true });
|
||||||
|
return prisma.user.findMany();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createNewUsers;
|
export default createNewUsers;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
|
import { exit } from 'process';
|
||||||
import logger from '../../config/pino/logger';
|
|
||||||
|
|
||||||
import cleanDatabase from './clean/cleanDatabase';
|
import cleanDatabase from './clean/cleanDatabase';
|
||||||
|
|
||||||
@@ -13,6 +12,9 @@ import createNewBreweryImages from './create/createNewBreweryImages';
|
|||||||
import createNewBreweryPostComments from './create/createNewBreweryPostComments';
|
import createNewBreweryPostComments from './create/createNewBreweryPostComments';
|
||||||
import createNewBreweryPosts from './create/createNewBreweryPosts';
|
import createNewBreweryPosts from './create/createNewBreweryPosts';
|
||||||
import createNewUsers from './create/createNewUsers';
|
import createNewUsers from './create/createNewUsers';
|
||||||
|
import createNewBreweryPostLikes from './create/createNewBreweryPostLikes';
|
||||||
|
import createNewLocations from './create/createNewLocations';
|
||||||
|
import logger from '../../config/pino/logger';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -20,71 +22,89 @@ import createNewUsers from './create/createNewUsers';
|
|||||||
|
|
||||||
logger.info('Clearing database.');
|
logger.info('Clearing database.');
|
||||||
await cleanDatabase();
|
await cleanDatabase();
|
||||||
|
|
||||||
logger.info('Database cleared successfully, preparing to seed.');
|
logger.info('Database cleared successfully, preparing to seed.');
|
||||||
|
|
||||||
const users = await createNewUsers({ numberOfUsers: 1000 });
|
const users = await createNewUsers({ numberOfUsers: 10000 });
|
||||||
|
logger.info('Users created successfully.');
|
||||||
|
|
||||||
|
const locations = await createNewLocations({
|
||||||
|
numberOfLocations: 500,
|
||||||
|
joinData: { users },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Locations created successfully.');
|
||||||
|
|
||||||
const [breweryPosts, beerTypes] = await Promise.all([
|
const [breweryPosts, beerTypes] = await Promise.all([
|
||||||
createNewBreweryPosts({ numberOfPosts: 100, joinData: { users } }),
|
createNewBreweryPosts({ numberOfPosts: 450, joinData: { users, locations } }),
|
||||||
createNewBeerTypes({ joinData: { users } }),
|
createNewBeerTypes({ joinData: { users } }),
|
||||||
]);
|
]);
|
||||||
|
logger.info('Brewery posts and beer types created successfully.');
|
||||||
|
|
||||||
const beerPosts = await createNewBeerPosts({
|
const beerPosts = await createNewBeerPosts({
|
||||||
numberOfPosts: 200,
|
numberOfPosts: 3000,
|
||||||
joinData: { breweryPosts, beerTypes, users },
|
joinData: { breweryPosts, beerTypes, users },
|
||||||
});
|
});
|
||||||
|
|
||||||
const [
|
logger.info('Beer posts created successfully.');
|
||||||
beerPostComments,
|
|
||||||
breweryPostComments,
|
const [beerPostComments, breweryPostComments] = await Promise.all([
|
||||||
beerPostLikes,
|
|
||||||
beerImages,
|
|
||||||
breweryImages,
|
|
||||||
] = await Promise.all([
|
|
||||||
createNewBeerPostComments({
|
createNewBeerPostComments({
|
||||||
numberOfComments: 45000,
|
numberOfComments: 100000,
|
||||||
joinData: { beerPosts, users },
|
joinData: { beerPosts, users },
|
||||||
}),
|
}),
|
||||||
createNewBreweryPostComments({
|
createNewBreweryPostComments({
|
||||||
numberOfComments: 45000,
|
numberOfComments: 100000,
|
||||||
joinData: { breweryPosts, users },
|
|
||||||
}),
|
|
||||||
createNewBeerPostLikes({
|
|
||||||
numberOfLikes: 10000,
|
|
||||||
joinData: { beerPosts, users },
|
|
||||||
}),
|
|
||||||
createNewBeerImages({
|
|
||||||
numberOfImages: 1000,
|
|
||||||
joinData: { beerPosts, users },
|
|
||||||
}),
|
|
||||||
createNewBreweryImages({
|
|
||||||
numberOfImages: 1000,
|
|
||||||
joinData: { breweryPosts, users },
|
joinData: { breweryPosts, users },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
logger.info('Created beer post comments and brewery post comments.');
|
||||||
|
|
||||||
|
const [beerPostLikes, breweryPostLikes] = await Promise.all([
|
||||||
|
createNewBeerPostLikes({
|
||||||
|
numberOfLikes: 100000,
|
||||||
|
joinData: { beerPosts, users },
|
||||||
|
}),
|
||||||
|
createNewBreweryPostLikes({
|
||||||
|
numberOfLikes: 100000,
|
||||||
|
joinData: { breweryPosts, users },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
logger.info('Created beer post likes, and brewery post likes.');
|
||||||
|
|
||||||
|
const [beerImages, breweryImages] = await Promise.all([
|
||||||
|
createNewBeerImages({
|
||||||
|
numberOfImages: 20000,
|
||||||
|
joinData: { beerPosts, users },
|
||||||
|
}),
|
||||||
|
createNewBreweryImages({
|
||||||
|
numberOfImages: 20000,
|
||||||
|
joinData: { breweryPosts, users },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
logger.info('Created beer images and brewery images.');
|
||||||
|
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
const timeElapsed = (end - start) / 1000;
|
const timeElapsed = (end - start) / 1000;
|
||||||
|
|
||||||
logger.info('Database seeded successfully.');
|
logger.info('Database seeded successfully.');
|
||||||
|
|
||||||
logger.info({
|
logger.info({
|
||||||
numberOfUsers: users.length,
|
numberOfUsers: users.length,
|
||||||
numberOfBreweryPosts: breweryPosts.length,
|
numberOfBreweryPosts: breweryPosts.length,
|
||||||
numberOfBeerPosts: beerPosts.length,
|
numberOfBeerPosts: beerPosts.length,
|
||||||
numberOfBeerTypes: beerTypes.length,
|
numberOfBeerTypes: beerTypes.length,
|
||||||
numberOfBeerPostLikes: beerPostLikes.length,
|
numberOfBeerPostLikes: beerPostLikes.length,
|
||||||
|
numberofBreweryPostLikes: breweryPostLikes.length,
|
||||||
numberOfBeerPostComments: beerPostComments.length,
|
numberOfBeerPostComments: beerPostComments.length,
|
||||||
numberOfBreweryPostComments: breweryPostComments.length,
|
numberOfBreweryPostComments: breweryPostComments.length,
|
||||||
numberOfBeerImages: beerImages.length,
|
numberOfBeerImages: beerImages.length,
|
||||||
numberOfBreweryImages: breweryImages.length,
|
numberOfBreweryImages: breweryImages.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Database seeded in ${timeElapsed.toFixed(2)} seconds.`);
|
logger.info(`Database seeded in ${timeElapsed.toFixed(2)} seconds.`);
|
||||||
|
|
||||||
process.exit(0);
|
exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error seeding database.');
|
logger.error('Error seeding database.');
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
process.exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
const sendLikeRequest = async (beerPostId: string) => {
|
const sendBeerPostLikeRequest = async (beerPostId: string) => {
|
||||||
const response = await fetch(`/api/beers/${beerPostId}/like`, {
|
const response = await fetch(`/api/beers/${beerPostId}/like`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -26,4 +22,4 @@ const sendLikeRequest = async (beerPostId: string) => {
|
|||||||
return { success, message };
|
return { success, message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default sendLikeRequest;
|
export default sendBeerPostLikeRequest;
|
||||||
18
src/requests/sendBreweryPostLikeRequest.ts
Normal file
18
src/requests/sendBreweryPostLikeRequest.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
const sendBreweryPostLikeRequest = async (breweryPostId: string) => {
|
||||||
|
const response = await fetch(`/api/breweries/${breweryPostId}/like`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('Invalid API response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendBreweryPostLikeRequest;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerCommentValidationSchemaWithId = BeerCommentValidationSchema.extend({
|
const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
|
||||||
beerPostId: z.string().uuid(),
|
beerPostId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,20 +18,19 @@ const sendCreateBeerCommentRequest = async ({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ beerPostId, content, rating }),
|
body: JSON.stringify({ beerPostId, content, rating }),
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
||||||
|
|
||||||
if (!parsedResponse.success) {
|
if (!parsedResponse.success) {
|
||||||
throw new Error('Invalid API response');
|
throw new Error('Invalid API response');
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = BeerCommentQueryResult.safeParse(parsedResponse.data.payload);
|
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error('Invalid API response payload');
|
throw new Error('Invalid API response payload');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema';
|
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
const CreateNewBeerCommentServiceSchema = BeerCommentValidationSchema.extend({
|
const CreateNewBeerCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
beerPostId: z.string().uuid(),
|
beerPostId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
import CommentQueryResult from '../types/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
const getAllBeerComments = async (
|
const getAllBeerComments = async (
|
||||||
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
|
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
|
||||||
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
||||||
) => {
|
) => {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
const beerComments: z.infer<typeof BeerCommentQueryResult>[] =
|
const beerComments: z.infer<typeof CommentQueryResult>[] =
|
||||||
await DBClient.instance.beerComment.findMany({
|
await DBClient.instance.beerComment.findMany({
|
||||||
skip,
|
skip,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
|||||||
},
|
},
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip,
|
skip,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
33
src/services/BreweryComment/createNewBreweryComment.ts
Normal file
33
src/services/BreweryComment/createNewBreweryComment.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
||||||
|
|
||||||
|
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
breweryPostId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createNewBreweryComment = async ({
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
breweryPostId,
|
||||||
|
userId,
|
||||||
|
}: z.infer<typeof CreateNewBreweryCommentServiceSchema>) => {
|
||||||
|
return DBClient.instance.breweryComment.create({
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
breweryPost: { connect: { id: breweryPostId } },
|
||||||
|
postedBy: { connect: { id: userId } },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
rating: true,
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createNewBreweryComment;
|
||||||
28
src/services/BreweryComment/getAllBreweryComments.ts
Normal file
28
src/services/BreweryComment/getAllBreweryComments.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import CommentQueryResult from '../types/CommentSchema/CommentQueryResult';
|
||||||
|
|
||||||
|
const getAllBreweryComments = async (
|
||||||
|
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
|
||||||
|
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
||||||
|
) => {
|
||||||
|
const skip = (pageNum - 1) * pageSize;
|
||||||
|
const breweryComments: z.infer<typeof CommentQueryResult>[] =
|
||||||
|
await DBClient.instance.breweryComment.findMany({
|
||||||
|
skip,
|
||||||
|
take: pageSize,
|
||||||
|
where: { breweryPostId: id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
rating: true,
|
||||||
|
createdAt: true,
|
||||||
|
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
return breweryComments;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllBreweryComments;
|
||||||
@@ -1,19 +1,37 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getAllBreweryPosts = async () => {
|
const getAllBreweryPosts = async (pageNum?: number, pageSize?: number) => {
|
||||||
|
const skip = pageNum && pageSize ? (pageNum - 1) * pageSize : undefined;
|
||||||
|
const take = pageNum && pageSize ? pageSize : undefined;
|
||||||
|
|
||||||
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
|
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
|
||||||
await prisma.breweryPost.findMany({
|
await prisma.breweryPost.findMany({
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
location: true,
|
location: {
|
||||||
|
select: {
|
||||||
|
city: true,
|
||||||
|
address: true,
|
||||||
|
coordinates: true,
|
||||||
|
country: true,
|
||||||
|
stateOrProvince: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: true,
|
||||||
name: true,
|
name: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
|
createdAt: true,
|
||||||
|
dateEstablished: true,
|
||||||
},
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
return breweryPosts;
|
return breweryPosts;
|
||||||
|
|||||||
@@ -9,10 +9,21 @@ const getBreweryPostById = async (id: string) => {
|
|||||||
await prisma.breweryPost.findFirst({
|
await prisma.breweryPost.findFirst({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
location: true,
|
location: {
|
||||||
|
select: {
|
||||||
|
city: true,
|
||||||
|
address: true,
|
||||||
|
coordinates: true,
|
||||||
|
country: true,
|
||||||
|
stateOrProvince: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: true,
|
||||||
name: true,
|
name: true,
|
||||||
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
postedBy: { select: { username: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
|
createdAt: true,
|
||||||
|
dateEstablished: true,
|
||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const BreweryPostQueryResult = z.object({
|
const BreweryPostQueryResult = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
location: z.string(),
|
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
location: z.object({
|
||||||
|
city: z.string(),
|
||||||
|
address: z.string(),
|
||||||
|
coordinates: z.array(z.number()),
|
||||||
|
country: z.string().nullable(),
|
||||||
|
stateOrProvince: z.string().nullable(),
|
||||||
|
}),
|
||||||
postedBy: z.object({ id: z.string(), username: z.string() }),
|
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||||
breweryImages: z.array(
|
breweryImages: z.array(
|
||||||
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||||
),
|
),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
dateEstablished: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BreweryPostQueryResult;
|
export default BreweryPostQueryResult;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '../../prisma/DBClient';
|
||||||
|
|
||||||
const findUserByEmail = async (email: string) =>
|
const findUserByEmail = async (email: string) =>
|
||||||
DBClient.instance.user.findFirst({
|
DBClient.instance.user.findFirst({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '../../prisma/DBClient';
|
||||||
|
|
||||||
const findUserByUsername = async (username: string) =>
|
const findUserByUsername = async (username: string) =>
|
||||||
DBClient.instance.user.findFirst({
|
DBClient.instance.user.findFirst({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerCommentQueryResult = z.object({
|
const CommentQueryResult = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
content: z.string().min(1).max(500),
|
content: z.string().min(1).max(500),
|
||||||
rating: z.number().int().min(1).max(5),
|
rating: z.number().int().min(1).max(5),
|
||||||
@@ -11,4 +11,4 @@ const BeerCommentQueryResult = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BeerCommentQueryResult;
|
export default CommentQueryResult;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerCommentValidationSchema = z.object({
|
const CreateCommentValidationSchema = z.object({
|
||||||
content: z
|
content: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Comment must not be empty.' })
|
.min(1, { message: 'Comment must not be empty.' })
|
||||||
@@ -12,4 +12,4 @@ const BeerCommentValidationSchema = z.object({
|
|||||||
.max(5, { message: 'Rating must be less than 5.' }),
|
.max(5, { message: 'Rating must be less than 5.' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BeerCommentValidationSchema;
|
export default CreateCommentValidationSchema;
|
||||||
@@ -27,9 +27,9 @@ const myThemes = {
|
|||||||
warning: 'hsl(40, 76%, 73%)',
|
warning: 'hsl(40, 76%, 73%)',
|
||||||
'primary-content': 'hsl(0, 0%, 0%)',
|
'primary-content': 'hsl(0, 0%, 0%)',
|
||||||
'error-content': 'hsl(0, 0%, 0%)',
|
'error-content': 'hsl(0, 0%, 0%)',
|
||||||
'base-100': 'hsl(180, 8%, 94%)',
|
'base-300': 'hsl(180, 10%, 88%)',
|
||||||
'base-200': 'hsl(180, 8%, 92%)',
|
'base-200': 'hsl(180, 10%, 92%)',
|
||||||
'base-300': 'hsl(180, 8%, 88%)',
|
'base-100': 'hsl(180, 10%, 95%)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user