Merge pull request #14 from aaronpo97/refactoring

Refactoring
This commit is contained in:
Aaron Po
2023-04-05 22:38:02 -04:00
committed by GitHub
73 changed files with 2840 additions and 2703 deletions

View File

@@ -18,8 +18,3 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
npm ci npm ci
- name: Prettier Action
# You may pin to the exact commit or the version.
# uses: creyD/prettier_action@6602189cf8bac1ce73ffe601925f6127ab7f21ac
uses: creyD/prettier_action@v4.2

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ next-env.d.ts
# uploaded images # uploaded images
public/uploads public/uploads
# vscode
.vscode

View File

@@ -1,13 +1,16 @@
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest'; import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema'; import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
import { FunctionComponent, useState, useEffect } from 'react'; import { FunctionComponent, useState, useEffect } from 'react';
import { Rating } from 'react-daisyui'; import { Rating } from 'react-daisyui';
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { KeyedMutator } from 'swr';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { useRouter } from 'next/router';
import Button from '../ui/forms/Button'; import Button from '../ui/forms/Button';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
@@ -16,10 +19,17 @@ import FormSegment from '../ui/forms/FormSegment';
import FormTextArea from '../ui/forms/FormTextArea'; import FormTextArea from '../ui/forms/FormTextArea';
interface BeerCommentFormProps { interface BeerCommentFormProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
mutate: KeyedMutator<{
comments: z.infer<typeof BeerCommentQueryResult>[];
pageCount: number;
}>;
} }
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => { const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
beerPost,
mutate,
}) => {
const { register, handleSubmit, formState, reset, setValue } = useForm< const { register, handleSubmit, formState, reset, setValue } = useForm<
z.infer<typeof BeerCommentValidationSchema> z.infer<typeof BeerCommentValidationSchema>
>({ >({
@@ -47,44 +57,58 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
beerPostId: beerPost.id, beerPostId: beerPost.id,
}); });
reset(); reset();
router.replace(`/beers/${beerPost.id}?comments_page=1`, undefined, { scroll: false });
const submitTasks: Promise<unknown>[] = [
router.push(`/beers/${beerPost.id}`, undefined, { scroll: false }),
mutate(),
];
await Promise.all(submitTasks);
}; };
const { errors } = formState; const { errors } = formState;
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<FormInfo> <div>
<FormLabel htmlFor="content">Leave a comment</FormLabel> <FormInfo>
<FormError>{errors.content?.message}</FormError> <FormLabel htmlFor="content">Leave a comment</FormLabel>
</FormInfo> <FormError>{errors.content?.message}</FormError>
<FormSegment> </FormInfo>
<FormTextArea <FormSegment>
id="content" <FormTextArea
formValidationSchema={register('content')} id="content"
placeholder="Comment" formValidationSchema={register('content')}
rows={5} placeholder="Comment"
error={!!errors.content?.message} rows={5}
/> error={!!errors.content?.message}
</FormSegment> disabled={formState.isSubmitting}
<FormInfo> />
<FormLabel htmlFor="rating">Rating</FormLabel> </FormSegment>
<FormError>{errors.rating?.message}</FormError> <FormInfo>
</FormInfo> <FormLabel htmlFor="rating">Rating</FormLabel>
<Rating <FormError>{errors.rating?.message}</FormError>
value={rating} </FormInfo>
onChange={(value) => { <Rating
setRating(value); value={rating}
setValue('rating', value); onChange={(value) => {
}} setRating(value);
> setValue('rating', value);
<Rating.Item name="rating-1" className="mask mask-star" /> }}
<Rating.Item name="rating-1" className="mask mask-star" /> >
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
</Rating> <Rating.Item name="rating-1" className="mask mask-star" />
<Button type="submit">Submit</Button> <Rating.Item name="rating-1" className="mask mask-star" />
</Rating>
</div>
<div>
<Button type="submit" isSubmitting={formState.isSubmitting}>
Submit
</Button>
</div>
</form> </form>
); );
}; };

View File

@@ -1,42 +1,26 @@
import Link from 'next/link'; import Link from 'next/link';
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { FC, useContext, useEffect, useState } from 'react'; import { FC, useContext } from 'react';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { FaRegEdit } from 'react-icons/fa'; import { FaRegEdit } from 'react-icons/fa';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
import useGetLikeCount from '@/hooks/useGetLikeCount';
import useTimeDistance from '@/hooks/useTimeDistance';
import BeerPostLikeButton from './BeerPostLikeButton'; import BeerPostLikeButton from './BeerPostLikeButton';
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({ const BeerInfoHeader: FC<{
beerPost, beerPost: z.infer<typeof beerPostQueryResult>;
initialLikeCount, }> = ({ beerPost }) => {
}) => { const createdAt = new Date(beerPost.createdAt);
const createdAtDate = new Date(beerPost.createdAt); const timeDistance = useTimeDistance(createdAt);
const [timeDistance, setTimeDistance] = useState('');
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const idMatches = user && beerPost.postedBy.id === user.id;
const isPostOwner = !!(user && idMatches);
const [likeCount, setLikeCount] = useState(initialLikeCount); const { likeCount, mutate } = useGetLikeCount(beerPost.id);
const [isPostOwner, setIsPostOwner] = useState(false);
useEffect(() => {
const idMatches = user && beerPost.postedBy.id === user.id;
if (!(user && idMatches)) {
setIsPostOwner(false);
return;
}
setIsPostOwner(true);
}, [user, beerPost]);
useEffect(() => {
setLikeCount(initialLikeCount);
}, [initialLikeCount]);
useEffect(() => {
setTimeDistance(formatDistanceStrict(new Date(beerPost.createdAt), new Date()));
}, [beerPost.createdAt]);
return ( return (
<main className="card flex flex-col justify-center bg-base-300"> <main className="card flex flex-col justify-center bg-base-300">
@@ -67,16 +51,18 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
</div> </div>
<h3 className="italic"> <h3 className="italic">
posted by{' '} {' posted by '}
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link"> <Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
{beerPost.postedBy.username} {`${beerPost.postedBy.username} `}
</Link> </Link>
<span {timeDistance && (
className="tooltip tooltip-bottom" <span
data-tip={format(createdAtDate, 'MM/dd/yyyy')} className="tooltip tooltip-right"
> data-tip={format(createdAt, 'MM/dd/yyyy')}
{` ${timeDistance}`} ago >
</span> {`${timeDistance} ago`}
</span>
)}
</h3> </h3>
<p>{beerPost.description}</p> <p>{beerPost.description}</p>
@@ -95,15 +81,15 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
<span className="text-lg font-medium">{beerPost.ibu} IBU</span> <span className="text-lg font-medium">{beerPost.ibu} IBU</span>
</div> </div>
<div> <div>
<span> {(!!likeCount || likeCount === 0) && (
Liked by {likeCount} user{likeCount !== 1 && 's'} <span>
</span> Liked by {likeCount} user{likeCount !== 1 && 's'}
</span>
)}
</div> </div>
</div> </div>
<div className="card-actions items-end"> <div className="card-actions items-end">
{user && ( {user && <BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />}
<BeerPostLikeButton beerPostId={beerPost.id} setLikeCount={setLikeCount} />
)}
</div> </div>
</div> </div>
</article> </article>

View File

@@ -1,11 +1,14 @@
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { FC } from 'react'; import { FC } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
interface BeerCommentsPaginationBarProps { interface BeerCommentsPaginationBarProps {
commentsPageNum: number; commentsPageNum: number;
commentsPageCount: number; commentsPageCount: number;
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
} }
const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
@@ -14,9 +17,9 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
beerPost, beerPost,
}) => ( }) => (
<div className="flex items-center justify-center" id="comments-pagination"> <div className="flex items-center justify-center" id="comments-pagination">
<div className="btn-group grid w-6/12 grid-cols-2"> <div className="btn-group">
<Link <Link
className={`btn-outline btn ${ className={`btn btn-ghost ${
commentsPageNum === 1 commentsPageNum === 1
? 'btn-disabled pointer-events-none' ? 'btn-disabled pointer-events-none'
: 'pointer-events-auto' : 'pointer-events-auto'
@@ -27,10 +30,11 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
}} }}
scroll={false} scroll={false}
> >
Next Comments <FaArrowLeft />
</Link> </Link>
<button className="btn btn-ghost pointer-events-none">{commentsPageNum}</button>
<Link <Link
className={`btn-outline btn ${ className={`btn btn-ghost ${
commentsPageNum === commentsPageCount commentsPageNum === commentsPageCount
? 'btn-disabled pointer-events-none' ? 'btn-disabled pointer-events-none'
: 'pointer-events-auto' : 'pointer-events-auto'
@@ -41,7 +45,7 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
}} }}
scroll={false} scroll={false}
> >
Previous Comments <FaArrowRight />
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -1,35 +1,41 @@
/* eslint-disable no-nested-ternary */
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { useRouter } from 'next/router';
import { FC, useContext } from 'react'; import { FC, useContext } from 'react';
import { z } from 'zod';
import useBeerPostComments from '@/hooks/useBeerPostComments';
import { useRouter } from 'next/router';
import BeerCommentForm from './BeerCommentForm'; import BeerCommentForm from './BeerCommentForm';
import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar'; import BeerCommentsPaginationBar from './BeerPostCommentsPaginationBar';
import CommentCard from './CommentCard'; import CommentCardBody from './CommentCardBody';
import NoCommentsCard from './NoCommentsCard';
import CommentLoadingCardBody from './CommentLoadingCardBody';
interface BeerPostCommentsSectionProps { interface BeerPostCommentsSectionProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
comments: BeerCommentQueryResultArrayT;
commentsPageCount: number;
} }
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost }) => {
beerPost,
comments,
commentsPageCount,
}) => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const router = useRouter(); const router = useRouter();
const { id } = beerPost;
const pageNum = parseInt(router.query.comments_page as string, 10) || 1;
const pageSize = 5;
const commentsPageNum = parseInt(router.query.comments_page as string, 10) || 1; const { comments, commentsPageCount, isLoading, mutate } = useBeerPostComments({
id,
pageNum,
pageSize,
});
return ( return (
<div className="w-full space-y-3 md:w-[60%]"> <div className="w-full space-y-3 md:w-[60%]">
<div className="card h-96 bg-base-300"> <div className="card h-96 bg-base-300">
<div className="card-body h-full"> <div className="card-body h-full">
{user ? ( {user ? (
<BeerCommentForm beerPost={beerPost} /> <BeerCommentForm beerPost={beerPost} mutate={mutate} />
) : ( ) : (
<div className="flex h-full flex-col items-center justify-center"> <div className="flex h-full flex-col items-center justify-center">
<span className="text-lg font-bold">Log in to leave a comment.</span> <span className="text-lg font-bold">Log in to leave a comment.</span>
@@ -37,23 +43,34 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
)} )}
</div> </div>
</div> </div>
{comments.length ? (
{comments && !!comments.length && !!commentsPageCount && !isLoading && (
<div className="card bg-base-300 pb-6"> <div className="card bg-base-300 pb-6">
{comments.map((comment) => ( {comments.map((comment) => (
<CommentCard key={comment.id} comment={comment} beerPostId={beerPost.id} /> <CommentCardBody key={comment.id} comment={comment} mutate={mutate} />
))} ))}
<BeerCommentsPaginationBar <BeerCommentsPaginationBar
commentsPageNum={commentsPageNum} commentsPageNum={pageNum}
commentsPageCount={commentsPageCount} commentsPageCount={commentsPageCount}
beerPost={beerPost} beerPost={beerPost}
/> />
</div> </div>
) : ( )}
<div className="card items-center bg-base-300">
<div className="card-body"> {!comments?.length && !isLoading && <NoCommentsCard />}
<span className="text-lg font-bold">No comments yet.</span>
</div> {isLoading && (
<div className="card bg-base-300 pb-6">
{Array.from({ length: pageSize }).map((_, i) => (
<CommentLoadingCardBody key={i} />
))}
<BeerCommentsPaginationBar
commentsPageNum={pageNum}
commentsPageCount={20}
beerPost={beerPost}
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,41 +1,28 @@
import UserContext from '@/contexts/userContext'; import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
import sendLikeRequest from '@/requests/sendLikeRequest'; import sendLikeRequest from '@/requests/sendLikeRequest';
import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest'; import { KeyedMutator } from 'swr';
const BeerPostLikeButton: FC<{ const BeerPostLikeButton: FC<{
beerPostId: string; beerPostId: string;
setLikeCount: Dispatch<SetStateAction<number>>; mutateCount: KeyedMutator<number>;
}> = ({ beerPostId, setLikeCount }) => { }> = ({ beerPostId, mutateCount }) => {
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isLiked, setIsLiked] = useState(false);
const { user } = useContext(UserContext);
useEffect(() => { useEffect(() => {
if (!user) { setLoading(false);
setLoading(false); }, [isLiked]);
return;
}
sendCheckIfUserLikesBeerPostRequest(beerPostId)
.then((currentLikeStatus) => {
setIsLiked(currentLikeStatus);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, [user, beerPostId]);
const handleLike = async () => { const handleLike = async () => {
try { try {
setLoading(true); setLoading(true);
await sendLikeRequest(beerPostId); await sendLikeRequest(beerPostId);
setIsLiked(!isLiked); await mutateCount();
setLikeCount((prevCount) => prevCount + (isLiked ? -1 : 1)); await mutateLikeStatus();
setLoading(false); setLoading(false);
} catch (error) { } catch (e) {
setLoading(false); setLoading(false);
} }
}; };

View File

@@ -10,7 +10,7 @@ const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
}) => { }) => {
return ( return (
<div className="card sticky top-2 h-full overflow-y-scroll bg-base-300"> <div className="card sticky top-2 h-full overflow-y-scroll bg-base-300">
<div className="card-body"> <div className="card-body space-y-3">
{beerRecommendations.map((beerPost) => ( {beerRecommendations.map((beerPost) => (
<div key={beerPost.id} className="w-full"> <div key={beerPost.id} className="w-full">
<div> <div>

View File

@@ -1,18 +1,22 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import useTimeDistance from '@/hooks/useTimeDistance';
import { format, formatDistanceStrict } from 'date-fns'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import format from 'date-fns/format';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useContext } from 'react';
import { useContext, useEffect, useState } from 'react';
import { Rating } from 'react-daisyui'; import { Rating } from 'react-daisyui';
import { FaEllipsisH } from 'react-icons/fa'; import { FaEllipsisH } from 'react-icons/fa';
import { KeyedMutator } from 'swr';
import { z } from 'zod';
const CommentCardDropdown: React.FC<{ const CommentCardDropdown: React.FC<{
comment: BeerCommentQueryResultT; comment: z.infer<typeof BeerCommentQueryResult>;
beerPostId: string; mutate: KeyedMutator<{
}> = ({ comment, beerPostId }) => { comments: z.infer<typeof BeerCommentQueryResult>[];
const router = useRouter(); pageCount: number;
}>;
}> = ({ comment, mutate }) => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const isCommentOwner = user?.id === comment.postedBy.id; const isCommentOwner = user?.id === comment.postedBy.id;
@@ -21,16 +25,17 @@ const CommentCardDropdown: React.FC<{
const response = await fetch(`/api/beer-comments/${comment.id}`, { const response = await fetch(`/api/beer-comments/${comment.id}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to delete comment'); throw new Error('Failed to delete comment');
} }
router.replace(`/beers/${beerPostId}?comments_page=1`, undefined, { scroll: false }); await mutate();
}; };
return ( return (
<div className="dropdown"> <div className="dropdown">
<label tabIndex={0} className="btn btn-ghost btn-sm m-1"> <label tabIndex={0} className="btn-ghost btn-sm btn m-1">
<FaEllipsisH /> <FaEllipsisH />
</label> </label>
<ul <ul
@@ -51,19 +56,20 @@ const CommentCardDropdown: React.FC<{
); );
}; };
const CommentCard: React.FC<{ const CommentCardBody: React.FC<{
comment: BeerCommentQueryResultT; comment: z.infer<typeof BeerCommentQueryResult>;
beerPostId: string;
}> = ({ comment, beerPostId }) => { mutate: KeyedMutator<{
const [timeDistance, setTimeDistance] = useState(''); comments: z.infer<typeof BeerCommentQueryResult>[];
pageCount: number;
}>;
}> = ({ comment, mutate }) => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
useEffect(() => { const timeDistance = useTimeDistance(new Date(comment.createdAt));
setTimeDistance(formatDistanceStrict(new Date(comment.createdAt), new Date()));
}, [comment.createdAt]);
return ( return (
<div className="card-body"> <div className="card-body animate-in fade-in-10">
<div className="flex flex-col justify-between sm:flex-row"> <div className="flex flex-col justify-between sm:flex-row">
<div> <div>
<h3 className="font-semibold sm:text-2xl"> <h3 className="font-semibold sm:text-2xl">
@@ -83,7 +89,7 @@ const CommentCard: React.FC<{
</h4> </h4>
</div> </div>
{user && <CommentCardDropdown comment={comment} beerPostId={beerPostId} />} {user && <CommentCardDropdown comment={comment} mutate={mutate} />}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -104,4 +110,4 @@ const CommentCard: React.FC<{
); );
}; };
export default CommentCard; export default CommentCardBody;

View 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;

View 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;

View File

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

View File

@@ -1,5 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { FC } from 'react'; import { FC } from 'react';
interface PaginationProps { interface PaginationProps {
@@ -14,18 +14,16 @@ const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) =>
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`} className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }} href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
scroll={false} scroll={false}
prefetch={true}
> >
« <FaArrowLeft />
</Link> </Link>
<button className="btn">Page {pageNum}</button> <button className="btn">Page {pageNum}</button>
<Link <Link
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`} className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }} href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
scroll={false} scroll={false}
prefetch={true}
> >
» <FaArrowRight />
</Link> </Link>
</div> </div>
); );

View File

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

View File

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

View File

@@ -29,9 +29,8 @@ const LoginForm = () => {
const onSubmit: SubmitHandler<LoginT> = async (data) => { const onSubmit: SubmitHandler<LoginT> = async (data) => {
try { try {
const response = await sendLoginUserRequest(data); await sendLoginUserRequest(data);
router.push(`/user/current`);
router.push(`/users/${response.id}`);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
setResponseError(error.message); setResponseError(error.message);
@@ -74,7 +73,7 @@ const LoginForm = () => {
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />} {responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
<div className="w-full"> <div className="w-full">
<button type="submit" className="btn btn-primary w-full"> <button type="submit" className="btn-primary btn w-full">
Login Login
</button> </button>
</div> </div>

View File

@@ -4,10 +4,10 @@ import Navbar from './Navbar';
const Layout: FC<{ children: ReactNode }> = ({ children }) => { const Layout: FC<{ children: ReactNode }> = ({ children }) => {
return ( return (
<div className="flex h-screen flex-col"> <div className="flex h-screen flex-col">
<header className="top-0"> <header className="sticky top-0 z-50">
<Navbar /> <Navbar />
</header> </header>
<div className="animate-in fade-in top-0 h-full flex-1">{children}</div> <div className="relative top-0 h-full flex-1">{children}</div>
</div> </div>
); );
}; };

View File

@@ -44,7 +44,7 @@ const Navbar = () => {
return ( return (
<nav className="navbar bg-primary text-primary-content"> <nav className="navbar bg-primary text-primary-content">
<div className="flex-1"> <div className="flex-1">
<Link className="btn-ghost btn text-3xl normal-case" href="/"> <Link className="btn btn-ghost text-3xl normal-case" href="/">
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span> <span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
</Link> </Link>
</div> </div>
@@ -68,8 +68,8 @@ const Navbar = () => {
</ul> </ul>
</div> </div>
<div className="flex-none lg:hidden"> <div className="flex-none lg:hidden">
<div className="dropdown-end dropdown"> <div className="dropdown dropdown-end">
<label tabIndex={0} className="btn-ghost btn-circle btn"> <label tabIndex={0} className="btn btn-ghost btn-circle">
<span className="w-10 rounded-full"> <span className="w-10 rounded-full">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,7 @@
import { FC } from 'react'; import { FC } from 'react';
interface SpinnerProps { interface SpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
} }
const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => { const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
@@ -10,13 +10,14 @@ const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
sm: 'w-[20px]', sm: 'w-[20px]',
md: 'w-[100px]', md: 'w-[100px]',
lg: 'w-[150px]', lg: 'w-[150px]',
xl: 'w-[200px]',
}; };
return ( return (
<div role="status" className="flex flex-col items-center justify-center rounded-3xl"> <div role="status" className="flex flex-col items-center justify-center rounded-3xl">
<svg <svg
aria-hidden="true" aria-hidden="true"
className={`${spinnerWidths[size]} animate-spin fill-success text-gray-500`} className={`${spinnerWidths[size]} animate-spin fill-secondary text-primary`}
viewBox="0 0 100 101" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

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

View File

@@ -2,6 +2,21 @@ import { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from
import { ParsedUrlQuery } from 'querystring'; import { ParsedUrlQuery } from 'querystring';
import { getLoginSession } from '../config/auth/session'; import { getLoginSession } from '../config/auth/session';
/**
* Represents a type definition for a function that handles server-side rendering with
* extended capabilities.
*
* @template P - A generic type that represents an object with string keys and any values.
* It defaults to an empty object.
* @template Q - A generic type that represents a parsed URL query object. It defaults to
* the ParsedUrlQuery type.
* @template D - A generic type that represents preview data. It defaults to the
* PreviewData type.
* @param context - The context object containing information about the incoming HTTP
* request.
* @param session - An awaited value of the return type of the getLoginSession function.
* @returns - A promise that resolves to the result of the server-side rendering process.
*/
export type ExtendedGetServerSideProps< export type ExtendedGetServerSideProps<
P extends { [key: string]: any } = { [key: string]: any }, P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery, Q extends ParsedUrlQuery = ParsedUrlQuery,
@@ -11,6 +26,20 @@ export type ExtendedGetServerSideProps<
session: Awaited<ReturnType<typeof getLoginSession>>, session: Awaited<ReturnType<typeof getLoginSession>>,
) => Promise<GetServerSidePropsResult<P>>; ) => Promise<GetServerSidePropsResult<P>>;
/**
* A Higher Order Function that adds authentication requirement to a Next.js server-side
* page component.
*
* @param fn An async function that receives the GetServerSidePropsContext and
* authenticated session as arguments and returns a GetServerSidePropsResult with props
* for the wrapped component.
* @returns A promise that resolves to a GetServerSidePropsResult object with props for
* the wrapped component. If authentication is successful, the GetServerSidePropsResult
* will include props generated by the wrapped component's getServerSideProps method. If
* authentication fails, the GetServerSidePropsResult will include a redirect to the
* login page.
*/
const withPageAuthRequired = const withPageAuthRequired =
<P extends { [key: string]: any } = { [key: string]: any }>( <P extends { [key: string]: any } = { [key: string]: any }>(
fn?: ExtendedGetServerSideProps<P>, fn?: ExtendedGetServerSideProps<P>,

View 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;

View File

@@ -1,6 +1,14 @@
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import useSWR from 'swr'; import useSWR from 'swr';
import { beerPostQueryResultArraySchema } from '@/services/BeerPost/schema/BeerPostQueryResult'; import { z } from 'zod';
/**
* A custom React hook that searches for beer posts that match a given query string.
*
* @param query The search query string to match beer posts against.
* @returns An object containing an array of search results matching the query, an error
* object if an error occurred during the search, and a boolean indicating if the
* request is currently loading.
*/
const useBeerPostSearch = (query: string | undefined) => { const useBeerPostSearch = (query: string | undefined) => {
const { data, isLoading, error } = useSWR( const { data, isLoading, error } = useSWR(
`/api/beers/search?search=${query}`, `/api/beers/search?search=${query}`,
@@ -13,7 +21,7 @@ const useBeerPostSearch = (query: string | undefined) => {
} }
const json = await response.json(); const json = await response.json();
const result = beerPostQueryResultArraySchema.parse(json); const result = z.array(beerPostQueryResult).parse(json);
return result; return result;
}, },

View 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
View 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;

View File

@@ -1,17 +1,22 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useContext, useEffect } from 'react'; import { useContext } from 'react';
const useRedirectWhenLoggedIn = () => { /**
* Custom React hook that redirects the user to the home page if they are logged in. This
* hook is used to prevent logged in users from accessing the login and signup pages. Must
* be used under the UserContext provider.
*
* @returns {void}
*/
const useRedirectWhenLoggedIn = (): void => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const router = useRouter(); const router = useRouter();
useEffect(() => { if (!user) {
if (!user) { return;
return; }
} router.push('/');
router.push('/');
}, [user, router]);
}; };
export default useRedirectWhenLoggedIn; export default useRedirectWhenLoggedIn;

20
hooks/useTimeDistance.ts Normal file
View 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;

View File

@@ -2,6 +2,15 @@ import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import useSWR from 'swr'; import useSWR from 'swr';
/**
* A custom React hook that fetches the current user's data from the server.
*
* @returns An object containing the current user's data, a boolean indicating if the
* request is currently loading, and an error object if an error occurred during the
* request.
* @throws When the user is not logged in, the server returns an error status code, or if
* the response data fails to validate against the expected schema.
*/
const useUser = () => { const useUser = () => {
const { const {
data: user, data: user,
@@ -26,7 +35,7 @@ const useUser = () => {
} }
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload); const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
console.log(parsedPayload);
if (!parsedPayload.success) { if (!parsedPayload.success) {
throw new Error(parsedPayload.error.message); throw new Error(parsedPayload.error.message);
} }

3581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,63 +13,64 @@
}, },
"dependencies": { "dependencies": {
"@hapi/iron": "^7.0.1", "@hapi/iron": "^7.0.1",
"@hookform/resolvers": "^2.9.10", "@hookform/resolvers": "^3.0.0",
"@prisma/client": "^4.10.1", "@prisma/client": "^4.12.0",
"@react-email/components": "^0.0.2", "@react-email/components": "^0.0.4",
"@react-email/render": "0.0.6", "@react-email/render": "^0.0.6",
"@react-email/tailwind": "0.0.6", "@react-email/tailwind": "^0.0.7",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"cloudinary": "^1.34.0", "cloudinary": "^1.35.0",
"cookie": "0.5.0", "cookie": "^0.5.0",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^2.0.0-rc.4", "multer": "^2.0.0-rc.4",
"multer-storage-cloudinary": "^4.0.0", "multer-storage-cloudinary": "^4.0.0",
"next": "^13.2.1", "next": "^13.2.4",
"next-connect": "^1.0.0-next.3", "next-connect": "^1.0.0-next.3",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pino": "^8.11.0", "pino": "^8.11.0",
"pino-pretty": "^9.3.0", "pino-pretty": "^10.0.0",
"react": "18.2.0", "react": "^18.2.0",
"react-daisyui": "^3.0.3", "react-daisyui": "^3.1.1",
"react-dom": "18.2.0", "react-dom": "^18.2.0",
"react-email": "^1.7.15", "react-email": "^1.9.0",
"react-hook-form": "^7.43.2", "react-hook-form": "^7.43.9",
"react-icons": "^4.7.1", "react-icons": "^4.8.0",
"sparkpost": "^2.1.4", "sparkpost": "^2.1.4",
"swr": "^2.0.3", "swr": "^2.1.2",
"zod": "^3.20.6" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^7.6.0",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.192",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.14.1", "@types/node": "^18.15.11",
"@types/passport-local": "^1.0.35", "@types/passport-local": "^1.0.35",
"@types/react": "^18.0.28", "@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/sparkpost": "^2.1.5", "@types/sparkpost": "^2.1.5",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"daisyui": "^2.51.0", "daisyui": "^2.51.5",
"dotenv-cli": "^7.0.0", "dotenv-cli": "^7.1.0",
"eslint": "^8.34.0", "eslint": "^8.37.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0", "eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-next": "^13.2.1", "eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"prettier": "^2.8.3", "prettier": "^2.8.7",
"prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-jsdoc": "^0.4.2",
"prettier-plugin-tailwindcss": "^0.2.3", "prettier-plugin-tailwindcss": "^0.2.6",
"prisma": "^4.10.1", "prisma": "^4.12.0",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.5" "typescript": "^5.0.3"
} }
} }

View File

@@ -3,12 +3,28 @@ import useUser from '@/hooks/useUser';
import '@/styles/globals.css'; import '@/styles/globals.css';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import { Roboto } from 'next/font/google';
const roboto = Roboto({
weight: ['100', '300', '400', '500', '700', '900'],
subsets: ['latin'],
});
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const { user, isLoading, error } = useUser(); const { user, isLoading, error } = useUser();
return ( return (
<UserContext.Provider value={{ user, isLoading, error }}> <>
<Component {...pageProps} /> <style jsx global>
</UserContext.Provider> {`
html {
font-family: ${roboto.style.fontFamily};
}
`}
</style>
<UserContext.Provider value={{ user, isLoading, error }}>
<Component {...pageProps} />
</UserContext.Provider>
</>
); );
} }

View File

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

View File

@@ -4,13 +4,14 @@ import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import { UserExtendedNextApiRequest } from '@/config/auth/types'; import { UserExtendedNextApiRequest } from '@/config/auth/types';
import { createRouter } from 'next-connect'; import { createRouter } from 'next-connect';
import { z } from 'zod'; import { z } from 'zod';
import { NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import ServerError from '@/config/util/ServerError'; import ServerError from '@/config/util/ServerError';
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike'; import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById'; import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById'; import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import DBClient from '@/prisma/DBClient';
const sendLikeRequest = async ( const sendLikeRequest = async (
req: UserExtendedNextApiRequest, req: UserExtendedNextApiRequest,
@@ -43,6 +44,24 @@ const sendLikeRequest = async (
res.status(200).json(jsonResponse); res.status(200).json(jsonResponse);
}; };
const getLikeCount = async (
req: NextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id as string;
const likes = await DBClient.instance.beerPostLike.count({
where: { beerPostId: id },
});
res.status(200).json({
success: true,
message: 'Successfully retrieved like count.',
statusCode: 200,
payload: { likeCount: likes },
});
};
const router = createRouter< const router = createRouter<
UserExtendedNextApiRequest, UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>> NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
@@ -54,5 +73,11 @@ router.post(
sendLikeRequest, sendLikeRequest,
); );
router.get(
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
getLikeCount,
);
const handler = router.handler(NextConnectOptions); const handler = router.handler(NextConnectOptions);
export default handler; export default handler;

View File

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

View File

@@ -9,6 +9,7 @@ import { z } from 'zod';
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema'; import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types'; import { UserExtendedNextApiRequest } from '@/config/auth/types';
import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
const router = createRouter< const router = createRouter<
UserExtendedNextApiRequest, UserExtendedNextApiRequest,
@@ -20,14 +21,18 @@ router.post(
expressWrapper(async (req, res, next) => { expressWrapper(async (req, res, next) => {
passport.initialize(); passport.initialize();
passport.use(localStrat); passport.use(localStrat);
passport.authenticate('local', { session: false }, (error, token) => { passport.authenticate(
if (error) { 'local',
next(error); { session: false },
return; (error: unknown, token: z.infer<typeof GetUserSchema>) => {
} if (error) {
req.user = token; next(error);
next(); return;
})(req, res, next); }
req.user = token;
next();
},
)(req, res, next);
}), }),
async (req, res) => { async (req, res) => {
const user = req.user!; const user = req.user!;

View File

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

View File

@@ -7,34 +7,23 @@ import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSecti
import BeerRecommendations from '@/components/BeerById/BeerRecommendations'; import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
import getBeerPostById from '@/services/BeerPost/getBeerPostById'; import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations'; import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPost } from '@prisma/client'; import { BeerPost } from '@prisma/client';
import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount'; import { z } from 'zod';
interface BeerPageProps { interface BeerPageProps {
beerPost: BeerPostQueryResult; beerPost: z.infer<typeof beerPostQueryResult>;
beerRecommendations: (BeerPost & { beerRecommendations: (BeerPost & {
brewery: { id: string; name: string }; brewery: { id: string; name: string };
beerImages: { id: string; alt: string; url: string }[]; beerImages: { id: string; alt: string; url: string }[];
})[]; })[];
beerComments: BeerCommentQueryResultArrayT;
commentsPageCount: number;
likeCount: number;
} }
const BeerByIdPage: NextPage<BeerPageProps> = ({ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }) => {
beerPost,
beerRecommendations,
beerComments,
commentsPageCount,
likeCount,
}) => {
return ( return (
<Layout> <Layout>
<Head> <Head>
@@ -54,13 +43,9 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
<div className="my-12 flex w-full items-center justify-center "> <div className="my-12 flex w-full items-center justify-center ">
<div className="w-11/12 space-y-3 xl:w-9/12"> <div className="w-11/12 space-y-3 xl:w-9/12">
<BeerInfoHeader beerPost={beerPost} initialLikeCount={likeCount} /> <BeerInfoHeader beerPost={beerPost} />
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3"> <div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
<BeerPostCommentsSection <BeerPostCommentsSection beerPost={beerPost} />
beerPost={beerPost}
comments={beerComments}
commentsPageCount={commentsPageCount}
/>
<div className="md:w-[40%]"> <div className="md:w-[40%]">
<BeerRecommendations beerRecommendations={beerRecommendations} /> <BeerRecommendations beerRecommendations={beerRecommendations} />
</div> </div>
@@ -74,7 +59,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => { export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const beerPost = await getBeerPostById(context.params!.id! as string); const beerPost = await getBeerPostById(context.params!.id! as string);
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
if (!beerPost) { if (!beerPost) {
return { notFound: true }; return { notFound: true };
@@ -83,23 +67,9 @@ export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (cont
const { type, brewery, id } = beerPost; const { type, brewery, id } = beerPost;
const beerRecommendations = await getBeerRecommendations({ type, brewery, id }); const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
const pageSize = 5;
const beerComments = await getAllBeerComments(
{ id: beerPost.id },
{ pageSize, pageNum: beerCommentPageNum },
);
const commentCount = await getBeerCommentCount(beerPost.id);
const commentPageCount = commentCount ? Math.ceil(commentCount / pageSize) : 0;
const likeCount = await getBeerPostLikeCount(beerPost.id);
const props = { const props = {
beerPost: JSON.parse(JSON.stringify(beerPost)), beerPost: JSON.parse(JSON.stringify(beerPost)),
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)), beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
beerComments: JSON.parse(JSON.stringify(beerComments)),
commentsPageCount: JSON.parse(JSON.stringify(commentPageCount)),
likeCount: JSON.parse(JSON.stringify(likeCount)),
}; };
return { props }; return { props };

View File

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

View File

@@ -6,11 +6,12 @@ import DBClient from '@/prisma/DBClient';
import Layout from '@/components/ui/Layout'; import Layout from '@/components/ui/Layout';
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar'; import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
import BeerCard from '@/components/BeerIndex/BeerCard'; import BeerCard from '@/components/BeerIndex/BeerCard';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import Head from 'next/head'; import Head from 'next/head';
import { z } from 'zod';
interface BeerPageProps { interface BeerPageProps {
initialBeerPosts: BeerPostQueryResult[]; initialBeerPosts: z.infer<typeof beerPostQueryResult>[];
pageCount: number; pageCount: number;
} }
@@ -26,7 +27,7 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
<meta name="description" content="Beer posts" /> <meta name="description" content="Beer posts" />
</Head> </Head>
<div className="flex items-center justify-center bg-base-100"> <div className="flex items-center justify-center bg-base-100">
<div className="my-10 flex w-10/12 flex-col space-y-4"> <div className="my-10 flex w-10/12 flex-col space-y-4">
<header className="my-10"> <header className="my-10">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-6xl font-bold">The Biergarten Index</h1> <h1 className="text-6xl font-bold">The Biergarten Index</h1>

View File

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

View File

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

View File

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

View File

@@ -7,21 +7,29 @@ import { GetServerSideProps, NextPage } from 'next';
import { useContext } from 'react'; import { useContext } from 'react';
const ProtectedPage: NextPage = () => { const ProtectedPage: NextPage = () => {
const { user, error, isLoading } = useContext(UserContext); const { user, isLoading } = useContext(UserContext);
const currentTime = new Date().getHours();
const isMorning = currentTime > 5 && currentTime < 12;
const isAfternoon = currentTime > 12 && currentTime < 18;
const isEvening = currentTime > 18 && currentTime < 24;
return ( return (
<Layout> <Layout>
<div className="flex h-full flex-col items-center justify-center"> <div className="flex h-full flex-col items-center justify-center space-y-3">
<h1 className="text-7xl font-bold text-white">Hello!</h1> {isLoading && <Spinner size="xl" />}
<> {user && (
{isLoading && <Spinner />} <>
{error && <p>Something went wrong.</p>} <h1 className="text-7xl font-bold">
{user && ( Good {isMorning && 'morning'}
<div> {isAfternoon && 'afternoon'}
<p>{user.username}</p> {isEvening && 'evening'}
</div> {`, ${user?.firstName}!`}
)} </h1>
</> <h2 className="text-4xl font-bold">Welcome to the Biergarten App!</h2>
</>
)}
</div> </div>
</Layout> </Layout>
); );

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isAccountVerified" BOOLEAN NOT NULL DEFAULT false;

View 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;

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually # Please do not edit this file manually
# It should be added in your version-control system (i.e. Git) # It should be added in your version-control system (i.e. Git)
provider = "postgresql" provider = "cockroachdb"

View File

@@ -6,7 +6,7 @@ generator client {
} }
datasource db { datasource db {
provider = "postgresql" provider = "cockroachdb"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
html {
font-family: 'Exo 2', sans-serif;
}
}

View File

@@ -9,26 +9,29 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [require('daisyui')], plugins: [
require('daisyui'),
require('tailwindcss-animate')
],
daisyui: { daisyui: {
logs: false, logs: false,
themes: [ themes: [
{ {
default: { default: {
primary: 'hsl(227, 46%, 25%)', primary: 'hsl(227, 23%, 20%)',
secondary: 'hsl(47, 100%, 80%)', secondary: '#ABA9C3',
error: '#c17c74',
accent: '#fe3bd9', accent: '#fe3bd9',
neutral: '#131520', neutral: '#131520',
info: '#0A7CFF', info: '#0A7CFF',
success: '#8ACE2B', success: '#8ACE2B',
warning: '#F9D002', warning: '#F9D002',
error: '#CF1259',
'primary-content': '#FAF9F6', 'primary-content': '#FAF9F6',
'error-content': '#FAF9F6', 'error-content': '#FAF9F6',
'base-100': 'hsl(190, 4%, 11%)', 'base-100': 'hsl(190, 4%, 9%)',
'base-200': 'hsl(190, 4%, 9%)', 'base-200': 'hsl(190, 4%, 8%)',
'base-300': 'hsl(190, 4%, 8%)', 'base-300': 'hsl(190, 4%, 5%)',
}, },
}, },
], ],