mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +00:00
Feat: Add create brewery comments and brewery cluster map
This commit is contained in:
@@ -3,19 +3,13 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentReques
|
||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { FunctionComponent, useState, useEffect } from 'react';
|
||||
import { Rating } from 'react-daisyui';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||
import Button from '../ui/forms/Button';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormSegment from '../ui/forms/FormSegment';
|
||||
import FormTextArea from '../ui/forms/FormTextArea';
|
||||
import CommentForm from '../ui/CommentForm';
|
||||
|
||||
interface BeerCommentFormProps {
|
||||
beerPost: z.infer<typeof beerPostQueryResult>;
|
||||
@@ -26,26 +20,16 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||
beerPost,
|
||||
mutate,
|
||||
}) => {
|
||||
const { register, handleSubmit, formState, reset, setValue } = useForm<
|
||||
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
|
||||
z.infer<typeof CreateCommentValidationSchema>
|
||||
>({
|
||||
defaultValues: {
|
||||
rating: 0,
|
||||
},
|
||||
defaultValues: { rating: 0 },
|
||||
resolver: zodResolver(CreateCommentValidationSchema),
|
||||
});
|
||||
|
||||
const [rating, setRating] = useState(0);
|
||||
useEffect(() => {
|
||||
setRating(0);
|
||||
reset({ rating: 0, content: '' });
|
||||
}, [reset]);
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||
data,
|
||||
) => {
|
||||
setValue('rating', 0);
|
||||
setRating(0);
|
||||
await sendCreateBeerCommentRequest({
|
||||
content: data.content,
|
||||
rating: data.rating,
|
||||
@@ -55,50 +39,15 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||
reset();
|
||||
};
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||
<FormError>{errors.content?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextArea
|
||||
id="content"
|
||||
formValidationSchema={register('content')}
|
||||
placeholder="Comment"
|
||||
rows={5}
|
||||
error={!!errors.content?.message}
|
||||
disabled={formState.isSubmitting}
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||
<FormError>{errors.rating?.message}</FormError>
|
||||
</FormInfo>
|
||||
<Rating
|
||||
value={rating}
|
||||
onChange={(value) => {
|
||||
setRating(value);
|
||||
setValue('rating', value);
|
||||
}}
|
||||
>
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
</Rating>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<CommentForm
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
register={register}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
55
src/components/BreweryById/BreweryPostMap.tsx
Normal file
55
src/components/BreweryById/BreweryPostMap.tsx
Normal 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;
|
||||
85
src/components/ui/CommentForm.tsx
Normal file
85
src/components/ui/CommentForm.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { FC } from 'react';
|
||||
import { Rating } from 'react-daisyui';
|
||||
import type {
|
||||
FormState,
|
||||
SubmitHandler,
|
||||
UseFormHandleSubmit,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
} from 'react-hook-form';
|
||||
import FormError from './forms/FormError';
|
||||
import FormInfo from './forms/FormInfo';
|
||||
import FormLabel from './forms/FormLabel';
|
||||
import FormSegment from './forms/FormSegment';
|
||||
import FormTextArea from './forms/FormTextArea';
|
||||
import Button from './forms/Button';
|
||||
|
||||
interface Comment {
|
||||
content: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface CommentFormProps {
|
||||
handleSubmit: UseFormHandleSubmit<Comment>;
|
||||
onSubmit: SubmitHandler<Comment>;
|
||||
watch: UseFormWatch<Comment>;
|
||||
setValue: UseFormSetValue<Comment>;
|
||||
formState: FormState<Comment>;
|
||||
register: UseFormRegister<Comment>;
|
||||
}
|
||||
|
||||
const CommentForm: FC<CommentFormProps> = ({
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState,
|
||||
register,
|
||||
}) => {
|
||||
const { errors } = formState;
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="content">Leave a comment</FormLabel>
|
||||
<FormError>{errors.content?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextArea
|
||||
id="content"
|
||||
formValidationSchema={register('content')}
|
||||
placeholder="Comment"
|
||||
rows={5}
|
||||
error={!!errors.content?.message}
|
||||
disabled={formState.isSubmitting}
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="rating">Rating</FormLabel>
|
||||
<FormError>{errors.rating?.message}</FormError>
|
||||
</FormInfo>
|
||||
<Rating
|
||||
value={watch('rating')}
|
||||
onChange={(value) => {
|
||||
setValue('rating', value);
|
||||
}}
|
||||
>
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
<Rating.Item name="rating-1" className="mask mask-star" />
|
||||
</Rating>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" isSubmitting={formState.isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentForm;
|
||||
8
src/components/ui/LocationMarker.tsx
Normal file
8
src/components/ui/LocationMarker.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { HiLocationMarker } from 'react-icons/hi';
|
||||
|
||||
const LocationMarker = () => {
|
||||
return <HiLocationMarker className="text-3xl" />;
|
||||
};
|
||||
|
||||
export default React.memo(LocationMarker);
|
||||
@@ -16,6 +16,7 @@ import { NextApiResponse } from 'next';
|
||||
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
|
||||
import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments';
|
||||
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||
import createNewBreweryComment from '@/services/BreweryComment/createNewBreweryComment';
|
||||
|
||||
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||
body: z.infer<typeof CreateCommentValidationSchema>;
|
||||
@@ -26,29 +27,31 @@ interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string; page_size: string; page_num: string };
|
||||
}
|
||||
|
||||
// const createComment = async (
|
||||
// req: CreateCommentRequest,
|
||||
// res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
// ) => {
|
||||
// const { content, rating } = req.body;
|
||||
const createComment = async (
|
||||
req: CreateCommentRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
const { content, rating } = req.body;
|
||||
|
||||
// const beerPostId = req.query.id;
|
||||
const breweryPostId = req.query.id;
|
||||
|
||||
// const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
|
||||
// await createNewBeerComment({
|
||||
// content,
|
||||
// rating,
|
||||
// beerPostId,
|
||||
// userId: req.user!.id,
|
||||
// });
|
||||
const user = req.user!;
|
||||
|
||||
// res.status(201).json({
|
||||
// message: 'Beer comment created successfully',
|
||||
// statusCode: 201,
|
||||
// payload: newBeerComment,
|
||||
// success: true,
|
||||
// });
|
||||
// };
|
||||
const newBreweryComment: z.infer<typeof CommentQueryResult> =
|
||||
await createNewBreweryComment({
|
||||
content,
|
||||
rating,
|
||||
breweryPostId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Beer comment created successfully',
|
||||
statusCode: 201,
|
||||
payload: newBreweryComment,
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
const getAll = async (
|
||||
req: GetAllCommentsRequest,
|
||||
@@ -83,14 +86,14 @@ const router = createRouter<
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
// router.post(
|
||||
// validateRequest({
|
||||
// bodySchema: CreateBeerCommentValidationSchema,
|
||||
// querySchema: z.object({ id: z.string().uuid() }),
|
||||
// }),
|
||||
// getCurrentUser,
|
||||
// createComment,
|
||||
// );
|
||||
router.post(
|
||||
validateRequest({
|
||||
bodySchema: CreateCommentValidationSchema,
|
||||
querySchema: z.object({ id: z.string().uuid() }),
|
||||
}),
|
||||
getCurrentUser,
|
||||
createComment,
|
||||
);
|
||||
|
||||
router.get(
|
||||
validateRequest({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { BeerPost } from '@prisma/client';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
||||
import 'react-responsive-carousel/lib/styles/carousel.min.css';
|
||||
import { Carousel } from 'react-responsive-carousel';
|
||||
import useMediaQuery from '@/hooks/useMediaQuery';
|
||||
import { Tab } from '@headlessui/react';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
|
||||
import { z } from 'zod';
|
||||
import Head from 'next/head';
|
||||
@@ -11,7 +10,7 @@ import { Carousel } from 'react-responsive-carousel';
|
||||
import useMediaQuery from '@/hooks/useMediaQuery';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader';
|
||||
import BreweryMap from '@/components/BreweryById/BreweryMap';
|
||||
import BreweryPostMap from '@/components/BreweryById/BreweryPostMap';
|
||||
import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx';
|
||||
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
|
||||
|
||||
@@ -63,13 +62,13 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
||||
<BreweryCommentsSection breweryPost={breweryPost} />
|
||||
</div>
|
||||
<div className="w-[40%] space-y-3">
|
||||
<BreweryMap latitude={latitude} longitude={longitude} />
|
||||
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
||||
<BreweryBeersSection />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<BreweryMap latitude={latitude} longitude={longitude} />
|
||||
<BreweryPostMap latitude={latitude} longitude={longitude} />
|
||||
<Tab.Group>
|
||||
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
||||
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||
|
||||
127
src/pages/breweries/map.tsx
Normal file
127
src/pages/breweries/map.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Map, {
|
||||
FullscreenControl,
|
||||
Marker,
|
||||
NavigationControl,
|
||||
Popup,
|
||||
ScaleControl,
|
||||
} from 'react-map-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
|
||||
import LocationMarker from '@/components/ui/LocationMarker';
|
||||
import Link from 'next/link';
|
||||
|
||||
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
|
||||
|
||||
interface BreweryMapPageProps {
|
||||
breweries: {
|
||||
location: {
|
||||
city: string;
|
||||
stateOrProvince: string | null;
|
||||
country: string | null;
|
||||
coordinates: number[];
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ breweries }) => {
|
||||
const windowIsDefined = typeof window !== 'undefined';
|
||||
const themeIsDefined = windowIsDefined && !!window.localStorage.getItem('theme');
|
||||
|
||||
const [popupInfo, setPopupInfo] = useState<BreweryMapPageProps['breweries'][0] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const theme = (
|
||||
windowIsDefined && themeIsDefined ? window.localStorage.getItem('theme') : 'light'
|
||||
) as 'light' | 'dark';
|
||||
|
||||
const mapStyles: MapStyles = {
|
||||
light: 'mapbox://styles/mapbox/light-v10',
|
||||
dark: 'mapbox://styles/mapbox/dark-v11',
|
||||
};
|
||||
|
||||
const pins = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{breweries.map((brewery) => {
|
||||
const [longitude, latitude] = brewery.location.coordinates;
|
||||
return (
|
||||
<Marker
|
||||
latitude={latitude}
|
||||
longitude={longitude}
|
||||
key={brewery.id}
|
||||
onClick={(e) => {
|
||||
e.originalEvent.stopPropagation();
|
||||
setPopupInfo(brewery);
|
||||
}}
|
||||
>
|
||||
<LocationMarker />
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
[breweries],
|
||||
);
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Map
|
||||
initialViewState={{ zoom: 2 }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
mapStyle={mapStyles[theme]}
|
||||
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}
|
||||
scrollZoom
|
||||
>
|
||||
<FullscreenControl position="top-left" />
|
||||
<NavigationControl position="top-left" />
|
||||
<ScaleControl />
|
||||
{pins}
|
||||
{popupInfo && (
|
||||
<Popup
|
||||
anchor="bottom"
|
||||
longitude={popupInfo.location.coordinates[0]}
|
||||
latitude={popupInfo.location.coordinates[1]}
|
||||
onClose={() => setPopupInfo(null)}
|
||||
>
|
||||
<div className="flex flex-col text-black ">
|
||||
<Link
|
||||
className="link-hover link text-base font-bold"
|
||||
href={`/breweries/${popupInfo.id}`}
|
||||
>
|
||||
{popupInfo.name}
|
||||
</Link>
|
||||
<p className="text-base">
|
||||
{popupInfo.location.city}
|
||||
{popupInfo.location.stateOrProvince
|
||||
? `, ${popupInfo.location.stateOrProvince}`
|
||||
: ''}
|
||||
{popupInfo.location.country ? `, ${popupInfo.location.country}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</Popup>
|
||||
)}
|
||||
</Map>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreweryMapPage;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<BreweryMapPageProps> = async () => {
|
||||
const breweries = await DBClient.instance.breweryPost.findMany({
|
||||
select: {
|
||||
location: {
|
||||
select: { coordinates: true, city: true, country: true, stateOrProvince: true },
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { props: { breweries } };
|
||||
};
|
||||
@@ -8,18 +8,11 @@ const sendBreweryPostLikeRequest = async (breweryPostId: string) => {
|
||||
const json = await response.json();
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid API response.');
|
||||
}
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid API response.');
|
||||
}
|
||||
|
||||
const { success, message } = parsed.data;
|
||||
|
||||
return { success, message };
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
export default sendBreweryPostLikeRequest;
|
||||
|
||||
@@ -18,13 +18,12 @@ const sendCreateBeerCommentRequest = async ({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beerPostId, content, rating }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
||||
|
||||
if (!parsedResponse.success) {
|
||||
|
||||
33
src/services/BreweryComment/createNewBreweryComment.ts
Normal file
33
src/services/BreweryComment/createNewBreweryComment.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import { z } from 'zod';
|
||||
import CreateCommentValidationSchema from '../types/CommentSchema/CreateCommentValidationSchema';
|
||||
|
||||
const CreateNewBreweryCommentServiceSchema = CreateCommentValidationSchema.extend({
|
||||
userId: z.string().uuid(),
|
||||
breweryPostId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const createNewBreweryComment = async ({
|
||||
content,
|
||||
rating,
|
||||
breweryPostId,
|
||||
userId,
|
||||
}: z.infer<typeof CreateNewBreweryCommentServiceSchema>) => {
|
||||
return DBClient.instance.breweryComment.create({
|
||||
data: {
|
||||
content,
|
||||
rating,
|
||||
breweryPost: { connect: { id: breweryPostId } },
|
||||
postedBy: { connect: { id: userId } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
rating: true,
|
||||
postedBy: { select: { id: true, username: true } },
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default createNewBreweryComment;
|
||||
Reference in New Issue
Block a user