mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
5
.github/workflows/github-actions-demo.yml
vendored
5
.github/workflows/github-actions-demo.yml
vendored
@@ -18,8 +18,3 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Prettier Action
|
|
||||||
# You may pin to the exact commit or the version.
|
|
||||||
# uses: creyD/prettier_action@6602189cf8bac1ce73ffe601925f6127ab7f21ac
|
|
||||||
uses: creyD/prettier_action@v4.2
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -41,3 +41,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# uploaded images
|
# uploaded images
|
||||||
public/uploads
|
public/uploads
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { FunctionComponent, useState, useEffect } from 'react';
|
import { FunctionComponent, useState, useEffect } 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 { KeyedMutator } from 'swr';
|
||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import Button from '../ui/forms/Button';
|
import Button from '../ui/forms/Button';
|
||||||
import FormError from '../ui/forms/FormError';
|
import FormError from '../ui/forms/FormError';
|
||||||
import FormInfo from '../ui/forms/FormInfo';
|
import FormInfo from '../ui/forms/FormInfo';
|
||||||
@@ -16,10 +19,17 @@ import FormSegment from '../ui/forms/FormSegment';
|
|||||||
import FormTextArea from '../ui/forms/FormTextArea';
|
import FormTextArea from '../ui/forms/FormTextArea';
|
||||||
|
|
||||||
interface BeerCommentFormProps {
|
interface BeerCommentFormProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
|
mutate: KeyedMutator<{
|
||||||
|
comments: z.infer<typeof BeerCommentQueryResult>[];
|
||||||
|
pageCount: number;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => {
|
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||||
|
beerPost,
|
||||||
|
mutate,
|
||||||
|
}) => {
|
||||||
const { register, handleSubmit, formState, reset, setValue } = useForm<
|
const { register, handleSubmit, formState, reset, setValue } = useForm<
|
||||||
z.infer<typeof BeerCommentValidationSchema>
|
z.infer<typeof BeerCommentValidationSchema>
|
||||||
>({
|
>({
|
||||||
@@ -47,44 +57,58 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
|
|||||||
beerPostId: beerPost.id,
|
beerPostId: beerPost.id,
|
||||||
});
|
});
|
||||||
reset();
|
reset();
|
||||||
router.replace(`/beers/${beerPost.id}?comments_page=1`, undefined, { scroll: false });
|
|
||||||
|
const submitTasks: Promise<unknown>[] = [
|
||||||
|
router.push(`/beers/${beerPost.id}`, undefined, { scroll: false }),
|
||||||
|
mutate(),
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(submitTasks);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||||
<FormInfo>
|
<div>
|
||||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
<FormInfo>
|
||||||
<FormError>{errors.content?.message}</FormError>
|
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||||
</FormInfo>
|
<FormError>{errors.content?.message}</FormError>
|
||||||
<FormSegment>
|
</FormInfo>
|
||||||
<FormTextArea
|
<FormSegment>
|
||||||
id="content"
|
<FormTextArea
|
||||||
formValidationSchema={register('content')}
|
id="content"
|
||||||
placeholder="Comment"
|
formValidationSchema={register('content')}
|
||||||
rows={5}
|
placeholder="Comment"
|
||||||
error={!!errors.content?.message}
|
rows={5}
|
||||||
/>
|
error={!!errors.content?.message}
|
||||||
</FormSegment>
|
disabled={formState.isSubmitting}
|
||||||
<FormInfo>
|
/>
|
||||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
</FormSegment>
|
||||||
<FormError>{errors.rating?.message}</FormError>
|
<FormInfo>
|
||||||
</FormInfo>
|
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||||
<Rating
|
<FormError>{errors.rating?.message}</FormError>
|
||||||
value={rating}
|
</FormInfo>
|
||||||
onChange={(value) => {
|
<Rating
|
||||||
setRating(value);
|
value={rating}
|
||||||
setValue('rating', value);
|
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.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>
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
<Button type="submit">Submit</Button>
|
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||||
|
</Rating>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +1,26 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
|
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import { FC, useContext, useEffect, useState } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
|
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import { FaRegEdit } from 'react-icons/fa';
|
import { FaRegEdit } from 'react-icons/fa';
|
||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useGetLikeCount from '@/hooks/useGetLikeCount';
|
||||||
|
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||||
import BeerPostLikeButton from './BeerPostLikeButton';
|
import BeerPostLikeButton from './BeerPostLikeButton';
|
||||||
|
|
||||||
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({
|
const BeerInfoHeader: FC<{
|
||||||
beerPost,
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
initialLikeCount,
|
}> = ({ beerPost }) => {
|
||||||
}) => {
|
const createdAt = new Date(beerPost.createdAt);
|
||||||
const createdAtDate = new Date(beerPost.createdAt);
|
const timeDistance = useTimeDistance(createdAt);
|
||||||
const [timeDistance, setTimeDistance] = useState('');
|
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
const idMatches = user && beerPost.postedBy.id === user.id;
|
||||||
|
const isPostOwner = !!(user && idMatches);
|
||||||
|
|
||||||
const [likeCount, setLikeCount] = useState(initialLikeCount);
|
const { likeCount, mutate } = useGetLikeCount(beerPost.id);
|
||||||
const [isPostOwner, setIsPostOwner] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const idMatches = user && beerPost.postedBy.id === user.id;
|
|
||||||
|
|
||||||
if (!(user && idMatches)) {
|
|
||||||
setIsPostOwner(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPostOwner(true);
|
|
||||||
}, [user, beerPost]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLikeCount(initialLikeCount);
|
|
||||||
}, [initialLikeCount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeDistance(formatDistanceStrict(new Date(beerPost.createdAt), new Date()));
|
|
||||||
}, [beerPost.createdAt]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="card flex flex-col justify-center bg-base-300">
|
<main className="card flex flex-col justify-center bg-base-300">
|
||||||
@@ -67,16 +51,18 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="italic">
|
<h3 className="italic">
|
||||||
posted by{' '}
|
{' posted by '}
|
||||||
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
||||||
{beerPost.postedBy.username}
|
{`${beerPost.postedBy.username} `}
|
||||||
</Link>
|
</Link>
|
||||||
<span
|
{timeDistance && (
|
||||||
className="tooltip tooltip-bottom"
|
<span
|
||||||
data-tip={format(createdAtDate, 'MM/dd/yyyy')}
|
className="tooltip tooltip-right"
|
||||||
>
|
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||||
{` ${timeDistance}`} ago
|
>
|
||||||
</span>
|
{`${timeDistance} ago`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p>{beerPost.description}</p>
|
<p>{beerPost.description}</p>
|
||||||
@@ -95,15 +81,15 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
|
|||||||
<span className="text-lg font-medium">{beerPost.ibu} IBU</span>
|
<span className="text-lg font-medium">{beerPost.ibu} IBU</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
{(!!likeCount || likeCount === 0) && (
|
||||||
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
<span>
|
||||||
</span>
|
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-actions items-end">
|
<div className="card-actions items-end">
|
||||||
{user && (
|
{user && <BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />}
|
||||||
<BeerPostLikeButton beerPostId={beerPost.id} setLikeCount={setLikeCount} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||||
|
|
||||||
interface BeerCommentsPaginationBarProps {
|
interface BeerCommentsPaginationBarProps {
|
||||||
commentsPageNum: number;
|
commentsPageNum: number;
|
||||||
commentsPageCount: number;
|
commentsPageCount: number;
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
||||||
@@ -14,9 +17,9 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
|||||||
beerPost,
|
beerPost,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex items-center justify-center" id="comments-pagination">
|
<div className="flex items-center justify-center" id="comments-pagination">
|
||||||
<div className="btn-group grid w-6/12 grid-cols-2">
|
<div className="btn-group">
|
||||||
<Link
|
<Link
|
||||||
className={`btn-outline btn ${
|
className={`btn btn-ghost ${
|
||||||
commentsPageNum === 1
|
commentsPageNum === 1
|
||||||
? 'btn-disabled pointer-events-none'
|
? 'btn-disabled pointer-events-none'
|
||||||
: 'pointer-events-auto'
|
: 'pointer-events-auto'
|
||||||
@@ -27,10 +30,11 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
|||||||
}}
|
}}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
>
|
>
|
||||||
Next Comments
|
<FaArrowLeft />
|
||||||
</Link>
|
</Link>
|
||||||
|
<button className="btn btn-ghost pointer-events-none">{commentsPageNum}</button>
|
||||||
<Link
|
<Link
|
||||||
className={`btn-outline btn ${
|
className={`btn btn-ghost ${
|
||||||
commentsPageNum === commentsPageCount
|
commentsPageNum === commentsPageCount
|
||||||
? 'btn-disabled pointer-events-none'
|
? 'btn-disabled pointer-events-none'
|
||||||
: 'pointer-events-auto'
|
: 'pointer-events-auto'
|
||||||
@@ -41,7 +45,7 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
|||||||
}}
|
}}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
>
|
>
|
||||||
Previous Comments
|
<FaArrowRight />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,41 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import BeerCommentForm from './BeerCommentForm';
|
import BeerCommentForm from './BeerCommentForm';
|
||||||
import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar';
|
import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar';
|
||||||
import CommentCard from './CommentCard';
|
import CommentCardBody from './CommentCardBody';
|
||||||
|
import NoCommentsCard from './NoCommentsCard';
|
||||||
|
import CommentLoadingCardBody from './CommentLoadingCardBody';
|
||||||
|
|
||||||
interface BeerPostCommentsSectionProps {
|
interface BeerPostCommentsSectionProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
comments: BeerCommentQueryResultArrayT;
|
|
||||||
commentsPageCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost }) => {
|
||||||
beerPost,
|
|
||||||
|
|
||||||
comments,
|
|
||||||
commentsPageCount,
|
|
||||||
}) => {
|
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { id } = beerPost;
|
||||||
|
const pageNum = parseInt(router.query.comments_page as string, 10) || 1;
|
||||||
|
const pageSize = 5;
|
||||||
|
|
||||||
const commentsPageNum = parseInt(router.query.comments_page as string, 10) || 1;
|
const { comments, commentsPageCount, isLoading, mutate } = useBeerPostComments({
|
||||||
|
id,
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-3 md:w-[60%]">
|
<div className="w-full space-y-3 md:w-[60%]">
|
||||||
<div className="card h-96 bg-base-300">
|
<div className="card h-96 bg-base-300">
|
||||||
<div className="card-body h-full">
|
<div className="card-body h-full">
|
||||||
{user ? (
|
{user ? (
|
||||||
<BeerCommentForm beerPost={beerPost} />
|
<BeerCommentForm beerPost={beerPost} mutate={mutate} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
||||||
@@ -37,23 +43,34 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{comments.length ? (
|
|
||||||
|
{comments && !!comments.length && !!commentsPageCount && !isLoading && (
|
||||||
<div className="card bg-base-300 pb-6">
|
<div className="card bg-base-300 pb-6">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<CommentCard key={comment.id} comment={comment} beerPostId={beerPost.id} />
|
<CommentCardBody key={comment.id} comment={comment} mutate={mutate} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<BeerCommentsPaginationBar
|
<BeerCommentsPaginationBar
|
||||||
commentsPageNum={commentsPageNum}
|
commentsPageNum={pageNum}
|
||||||
commentsPageCount={commentsPageCount}
|
commentsPageCount={commentsPageCount}
|
||||||
beerPost={beerPost}
|
beerPost={beerPost}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="card items-center bg-base-300">
|
|
||||||
<div className="card-body">
|
{!comments?.length && !isLoading && <NoCommentsCard />}
|
||||||
<span className="text-lg font-bold">No comments yet.</span>
|
|
||||||
</div>
|
{isLoading && (
|
||||||
|
<div className="card bg-base-300 pb-6">
|
||||||
|
{Array.from({ length: pageSize }).map((_, i) => (
|
||||||
|
<CommentLoadingCardBody key={i} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<BeerCommentsPaginationBar
|
||||||
|
commentsPageNum={pageNum}
|
||||||
|
commentsPageCount={20}
|
||||||
|
beerPost={beerPost}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,28 @@
|
|||||||
import UserContext from '@/contexts/userContext';
|
import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
|
||||||
import sendLikeRequest from '@/requests/sendLikeRequest';
|
import sendLikeRequest from '@/requests/sendLikeRequest';
|
||||||
import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
|
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
|
||||||
import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest';
|
import { KeyedMutator } from 'swr';
|
||||||
|
|
||||||
const BeerPostLikeButton: FC<{
|
const BeerPostLikeButton: FC<{
|
||||||
beerPostId: string;
|
beerPostId: string;
|
||||||
setLikeCount: Dispatch<SetStateAction<number>>;
|
mutateCount: KeyedMutator<number>;
|
||||||
}> = ({ beerPostId, setLikeCount }) => {
|
}> = ({ beerPostId, mutateCount }) => {
|
||||||
|
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
|
||||||
|
|
||||||
const { user } = useContext(UserContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
setLoading(false);
|
||||||
setLoading(false);
|
}, [isLiked]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendCheckIfUserLikesBeerPostRequest(beerPostId)
|
|
||||||
.then((currentLikeStatus) => {
|
|
||||||
setIsLiked(currentLikeStatus);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, [user, beerPostId]);
|
|
||||||
|
|
||||||
const handleLike = async () => {
|
const handleLike = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await sendLikeRequest(beerPostId);
|
await sendLikeRequest(beerPostId);
|
||||||
setIsLiked(!isLiked);
|
await mutateCount();
|
||||||
setLikeCount((prevCount) => prevCount + (isLiked ? -1 : 1));
|
await mutateLikeStatus();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="card sticky top-2 h-full overflow-y-scroll bg-base-300">
|
<div className="card sticky top-2 h-full overflow-y-scroll bg-base-300">
|
||||||
<div className="card-body">
|
<div className="card-body space-y-3">
|
||||||
{beerRecommendations.map((beerPost) => (
|
{beerRecommendations.map((beerPost) => (
|
||||||
<div key={beerPost.id} className="w-full">
|
<div key={beerPost.id} className="w-full">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||||
import { format, formatDistanceStrict } from 'date-fns';
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import format from 'date-fns/format';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useContext } from 'react';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
|
||||||
import { Rating } from 'react-daisyui';
|
import { Rating } from 'react-daisyui';
|
||||||
|
|
||||||
import { FaEllipsisH } from 'react-icons/fa';
|
import { FaEllipsisH } from 'react-icons/fa';
|
||||||
|
import { KeyedMutator } from 'swr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const CommentCardDropdown: React.FC<{
|
const CommentCardDropdown: React.FC<{
|
||||||
comment: BeerCommentQueryResultT;
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
beerPostId: string;
|
mutate: KeyedMutator<{
|
||||||
}> = ({ comment, beerPostId }) => {
|
comments: z.infer<typeof BeerCommentQueryResult>[];
|
||||||
const router = useRouter();
|
pageCount: number;
|
||||||
|
}>;
|
||||||
|
}> = ({ comment, mutate }) => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
const isCommentOwner = user?.id === comment.postedBy.id;
|
const isCommentOwner = user?.id === comment.postedBy.id;
|
||||||
@@ -21,16 +25,17 @@ const CommentCardDropdown: React.FC<{
|
|||||||
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete comment');
|
throw new Error('Failed to delete comment');
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace(`/beers/${beerPostId}?comments_page=1`, undefined, { scroll: false });
|
await mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-sm m-1">
|
<label tabIndex={0} className="btn-ghost btn-sm btn m-1">
|
||||||
<FaEllipsisH />
|
<FaEllipsisH />
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
@@ -51,19 +56,20 @@ const CommentCardDropdown: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentCard: React.FC<{
|
const CommentCardBody: React.FC<{
|
||||||
comment: BeerCommentQueryResultT;
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
beerPostId: string;
|
|
||||||
}> = ({ comment, beerPostId }) => {
|
mutate: KeyedMutator<{
|
||||||
const [timeDistance, setTimeDistance] = useState('');
|
comments: z.infer<typeof BeerCommentQueryResult>[];
|
||||||
|
pageCount: number;
|
||||||
|
}>;
|
||||||
|
}> = ({ comment, mutate }) => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
useEffect(() => {
|
const timeDistance = useTimeDistance(new Date(comment.createdAt));
|
||||||
setTimeDistance(formatDistanceStrict(new Date(comment.createdAt), new Date()));
|
|
||||||
}, [comment.createdAt]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body animate-in fade-in-10">
|
||||||
<div className="flex flex-col justify-between sm:flex-row">
|
<div className="flex flex-col justify-between sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold sm:text-2xl">
|
<h3 className="font-semibold sm:text-2xl">
|
||||||
@@ -83,7 +89,7 @@ const CommentCard: React.FC<{
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user && <CommentCardDropdown comment={comment} beerPostId={beerPostId} />}
|
{user && <CommentCardDropdown comment={comment} mutate={mutate} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -104,4 +110,4 @@ const CommentCard: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentCard;
|
export default CommentCardBody;
|
||||||
17
components/BeerById/CommentLoadingCardBody.tsx
Normal file
17
components/BeerById/CommentLoadingCardBody.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const CommentLoadingCardBody = () => {
|
||||||
|
return (
|
||||||
|
<div className="animate card-body h-64 fade-in-10">
|
||||||
|
<div className="flex animate-pulse space-x-4 slide-in-from-top">
|
||||||
|
<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-5/6 rounded bg-base-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentLoadingCardBody;
|
||||||
13
components/BeerById/NoCommentsCard.tsx
Normal file
13
components/BeerById/NoCommentsCard.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const NoCommentsCard = () => {
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-300">
|
||||||
|
<div className="card-body h-64">
|
||||||
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
|
<span className="text-lg font-bold">No comments yet.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoCommentsCard;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => {
|
const BeerCard: FC<{ post: z.infer<typeof beerPostQueryResult> }> = ({ post }) => {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-300" key={post.id}>
|
<div className="card bg-base-300" key={post.id}>
|
||||||
<figure className="card-image h-96">
|
<figure className="card-image h-96">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
@@ -14,18 +14,16 @@ const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) =>
|
|||||||
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
|
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
|
||||||
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
|
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
prefetch={true}
|
|
||||||
>
|
>
|
||||||
«
|
<FaArrowLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<button className="btn">Page {pageNum}</button>
|
<button className="btn">Page {pageNum}</button>
|
||||||
<Link
|
<Link
|
||||||
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
|
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
|
||||||
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
|
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
prefetch={true}
|
|
||||||
>
|
>
|
||||||
»
|
<FaArrowRight />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
|
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
|
||||||
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { BeerType } from '@prisma/client';
|
import { BeerType } from '@prisma/client';
|
||||||
import router from 'next/router';
|
import router from 'next/router';
|
||||||
import { FunctionComponent, useState } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import ErrorAlert from './ui/alerts/ErrorAlert';
|
import ErrorAlert from './ui/alerts/ErrorAlert';
|
||||||
import Button from './ui/forms/Button';
|
import Button from './ui/forms/Button';
|
||||||
import FormError from './ui/forms/FormError';
|
import FormError from './ui/forms/FormError';
|
||||||
@@ -20,7 +20,7 @@ import FormTextInput from './ui/forms/FormTextInput';
|
|||||||
type CreateBeerPostSchema = z.infer<typeof CreateBeerPostValidationSchema>;
|
type CreateBeerPostSchema = z.infer<typeof CreateBeerPostValidationSchema>;
|
||||||
|
|
||||||
interface BeerFormProps {
|
interface BeerFormProps {
|
||||||
breweries: BreweryPostQueryResult[];
|
breweries: z.infer<typeof BreweryPostQueryResult>[];
|
||||||
types: BeerType[];
|
types: BeerType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
|
|||||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
||||||
try {
|
try {
|
||||||
const response = await sendLoginUserRequest(data);
|
await sendLoginUserRequest(data);
|
||||||
|
router.push(`/user/current`);
|
||||||
router.push(`/users/${response.id}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
setResponseError(error.message);
|
setResponseError(error.message);
|
||||||
@@ -74,7 +73,7 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
|
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<button type="submit" className="btn btn-primary w-full">
|
<button type="submit" className="btn-primary btn w-full">
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import Navbar from './Navbar';
|
|||||||
const Layout: FC<{ children: ReactNode }> = ({ children }) => {
|
const Layout: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<header className="top-0">
|
<header className="sticky top-0 z-50">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</header>
|
</header>
|
||||||
<div className="animate-in fade-in top-0 h-full flex-1">{children}</div>
|
<div className="relative top-0 h-full flex-1">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const Navbar = () => {
|
|||||||
return (
|
return (
|
||||||
<nav className="navbar bg-primary text-primary-content">
|
<nav className="navbar bg-primary text-primary-content">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Link className="btn-ghost btn text-3xl normal-case" href="/">
|
<Link className="btn btn-ghost text-3xl normal-case" href="/">
|
||||||
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,8 +68,8 @@ const Navbar = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<div className="dropdown-end dropdown">
|
<div className="dropdown dropdown-end">
|
||||||
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
||||||
<span className="w-10 rounded-full">
|
<span className="w-10 rounded-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface SpinnerProps {
|
interface SpinnerProps {
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
|
const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
|
||||||
@@ -10,13 +10,14 @@ const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
|
|||||||
sm: 'w-[20px]',
|
sm: 'w-[20px]',
|
||||||
md: 'w-[100px]',
|
md: 'w-[100px]',
|
||||||
lg: 'w-[150px]',
|
lg: 'w-[150px]',
|
||||||
|
xl: 'w-[200px]',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
|
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`${spinnerWidths[size]} animate-spin fill-success text-gray-500`}
|
className={`${spinnerWidths[size]} animate-spin fill-secondary text-primary`}
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
|
|||||||
<div className="align-center my-20 flex h-fit flex-col items-center justify-center">
|
<div className="align-center my-20 flex h-fit flex-col items-center justify-center">
|
||||||
<div className="w-8/12">
|
<div className="w-8/12">
|
||||||
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
|
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
|
||||||
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
<Link href={backLink} className="btn btn-ghost btn-sm p-0">
|
||||||
<BiArrowBack className="text-xl" />
|
<BiArrowBack className="text-xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from
|
|||||||
import { ParsedUrlQuery } from 'querystring';
|
import { ParsedUrlQuery } from 'querystring';
|
||||||
import { getLoginSession } from '../config/auth/session';
|
import { getLoginSession } from '../config/auth/session';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type definition for a function that handles server-side rendering with
|
||||||
|
* extended capabilities.
|
||||||
|
*
|
||||||
|
* @template P - A generic type that represents an object with string keys and any values.
|
||||||
|
* It defaults to an empty object.
|
||||||
|
* @template Q - A generic type that represents a parsed URL query object. It defaults to
|
||||||
|
* the ParsedUrlQuery type.
|
||||||
|
* @template D - A generic type that represents preview data. It defaults to the
|
||||||
|
* PreviewData type.
|
||||||
|
* @param context - The context object containing information about the incoming HTTP
|
||||||
|
* request.
|
||||||
|
* @param session - An awaited value of the return type of the getLoginSession function.
|
||||||
|
* @returns - A promise that resolves to the result of the server-side rendering process.
|
||||||
|
*/
|
||||||
export type ExtendedGetServerSideProps<
|
export type ExtendedGetServerSideProps<
|
||||||
P extends { [key: string]: any } = { [key: string]: any },
|
P extends { [key: string]: any } = { [key: string]: any },
|
||||||
Q extends ParsedUrlQuery = ParsedUrlQuery,
|
Q extends ParsedUrlQuery = ParsedUrlQuery,
|
||||||
@@ -11,6 +26,20 @@ export type ExtendedGetServerSideProps<
|
|||||||
session: Awaited<ReturnType<typeof getLoginSession>>,
|
session: Awaited<ReturnType<typeof getLoginSession>>,
|
||||||
) => Promise<GetServerSidePropsResult<P>>;
|
) => Promise<GetServerSidePropsResult<P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Higher Order Function that adds authentication requirement to a Next.js server-side
|
||||||
|
* page component.
|
||||||
|
*
|
||||||
|
* @param fn An async function that receives the GetServerSidePropsContext and
|
||||||
|
* authenticated session as arguments and returns a GetServerSidePropsResult with props
|
||||||
|
* for the wrapped component.
|
||||||
|
* @returns A promise that resolves to a GetServerSidePropsResult object with props for
|
||||||
|
* the wrapped component. If authentication is successful, the GetServerSidePropsResult
|
||||||
|
* will include props generated by the wrapped component's getServerSideProps method. If
|
||||||
|
* authentication fails, the GetServerSidePropsResult will include a redirect to the
|
||||||
|
* login page.
|
||||||
|
*/
|
||||||
|
|
||||||
const withPageAuthRequired =
|
const withPageAuthRequired =
|
||||||
<P extends { [key: string]: any } = { [key: string]: any }>(
|
<P extends { [key: string]: any } = { [key: string]: any }>(
|
||||||
fn?: ExtendedGetServerSideProps<P>,
|
fn?: ExtendedGetServerSideProps<P>,
|
||||||
|
|||||||
56
hooks/useBeerPostComments.ts
Normal file
56
hooks/useBeerPostComments.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
interface UseBeerPostCommentsProps {
|
||||||
|
pageNum: number;
|
||||||
|
id: string;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that fetches comments for a specific beer post.
|
||||||
|
*
|
||||||
|
* @param props - The props object.
|
||||||
|
* @param props.pageNum - The page number of the comments to fetch.
|
||||||
|
* @param props.id - The ID of the beer 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 useBeerPostComments = ({ pageNum, id, pageSize }: UseBeerPostCommentsProps) => {
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
`/api/beers/${id}/comments?page_num=${pageNum}&page_size=${pageSize}`,
|
||||||
|
async (url) => {
|
||||||
|
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(BeerCommentQueryResult)
|
||||||
|
.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 };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
comments: data?.comments,
|
||||||
|
commentsPageCount: data?.pageCount,
|
||||||
|
isLoading,
|
||||||
|
error: error as undefined,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBeerPostComments;
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { beerPostQueryResultArraySchema } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { z } from 'zod';
|
||||||
|
/**
|
||||||
|
* A custom React hook that searches for beer posts that match a given query string.
|
||||||
|
*
|
||||||
|
* @param query The search query string to match beer posts against.
|
||||||
|
* @returns An object containing an array of search results matching the query, an error
|
||||||
|
* object if an error occurred during the search, and a boolean indicating if the
|
||||||
|
* request is currently loading.
|
||||||
|
*/
|
||||||
const useBeerPostSearch = (query: string | undefined) => {
|
const useBeerPostSearch = (query: string | undefined) => {
|
||||||
const { data, isLoading, error } = useSWR(
|
const { data, isLoading, error } = useSWR(
|
||||||
`/api/beers/search?search=${query}`,
|
`/api/beers/search?search=${query}`,
|
||||||
@@ -13,7 +21,7 @@ const useBeerPostSearch = (query: string | undefined) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const result = beerPostQueryResultArraySchema.parse(json);
|
const result = z.array(beerPostQueryResult).parse(json);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|||||||
56
hooks/useCheckIfUserLikesBeerPost.ts
Normal file
56
hooks/useCheckIfUserLikesBeerPost.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that checks if the current user has liked a beer post by fetching
|
||||||
|
* data from the server.
|
||||||
|
*
|
||||||
|
* @param beerPostId The ID of the beer post to check for likes.
|
||||||
|
* @returns An object containing a boolean indicating if the user has liked the beer post,
|
||||||
|
* an error object if an error occurred during the request, and a boolean indicating if
|
||||||
|
* the request is currently loading.
|
||||||
|
* @throws When the user is not logged in, the server returns an error status code, or if
|
||||||
|
* the response data fails to validate against the expected schema.
|
||||||
|
*/
|
||||||
|
const useCheckIfUserLikesBeerPost = (beerPostId: string) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
`/api/beers/${beerPostId}/like/is-liked`,
|
||||||
|
async () => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User is not logged in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/beers/${beerPostId}/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 useCheckIfUserLikesBeerPost;
|
||||||
48
hooks/useGetLikeCount.ts
Normal file
48
hooks/useGetLikeCount.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to fetch the like count for a beer post from the server.
|
||||||
|
*
|
||||||
|
* @param beerPostId - The ID of the beer post to fetch the like count for.
|
||||||
|
* @returns An object with the current like count, as well as metadata about the current
|
||||||
|
* state of the request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useGetLikeCount = (beerPostId: string) => {
|
||||||
|
const { error, mutate, data, isLoading } = useSWR(
|
||||||
|
`/api/beers/${beerPostId}/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 useGetLikeCount;
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
const useRedirectWhenLoggedIn = () => {
|
/**
|
||||||
|
* Custom React hook that redirects the user to the home page if they are logged in. This
|
||||||
|
* hook is used to prevent logged in users from accessing the login and signup pages. Must
|
||||||
|
* be used under the UserContext provider.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const useRedirectWhenLoggedIn = (): void => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
if (!user) {
|
||||||
if (!user) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
router.push('/');
|
||||||
router.push('/');
|
|
||||||
}, [user, router]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useRedirectWhenLoggedIn;
|
export default useRedirectWhenLoggedIn;
|
||||||
|
|||||||
20
hooks/useTimeDistance.ts
Normal file
20
hooks/useTimeDistance.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time distance between the provided date and the current time, using the
|
||||||
|
* `date-fns` `formatDistanceStrict` function. This hook ensures that the same result is
|
||||||
|
* calculated on both the server and client, preventing hydration errors.
|
||||||
|
*
|
||||||
|
* @param createdAt The date to calculate the time distance from.
|
||||||
|
* @returns The time distance between the provided date and the current time.
|
||||||
|
*/
|
||||||
|
const useTimeDistance = (createdAt: Date) => {
|
||||||
|
const [timeDistance, setTimeDistance] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeDistance(formatDistanceStrict(createdAt, new Date()));
|
||||||
|
}, [createdAt]);
|
||||||
|
return timeDistance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTimeDistance;
|
||||||
@@ -2,6 +2,15 @@ import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom React hook that fetches the current user's data from the server.
|
||||||
|
*
|
||||||
|
* @returns An object containing the current user's data, a boolean indicating if the
|
||||||
|
* request is currently loading, and an error object if an error occurred during the
|
||||||
|
* request.
|
||||||
|
* @throws When the user is not logged in, the server returns an error status code, or if
|
||||||
|
* the response data fails to validate against the expected schema.
|
||||||
|
*/
|
||||||
const useUser = () => {
|
const useUser = () => {
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
@@ -26,7 +35,7 @@ const useUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
||||||
console.log(parsedPayload);
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error(parsedPayload.error.message);
|
throw new Error(parsedPayload.error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
3581
package-lock.json
generated
3581
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -13,63 +13,64 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/iron": "^7.0.1",
|
"@hapi/iron": "^7.0.1",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^3.0.0",
|
||||||
"@prisma/client": "^4.10.1",
|
"@prisma/client": "^4.12.0",
|
||||||
"@react-email/components": "^0.0.2",
|
"@react-email/components": "^0.0.4",
|
||||||
"@react-email/render": "0.0.6",
|
"@react-email/render": "^0.0.6",
|
||||||
"@react-email/tailwind": "0.0.6",
|
"@react-email/tailwind": "^0.0.7",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"cloudinary": "^1.34.0",
|
"cloudinary": "^1.35.0",
|
||||||
"cookie": "0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"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.1",
|
"next": "^13.2.4",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pino": "^8.11.0",
|
"pino": "^8.11.0",
|
||||||
"pino-pretty": "^9.3.0",
|
"pino-pretty": "^10.0.0",
|
||||||
"react": "18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-daisyui": "^3.0.3",
|
"react-daisyui": "^3.1.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-email": "^1.7.15",
|
"react-email": "^1.9.0",
|
||||||
"react-hook-form": "^7.43.2",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.8.0",
|
||||||
"sparkpost": "^2.1.4",
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.1.2",
|
||||||
"zod": "^3.20.6"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.192",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.14.1",
|
"@types/node": "^18.15.11",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.33",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/sparkpost": "^2.1.5",
|
"@types/sparkpost": "^2.1.5",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.14",
|
||||||
"daisyui": "^2.51.0",
|
"daisyui": "^2.51.5",
|
||||||
"dotenv-cli": "^7.0.0",
|
"dotenv-cli": "^7.1.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.0.0",
|
"eslint-config-airbnb-typescript": "17.0.0",
|
||||||
"eslint-config-next": "^13.2.1",
|
"eslint-config-next": "^13.2.4",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.3",
|
"prettier-plugin-tailwindcss": "^0.2.6",
|
||||||
"prisma": "^4.10.1",
|
"prisma": "^4.12.0",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,28 @@ import useUser from '@/hooks/useUser';
|
|||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
|
|
||||||
|
import { Roboto } from 'next/font/google';
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
weight: ['100', '300', '400', '500', '700', '900'],
|
||||||
|
subsets: ['latin'],
|
||||||
|
});
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
const { user, isLoading, error } = useUser();
|
const { user, isLoading, error } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ user, isLoading, error }}>
|
<>
|
||||||
<Component {...pageProps} />
|
<style jsx global>
|
||||||
</UserContext.Provider>
|
{`
|
||||||
|
html {
|
||||||
|
font-family: ${roboto.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<UserContext.Provider value={{ user, isLoading, error }}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</UserContext.Provider>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
||||||
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
|
|
||||||
import { createRouter } from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
|
||||||
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||||
body: z.infer<typeof BeerCommentValidationSchema>;
|
body: z.infer<typeof BeerCommentValidationSchema>;
|
||||||
query: { id: string };
|
query: { id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string; page_size: string; page_num: string };
|
||||||
|
}
|
||||||
|
|
||||||
const createComment = async (
|
const createComment = async (
|
||||||
req: CreateCommentRequest,
|
req: CreateCommentRequest,
|
||||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
@@ -24,12 +31,13 @@ const createComment = async (
|
|||||||
|
|
||||||
const beerPostId = req.query.id;
|
const beerPostId = req.query.id;
|
||||||
|
|
||||||
const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({
|
const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
|
||||||
content,
|
await createNewBeerComment({
|
||||||
rating,
|
content,
|
||||||
beerPostId,
|
rating,
|
||||||
userId: req.user!.id,
|
beerPostId,
|
||||||
});
|
userId: req.user!.id,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
message: 'Beer comment created successfully',
|
message: 'Beer comment created successfully',
|
||||||
@@ -39,8 +47,34 @@ const createComment = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAll = async (
|
||||||
|
req: GetAllCommentsRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const beerPostId = req.query.id;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { page_size, page_num } = req.query;
|
||||||
|
|
||||||
|
const comments = await getAllBeerComments(
|
||||||
|
{ id: beerPostId },
|
||||||
|
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageCount = await DBClient.instance.beerComment.count({ where: { beerPostId } });
|
||||||
|
|
||||||
|
res.setHeader('X-Total-Count', pageCount);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Beer comments fetched successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: comments,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const router = createRouter<
|
const router = createRouter<
|
||||||
CreateCommentRequest,
|
// I don't want to use any, but I can't figure out how to get the types to work
|
||||||
|
any,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -53,5 +87,16 @@ router.post(
|
|||||||
createComment,
|
createComment,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
page_size: z.coerce.number().int().positive(),
|
||||||
|
page_num: z.coerce.number().int().positive(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getAll,
|
||||||
|
);
|
||||||
|
|
||||||
const handler = router.handler(NextConnectOptions);
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
|||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import { createRouter } from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
|
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
|
||||||
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
|
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
|
||||||
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
|
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
|
||||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
const sendLikeRequest = async (
|
const sendLikeRequest = async (
|
||||||
req: UserExtendedNextApiRequest,
|
req: UserExtendedNextApiRequest,
|
||||||
@@ -43,6 +44,24 @@ const sendLikeRequest = async (
|
|||||||
res.status(200).json(jsonResponse);
|
res.status(200).json(jsonResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLikeCount = async (
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const id = req.query.id as string;
|
||||||
|
|
||||||
|
const likes = await DBClient.instance.beerPostLike.count({
|
||||||
|
where: { beerPostId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Successfully retrieved like count.',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: { likeCount: likes },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const router = createRouter<
|
const router = createRouter<
|
||||||
UserExtendedNextApiRequest,
|
UserExtendedNextApiRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
@@ -54,5 +73,11 @@ router.post(
|
|||||||
sendLikeRequest,
|
sendLikeRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
||||||
|
getLikeCount,
|
||||||
|
);
|
||||||
|
|
||||||
const handler = router.handler(NextConnectOptions);
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { createRouter } from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const SearchSchema = z.object({
|
const SearchSchema = z.object({
|
||||||
search: z.string().min(1),
|
search: z.string().min(1),
|
||||||
@@ -18,29 +18,30 @@ interface SearchAPIRequest extends NextApiRequest {
|
|||||||
const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
|
const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
|
||||||
const { search: query } = req.query;
|
const { search: query } = req.query;
|
||||||
|
|
||||||
const beers: BeerPostQueryResult[] = await DBClient.instance.beerPost.findMany({
|
const beers: z.infer<typeof beerPostQueryResult>[] =
|
||||||
select: {
|
await DBClient.instance.beerPost.findMany({
|
||||||
id: true,
|
select: {
|
||||||
name: true,
|
id: true,
|
||||||
ibu: true,
|
name: true,
|
||||||
abv: true,
|
ibu: true,
|
||||||
createdAt: true,
|
abv: true,
|
||||||
description: true,
|
createdAt: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
description: true,
|
||||||
brewery: { select: { name: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
type: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
type: { select: { name: true, id: true } },
|
||||||
},
|
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||||
where: {
|
},
|
||||||
OR: [
|
where: {
|
||||||
{ name: { contains: query, mode: 'insensitive' } },
|
OR: [
|
||||||
{ description: { contains: query, mode: 'insensitive' } },
|
{ name: { contains: query, mode: 'insensitive' } },
|
||||||
|
{ description: { contains: query, mode: 'insensitive' } },
|
||||||
|
|
||||||
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
|
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
|
||||||
{ type: { name: { contains: query, mode: 'insensitive' } } },
|
{ type: { name: { contains: query, mode: 'insensitive' } } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json(beers);
|
res.status(200).json(beers);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
|||||||
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||||
|
|
||||||
const router = createRouter<
|
const router = createRouter<
|
||||||
UserExtendedNextApiRequest,
|
UserExtendedNextApiRequest,
|
||||||
@@ -20,14 +21,18 @@ router.post(
|
|||||||
expressWrapper(async (req, res, next) => {
|
expressWrapper(async (req, res, next) => {
|
||||||
passport.initialize();
|
passport.initialize();
|
||||||
passport.use(localStrat);
|
passport.use(localStrat);
|
||||||
passport.authenticate('local', { session: false }, (error, token) => {
|
passport.authenticate(
|
||||||
if (error) {
|
'local',
|
||||||
next(error);
|
{ session: false },
|
||||||
return;
|
(error: unknown, token: z.infer<typeof GetUserSchema>) => {
|
||||||
}
|
if (error) {
|
||||||
req.user = token;
|
next(error);
|
||||||
next();
|
return;
|
||||||
})(req, res, next);
|
}
|
||||||
|
req.user = token;
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
)(req, res, next);
|
||||||
}),
|
}),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const user = req.user!;
|
const user = req.user!;
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import React from 'react';
|
|||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import EditBeerPostForm from '@/components/EditBeerPostForm';
|
import EditBeerPostForm from '@/components/EditBeerPostForm';
|
||||||
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
||||||
import { BiBeer } from 'react-icons/bi';
|
import { BiBeer } from 'react-icons/bi';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface EditPageProps {
|
interface EditPageProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
||||||
|
|||||||
@@ -7,34 +7,23 @@ import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSecti
|
|||||||
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
|
|
||||||
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
||||||
|
|
||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
import { BeerPost } from '@prisma/client';
|
import { BeerPost } from '@prisma/client';
|
||||||
import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
|
|
||||||
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface BeerPageProps {
|
interface BeerPageProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||||
beerRecommendations: (BeerPost & {
|
beerRecommendations: (BeerPost & {
|
||||||
brewery: { id: string; name: string };
|
brewery: { id: string; name: string };
|
||||||
beerImages: { id: string; alt: string; url: string }[];
|
beerImages: { id: string; alt: string; url: string }[];
|
||||||
})[];
|
})[];
|
||||||
beerComments: BeerCommentQueryResultArrayT;
|
|
||||||
commentsPageCount: number;
|
|
||||||
likeCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerByIdPage: NextPage<BeerPageProps> = ({
|
const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }) => {
|
||||||
beerPost,
|
|
||||||
beerRecommendations,
|
|
||||||
beerComments,
|
|
||||||
commentsPageCount,
|
|
||||||
likeCount,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -54,13 +43,9 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
|
|
||||||
<div className="my-12 flex w-full items-center justify-center ">
|
<div className="my-12 flex w-full items-center justify-center ">
|
||||||
<div className="w-11/12 space-y-3 xl:w-9/12">
|
<div className="w-11/12 space-y-3 xl:w-9/12">
|
||||||
<BeerInfoHeader beerPost={beerPost} initialLikeCount={likeCount} />
|
<BeerInfoHeader beerPost={beerPost} />
|
||||||
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
|
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
|
||||||
<BeerPostCommentsSection
|
<BeerPostCommentsSection beerPost={beerPost} />
|
||||||
beerPost={beerPost}
|
|
||||||
comments={beerComments}
|
|
||||||
commentsPageCount={commentsPageCount}
|
|
||||||
/>
|
|
||||||
<div className="md:w-[40%]">
|
<div className="md:w-[40%]">
|
||||||
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +59,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
||||||
const beerPost = await getBeerPostById(context.params!.id! as string);
|
const beerPost = await getBeerPostById(context.params!.id! as string);
|
||||||
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
|
|
||||||
|
|
||||||
if (!beerPost) {
|
if (!beerPost) {
|
||||||
return { notFound: true };
|
return { notFound: true };
|
||||||
@@ -83,23 +67,9 @@ export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (cont
|
|||||||
const { type, brewery, id } = beerPost;
|
const { type, brewery, id } = beerPost;
|
||||||
const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
|
const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
|
||||||
|
|
||||||
const pageSize = 5;
|
|
||||||
const beerComments = await getAllBeerComments(
|
|
||||||
{ id: beerPost.id },
|
|
||||||
{ pageSize, pageNum: beerCommentPageNum },
|
|
||||||
);
|
|
||||||
|
|
||||||
const commentCount = await getBeerCommentCount(beerPost.id);
|
|
||||||
|
|
||||||
const commentPageCount = commentCount ? Math.ceil(commentCount / pageSize) : 0;
|
|
||||||
const likeCount = await getBeerPostLikeCount(beerPost.id);
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
||||||
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
||||||
beerComments: JSON.parse(JSON.stringify(beerComments)),
|
|
||||||
commentsPageCount: JSON.parse(JSON.stringify(commentPageCount)),
|
|
||||||
likeCount: JSON.parse(JSON.stringify(likeCount)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { props };
|
return { props };
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
|
|||||||
import { BeerType } from '@prisma/client';
|
import { BeerType } from '@prisma/client';
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import { BiBeer } from 'react-icons/bi';
|
import { BiBeer } from 'react-icons/bi';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface CreateBeerPageProps {
|
interface CreateBeerPageProps {
|
||||||
breweries: BreweryPostQueryResult[];
|
breweries: z.infer<typeof BreweryPostQueryResult>[];
|
||||||
types: BeerType[];
|
types: BeerType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import DBClient from '@/prisma/DBClient';
|
|||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
|
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
|
||||||
import BeerCard from '@/components/BeerIndex/BeerCard';
|
import BeerCard from '@/components/BeerIndex/BeerCard';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface BeerPageProps {
|
interface BeerPageProps {
|
||||||
initialBeerPosts: BeerPostQueryResult[];
|
initialBeerPosts: z.infer<typeof beerPostQueryResult>[];
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
|
|||||||
<meta name="description" content="Beer posts" />
|
<meta name="description" content="Beer posts" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex items-center justify-center bg-base-100">
|
<div className="flex items-center justify-center bg-base-100">
|
||||||
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
||||||
<header className="my-10">
|
<header className="my-10">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
|
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const DEBOUNCE_DELAY = 300;
|
|||||||
|
|
||||||
const SearchPage: NextPage = () => {
|
const SearchPage: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const querySearch = router.query.search as string | undefined;
|
const querySearch = (router.query.search as string) || '';
|
||||||
const [searchValue, setSearchValue] = useState(querySearch || '');
|
const [searchValue, setSearchValue] = useState(querySearch);
|
||||||
const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue);
|
const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue);
|
||||||
|
|
||||||
const debounceSearch = debounce((value: string) => {
|
const debounceSearch = debounce((value: string) => {
|
||||||
@@ -36,7 +36,7 @@ const SearchPage: NextPage = () => {
|
|||||||
if (!querySearch || searchValue) {
|
if (!querySearch || searchValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearchValue(searchValue);
|
setSearchValue(querySearch);
|
||||||
}, DEBOUNCE_DELAY)();
|
}, DEBOUNCE_DELAY)();
|
||||||
}, [querySearch, searchValue]);
|
}, [querySearch, searchValue]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface BreweryPageProps {
|
interface BreweryPageProps {
|
||||||
breweryPost: BeerPostQueryResult;
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
||||||
|
|||||||
@@ -4,24 +4,58 @@ import Link from 'next/link';
|
|||||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface BreweryPageProps {
|
interface BreweryPageProps {
|
||||||
breweryPosts: BreweryPostQueryResult[];
|
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
||||||
|
brewery,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-300" key={brewery.id}>
|
||||||
|
<figure className="card-image h-96">
|
||||||
|
{brewery.breweryImages.length > 0 && (
|
||||||
|
<Image
|
||||||
|
src={brewery.breweryImages[0].path}
|
||||||
|
alt={brewery.name}
|
||||||
|
width="1029"
|
||||||
|
height="110"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
<div className="card-body space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-3xl font-bold">
|
||||||
|
<Link href={`/breweries/${brewery.id}`}>{brewery.name}</Link>
|
||||||
|
</h2>
|
||||||
|
<h3 className="text-xl font-semibold">{brewery.location}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<h1 className="text-3xl font-bold underline">Brewery Posts</h1>
|
<div className="flex items-center justify-center bg-base-100">
|
||||||
{breweryPosts.map((post) => {
|
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
||||||
return (
|
<header className="my-10">
|
||||||
<div key={post.id}>
|
<div className="space-y-2">
|
||||||
<h2>
|
<h1 className="text-6xl font-bold">Breweries</h1>
|
||||||
<Link href={`/breweries/${post.id}`}>{post.name}</Link>
|
</div>
|
||||||
</h2>
|
</header>
|
||||||
|
<div className="grid gap-5 md:grid-cols-1 xl:grid-cols-2">
|
||||||
|
{breweryPosts.map((brewery) => {
|
||||||
|
return <BreweryCard brewery={brewery} key={brewery.id} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,21 +7,29 @@ import { GetServerSideProps, NextPage } from 'next';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
const ProtectedPage: NextPage = () => {
|
const ProtectedPage: NextPage = () => {
|
||||||
const { user, error, isLoading } = useContext(UserContext);
|
const { user, isLoading } = useContext(UserContext);
|
||||||
|
|
||||||
|
const currentTime = new Date().getHours();
|
||||||
|
|
||||||
|
const isMorning = currentTime > 5 && currentTime < 12;
|
||||||
|
const isAfternoon = currentTime > 12 && currentTime < 18;
|
||||||
|
const isEvening = currentTime > 18 && currentTime < 24;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center space-y-3">
|
||||||
<h1 className="text-7xl font-bold text-white">Hello!</h1>
|
{isLoading && <Spinner size="xl" />}
|
||||||
<>
|
{user && (
|
||||||
{isLoading && <Spinner />}
|
<>
|
||||||
{error && <p>Something went wrong.</p>}
|
<h1 className="text-7xl font-bold">
|
||||||
{user && (
|
Good {isMorning && 'morning'}
|
||||||
<div>
|
{isAfternoon && 'afternoon'}
|
||||||
<p>{user.username}</p>
|
{isEvening && 'evening'}
|
||||||
</div>
|
{`, ${user?.firstName}!`}
|
||||||
)}
|
</h1>
|
||||||
</>
|
<h2 className="text-4xl font-bold">Welcome to the Biergarten App!</h2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "User" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"firstName" TEXT NOT NULL,
|
|
||||||
"lastName" TEXT NOT NULL,
|
|
||||||
"email" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BeerPost" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"ibu" DOUBLE PRECISION NOT NULL,
|
|
||||||
"abv" DOUBLE PRECISION NOT NULL,
|
|
||||||
"postedById" TEXT NOT NULL,
|
|
||||||
"breweryId" TEXT NOT NULL,
|
|
||||||
"typeId" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "BeerPost_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BeerComment" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"beerPostId" TEXT NOT NULL,
|
|
||||||
"postedById" TEXT NOT NULL,
|
|
||||||
"content" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "BeerComment_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BeerType" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
"postedById" TEXT NOT NULL,
|
|
||||||
CONSTRAINT "BeerType_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BreweryPost" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"location" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
"postedById" TEXT NOT NULL,
|
|
||||||
CONSTRAINT "BreweryPost_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BreweryComment" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"breweryPostId" TEXT NOT NULL,
|
|
||||||
"postedById" TEXT NOT NULL,
|
|
||||||
"content" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "BreweryComment_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_breweryId_fkey" FOREIGN KEY ("breweryId") REFERENCES "BreweryPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "BeerType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerComment_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerType"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerType_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryComment_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `description` to the `BeerPost` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `description` to the `BreweryPost` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `dateOfBirth` to the `User` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
COLUMN "description" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryPost"
|
|
||||||
ADD
|
|
||||||
COLUMN "description" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE
|
|
||||||
"User"
|
|
||||||
ADD
|
|
||||||
COLUMN "dateOfBirth" TIMESTAMP(3) NOT NULL,
|
|
||||||
ADD
|
|
||||||
COLUMN "username" TEXT NOT NULL;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment" DROP CONSTRAINT "BeerComment_beerPostId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment" DROP CONSTRAINT "BeerComment_postedById_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost" DROP CONSTRAINT "BeerPost_breweryId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost" DROP CONSTRAINT "BeerPost_postedById_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost" DROP CONSTRAINT "BeerPost_typeId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerType" DROP CONSTRAINT "BeerType_postedById_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment" DROP CONSTRAINT "BreweryComment_postedById_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryPost" DROP CONSTRAINT "BreweryPost_postedById_fkey";
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BeerImage" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"beerPostId" TEXT NOT NULL,
|
|
||||||
"url" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "BeerImage_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BreweryImage" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"breweryPostId" TEXT NOT NULL,
|
|
||||||
"url" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
CONSTRAINT "BreweryImage_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_breweryId_fkey" FOREIGN KEY ("breweryId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerPost_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "BeerType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerComment_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerType"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerType_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryPost"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerImage"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerImage_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryImage"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryImage_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerImage" DROP CONSTRAINT "BeerImage_beerPostId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment" DROP CONSTRAINT "BreweryComment_breweryPostId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryImage" DROP CONSTRAINT "BreweryImage_breweryPostId_fkey";
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryComment"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryComment_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BeerImage"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BeerImage_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE
|
|
||||||
"BreweryImage"
|
|
||||||
ADD
|
|
||||||
CONSTRAINT "BreweryImage_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `alt` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `alt` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BeerImage" ADD COLUMN "alt" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BreweryImage" ADD COLUMN "alt" TEXT NOT NULL;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `rating` to the `BeerComment` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `rating` to the `BreweryComment` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BeerComment" ADD COLUMN "rating" INTEGER NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BreweryComment" ADD COLUMN "rating" INTEGER NOT NULL;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `hash` to the `User` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "User" ADD COLUMN "hash" TEXT NOT NULL;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "BeerPostLike" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"beerPostId" TEXT NOT NULL,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMPTZ(3),
|
|
||||||
|
|
||||||
CONSTRAINT "BeerPostLike_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `url` on the `BeerImage` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the column `url` on the `BreweryImage` table. All the data in the column will be lost.
|
|
||||||
- Added the required column `caption` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `path` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `postedById` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `caption` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `path` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `postedById` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BeerImage" DROP COLUMN "url",
|
|
||||||
ADD COLUMN "caption" TEXT NOT NULL,
|
|
||||||
ADD COLUMN "path" TEXT NOT NULL,
|
|
||||||
ADD COLUMN "postedById" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BreweryImage" DROP COLUMN "url",
|
|
||||||
ADD COLUMN "caption" TEXT NOT NULL,
|
|
||||||
ADD COLUMN "path" TEXT NOT NULL,
|
|
||||||
ADD COLUMN "postedById" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "BeerImage" ADD CONSTRAINT "BeerImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "BreweryImage" ADD CONSTRAINT "BreweryImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `userId` on the `BeerPostLike` table. All the data in the column will be lost.
|
|
||||||
- Added the required column `likedById` to the `BeerPostLike` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "BeerPostLike" DROP CONSTRAINT "BeerPostLike_userId_fkey";
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "BeerPostLike" DROP COLUMN "userId",
|
|
||||||
ADD COLUMN "likedById" TEXT NOT NULL;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "User" ADD COLUMN "isAccountVerified" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
171
prisma/migrations/20230406013055_/migration.sql
Normal file
171
prisma/migrations/20230406013055_/migration.sql
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"username" STRING NOT NULL,
|
||||||
|
"firstName" STRING NOT NULL,
|
||||||
|
"lastName" STRING NOT NULL,
|
||||||
|
"hash" STRING NOT NULL,
|
||||||
|
"email" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
"isAccountVerified" BOOL NOT NULL DEFAULT false,
|
||||||
|
"dateOfBirth" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerPost" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"name" STRING NOT NULL,
|
||||||
|
"ibu" FLOAT8 NOT NULL,
|
||||||
|
"abv" FLOAT8 NOT NULL,
|
||||||
|
"description" STRING NOT NULL,
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
"breweryId" STRING NOT NULL,
|
||||||
|
"typeId" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BeerPost_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerPostLike" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"beerPostId" STRING NOT NULL,
|
||||||
|
"likedById" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BeerPostLike_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerComment" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"rating" INT4 NOT NULL,
|
||||||
|
"beerPostId" STRING NOT NULL,
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
"content" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BeerComment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerType" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"name" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "BeerType_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BreweryPost" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"name" STRING NOT NULL,
|
||||||
|
"location" STRING NOT NULL,
|
||||||
|
"description" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "BreweryPost_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BreweryComment" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"rating" INT4 NOT NULL,
|
||||||
|
"breweryPostId" STRING NOT NULL,
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
"content" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
|
||||||
|
CONSTRAINT "BreweryComment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BeerImage" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"beerPostId" STRING NOT NULL,
|
||||||
|
"path" STRING NOT NULL,
|
||||||
|
"alt" STRING NOT NULL,
|
||||||
|
"caption" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "BeerImage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BreweryImage" (
|
||||||
|
"id" STRING NOT NULL,
|
||||||
|
"breweryPostId" STRING NOT NULL,
|
||||||
|
"path" STRING NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ(3),
|
||||||
|
"caption" STRING NOT NULL,
|
||||||
|
"alt" STRING NOT NULL,
|
||||||
|
"postedById" STRING NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "BreweryImage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPost" ADD CONSTRAINT "BeerPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPost" ADD CONSTRAINT "BeerPost_breweryId_fkey" FOREIGN KEY ("breweryId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPost" ADD CONSTRAINT "BeerPost_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "BeerType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerComment" ADD CONSTRAINT "BeerComment_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerComment" ADD CONSTRAINT "BeerComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerType" ADD CONSTRAINT "BeerType_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryPost" ADD CONSTRAINT "BreweryPost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryComment" ADD CONSTRAINT "BreweryComment_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryComment" ADD CONSTRAINT "BreweryComment_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerImage" ADD CONSTRAINT "BeerImage_beerPostId_fkey" FOREIGN KEY ("beerPostId") REFERENCES "BeerPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerImage" ADD CONSTRAINT "BeerImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryImage" ADD CONSTRAINT "BreweryImage_breweryPostId_fkey" FOREIGN KEY ("breweryPostId") REFERENCES "BreweryPost"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryImage" ADD CONSTRAINT "BreweryImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (i.e. Git)
|
# It should be added in your version-control system (i.e. Git)
|
||||||
provider = "postgresql"
|
provider = "cockroachdb"
|
||||||
@@ -6,7 +6,7 @@ generator client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "cockroachdb"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const sendCheckIfUserLikesBeerPostRequest = async (beerPostId: string) => {
|
|
||||||
const response = await fetch(`/api/beers/${beerPostId}/like/is-liked`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const parsed = APIResponseValidationSchema.safeParse(data);
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sendCheckIfUserLikesBeerPostRequest;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BeerCommentQueryResult } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -14,14 +14,8 @@ const sendCreateBeerCommentRequest = async ({
|
|||||||
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
|
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
|
||||||
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
|
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ beerPostId, content, rating }),
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
beerPostId,
|
|
||||||
content,
|
|
||||||
rating,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { beerPostQueryResultSchema } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -24,7 +24,7 @@ const sendCreateBeerPostRequest = async (
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = beerPostQueryResultSchema.safeParse(payload);
|
const parsedPayload = beerPostQueryResult.safeParse(payload);
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error('Invalid API response payload');
|
throw new Error('Invalid API response payload');
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { BeerCommentQueryResultArrayT } from './schema/BeerCommentQueryResult';
|
import { z } from 'zod';
|
||||||
|
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
||||||
|
|
||||||
const getAllBeerComments = async (
|
const getAllBeerComments = async (
|
||||||
{ id }: Pick<BeerPostQueryResult, 'id'>,
|
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
|
||||||
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
||||||
) => {
|
) => {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
const beerComments: BeerCommentQueryResultArrayT =
|
const beerComments: z.infer<typeof BeerCommentQueryResult>[] =
|
||||||
await DBClient.instance.beerComment.findMany({
|
await DBClient.instance.beerComment.findMany({
|
||||||
skip,
|
skip,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const BeerCommentQueryResult = z.object({
|
const BeerCommentQueryResult = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
content: z.string().min(1).max(300),
|
content: z.string().min(1).max(500),
|
||||||
rating: z.number().int().min(1).max(5),
|
rating: z.number().int().min(1).max(5),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
postedBy: z.object({
|
postedBy: z.object({
|
||||||
@@ -10,6 +10,5 @@ export const BeerCommentQueryResult = z.object({
|
|||||||
username: z.string().min(1).max(50),
|
username: z.string().min(1).max(50),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export const BeerCommentQueryResultArray = z.array(BeerCommentQueryResult);
|
|
||||||
export type BeerCommentQueryResultT = z.infer<typeof BeerCommentQueryResult>;
|
export default BeerCommentQueryResult;
|
||||||
export type BeerCommentQueryResultArrayT = z.infer<typeof BeerCommentQueryResultArray>;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { BeerPostQueryResult } from './schema/BeerPostQueryResult';
|
import beerPostQueryResult from './schema/BeerPostQueryResult';
|
||||||
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
||||||
|
|
||||||
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
||||||
@@ -16,29 +16,30 @@ const createNewBeerPost = async ({
|
|||||||
breweryId,
|
breweryId,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
||||||
const newBeerPost: BeerPostQueryResult = await DBClient.instance.beerPost.create({
|
const newBeerPost: z.infer<typeof beerPostQueryResult> =
|
||||||
data: {
|
await DBClient.instance.beerPost.create({
|
||||||
name,
|
data: {
|
||||||
description,
|
name,
|
||||||
abv,
|
description,
|
||||||
ibu,
|
abv,
|
||||||
type: { connect: { id: typeId } },
|
ibu,
|
||||||
postedBy: { connect: { id: userId } },
|
type: { connect: { id: typeId } },
|
||||||
brewery: { connect: { id: breweryId } },
|
postedBy: { connect: { id: userId } },
|
||||||
},
|
brewery: { connect: { id: breweryId } },
|
||||||
select: {
|
},
|
||||||
id: true,
|
select: {
|
||||||
name: true,
|
id: true,
|
||||||
description: true,
|
name: true,
|
||||||
abv: true,
|
description: true,
|
||||||
ibu: true,
|
abv: true,
|
||||||
createdAt: true,
|
ibu: true,
|
||||||
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
createdAt: true,
|
||||||
brewery: { select: { id: true, name: true } },
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
type: { select: { id: true, name: true } },
|
brewery: { select: { id: true, name: true } },
|
||||||
postedBy: { select: { id: true, username: true } },
|
type: { select: { id: true, name: true } },
|
||||||
},
|
postedBy: { select: { id: true, username: true } },
|
||||||
});
|
},
|
||||||
|
});
|
||||||
return newBeerPost;
|
return newBeerPost;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
|
|
||||||
const beerPosts: BeerPostQueryResult[] = await prisma.beerPost.findMany({
|
const beerPosts: z.infer<typeof beerPostQueryResult>[] = await prisma.beerPost.findMany(
|
||||||
select: {
|
{
|
||||||
id: true,
|
select: {
|
||||||
name: true,
|
id: true,
|
||||||
ibu: true,
|
name: true,
|
||||||
abv: true,
|
ibu: true,
|
||||||
description: true,
|
abv: true,
|
||||||
createdAt: true,
|
description: true,
|
||||||
type: { select: { name: true, id: true } },
|
createdAt: true,
|
||||||
brewery: { select: { name: true, id: true } },
|
type: { select: { name: true, id: true } },
|
||||||
postedBy: { select: { id: true, username: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
|
},
|
||||||
|
take: pageSize,
|
||||||
|
skip,
|
||||||
},
|
},
|
||||||
take: pageSize,
|
);
|
||||||
skip,
|
|
||||||
});
|
|
||||||
|
|
||||||
return beerPosts;
|
return beerPosts;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getBeerPostById = async (id: string) => {
|
const getBeerPostById = async (id: string) => {
|
||||||
const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({
|
const beerPost: z.infer<typeof beerPostQueryResult> | null =
|
||||||
select: {
|
await prisma.beerPost.findFirst({
|
||||||
id: true,
|
select: {
|
||||||
name: true,
|
id: true,
|
||||||
ibu: true,
|
name: true,
|
||||||
abv: true,
|
ibu: true,
|
||||||
createdAt: true,
|
abv: true,
|
||||||
description: true,
|
createdAt: true,
|
||||||
postedBy: { select: { username: true, id: true } },
|
description: true,
|
||||||
brewery: { select: { name: true, id: true } },
|
postedBy: { select: { username: true, id: true } },
|
||||||
type: { select: { name: true, id: true } },
|
brewery: { select: { name: true, id: true } },
|
||||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
type: { select: { name: true, id: true } },
|
||||||
},
|
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||||
where: { id },
|
},
|
||||||
});
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
return beerPost;
|
return beerPost;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const getBeerRecommendations = async (
|
const getBeerRecommendations = async (
|
||||||
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery' | 'id'>,
|
beerPost: Pick<z.infer<typeof beerPostQueryResult>, 'type' | 'brewery' | 'id'>,
|
||||||
) => {
|
) => {
|
||||||
const beerRecommendations = await DBClient.instance.beerPost.findMany({
|
const beerRecommendations = await DBClient.instance.beerPost.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,36 +1,18 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const beerPostQueryResultSchema = z.object({
|
const beerPostQueryResult = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
brewery: z.object({
|
brewery: z.object({ id: z.string(), name: z.string() }),
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
beerImages: z.array(
|
beerImages: z.array(
|
||||||
z.object({
|
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||||
path: z.string(),
|
|
||||||
caption: z.string(),
|
|
||||||
id: z.string(),
|
|
||||||
alt: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
ibu: z.number(),
|
ibu: z.number(),
|
||||||
abv: z.number(),
|
abv: z.number(),
|
||||||
type: z.object({
|
type: z.object({ id: z.string(), name: z.string() }),
|
||||||
id: z.string(),
|
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
postedBy: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
username: z.string(),
|
|
||||||
}),
|
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const beerPostQueryResultArraySchema = z.array(beerPostQueryResultSchema);
|
export default beerPostQueryResult;
|
||||||
|
|
||||||
export type BeerPostQueryResult = z.infer<typeof beerPostQueryResultSchema>;
|
|
||||||
|
|
||||||
export type BeerPostQueryResultArray = z.infer<typeof beerPostQueryResultArraySchema>;
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BreweryPostQueryResult from './types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getAllBreweryPosts = async () => {
|
const getAllBreweryPosts = async () => {
|
||||||
const breweryPosts: BreweryPostQueryResult[] = await prisma.breweryPost.findMany({
|
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
|
||||||
select: {
|
await prisma.breweryPost.findMany({
|
||||||
id: true,
|
select: {
|
||||||
location: true,
|
id: true,
|
||||||
name: true,
|
location: true,
|
||||||
postedBy: { select: { firstName: true, lastName: true, id: true } },
|
name: true,
|
||||||
},
|
postedBy: { select: { username: true, id: true } },
|
||||||
});
|
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return breweryPosts;
|
return breweryPosts;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BreweryPostQueryResult from './types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getBreweryPostById = async (id: string) => {
|
const getBreweryPostById = async (id: string) => {
|
||||||
const breweryPost: BreweryPostQueryResult | null = await prisma.breweryPost.findFirst({
|
const breweryPost: z.infer<typeof BreweryPostQueryResult> | null =
|
||||||
select: {
|
await prisma.breweryPost.findFirst({
|
||||||
id: true,
|
select: {
|
||||||
location: true,
|
id: true,
|
||||||
name: true,
|
location: true,
|
||||||
postedBy: {
|
name: true,
|
||||||
select: {
|
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
firstName: true,
|
postedBy: { select: { username: true, id: true } },
|
||||||
lastName: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
where: { id },
|
||||||
where: {
|
});
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return breweryPost;
|
return breweryPost;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
export default interface BreweryPostQueryResult {
|
import { z } from 'zod';
|
||||||
id: string;
|
|
||||||
location: string;
|
const BreweryPostQueryResult = z.object({
|
||||||
name: string;
|
id: z.string(),
|
||||||
postedBy: {
|
location: z.string(),
|
||||||
id: string;
|
name: z.string(),
|
||||||
firstName: string;
|
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||||
lastName: string;
|
breweryImages: z.array(
|
||||||
};
|
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||||
}
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BreweryPostQueryResult;
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
|
||||||
html {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,26 +9,29 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require('daisyui')],
|
plugins: [
|
||||||
|
require('daisyui'),
|
||||||
|
require('tailwindcss-animate')
|
||||||
|
],
|
||||||
|
|
||||||
daisyui: {
|
daisyui: {
|
||||||
logs: false,
|
logs: false,
|
||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
default: {
|
default: {
|
||||||
primary: 'hsl(227, 46%, 25%)',
|
primary: 'hsl(227, 23%, 20%)',
|
||||||
secondary: 'hsl(47, 100%, 80%)',
|
secondary: '#ABA9C3',
|
||||||
|
error: '#c17c74',
|
||||||
accent: '#fe3bd9',
|
accent: '#fe3bd9',
|
||||||
neutral: '#131520',
|
neutral: '#131520',
|
||||||
info: '#0A7CFF',
|
info: '#0A7CFF',
|
||||||
success: '#8ACE2B',
|
success: '#8ACE2B',
|
||||||
warning: '#F9D002',
|
warning: '#F9D002',
|
||||||
error: '#CF1259',
|
|
||||||
'primary-content': '#FAF9F6',
|
'primary-content': '#FAF9F6',
|
||||||
'error-content': '#FAF9F6',
|
'error-content': '#FAF9F6',
|
||||||
'base-100': 'hsl(190, 4%, 11%)',
|
'base-100': 'hsl(190, 4%, 9%)',
|
||||||
'base-200': 'hsl(190, 4%, 9%)',
|
'base-200': 'hsl(190, 4%, 8%)',
|
||||||
'base-300': 'hsl(190, 4%, 8%)',
|
'base-300': 'hsl(190, 4%, 5%)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user