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
|
||||
run: |
|
||||
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
|
||||
public/uploads
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { FunctionComponent, useState, useEffect } from 'react';
|
||||
import { Rating } from 'react-daisyui';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
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 FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
@@ -16,10 +19,17 @@ import FormSegment from '../ui/forms/FormSegment';
|
||||
import FormTextArea from '../ui/forms/FormTextArea';
|
||||
|
||||
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<
|
||||
z.infer<typeof BeerCommentValidationSchema>
|
||||
>({
|
||||
@@ -47,44 +57,58 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
|
||||
beerPostId: beerPost.id,
|
||||
});
|
||||
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;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||
<FormError>{errors.content?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextArea
|
||||
id="content"
|
||||
formValidationSchema={register('content')}
|
||||
placeholder="Comment"
|
||||
rows={5}
|
||||
error={!!errors.content?.message}
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||
<FormError>{errors.rating?.message}</FormError>
|
||||
</FormInfo>
|
||||
<Rating
|
||||
value={rating}
|
||||
onChange={(value) => {
|
||||
setRating(value);
|
||||
setValue('rating', value);
|
||||
}}
|
||||
>
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
</Rating>
|
||||
<Button type="submit">Submit</Button>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||
<FormError>{errors.content?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextArea
|
||||
id="content"
|
||||
formValidationSchema={register('content')}
|
||||
placeholder="Comment"
|
||||
rows={5}
|
||||
error={!!errors.content?.message}
|
||||
disabled={formState.isSubmitting}
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||
<FormError>{errors.rating?.message}</FormError>
|
||||
</FormInfo>
|
||||
<Rating
|
||||
value={rating}
|
||||
onChange={(value) => {
|
||||
setRating(value);
|
||||
setValue('rating', value);
|
||||
}}
|
||||
>
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
</Rating>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,42 +1,26 @@
|
||||
import Link from 'next/link';
|
||||
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
|
||||
import format from 'date-fns/format';
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import UserContext from '@/contexts/userContext';
|
||||
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';
|
||||
|
||||
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({
|
||||
beerPost,
|
||||
initialLikeCount,
|
||||
}) => {
|
||||
const createdAtDate = new Date(beerPost.createdAt);
|
||||
const [timeDistance, setTimeDistance] = useState('');
|
||||
const BeerInfoHeader: FC<{
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
}> = ({ beerPost }) => {
|
||||
const createdAt = new Date(beerPost.createdAt);
|
||||
const timeDistance = useTimeDistance(createdAt);
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
const idMatches = user && beerPost.postedBy.id === user.id;
|
||||
const isPostOwner = !!(user && idMatches);
|
||||
|
||||
const [likeCount, setLikeCount] = useState(initialLikeCount);
|
||||
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]);
|
||||
const { likeCount, mutate } = useGetLikeCount(beerPost.id);
|
||||
|
||||
return (
|
||||
<main className="card flex flex-col justify-center bg-base-300">
|
||||
@@ -67,16 +51,18 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
|
||||
</div>
|
||||
|
||||
<h3 className="italic">
|
||||
posted by{' '}
|
||||
{' posted by '}
|
||||
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
||||
{beerPost.postedBy.username}
|
||||
{`${beerPost.postedBy.username} `}
|
||||
</Link>
|
||||
<span
|
||||
className="tooltip tooltip-bottom"
|
||||
data-tip={format(createdAtDate, 'MM/dd/yyyy')}
|
||||
>
|
||||
{` ${timeDistance}`} ago
|
||||
</span>
|
||||
{timeDistance && (
|
||||
<span
|
||||
className="tooltip tooltip-right"
|
||||
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||
>
|
||||
{`${timeDistance} ago`}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
||||
</span>
|
||||
{(!!likeCount || likeCount === 0) && (
|
||||
<span>
|
||||
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions items-end">
|
||||
{user && (
|
||||
<BeerPostLikeButton beerPostId={beerPost.id} setLikeCount={setLikeCount} />
|
||||
)}
|
||||
{user && <BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { FC } from 'react';
|
||||
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 {
|
||||
commentsPageNum: number;
|
||||
commentsPageCount: number;
|
||||
beerPost: BeerPostQueryResult;
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
}
|
||||
|
||||
const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
||||
@@ -14,9 +17,9 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
||||
beerPost,
|
||||
}) => (
|
||||
<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
|
||||
className={`btn-outline btn ${
|
||||
className={`btn btn-ghost ${
|
||||
commentsPageNum === 1
|
||||
? 'btn-disabled pointer-events-none'
|
||||
: 'pointer-events-auto'
|
||||
@@ -27,10 +30,11 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
||||
}}
|
||||
scroll={false}
|
||||
>
|
||||
Next Comments
|
||||
<FaArrowLeft />
|
||||
</Link>
|
||||
<button className="btn btn-ghost pointer-events-none">{commentsPageNum}</button>
|
||||
<Link
|
||||
className={`btn-outline btn ${
|
||||
className={`btn btn-ghost ${
|
||||
commentsPageNum === commentsPageCount
|
||||
? 'btn-disabled pointer-events-none'
|
||||
: 'pointer-events-auto'
|
||||
@@ -41,7 +45,7 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
||||
}}
|
||||
scroll={false}
|
||||
>
|
||||
Previous Comments
|
||||
<FaArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import UserContext from '@/contexts/userContext';
|
||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
|
||||
import { FC, useContext } from 'react';
|
||||
import { z } from 'zod';
|
||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||
import { useRouter } from 'next/router';
|
||||
import BeerCommentForm from './BeerCommentForm';
|
||||
import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar';
|
||||
import CommentCard from './CommentCard';
|
||||
import CommentCardBody from './CommentCardBody';
|
||||
import NoCommentsCard from './NoCommentsCard';
|
||||
import CommentLoadingCardBody from './CommentLoadingCardBody';
|
||||
|
||||
interface BeerPostCommentsSectionProps {
|
||||
beerPost: BeerPostQueryResult;
|
||||
comments: BeerCommentQueryResultArrayT;
|
||||
commentsPageCount: number;
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
}
|
||||
|
||||
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
||||
beerPost,
|
||||
|
||||
comments,
|
||||
commentsPageCount,
|
||||
}) => {
|
||||
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
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 (
|
||||
<div className="w-full space-y-3 md:w-[60%]">
|
||||
<div className="card h-96 bg-base-300">
|
||||
<div className="card-body h-full">
|
||||
{user ? (
|
||||
<BeerCommentForm beerPost={beerPost} />
|
||||
<BeerCommentForm beerPost={beerPost} mutate={mutate} />
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
||||
@@ -37,23 +43,34 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{comments.length ? (
|
||||
|
||||
{comments && !!comments.length && !!commentsPageCount && !isLoading && (
|
||||
<div className="card bg-base-300 pb-6">
|
||||
{comments.map((comment) => (
|
||||
<CommentCard key={comment.id} comment={comment} beerPostId={beerPost.id} />
|
||||
<CommentCardBody key={comment.id} comment={comment} mutate={mutate} />
|
||||
))}
|
||||
|
||||
<BeerCommentsPaginationBar
|
||||
commentsPageNum={commentsPageNum}
|
||||
commentsPageNum={pageNum}
|
||||
commentsPageCount={commentsPageCount}
|
||||
beerPost={beerPost}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card items-center bg-base-300">
|
||||
<div className="card-body">
|
||||
<span className="text-lg font-bold">No comments yet.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!comments?.length && !isLoading && <NoCommentsCard />}
|
||||
|
||||
{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>
|
||||
|
||||
@@ -1,41 +1,28 @@
|
||||
import UserContext from '@/contexts/userContext';
|
||||
import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
|
||||
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 sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest';
|
||||
import { KeyedMutator } from 'swr';
|
||||
|
||||
const BeerPostLikeButton: FC<{
|
||||
beerPostId: string;
|
||||
setLikeCount: Dispatch<SetStateAction<number>>;
|
||||
}> = ({ beerPostId, setLikeCount }) => {
|
||||
mutateCount: KeyedMutator<number>;
|
||||
}> = ({ beerPostId, mutateCount }) => {
|
||||
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isLiked, setIsLiked] = useState(false);
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
sendCheckIfUserLikesBeerPostRequest(beerPostId)
|
||||
.then((currentLikeStatus) => {
|
||||
setIsLiked(currentLikeStatus);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [user, beerPostId]);
|
||||
setLoading(false);
|
||||
}, [isLiked]);
|
||||
|
||||
const handleLike = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await sendLikeRequest(beerPostId);
|
||||
setIsLiked(!isLiked);
|
||||
setLikeCount((prevCount) => prevCount + (isLiked ? -1 : 1));
|
||||
await mutateCount();
|
||||
await mutateLikeStatus();
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<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) => (
|
||||
<div key={beerPost.id} className="w-full">
|
||||
<div>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import UserContext from '@/contexts/userContext';
|
||||
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
import { format, formatDistanceStrict } from 'date-fns';
|
||||
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
import format from 'date-fns/format';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { Rating } from 'react-daisyui';
|
||||
|
||||
import { FaEllipsisH } from 'react-icons/fa';
|
||||
import { KeyedMutator } from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
||||
const CommentCardDropdown: React.FC<{
|
||||
comment: BeerCommentQueryResultT;
|
||||
beerPostId: string;
|
||||
}> = ({ comment, beerPostId }) => {
|
||||
const router = useRouter();
|
||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||
mutate: KeyedMutator<{
|
||||
comments: z.infer<typeof BeerCommentQueryResult>[];
|
||||
pageCount: number;
|
||||
}>;
|
||||
}> = ({ comment, mutate }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const isCommentOwner = user?.id === comment.postedBy.id;
|
||||
@@ -21,16 +25,17 @@ const CommentCardDropdown: React.FC<{
|
||||
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete comment');
|
||||
}
|
||||
|
||||
router.replace(`/beers/${beerPostId}?comments_page=1`, undefined, { scroll: false });
|
||||
await mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<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 />
|
||||
</label>
|
||||
<ul
|
||||
@@ -51,19 +56,20 @@ const CommentCardDropdown: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const CommentCard: React.FC<{
|
||||
comment: BeerCommentQueryResultT;
|
||||
beerPostId: string;
|
||||
}> = ({ comment, beerPostId }) => {
|
||||
const [timeDistance, setTimeDistance] = useState('');
|
||||
const CommentCardBody: React.FC<{
|
||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||
|
||||
mutate: KeyedMutator<{
|
||||
comments: z.infer<typeof BeerCommentQueryResult>[];
|
||||
pageCount: number;
|
||||
}>;
|
||||
}> = ({ comment, mutate }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeDistance(formatDistanceStrict(new Date(comment.createdAt), new Date()));
|
||||
}, [comment.createdAt]);
|
||||
const timeDistance = useTimeDistance(new Date(comment.createdAt));
|
||||
|
||||
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>
|
||||
<h3 className="font-semibold sm:text-2xl">
|
||||
@@ -83,7 +89,7 @@ const CommentCard: React.FC<{
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{user && <CommentCardDropdown comment={comment} beerPostId={beerPostId} />}
|
||||
{user && <CommentCardDropdown comment={comment} mutate={mutate} />}
|
||||
</div>
|
||||
|
||||
<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 { FC } from 'react';
|
||||
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 (
|
||||
<div className="card bg-base-300" key={post.id}>
|
||||
<figure className="card-image h-96">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface PaginationProps {
|
||||
@@ -14,18 +14,16 @@ const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) =>
|
||||
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
|
||||
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
|
||||
scroll={false}
|
||||
prefetch={true}
|
||||
>
|
||||
«
|
||||
<FaArrowLeft />
|
||||
</Link>
|
||||
<button className="btn">Page {pageNum}</button>
|
||||
<Link
|
||||
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
|
||||
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
|
||||
scroll={false}
|
||||
prefetch={true}
|
||||
>
|
||||
»
|
||||
<FaArrowRight />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
|
||||
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { BeerType } from '@prisma/client';
|
||||
import router from 'next/router';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||
import ErrorAlert from './ui/alerts/ErrorAlert';
|
||||
import Button from './ui/forms/Button';
|
||||
import FormError from './ui/forms/FormError';
|
||||
@@ -20,7 +20,7 @@ import FormTextInput from './ui/forms/FormTextInput';
|
||||
type CreateBeerPostSchema = z.infer<typeof CreateBeerPostValidationSchema>;
|
||||
|
||||
interface BeerFormProps {
|
||||
breweries: BreweryPostQueryResult[];
|
||||
breweries: z.infer<typeof BreweryPostQueryResult>[];
|
||||
types: BeerType[];
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
<button
|
||||
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||
className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
>
|
||||
|
||||
@@ -29,9 +29,8 @@ const LoginForm = () => {
|
||||
|
||||
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
||||
try {
|
||||
const response = await sendLoginUserRequest(data);
|
||||
|
||||
router.push(`/users/${response.id}`);
|
||||
await sendLoginUserRequest(data);
|
||||
router.push(`/user/current`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setResponseError(error.message);
|
||||
@@ -74,7 +73,7 @@ const LoginForm = () => {
|
||||
|
||||
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
|
||||
<div className="w-full">
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
<button type="submit" className="btn-primary btn w-full">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@ import Navbar from './Navbar';
|
||||
const Layout: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<header className="top-0">
|
||||
<header className="sticky top-0 z-50">
|
||||
<Navbar />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ const Navbar = () => {
|
||||
return (
|
||||
<nav className="navbar bg-primary text-primary-content">
|
||||
<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>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -68,8 +68,8 @@ const Navbar = () => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-none lg:hidden">
|
||||
<div className="dropdown-end dropdown">
|
||||
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
||||
<div className="dropdown dropdown-end">
|
||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
||||
<span className="w-10 rounded-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
|
||||
@@ -10,13 +10,14 @@ const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
|
||||
sm: 'w-[20px]',
|
||||
md: 'w-[100px]',
|
||||
lg: 'w-[150px]',
|
||||
xl: 'w-[200px]',
|
||||
};
|
||||
|
||||
return (
|
||||
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
|
||||
<svg
|
||||
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"
|
||||
fill="none"
|
||||
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="w-8/12">
|
||||
<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" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,21 @@ import { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
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<
|
||||
P extends { [key: string]: any } = { [key: string]: any },
|
||||
Q extends ParsedUrlQuery = ParsedUrlQuery,
|
||||
@@ -11,6 +26,20 @@ export type ExtendedGetServerSideProps<
|
||||
session: Awaited<ReturnType<typeof getLoginSession>>,
|
||||
) => 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 =
|
||||
<P extends { [key: string]: any } = { [key: string]: any }>(
|
||||
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 { 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 { data, isLoading, error } = useSWR(
|
||||
`/api/beers/search?search=${query}`,
|
||||
@@ -13,7 +21,7 @@ const useBeerPostSearch = (query: string | undefined) => {
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const result = beerPostQueryResultArraySchema.parse(json);
|
||||
const result = z.array(beerPostQueryResult).parse(json);
|
||||
|
||||
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 { 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 router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
router.push('/');
|
||||
}, [user, router]);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
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 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 {
|
||||
data: user,
|
||||
@@ -26,7 +35,7 @@ const useUser = () => {
|
||||
}
|
||||
|
||||
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
||||
console.log(parsedPayload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
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": {
|
||||
"@hapi/iron": "^7.0.1",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@prisma/client": "^4.10.1",
|
||||
"@react-email/components": "^0.0.2",
|
||||
"@react-email/render": "0.0.6",
|
||||
"@react-email/tailwind": "0.0.6",
|
||||
"@hookform/resolvers": "^3.0.0",
|
||||
"@prisma/client": "^4.12.0",
|
||||
"@react-email/components": "^0.0.4",
|
||||
"@react-email/render": "^0.0.6",
|
||||
"@react-email/tailwind": "^0.0.7",
|
||||
"argon2": "^0.30.3",
|
||||
"cloudinary": "^1.34.0",
|
||||
"cookie": "0.5.0",
|
||||
"cloudinary": "^1.35.0",
|
||||
"cookie": "^0.5.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^2.0.0-rc.4",
|
||||
"multer-storage-cloudinary": "^4.0.0",
|
||||
"next": "^13.2.1",
|
||||
"next": "^13.2.4",
|
||||
"next-connect": "^1.0.0-next.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.11.0",
|
||||
"pino-pretty": "^9.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-daisyui": "^3.0.3",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "^1.7.15",
|
||||
"react-hook-form": "^7.43.2",
|
||||
"react-icons": "^4.7.1",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-daisyui": "^3.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-email": "^1.9.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-icons": "^4.8.0",
|
||||
"sparkpost": "^2.1.4",
|
||||
"swr": "^2.0.3",
|
||||
"zod": "^3.20.6"
|
||||
"swr": "^2.1.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/ejs": "^3.1.2",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/sparkpost": "^2.1.5",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"daisyui": "^2.51.0",
|
||||
"dotenv-cli": "^7.0.0",
|
||||
"eslint": "^8.34.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^2.51.5",
|
||||
"dotenv-cli": "^7.1.0",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.0.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-next": "^13.2.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-jsdoc": "^0.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.2.3",
|
||||
"prisma": "^4.10.1",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6",
|
||||
"prisma": "^4.12.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"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 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) {
|
||||
const { user, isLoading, error } = useUser();
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user, isLoading, error }}>
|
||||
<Component {...pageProps} />
|
||||
</UserContext.Provider>
|
||||
<>
|
||||
<style jsx global>
|
||||
{`
|
||||
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 APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
||||
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
|
||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import { NextApiResponse } from 'next';
|
||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
|
||||
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||
body: z.infer<typeof BeerCommentValidationSchema>;
|
||||
query: { id: string };
|
||||
}
|
||||
|
||||
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string; page_size: string; page_num: string };
|
||||
}
|
||||
|
||||
const createComment = async (
|
||||
req: CreateCommentRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
@@ -24,12 +31,13 @@ const createComment = async (
|
||||
|
||||
const beerPostId = req.query.id;
|
||||
|
||||
const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({
|
||||
content,
|
||||
rating,
|
||||
beerPostId,
|
||||
userId: req.user!.id,
|
||||
});
|
||||
const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
|
||||
await createNewBeerComment({
|
||||
content,
|
||||
rating,
|
||||
beerPostId,
|
||||
userId: req.user!.id,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
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<
|
||||
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>>
|
||||
>();
|
||||
|
||||
@@ -53,5 +87,16 @@ router.post(
|
||||
createComment,
|
||||
);
|
||||
|
||||
router.get(
|
||||
validateRequest({
|
||||
querySchema: z.object({
|
||||
id: z.string().uuid(),
|
||||
page_size: z.coerce.number().int().positive(),
|
||||
page_num: z.coerce.number().int().positive(),
|
||||
}),
|
||||
}),
|
||||
getAll,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
export default handler;
|
||||
|
||||
@@ -4,13 +4,14 @@ import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
|
||||
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
|
||||
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
|
||||
const sendLikeRequest = async (
|
||||
req: UserExtendedNextApiRequest,
|
||||
@@ -43,6 +44,24 @@ const sendLikeRequest = async (
|
||||
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<
|
||||
UserExtendedNextApiRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
@@ -54,5 +73,11 @@ router.post(
|
||||
sendLikeRequest,
|
||||
);
|
||||
|
||||
router.get(
|
||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
||||
getLikeCount,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
|
||||
const SearchSchema = z.object({
|
||||
search: z.string().min(1),
|
||||
@@ -18,29 +18,30 @@ interface SearchAPIRequest extends NextApiRequest {
|
||||
const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
|
||||
const { search: query } = req.query;
|
||||
|
||||
const beers: BeerPostQueryResult[] = await DBClient.instance.beerPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
createdAt: true,
|
||||
description: true,
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: true } },
|
||||
type: { select: { name: true, id: true } },
|
||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{ name: { contains: query, mode: 'insensitive' } },
|
||||
{ description: { contains: query, mode: 'insensitive' } },
|
||||
const beers: z.infer<typeof beerPostQueryResult>[] =
|
||||
await DBClient.instance.beerPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
createdAt: true,
|
||||
description: true,
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: true } },
|
||||
type: { select: { name: true, id: true } },
|
||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{ name: { contains: query, mode: 'insensitive' } },
|
||||
{ description: { contains: query, mode: 'insensitive' } },
|
||||
|
||||
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
|
||||
{ type: { name: { contains: query, mode: 'insensitive' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
|
||||
{ type: { name: { contains: query, mode: 'insensitive' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json(beers);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
||||
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
|
||||
const router = createRouter<
|
||||
UserExtendedNextApiRequest,
|
||||
@@ -20,14 +21,18 @@ router.post(
|
||||
expressWrapper(async (req, res, next) => {
|
||||
passport.initialize();
|
||||
passport.use(localStrat);
|
||||
passport.authenticate('local', { session: false }, (error, token) => {
|
||||
if (error) {
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
req.user = token;
|
||||
next();
|
||||
})(req, res, next);
|
||||
passport.authenticate(
|
||||
'local',
|
||||
{ session: false },
|
||||
(error: unknown, token: z.infer<typeof GetUserSchema>) => {
|
||||
if (error) {
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
req.user = token;
|
||||
next();
|
||||
},
|
||||
)(req, res, next);
|
||||
}),
|
||||
async (req, res) => {
|
||||
const user = req.user!;
|
||||
|
||||
@@ -5,13 +5,14 @@ import React from 'react';
|
||||
import Layout from '@/components/ui/Layout';
|
||||
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
||||
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 FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
||||
import { BiBeer } from 'react-icons/bi';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface EditPageProps {
|
||||
beerPost: BeerPostQueryResult;
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
}
|
||||
|
||||
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
||||
|
||||
@@ -7,34 +7,23 @@ import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSecti
|
||||
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
||||
import Layout from '@/components/ui/Layout';
|
||||
|
||||
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||
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 getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
|
||||
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
interface BeerPageProps {
|
||||
beerPost: BeerPostQueryResult;
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
beerRecommendations: (BeerPost & {
|
||||
brewery: { id: string; name: string };
|
||||
beerImages: { id: string; alt: string; url: string }[];
|
||||
})[];
|
||||
beerComments: BeerCommentQueryResultArrayT;
|
||||
commentsPageCount: number;
|
||||
likeCount: number;
|
||||
}
|
||||
|
||||
const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
beerPost,
|
||||
beerRecommendations,
|
||||
beerComments,
|
||||
commentsPageCount,
|
||||
likeCount,
|
||||
}) => {
|
||||
const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
@@ -54,13 +43,9 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
|
||||
<div className="my-12 flex w-full items-center justify-center ">
|
||||
<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">
|
||||
<BeerPostCommentsSection
|
||||
beerPost={beerPost}
|
||||
comments={beerComments}
|
||||
commentsPageCount={commentsPageCount}
|
||||
/>
|
||||
<BeerPostCommentsSection beerPost={beerPost} />
|
||||
<div className="md:w-[40%]">
|
||||
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
||||
</div>
|
||||
@@ -74,7 +59,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
||||
const beerPost = await getBeerPostById(context.params!.id! as string);
|
||||
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
|
||||
|
||||
if (!beerPost) {
|
||||
return { notFound: true };
|
||||
@@ -83,23 +67,9 @@ export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (cont
|
||||
const { type, brewery, id } = beerPost;
|
||||
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 = {
|
||||
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
||||
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 };
|
||||
|
||||
@@ -8,9 +8,10 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
|
||||
import { BeerType } from '@prisma/client';
|
||||
import { NextPage } from 'next';
|
||||
import { BiBeer } from 'react-icons/bi';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface CreateBeerPageProps {
|
||||
breweries: BreweryPostQueryResult[];
|
||||
breweries: z.infer<typeof BreweryPostQueryResult>[];
|
||||
types: BeerType[];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import DBClient from '@/prisma/DBClient';
|
||||
import Layout from '@/components/ui/Layout';
|
||||
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
|
||||
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 { z } from 'zod';
|
||||
|
||||
interface BeerPageProps {
|
||||
initialBeerPosts: BeerPostQueryResult[];
|
||||
initialBeerPosts: z.infer<typeof beerPostQueryResult>[];
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
|
||||
<meta name="description" content="Beer posts" />
|
||||
</Head>
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
|
||||
|
||||
@@ -14,8 +14,8 @@ const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const SearchPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const querySearch = router.query.search as string | undefined;
|
||||
const [searchValue, setSearchValue] = useState(querySearch || '');
|
||||
const querySearch = (router.query.search as string) || '';
|
||||
const [searchValue, setSearchValue] = useState(querySearch);
|
||||
const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue);
|
||||
|
||||
const debounceSearch = debounce((value: string) => {
|
||||
@@ -36,7 +36,7 @@ const SearchPage: NextPage = () => {
|
||||
if (!querySearch || searchValue) {
|
||||
return;
|
||||
}
|
||||
setSearchValue(searchValue);
|
||||
setSearchValue(querySearch);
|
||||
}, DEBOUNCE_DELAY)();
|
||||
}, [querySearch, searchValue]);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import Layout from '@/components/ui/Layout';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
|
||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface BreweryPageProps {
|
||||
breweryPost: BeerPostQueryResult;
|
||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||
}
|
||||
|
||||
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
||||
|
||||
@@ -4,24 +4,58 @@ import Link from 'next/link';
|
||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||
import Layout from '@/components/ui/Layout';
|
||||
import { FC } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { z } from 'zod';
|
||||
|
||||
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 }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<h1 className="text-3xl font-bold underline">Brewery Posts</h1>
|
||||
{breweryPosts.map((post) => {
|
||||
return (
|
||||
<div key={post.id}>
|
||||
<h2>
|
||||
<Link href={`/breweries/${post.id}`}>{post.name}</Link>
|
||||
</h2>
|
||||
<div className="flex items-center justify-center bg-base-100">
|
||||
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
||||
<header className="my-10">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-6xl font-bold">Breweries</h1>
|
||||
</div>
|
||||
</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>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,21 +7,29 @@ import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useContext } from 'react';
|
||||
|
||||
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 (
|
||||
<Layout>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<h1 className="text-7xl font-bold text-white">Hello!</h1>
|
||||
<>
|
||||
{isLoading && <Spinner />}
|
||||
{error && <p>Something went wrong.</p>}
|
||||
{user && (
|
||||
<div>
|
||||
<p>{user.username}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-3">
|
||||
{isLoading && <Spinner size="xl" />}
|
||||
{user && (
|
||||
<>
|
||||
<h1 className="text-7xl font-bold">
|
||||
Good {isMorning && 'morning'}
|
||||
{isAfternoon && 'afternoon'}
|
||||
{isEvening && 'evening'}
|
||||
{`, ${user?.firstName}!`}
|
||||
</h1>
|
||||
<h2 className="text-4xl font-bold">Welcome to the Biergarten App!</h2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</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
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "cockroachdb"
|
||||
@@ -6,7 +6,7 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
provider = "cockroachdb"
|
||||
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 APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { z } from 'zod';
|
||||
@@ -14,14 +14,8 @@ const sendCreateBeerCommentRequest = async ({
|
||||
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
|
||||
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
beerPostId,
|
||||
content,
|
||||
rating,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beerPostId, content, rating }),
|
||||
});
|
||||
|
||||
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 APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { z } from 'zod';
|
||||
@@ -24,7 +24,7 @@ const sendCreateBeerPostRequest = async (
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const parsedPayload = beerPostQueryResultSchema.safeParse(payload);
|
||||
const parsedPayload = beerPostQueryResult.safeParse(payload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('Invalid API response payload');
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { BeerCommentQueryResultArrayT } from './schema/BeerCommentQueryResult';
|
||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { z } from 'zod';
|
||||
import BeerCommentQueryResult from './schema/BeerCommentQueryResult';
|
||||
|
||||
const getAllBeerComments = async (
|
||||
{ id }: Pick<BeerPostQueryResult, 'id'>,
|
||||
{ id }: Pick<z.infer<typeof beerPostQueryResult>, 'id'>,
|
||||
{ pageSize, pageNum = 0 }: { pageSize: number; pageNum?: number },
|
||||
) => {
|
||||
const skip = (pageNum - 1) * pageSize;
|
||||
const beerComments: BeerCommentQueryResultArrayT =
|
||||
const beerComments: z.infer<typeof BeerCommentQueryResult>[] =
|
||||
await DBClient.instance.beerComment.findMany({
|
||||
skip,
|
||||
take: pageSize,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const BeerCommentQueryResult = z.object({
|
||||
const BeerCommentQueryResult = z.object({
|
||||
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),
|
||||
createdAt: z.coerce.date(),
|
||||
postedBy: z.object({
|
||||
@@ -10,6 +10,5 @@ export const BeerCommentQueryResult = z.object({
|
||||
username: z.string().min(1).max(50),
|
||||
}),
|
||||
});
|
||||
export const BeerCommentQueryResultArray = z.array(BeerCommentQueryResult);
|
||||
export type BeerCommentQueryResultT = z.infer<typeof BeerCommentQueryResult>;
|
||||
export type BeerCommentQueryResultArrayT = z.infer<typeof BeerCommentQueryResultArray>;
|
||||
|
||||
export default BeerCommentQueryResult;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { z } from 'zod';
|
||||
import { BeerPostQueryResult } from './schema/BeerPostQueryResult';
|
||||
import beerPostQueryResult from './schema/BeerPostQueryResult';
|
||||
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
||||
|
||||
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
||||
@@ -16,29 +16,30 @@ const createNewBeerPost = async ({
|
||||
breweryId,
|
||||
userId,
|
||||
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
||||
const newBeerPost: BeerPostQueryResult = await DBClient.instance.beerPost.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
abv,
|
||||
ibu,
|
||||
type: { connect: { id: typeId } },
|
||||
postedBy: { connect: { id: userId } },
|
||||
brewery: { connect: { id: breweryId } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
abv: true,
|
||||
ibu: true,
|
||||
createdAt: true,
|
||||
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||
brewery: { select: { id: true, name: true } },
|
||||
type: { select: { id: true, name: true } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
},
|
||||
});
|
||||
const newBeerPost: z.infer<typeof beerPostQueryResult> =
|
||||
await DBClient.instance.beerPost.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
abv,
|
||||
ibu,
|
||||
type: { connect: { id: typeId } },
|
||||
postedBy: { connect: { id: userId } },
|
||||
brewery: { connect: { id: breweryId } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
abv: true,
|
||||
ibu: true,
|
||||
createdAt: true,
|
||||
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||
brewery: { select: { id: true, name: true } },
|
||||
type: { select: { id: true, name: true } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
},
|
||||
});
|
||||
return newBeerPost;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
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 getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
||||
const skip = (pageNum - 1) * pageSize;
|
||||
|
||||
const beerPosts: BeerPostQueryResult[] = await prisma.beerPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
type: { select: { name: true, id: true } },
|
||||
brewery: { select: { name: true, id: true } },
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||
const beerPosts: z.infer<typeof beerPostQueryResult>[] = await prisma.beerPost.findMany(
|
||||
{
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
type: { select: { name: true, id: true } },
|
||||
brewery: { select: { name: true, id: 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;
|
||||
};
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
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 getBeerPostById = async (id: string) => {
|
||||
const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
createdAt: true,
|
||||
description: true,
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: true } },
|
||||
type: { select: { name: true, id: true } },
|
||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
const beerPost: z.infer<typeof beerPostQueryResult> | null =
|
||||
await prisma.beerPost.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ibu: true,
|
||||
abv: true,
|
||||
createdAt: true,
|
||||
description: true,
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
brewery: { select: { name: true, id: true } },
|
||||
type: { select: { name: true, id: true } },
|
||||
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return beerPost;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 (
|
||||
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery' | 'id'>,
|
||||
beerPost: Pick<z.infer<typeof beerPostQueryResult>, 'type' | 'brewery' | 'id'>,
|
||||
) => {
|
||||
const beerRecommendations = await DBClient.instance.beerPost.findMany({
|
||||
where: {
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const beerPostQueryResultSchema = z.object({
|
||||
const beerPostQueryResult = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
brewery: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
brewery: z.object({ id: z.string(), name: z.string() }),
|
||||
description: z.string(),
|
||||
beerImages: z.array(
|
||||
z.object({
|
||||
path: z.string(),
|
||||
caption: z.string(),
|
||||
id: z.string(),
|
||||
alt: z.string(),
|
||||
}),
|
||||
z.object({ path: z.string(), caption: z.string(), id: z.string(), alt: z.string() }),
|
||||
),
|
||||
ibu: z.number(),
|
||||
abv: z.number(),
|
||||
type: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
postedBy: z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
}),
|
||||
type: z.object({ id: z.string(), name: z.string() }),
|
||||
postedBy: z.object({ id: z.string(), username: z.string() }),
|
||||
createdAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const beerPostQueryResultArraySchema = z.array(beerPostQueryResultSchema);
|
||||
|
||||
export type BeerPostQueryResult = z.infer<typeof beerPostQueryResultSchema>;
|
||||
|
||||
export type BeerPostQueryResultArray = z.infer<typeof beerPostQueryResultArraySchema>;
|
||||
export default beerPostQueryResult;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
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 getAllBreweryPosts = async () => {
|
||||
const breweryPosts: BreweryPostQueryResult[] = await prisma.breweryPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
location: true,
|
||||
name: true,
|
||||
postedBy: { select: { firstName: true, lastName: true, id: true } },
|
||||
},
|
||||
});
|
||||
const breweryPosts: z.infer<typeof BreweryPostQueryResult>[] =
|
||||
await prisma.breweryPost.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
location: true,
|
||||
name: true,
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return breweryPosts;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
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 getBreweryPostById = async (id: string) => {
|
||||
const breweryPost: BreweryPostQueryResult | null = await prisma.breweryPost.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
location: true,
|
||||
name: true,
|
||||
postedBy: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true,
|
||||
},
|
||||
const breweryPost: z.infer<typeof BreweryPostQueryResult> | null =
|
||||
await prisma.breweryPost.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
location: true,
|
||||
name: true,
|
||||
breweryImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||
postedBy: { select: { username: true, id: true } },
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return breweryPost;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
export default interface BreweryPostQueryResult {
|
||||
id: string;
|
||||
location: string;
|
||||
name: string;
|
||||
postedBy: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
}
|
||||
import { z } from 'zod';
|
||||
|
||||
const BreweryPostQueryResult = z.object({
|
||||
id: z.string(),
|
||||
location: z.string(),
|
||||
name: z.string(),
|
||||
postedBy: z.object({ id: z.string(), username: z.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 components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: 'Exo 2', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,26 +9,29 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('daisyui')],
|
||||
plugins: [
|
||||
require('daisyui'),
|
||||
require('tailwindcss-animate')
|
||||
],
|
||||
|
||||
daisyui: {
|
||||
logs: false,
|
||||
themes: [
|
||||
{
|
||||
default: {
|
||||
primary: 'hsl(227, 46%, 25%)',
|
||||
secondary: 'hsl(47, 100%, 80%)',
|
||||
primary: 'hsl(227, 23%, 20%)',
|
||||
secondary: '#ABA9C3',
|
||||
error: '#c17c74',
|
||||
accent: '#fe3bd9',
|
||||
neutral: '#131520',
|
||||
info: '#0A7CFF',
|
||||
success: '#8ACE2B',
|
||||
warning: '#F9D002',
|
||||
error: '#CF1259',
|
||||
'primary-content': '#FAF9F6',
|
||||
'error-content': '#FAF9F6',
|
||||
'base-100': 'hsl(190, 4%, 11%)',
|
||||
'base-200': 'hsl(190, 4%, 9%)',
|
||||
'base-300': 'hsl(190, 4%, 8%)',
|
||||
'base-100': 'hsl(190, 4%, 9%)',
|
||||
'base-200': 'hsl(190, 4%, 8%)',
|
||||
'base-300': 'hsl(190, 4%, 5%)',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user