Feat: Implement infinite scrolling brewery comment section

Refactor beer comment schemas to work on brewery comments as well. Add robots.txt to block crawling for now.
This commit is contained in:
Aaron William Po
2023-04-30 13:43:51 -04:00
parent 99e3eba7d6
commit b3b1d5b6d1
27 changed files with 670 additions and 261 deletions

View File

@@ -4,7 +4,8 @@ import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter, NextHandler } from 'next-connect';
@@ -16,7 +17,7 @@ interface DeleteCommentRequest extends UserExtendedNextApiRequest {
interface EditCommentRequest extends UserExtendedNextApiRequest {
query: { id: string };
body: z.infer<typeof BeerCommentValidationSchema>;
body: z.infer<typeof CreateCommentValidationSchema>;
}
const checkIfCommentOwner = async (
@@ -96,7 +97,7 @@ router
.put(
validateRequest({
querySchema: z.object({ id: z.string().uuid() }),
bodySchema: BeerCommentValidationSchema,
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,
checkIfCommentOwner,

View File

@@ -6,16 +6,15 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
interface CreateCommentRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof BeerCommentValidationSchema>;
body: z.infer<typeof CreateCommentValidationSchema>;
query: { id: string };
}
@@ -31,13 +30,12 @@ const createComment = async (
const beerPostId = req.query.id;
const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
await createNewBeerComment({
content,
rating,
beerPostId,
userId: req.user!.id,
});
const newBeerComment: z.infer<typeof CommentQueryResult> = await createNewBeerComment({
content,
rating,
beerPostId,
userId: req.user!.id,
});
res.status(201).json({
message: 'Beer comment created successfully',
@@ -80,7 +78,7 @@ const router = createRouter<
router.post(
validateRequest({
bodySchema: BeerCommentValidationSchema,
bodySchema: CreateCommentValidationSchema,
querySchema: z.object({ id: z.string().uuid() }),
}),
getCurrentUser,

View File

@@ -0,0 +1,107 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import DBClient from '@/prisma/DBClient';
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import getAllBreweryComments from '@/services/BreweryComment/getAllBreweryComments';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
interface CreateCommentRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof CreateCommentValidationSchema>;
query: { id: string };
}
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 beerPostId = req.query.id;
// const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
// await createNewBeerComment({
// content,
// rating,
// beerPostId,
// userId: req.user!.id,
// });
// res.status(201).json({
// message: 'Beer comment created successfully',
// statusCode: 201,
// payload: newBeerComment,
// success: true,
// });
// };
const getAll = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const breweryPostId = req.query.id;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;
const comments = await getAllBreweryComments(
{ id: breweryPostId },
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
);
const pageCount = await DBClient.instance.breweryComment.count({
where: { breweryPostId },
});
res.setHeader('X-Total-Count', pageCount);
res.status(200).json({
message: 'Beer comments fetched successfully',
statusCode: 200,
payload: comments,
success: true,
});
};
const router = createRouter<
// I don't want to use any, but I can't figure out how to get the types to work
any,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
// router.post(
// validateRequest({
// bodySchema: CreateBeerCommentValidationSchema,
// querySchema: z.object({ id: z.string().uuid() }),
// }),
// getCurrentUser,
// createComment,
// );
router.get(
validateRequest({
querySchema: z.object({
id: z.string().uuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
}),
getAll,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -2,135 +2,26 @@ 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 MapGL, { Marker } from 'react-map-gl';
import { z } from 'zod';
import { FC, useContext } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
import { Carousel } from 'react-responsive-carousel';
import useGetBreweryPostLikeCount from '@/hooks/useGetBreweryPostLikeCount';
import useTimeDistance from '@/hooks/useTimeDistance';
import UserContext from '@/contexts/userContext';
import Link from 'next/link';
import { FaRegEdit } from 'react-icons/fa';
import format from 'date-fns/format';
import BreweryPostLikeButton from '@/components/BreweryIndex/BreweryPostLikeButton';
import useMediaQuery from '@/hooks/useMediaQuery';
import { Tab } from '@headlessui/react';
import BreweryInfoHeader from '@/components/BreweryById/BreweryInfoHeader';
import BreweryMap from '@/components/BreweryById/BreweryMap';
import BreweryBeersSection from '@/components/BreweryById/BreweryBeerSection.tsx';
import BreweryCommentsSection from '@/components/BreweryById/BreweryCommentsSection';
interface BreweryPageProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
}
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="space-y-2">
<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>
<h3 className="italic">
{' posted by '}
<Link
href={`/users/${breweryPost.postedBy.id}`}
className="link-hover link"
>
{`${breweryPost.postedBy.username} `}
</Link>
{timeDistance && (
<span
className="tooltip tooltip-right"
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-ghost btn-xs btn"
>
<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} user{likeCount !== 1 && 's'}
</span>
)}
</div>
</div>
<div className="card-actions">
{user && (
<BreweryPostLikeButton
breweryPostId={breweryPost.id}
mutateCount={mutate}
/>
)}
</div>
</div>
</div>
</div>
</article>
);
};
interface BreweryMapProps {
latitude: number;
longitude: number;
}
const BreweryMap: FC<BreweryMapProps> = ({ latitude, longitude }) => {
return (
<MapGL
initialViewState={{
latitude,
longitude,
zoom: 17,
}}
style={{
width: '100%',
height: 450,
}}
mapStyle="mapbox://styles/mapbox/streets-v12"
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string}
scrollZoom={true}
>
<Marker latitude={latitude} longitude={longitude} />
</MapGL>
);
};
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
const [longitude, latitude] = breweryPost.location.coordinates;
const isDesktop = useMediaQuery('(min-width: 1024px)');
return (
<>
<Head>
@@ -166,8 +57,39 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
<div className="mb-12 mt-10 flex w-full items-center justify-center">
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
<BreweryInfoHeader breweryPost={breweryPost} />
<BreweryMap latitude={latitude} longitude={longitude} />
{isDesktop ? (
<div className="mt-4 flex flex-row space-x-3 space-y-0">
<div className="w-[60%]">
<BreweryCommentsSection breweryPost={breweryPost} />
</div>
<div className="w-[40%] space-y-3">
<BreweryMap latitude={latitude} longitude={longitude} />
<BreweryBeersSection />
</div>
</div>
) : (
<>
<BreweryMap 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">
Comments
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Beers
</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel>
<BreweryCommentsSection breweryPost={breweryPost} />
</Tab.Panel>
<Tab.Panel>
<BreweryBeersSection />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</>
)}
</div>
</div>
</>