mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 10:04:00 +00:00
Move next js project to archive (#207)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
|
||||
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import { sendCreateBeerCommentRequest } from '@/requests/comments/beer-comment';
|
||||
import CommentForm from '../Comments/CommentForm';
|
||||
|
||||
interface BeerCommentFormProps {
|
||||
beerPost: z.infer<typeof BeerPostQueryResult>;
|
||||
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
||||
}
|
||||
|
||||
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||
beerPost,
|
||||
mutate,
|
||||
}) => {
|
||||
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
|
||||
z.infer<typeof CreateCommentValidationSchema>
|
||||
>({
|
||||
defaultValues: { rating: 0 },
|
||||
resolver: zodResolver(CreateCommentValidationSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||
data,
|
||||
) => {
|
||||
const loadingToast = toast.loading('Posting a new comment...');
|
||||
try {
|
||||
await sendCreateBeerCommentRequest({ body: data, beerPostId: beerPost.id });
|
||||
reset();
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Comment posted successfully.');
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
await mutate();
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommentForm
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
register={register}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeerCommentForm;
|
||||
@@ -0,0 +1,109 @@
|
||||
import Link from 'next/link';
|
||||
import format from 'date-fns/format';
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
|
||||
import { z } from 'zod';
|
||||
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||
import BeerPostLikeButton from './BeerPostLikeButton';
|
||||
|
||||
interface BeerInfoHeaderProps {
|
||||
beerPost: z.infer<typeof BeerPostQueryResult>;
|
||||
}
|
||||
|
||||
const BeerInfoHeader: FC<BeerInfoHeaderProps> = ({ beerPost }) => {
|
||||
const createdAt = new Date(beerPost.createdAt);
|
||||
const timeDistance = useTimeDistance(createdAt);
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
const idMatches = user && beerPost.postedBy.id === user.id;
|
||||
const isPostOwner = !!(user && idMatches);
|
||||
|
||||
const { likeCount, mutate } = useGetBeerPostLikeCount(beerPost.id);
|
||||
|
||||
return (
|
||||
<article className="card flex flex-col justify-center bg-base-300">
|
||||
<div className="card-body">
|
||||
<header className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold lg:text-4xl">{beerPost.name}</h1>
|
||||
<h2 className="text-lg font-semibold lg:text-2xl">
|
||||
by{' '}
|
||||
<Link
|
||||
href={`/breweries/${beerPost.brewery.id}`}
|
||||
className="link-hover link font-semibold"
|
||||
>
|
||||
{beerPost.brewery.name}
|
||||
</Link>
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="italic">
|
||||
{' posted by '}
|
||||
<Link href={`/users/${beerPost.postedBy.id}`} className="link-hover link">
|
||||
{`${beerPost.postedBy.username} `}
|
||||
</Link>
|
||||
{timeDistance && (
|
||||
<span
|
||||
className="tooltip tooltip-bottom"
|
||||
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||
>
|
||||
{`${timeDistance} ago`}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPostOwner && (
|
||||
<div className="tooltip tooltip-left" data-tip={`Edit '${beerPost.name}'`}>
|
||||
<Link href={`/beers/${beerPost.id}/edit`} className="btn btn-ghost btn-xs">
|
||||
<FaRegEdit className="text-xl" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className="space-y-2">
|
||||
<p>{beerPost.description}</p>
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<Link
|
||||
className="link-hover link text-lg font-bold"
|
||||
href={`/beers/styles/${beerPost.style.id}`}
|
||||
>
|
||||
{beerPost.style.name}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-4 text-lg font-medium">
|
||||
{beerPost.abv.toFixed(1)}% ABV
|
||||
</span>
|
||||
<span className="text-lg font-medium">{beerPost.ibu.toFixed(1)} IBU</span>
|
||||
</div>
|
||||
<div>
|
||||
{(!!likeCount || likeCount === 0) && (
|
||||
<span>
|
||||
Liked by {likeCount}
|
||||
{likeCount !== 1 ? ' users' : ' user'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions items-end">
|
||||
{user && (
|
||||
<BeerPostLikeButton beerPostId={beerPost.id} mutateCount={mutate} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeerInfoHeader;
|
||||
@@ -0,0 +1,88 @@
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
|
||||
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
|
||||
|
||||
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||
import { z } from 'zod';
|
||||
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import {
|
||||
deleteBeerPostCommentRequest,
|
||||
editBeerPostCommentRequest,
|
||||
} from '@/requests/comments/beer-comment';
|
||||
import BeerCommentForm from './BeerCommentForm';
|
||||
|
||||
import CommentLoadingComponent from '../Comments/CommentLoadingComponent';
|
||||
import CommentsComponent from '../Comments/CommentsComponent';
|
||||
|
||||
interface BeerPostCommentsSectionProps {
|
||||
beerPost: z.infer<typeof BeerPostQueryResult>;
|
||||
}
|
||||
|
||||
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
const router = useRouter();
|
||||
|
||||
const pageNum = parseInt(router.query.comments_page as string, 10) || 1;
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
const { comments, isLoading, mutate, setSize, size, isLoadingMore, isAtEnd } =
|
||||
useBeerPostComments({ id: beerPost.id, pageNum, pageSize: PAGE_SIZE });
|
||||
|
||||
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-3" ref={commentSectionRef}>
|
||||
<div className="card bg-base-300">
|
||||
<div className="card-body h-full">
|
||||
{user ? (
|
||||
<BeerCommentForm beerPost={beerPost} mutate={mutate} />
|
||||
) : (
|
||||
<div className="flex h-52 flex-col items-center justify-center">
|
||||
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/**
|
||||
* If the comments are loading, show a loading component. Otherwise, show the
|
||||
* comments.
|
||||
*/
|
||||
isLoading ? (
|
||||
<div className="card bg-base-300 pb-6">
|
||||
<CommentLoadingComponent length={PAGE_SIZE} />
|
||||
</div>
|
||||
) : (
|
||||
<CommentsComponent
|
||||
commentSectionRef={commentSectionRef}
|
||||
comments={comments}
|
||||
isLoadingMore={isLoadingMore}
|
||||
isAtEnd={isAtEnd}
|
||||
pageSize={PAGE_SIZE}
|
||||
setSize={setSize}
|
||||
size={size}
|
||||
mutate={mutate}
|
||||
handleDeleteCommentRequest={(id) => {
|
||||
return deleteBeerPostCommentRequest({
|
||||
commentId: id,
|
||||
beerPostId: beerPost.id,
|
||||
});
|
||||
}}
|
||||
handleEditCommentRequest={(id, data) => {
|
||||
return editBeerPostCommentRequest({
|
||||
body: data,
|
||||
commentId: id,
|
||||
beerPostId: beerPost.id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeerPostCommentsSection;
|
||||
@@ -0,0 +1,35 @@
|
||||
import useCheckIfUserLikesBeerPost from '@/hooks/data-fetching/beer-likes/useCheckIfUserLikesBeerPost';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
|
||||
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
||||
import sendBeerPostLikeRequest from '@/requests/likes/beer-post-like/sendBeerPostLikeRequest';
|
||||
import LikeButton from '../ui/LikeButton';
|
||||
|
||||
const BeerPostLikeButton: FC<{
|
||||
beerPostId: string;
|
||||
mutateCount: ReturnType<typeof useGetBeerPostLikeCount>['mutate'];
|
||||
}> = ({ beerPostId, mutateCount }) => {
|
||||
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [isLiked]);
|
||||
|
||||
const handleLike = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await sendBeerPostLikeRequest(beerPostId);
|
||||
|
||||
await Promise.all([mutateCount(), mutateLikeStatus()]);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <LikeButton isLiked={!!isLiked} handleLike={handleLike} loading={loading} />;
|
||||
};
|
||||
|
||||
export default BeerPostLikeButton;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FC } from 'react';
|
||||
import Spinner from '../ui/Spinner';
|
||||
|
||||
interface BeerRecommendationLoadingComponentProps {
|
||||
length: number;
|
||||
}
|
||||
|
||||
const BeerRecommendationLoadingComponent: FC<BeerRecommendationLoadingComponentProps> = ({
|
||||
length,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length }).map((_, i) => (
|
||||
<div className="animate my-3 fade-in-10" key={i}>
|
||||
<div className="flex animate-pulse space-x-4">
|
||||
<div className="flex-1 space-y-4 py-1">
|
||||
<div className="h-4 w-3/4 rounded bg-base-100" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 rounded bg-base-100" />
|
||||
<div className="h-4 w-11/12 rounded bg-base-100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="p-1">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeerRecommendationLoadingComponent;
|
||||
@@ -0,0 +1,106 @@
|
||||
import Link from 'next/link';
|
||||
import { FC } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { z } from 'zod';
|
||||
import useBeerRecommendations from '@/hooks/data-fetching/beer-posts/useBeerRecommendations';
|
||||
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import BeerRecommendationLoadingComponent from './BeerRecommendationLoadingComponent';
|
||||
|
||||
const BeerRecommendationsSection: FC<{
|
||||
beerPost: z.infer<typeof BeerPostQueryResult>;
|
||||
}> = ({ beerPost }) => {
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const { beerPosts, isAtEnd, isLoadingMore, setSize, size } = useBeerRecommendations({
|
||||
beerPost,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
|
||||
const { ref: penultimateBeerPostRef } = useInView({
|
||||
/**
|
||||
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||
* load more beer posts.
|
||||
*/
|
||||
onChange: (visible) => {
|
||||
if (!visible || isAtEnd) return;
|
||||
debounce(() => setSize(size + 1), 200)();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card h-full">
|
||||
<div className="card-body">
|
||||
<>
|
||||
<div className="my-2 flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold">Also check out</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!beerPosts.length && (
|
||||
<div className="space-y-5">
|
||||
{beerPosts.map((post, index) => {
|
||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||
|
||||
/**
|
||||
* Attach a ref to the second last beer post in the list. When it comes
|
||||
* into view, the component will call setSize to load more beer posts.
|
||||
*/
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isPenultimateBeerPost ? penultimateBeerPostRef : undefined}
|
||||
key={post.id}
|
||||
className="animate-fade"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Link className="link-hover link" href={`/beers/${post.id}`}>
|
||||
<span className="text-xl font-bold">{post.name}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="link-hover link"
|
||||
href={`/breweries/${post.brewery.id}`}
|
||||
>
|
||||
<span className="text-lg font-semibold">{post.brewery.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<Link
|
||||
className="link-hover link"
|
||||
href={`/beers/styles/${post.style.id}`}
|
||||
>
|
||||
<span className="font-medium">{post.style.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<span>{post.abv.toFixed(1)}% ABV</span>
|
||||
<span>{post.ibu.toFixed(1)} IBU</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
/**
|
||||
* If there are more beer posts to load, show a loading component with a
|
||||
* skeleton loader and a loading spinner.
|
||||
*/
|
||||
!!isLoadingMore && !isAtEnd && (
|
||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeerRecommendationsSection;
|
||||
Reference in New Issue
Block a user