Feat: Add create brewery comments and brewery cluster map

This commit is contained in:
Aaron William Po
2023-04-30 23:09:03 -04:00
parent b3b1d5b6d1
commit adf1b55d10
13 changed files with 452 additions and 165 deletions

View File

@@ -3,39 +3,118 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod';
import useBreweryPostComments from '@/hooks/useBreweryPostComments';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler } from 'react-hook-form';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import LoadingComponent from '../BeerById/LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent';
import CommentForm from '../ui/CommentForm';
interface BreweryBeerSectionProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
}
const BreweryCommentForm: FC = () => {
return null;
interface BreweryCommentFormProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
mutate: ReturnType<typeof useBreweryPostComments>['mutate'];
}
const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
breweryPostId: z.string(),
});
const sendCreateBreweryCommentRequest = async ({
content,
rating,
breweryPostId,
}: z.infer<typeof BreweryCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/breweries/${breweryPostId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) {
throw new Error('Invalid API response');
}
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
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,
) => {
await sendCreateBreweryCommentRequest({
content: data.content,
rating: data.rating,
breweryPostId: breweryPost.id,
});
await mutate();
reset();
};
return (
<CommentForm
handleSubmit={handleSubmit}
onSubmit={onSubmit}
watch={watch}
setValue={setValue}
formState={formState}
register={register}
/>
);
};
const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => {
const { user } = useContext(UserContext);
const { id } = breweryPost;
const PAGE_SIZE = 4;
const { comments, isLoading, setSize, size, isLoadingMore, isAtEnd } =
useBreweryPostComments({ id, pageSize: PAGE_SIZE });
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">
{user ? (
<BreweryCommentForm />
) : (
<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 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>
{
/**
@@ -48,7 +127,7 @@ const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) =>
</div>
) : (
<CommentsComponent
comments={comments}
comments={breweryComments}
isLoadingMore={isLoadingMore}
isAtEnd={isAtEnd}
pageSize={PAGE_SIZE}

View File

@@ -1,43 +0,0 @@
import useMediaQuery from '@/hooks/useMediaQuery';
import { FC } from 'react';
import Map, { Marker } from 'react-map-gl';
interface BreweryMapProps {
latitude: number;
longitude: number;
}
const BreweryMap: FC<BreweryMapProps> = ({ latitude, longitude }) => {
const isDesktop = useMediaQuery('(min-width: 1024px)');
const theme =
typeof window !== 'undefined' ? window.localStorage.getItem('theme') : 'dark';
const mapStyle =
theme === 'dark'
? 'mapbox://styles/mapbox/dark-v11'
: 'mapbox://styles/mapbox/light-v10';
return (
<div className="card">
<div className="card-body">
<Map
initialViewState={{
latitude,
longitude,
zoom: 17,
}}
style={{
width: '100%',
height: isDesktop ? 400 : 200,
}}
mapStyle={mapStyle}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string}
scrollZoom={true}
>
<Marker latitude={latitude} longitude={longitude} />
</Map>
</div>
</div>
);
};
export default BreweryMap;

View File

@@ -0,0 +1,55 @@
import useMediaQuery from '@/hooks/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';
interface BreweryMapProps {
latitude: number;
longitude: number;
}
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
const BreweryPostMap: FC<BreweryMapProps> = ({ latitude, longitude }) => {
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={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string}
scrollZoom
>
{pin}
</Map>
</div>
</div>
);
};
export default BreweryPostMap;