mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 01:54:00 +00:00
Move next js project to archive (#207)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import UseBeerPostsByBrewery from '@/hooks/data-fetching/beer-posts/useBeerPostsByBrewery';
|
||||
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
|
||||
import Link from 'next/link';
|
||||
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { z } from 'zod';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import BeerRecommendationLoadingComponent from '../BeerById/BeerRecommendationLoadingComponent';
|
||||
|
||||
interface BreweryCommentsSectionProps {
|
||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||
}
|
||||
|
||||
const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) => {
|
||||
const PAGE_SIZE = 2;
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const { beerPosts, isAtEnd, isLoadingMore, setSize, size } = UseBeerPostsByBrewery({
|
||||
breweryId: breweryPost.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
const { ref: penultimateBeerPostRef } = useInView({
|
||||
/**
|
||||
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||
* load more beer posts.
|
||||
*/
|
||||
onChange: (visible) => {
|
||||
if (!visible || isAtEnd) return;
|
||||
setSize(size + 1);
|
||||
},
|
||||
});
|
||||
|
||||
const beerRecommendationsRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
|
||||
return (
|
||||
<div className="card h-full" ref={beerRecommendationsRef}>
|
||||
<div className="card-body">
|
||||
<>
|
||||
<div className="my-2 flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold">Brews</h3>
|
||||
</div>
|
||||
<div>
|
||||
{user && (
|
||||
<Link
|
||||
className={`btn btn-ghost btn-sm gap-2 rounded-2xl outline`}
|
||||
href={`/breweries/${breweryPost.id}/beers/create`}
|
||||
>
|
||||
<FaPlus className="text-xl" />
|
||||
Add Beer
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!beerPosts.length && (
|
||||
<div className="space-y-5">
|
||||
{beerPosts.map((beerPost, index) => {
|
||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||
|
||||
/**
|
||||
* Attach a ref to the second last beer post in the list. When it comes
|
||||
* into view, the component will call setSize to load more beer posts.
|
||||
*/
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isPenultimateBeerPost ? penultimateBeerPostRef : undefined}
|
||||
key={beerPost.id}
|
||||
>
|
||||
<div>
|
||||
<Link className="link-hover link" href={`/beers/${beerPost.id}`}>
|
||||
<span className="text-xl font-semibold">{beerPost.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link
|
||||
className="link-hover link text-lg font-medium"
|
||||
href={`/beers/styles/${beerPost.style.id}`}
|
||||
>
|
||||
{beerPost.style.name}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<span>{beerPost.abv}% ABV</span>
|
||||
<span>{beerPost.ibu} IBU</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
/**
|
||||
* If there are more beer posts to load, show a loading component with a
|
||||
* skeleton loader and a loading spinner.
|
||||
*/
|
||||
!!isLoadingMore && !isAtEnd && (
|
||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreweryBeersSection;
|
||||
@@ -0,0 +1,60 @@
|
||||
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
|
||||
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
|
||||
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { FC } from 'react';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { z } from 'zod';
|
||||
import sendCreateBreweryCommentRequest from '@/requests/comments/brewery-comment/sendCreateBreweryCommentRequest';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import CommentForm from '../Comments/CommentForm';
|
||||
|
||||
interface BreweryCommentFormProps {
|
||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||
mutate: ReturnType<typeof useBreweryPostComments>['mutate'];
|
||||
}
|
||||
|
||||
const BreweryCommentForm: FC<BreweryCommentFormProps> = ({ breweryPost, mutate }) => {
|
||||
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
|
||||
z.infer<typeof CreateCommentValidationSchema>
|
||||
>({
|
||||
defaultValues: { rating: 0 },
|
||||
resolver: zodResolver(CreateCommentValidationSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||
data,
|
||||
) => {
|
||||
const loadingToast = toast.loading('Posting a new comment...');
|
||||
try {
|
||||
await sendCreateBreweryCommentRequest({
|
||||
content: data.content,
|
||||
rating: data.rating,
|
||||
breweryPostId: breweryPost.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 BreweryCommentForm;
|
||||
@@ -0,0 +1,88 @@
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
|
||||
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
|
||||
import {
|
||||
sendDeleteBreweryPostCommentRequest,
|
||||
sendEditBreweryPostCommentRequest,
|
||||
} from '@/requests/comments/brewery-comment';
|
||||
import CommentLoadingComponent from '../Comments/CommentLoadingComponent';
|
||||
import CommentsComponent from '../Comments/CommentsComponent';
|
||||
import BreweryCommentForm from './BreweryCommentForm';
|
||||
|
||||
interface BreweryBeerSectionProps {
|
||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||
}
|
||||
|
||||
const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const PAGE_SIZE = 4;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
setSize,
|
||||
size,
|
||||
isLoadingMore,
|
||||
isAtEnd,
|
||||
mutate,
|
||||
comments: breweryComments,
|
||||
} = useBreweryPostComments({ id: breweryPost.id, pageSize: PAGE_SIZE });
|
||||
|
||||
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-3" ref={commentSectionRef}>
|
||||
<div className="card">
|
||||
<div className="card-body h-full">
|
||||
{user ? (
|
||||
<BreweryCommentForm breweryPost={breweryPost} mutate={mutate} />
|
||||
) : (
|
||||
<div className="flex h-52 flex-col items-center justify-center">
|
||||
<div className="text-lg font-bold">Log in to leave a comment.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
/**
|
||||
* If the comments are loading, show a loading component. Otherwise, show the
|
||||
* comments.
|
||||
*/
|
||||
isLoading ? (
|
||||
<div className="card pb-6">
|
||||
<CommentLoadingComponent length={PAGE_SIZE} />
|
||||
</div>
|
||||
) : (
|
||||
<CommentsComponent
|
||||
comments={breweryComments}
|
||||
isLoadingMore={isLoadingMore}
|
||||
isAtEnd={isAtEnd}
|
||||
pageSize={PAGE_SIZE}
|
||||
setSize={setSize}
|
||||
size={size}
|
||||
commentSectionRef={commentSectionRef}
|
||||
mutate={mutate}
|
||||
handleDeleteCommentRequest={(id) => {
|
||||
return sendDeleteBreweryPostCommentRequest({
|
||||
breweryPostId: breweryPost.id,
|
||||
commentId: id,
|
||||
});
|
||||
}}
|
||||
handleEditCommentRequest={(commentId, data) => {
|
||||
return sendEditBreweryPostCommentRequest({
|
||||
breweryPostId: breweryPost.id,
|
||||
commentId,
|
||||
body: { content: data.content, rating: data.rating },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreweryCommentsSection;
|
||||
@@ -0,0 +1,100 @@
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import useGetBreweryPostLikeCount from '@/hooks/data-fetching/brewery-likes/useGetBreweryPostLikeCount';
|
||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
|
||||
import { format } from 'date-fns';
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
|
||||
import { z } from 'zod';
|
||||
import Link from 'next/link';
|
||||
import BreweryPostLikeButton from '../BreweryIndex/BreweryPostLikeButton';
|
||||
|
||||
interface BreweryInfoHeaderProps {
|
||||
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||
}
|
||||
const BreweryInfoHeader: FC<BreweryInfoHeaderProps> = ({ breweryPost }) => {
|
||||
const createdAt = new Date(breweryPost.createdAt);
|
||||
const timeDistance = useTimeDistance(createdAt);
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
const idMatches = user && breweryPost.postedBy.id === user.id;
|
||||
const isPostOwner = !!(user && idMatches);
|
||||
|
||||
const { likeCount, mutate } = useGetBreweryPostLikeCount(breweryPost.id);
|
||||
|
||||
return (
|
||||
<article className="card flex flex-col justify-center bg-base-300">
|
||||
<div className="card-body">
|
||||
<header className="flex justify-between">
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold lg:text-4xl">{breweryPost.name}</h1>
|
||||
<h2 className="text-lg font-semibold lg:text-2xl">
|
||||
Located in
|
||||
{` ${breweryPost.location.city}, ${
|
||||
breweryPost.location.stateOrProvince || breweryPost.location.country
|
||||
}`}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="italic">
|
||||
{' posted by '}
|
||||
<Link
|
||||
href={`/users/${breweryPost.postedBy.id}`}
|
||||
className="link-hover link"
|
||||
>
|
||||
{`${breweryPost.postedBy.username} `}
|
||||
</Link>
|
||||
{timeDistance && (
|
||||
<span
|
||||
className="tooltip tooltip-bottom"
|
||||
data-tip={format(createdAt, 'MM/dd/yyyy')}
|
||||
>{`${timeDistance} ago`}</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{isPostOwner && (
|
||||
<div className="tooltip tooltip-left" data-tip={`Edit '${breweryPost.name}'`}>
|
||||
<Link
|
||||
href={`/breweries/${breweryPost.id}/edit`}
|
||||
className="btn btn-ghost btn-xs"
|
||||
>
|
||||
<FaRegEdit className="text-xl" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className="space-y-2">
|
||||
<p>{breweryPost.description}</p>
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
{(!!likeCount || likeCount === 0) && (
|
||||
<span>
|
||||
Liked by {likeCount} {likeCount === 1 ? 'user' : 'users'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
{user && (
|
||||
<BreweryPostLikeButton
|
||||
breweryPostId={breweryPost.id}
|
||||
mutateCount={mutate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreweryInfoHeader;
|
||||
@@ -0,0 +1,60 @@
|
||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { FC, useMemo } from 'react';
|
||||
import Map, { Marker } from 'react-map-gl';
|
||||
|
||||
import LocationMarker from '../ui/LocationMarker';
|
||||
import ControlPanel from '../ui/maps/ControlPanel';
|
||||
|
||||
interface BreweryMapProps {
|
||||
coordinates: { latitude: number; longitude: number };
|
||||
token: string;
|
||||
}
|
||||
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
||||
|
||||
const BreweryPostMap: FC<BreweryMapProps> = ({
|
||||
coordinates: { latitude, longitude },
|
||||
token,
|
||||
}) => {
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
const windowIsDefined = typeof window !== 'undefined';
|
||||
const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme');
|
||||
|
||||
const theme = (
|
||||
windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light'
|
||||
) as 'light' | 'dark';
|
||||
|
||||
const pin = useMemo(
|
||||
() => (
|
||||
<Marker latitude={latitude} longitude={longitude}>
|
||||
<LocationMarker />
|
||||
</Marker>
|
||||
),
|
||||
[latitude, longitude],
|
||||
);
|
||||
|
||||
const mapStyles: MapStyles = {
|
||||
light: 'mapbox://styles/mapbox/light-v10',
|
||||
dark: 'mapbox://styles/mapbox/dark-v11',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<Map
|
||||
initialViewState={{ latitude, longitude, zoom: 17 }}
|
||||
style={{ width: '100%', height: isDesktop ? 480 : 240 }}
|
||||
mapStyle={mapStyles[theme]}
|
||||
mapboxAccessToken={token}
|
||||
scrollZoom
|
||||
>
|
||||
<ControlPanel />
|
||||
{pin}
|
||||
</Map>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreweryPostMap;
|
||||
Reference in New Issue
Block a user