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,19 +3,13 @@ import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentReques
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 { FunctionComponent, useState, useEffect } from 'react'; import { FunctionComponent } from 'react';
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 useBeerPostComments from '@/hooks/useBeerPostComments'; import useBeerPostComments from '@/hooks/useBeerPostComments';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import Button from '../ui/forms/Button'; import CommentForm from '../ui/CommentForm';
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';
interface BeerCommentFormProps { interface BeerCommentFormProps {
beerPost: z.infer<typeof beerPostQueryResult>; beerPost: z.infer<typeof beerPostQueryResult>;
@@ -26,26 +20,16 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
beerPost, beerPost,
mutate, mutate,
}) => { }) => {
const { register, handleSubmit, formState, reset, setValue } = useForm< const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
z.infer<typeof CreateCommentValidationSchema> z.infer<typeof CreateCommentValidationSchema>
>({ >({
defaultValues: { defaultValues: { rating: 0 },
rating: 0,
},
resolver: zodResolver(CreateCommentValidationSchema), resolver: zodResolver(CreateCommentValidationSchema),
}); });
const [rating, setRating] = useState(0);
useEffect(() => {
setRating(0);
reset({ rating: 0, content: '' });
}, [reset]);
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async ( const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
data, data,
) => { ) => {
setValue('rating', 0);
setRating(0);
await sendCreateBeerCommentRequest({ await sendCreateBeerCommentRequest({
content: data.content, content: data.content,
rating: data.rating, rating: data.rating,
@@ -55,50 +39,15 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
reset(); reset();
}; };
const { errors } = formState;
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5"> <CommentForm
<div> handleSubmit={handleSubmit}
<FormInfo> onSubmit={onSubmit}
<FormLabel htmlFor="content">Leave a comment</FormLabel> watch={watch}
<FormError>{errors.content?.message}</FormError> setValue={setValue}
</FormInfo> formState={formState}
<FormSegment> register={register}
<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>
); );
}; };

View File

@@ -3,39 +3,118 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
import { FC, MutableRefObject, useContext, useRef } from 'react'; import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import useBreweryPostComments from '@/hooks/useBreweryPostComments'; 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 LoadingComponent from '../BeerById/LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent'; import CommentsComponent from '../ui/CommentsComponent';
import CommentForm from '../ui/CommentForm';
interface BreweryBeerSectionProps { interface BreweryBeerSectionProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>; breweryPost: z.infer<typeof BreweryPostQueryResult>;
} }
const BreweryCommentForm: FC = () => { interface BreweryCommentFormProps {
return null; 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 BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const { id } = breweryPost;
const PAGE_SIZE = 4; const PAGE_SIZE = 4;
const { comments, isLoading, setSize, size, isLoadingMore, isAtEnd } = const {
useBreweryPostComments({ id, pageSize: PAGE_SIZE }); isLoading,
setSize,
size,
isLoadingMore,
isAtEnd,
mutate,
comments: breweryComments,
} = useBreweryPostComments({ id: breweryPost.id, pageSize: PAGE_SIZE });
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null); const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
return ( return (
<div className="w-full space-y-3" ref={commentSectionRef}> <div className="w-full space-y-3" ref={commentSectionRef}>
<div className="card"> <div className="card">
{user ? ( <div className="card-body h-full">
<BreweryCommentForm /> {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 className="flex h-52 flex-col items-center justify-center">
</div> <div className="text-lg font-bold">Log in to leave a comment.</div>
)} </div>
)}
</div>
</div> </div>
{ {
/** /**
@@ -48,7 +127,7 @@ const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) =>
</div> </div>
) : ( ) : (
<CommentsComponent <CommentsComponent
comments={comments} comments={breweryComments}
isLoadingMore={isLoadingMore} isLoadingMore={isLoadingMore}
isAtEnd={isAtEnd} isAtEnd={isAtEnd}
pageSize={PAGE_SIZE} 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;

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

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

View File

@@ -16,6 +16,7 @@ import { NextApiResponse } from 'next';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments'; import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import createNewBreweryComment from '@/services/BreweryComment/createNewBreweryComment';
interface CreateCommentRequest extends UserExtendedNextApiRequest { interface CreateCommentRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof CreateCommentValidationSchema>; body: z.infer<typeof CreateCommentValidationSchema>;
@@ -26,29 +27,31 @@ interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string }; 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>>,
// ) => { ) => {
// const { content, rating } = req.body; const { content, rating } = req.body;
// const beerPostId = req.query.id; const breweryPostId = req.query.id;
// const newBeerComment: z.infer<typeof BeerCommentQueryResult> = const user = req.user!;
// await createNewBeerComment({
// content,
// rating,
// beerPostId,
// userId: req.user!.id,
// });
// res.status(201).json({ const newBreweryComment: z.infer<typeof CommentQueryResult> =
// message: 'Beer comment created successfully', await createNewBreweryComment({
// statusCode: 201, content,
// payload: newBeerComment, rating,
// success: true, breweryPostId,
// }); userId: user.id,
// }; });
res.status(201).json({
message: 'Beer comment created successfully',
statusCode: 201,
payload: newBreweryComment,
success: true,
});
};
const getAll = async ( const getAll = async (
req: GetAllCommentsRequest, req: GetAllCommentsRequest,
@@ -83,14 +86,14 @@ const router = createRouter<
NextApiResponse<z.infer<typeof APIResponseValidationSchema>> NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>(); >();
// router.post( router.post(
// validateRequest({ validateRequest({
// bodySchema: CreateBeerCommentValidationSchema, bodySchema: CreateCommentValidationSchema,
// querySchema: z.object({ id: z.string().uuid() }), querySchema: z.object({ id: z.string().uuid() }),
// }), }),
// getCurrentUser, getCurrentUser,
// createComment, createComment,
// ); );
router.get( router.get(
validateRequest({ validateRequest({

View File

@@ -14,7 +14,7 @@ import { BeerPost } from '@prisma/client';
import { z } from 'zod'; 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 { Carousel } from 'react-responsive-carousel';
import useMediaQuery from '@/hooks/useMediaQuery'; import useMediaQuery from '@/hooks/useMediaQuery';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';

View File

@@ -1,7 +1,6 @@
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById'; import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { GetServerSideProps, NextPage } from 'next'; import { GetServerSideProps, NextPage } from 'next';
import 'mapbox-gl/dist/mapbox-gl.css';
import { z } from 'zod'; import { z } from 'zod';
import Head from 'next/head'; import Head from 'next/head';
@@ -11,7 +10,7 @@ import { Carousel } from 'react-responsive-carousel';
import useMediaQuery from '@/hooks/useMediaQuery'; import useMediaQuery from '@/hooks/useMediaQuery';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader'; 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 BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx';
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection'; import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
@@ -63,13 +62,13 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
<BreweryCommentsSection breweryPost={breweryPost} /> <BreweryCommentsSection breweryPost={breweryPost} />
</div> </div>
<div className="w-[40%] space-y-3"> <div className="w-[40%] space-y-3">
<BreweryMap latitude={latitude} longitude={longitude} /> <BreweryPostMap latitude={latitude} longitude={longitude} />
<BreweryBeersSection /> <BreweryBeersSection />
</div> </div>
</div> </div>
) : ( ) : (
<> <>
<BreweryMap latitude={latitude} longitude={longitude} /> <BreweryPostMap latitude={latitude} longitude={longitude} />
<Tab.Group> <Tab.Group>
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl"> <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"> <Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">

127
src/pages/breweries/map.tsx Normal file
View 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 } };
};

View File

@@ -8,18 +8,11 @@ const sendBreweryPostLikeRequest = async (breweryPostId: string) => {
const json = await response.json(); const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json); const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) { if (!parsed.success) {
throw new Error('Invalid API response.'); throw new Error('Invalid API response.');
} }
if (!parsed.success) { return parsed.data;
throw new Error('Invalid API response.');
}
const { success, message } = parsed.data;
return { success, message };
}; };
export default sendBreweryPostLikeRequest; export default sendBreweryPostLikeRequest;

View File

@@ -18,13 +18,12 @@ const sendCreateBeerCommentRequest = async ({
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beerPostId, content, rating }), body: JSON.stringify({ beerPostId, content, rating }),
}); });
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json(); const data = await response.json();
if (!response.ok) {
throw new Error(data.message);
}
const parsedResponse = APIResponseValidationSchema.safeParse(data); const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) { if (!parsedResponse.success) {

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