Continue work on brewery page, implement like system

This commit is contained in:
Aaron William Po
2023-04-23 17:25:39 -04:00
parent 9504da33d6
commit 58d30b605f
27 changed files with 699 additions and 125 deletions

View File

@@ -50,7 +50,7 @@ const getLikeCount = async (
) => {
const id = req.query.id as string;
const likes = await DBClient.instance.beerPostLike.count({
const likeCount = await DBClient.instance.beerPostLike.count({
where: { beerPostId: id },
});
@@ -58,7 +58,7 @@ const getLikeCount = async (
success: true,
message: 'Successfully retrieved like count.',
statusCode: 200,
payload: { likeCount: likes },
payload: { likeCount },
});
};

View File

@@ -0,0 +1,97 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const sendLikeRequest = async (
req: UserExtendedNextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id! as string;
const user = req.user!;
const breweryPost = await DBClient.instance.breweryPost.findUnique({
where: { id },
});
if (!breweryPost) {
throw new ServerError('Could not find a brewery post with that id', 404);
}
const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({
where: { breweryPostId: breweryPost.id, likedById: user.id },
});
const jsonResponse = {
success: true as const,
message: '',
statusCode: 200 as const,
};
if (alreadyLiked) {
await DBClient.instance.breweryPostLike.delete({
where: { id: alreadyLiked.id },
});
jsonResponse.message = 'Successfully unliked brewery post';
} else {
await DBClient.instance.breweryPostLike.create({
data: { breweryPostId: breweryPost.id, likedById: user.id },
});
jsonResponse.message = 'Successfully liked brewery post';
}
res.status(200).json(jsonResponse);
};
const getLikeCount = async (
req: NextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id! as string;
const breweryPost = await DBClient.instance.breweryPost.findUnique({
where: { id },
});
if (!breweryPost) {
throw new ServerError('Could not find a brewery post with that id', 404);
}
const likeCount = await DBClient.instance.breweryPostLike.count({
where: { breweryPostId: breweryPost.id },
});
res.status(200).json({
success: true,
message: 'Successfully retrieved like count',
statusCode: 200,
payload: { likeCount },
});
};
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
sendLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
getLikeCount,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,49 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import DBClient from '@/prisma/DBClient';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const checkIfLiked = async (
req: UserExtendedNextApiRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const id = req.query.id as string;
const alreadyLiked = await DBClient.instance.breweryPostLike.findFirst({
where: {
breweryPostId: id,
likedById: user.id,
},
});
res.status(200).json({
success: true,
message: alreadyLiked ? 'Brewery post is liked.' : 'Brewery post is not liked.',
statusCode: 200,
payload: { isLiked: !!alreadyLiked },
});
};
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({
querySchema: z.object({
id: z.string().uuid(),
}),
}),
checkIfLiked,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,54 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import DBClient from '@/prisma/DBClient';
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface GetBreweryPostsRequest extends NextApiRequest {
query: {
pageNum: string;
pageSize: string;
};
}
const getBreweryPosts = async (
req: GetBreweryPostsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const pageNum = parseInt(req.query.pageNum, 10);
const pageSize = parseInt(req.query.pageSize, 10);
const breweryPosts = await getAllBreweryPosts(pageNum, pageSize);
const breweryPostCount = await DBClient.instance.breweryPost.count();
res.setHeader('X-Total-Count', breweryPostCount);
res.status(200).json({
message: 'Brewery posts retrieved successfully',
statusCode: 200,
payload: breweryPosts,
success: true,
});
};
const router = createRouter<
GetBreweryPostsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: z.object({
pageNum: z.string().regex(/^\d+$/),
pageSize: z.string().regex(/^\d+$/),
}),
}),
getBreweryPosts,
);
const handler = router.handler();
export default handler;

View File

@@ -15,7 +15,7 @@ interface CreateBeerPageProps {
types: BeerType[];
}
const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
const CreateBeerPost: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
return (
<FormPageLayout
headingText="Create a new beer"
@@ -40,4 +40,4 @@ export const getServerSideProps = withPageAuthRequired<CreateBeerPageProps>(asyn
};
});
export default Create;
export default CreateBeerPost;

View File

@@ -10,8 +10,8 @@ import { useInView } from 'react-intersection-observer';
import Spinner from '@/components/ui/Spinner';
import useBeerPosts from '@/hooks/useBeerPosts';
import BeerPostLoadingCard from '@/components/BeerIndex/BeerPostLoadingCard';
import { FaArrowUp, FaPlus } from 'react-icons/fa';
import LoadingCard from '@/components/ui/LoadingCard';
const BeerPage: NextPage = () => {
const { user } = useContext(UserContext);
@@ -40,7 +40,10 @@ const BeerPage: NextPage = () => {
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
<header className="my-10 flex justify-between lg:flex-row">
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
<div>
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
<h2 className="text-2xl font-bold lg:text-4xl">Beers</h2>
</div>
{!!user && (
<div
className="tooltip tooltip-left h-full"
@@ -70,7 +73,7 @@ const BeerPage: NextPage = () => {
{(isLoading || isLoadingMore) && (
<>
{Array.from({ length: PAGE_SIZE }, (_, i) => (
<BeerPostLoadingCard key={i} />
<LoadingCard key={i} />
))}
</>
)}

View File

@@ -1,70 +1,115 @@
import { GetServerSideProps, NextPage } from 'next';
import Link from 'next/link';
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
import BreweryCard from '@/components/BreweryIndex/BreweryCard';
import LoadingCard from '@/components/ui/LoadingCard';
import Spinner from '@/components/ui/Spinner';
import UserContext from '@/contexts/userContext';
import useBreweryPosts from '@/hooks/useBreweryPosts';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { FC } from 'react';
import Image from 'next/image';
import { NextPage } from 'next';
import { useContext, MutableRefObject, useRef } from 'react';
import { Link } from 'react-daisyui';
import { FaPlus, FaArrowUp } from 'react-icons/fa';
import { useInView } from 'react-intersection-observer';
import { z } from 'zod';
interface BreweryPageProps {
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
}
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
brewery,
}) => {
const BreweryPage: NextPage<BreweryPageProps> = () => {
const PAGE_SIZE = 6;
const { breweryPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } =
useBreweryPosts({
pageSize: PAGE_SIZE,
});
const { ref: lastBreweryPostRef } = useInView({
onChange: (visible) => {
if (!visible || isAtEnd) return;
setSize(size + 1);
},
});
const { user } = useContext(UserContext);
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
return (
<div className="card" key={brewery.id}>
<figure className="card-image h-96">
{brewery.breweryImages.length > 0 && (
<Image
src={brewery.breweryImages[0].path}
alt={brewery.name}
width="1029"
height="110"
/>
)}
</figure>
<div className="card-body space-y-3">
<div>
<h2 className="text-3xl font-bold">
<Link href={`/breweries/${brewery.id}`}>{brewery.name}</Link>
</h2>
<h3 className="text-xl font-semibold">{brewery.location}</h3>
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
<header className="my-10 flex justify-between lg:flex-row">
<div>
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
<h2 className="text-2xl font-bold lg:text-4xl">Breweries</h2>
</div>
{!!user && (
<div
className="tooltip tooltip-left h-full"
data-tip="Create a new brewery post"
>
<Link href="/breweries/create" className="btn-ghost btn-sm btn">
<FaPlus />
</Link>
</div>
)}
</header>
<div className="grid gap-6 xl:grid-cols-2">
{!!breweryPosts.length && !isLoading && (
<>
{breweryPosts.map((breweryPost) => {
return (
<div
key={breweryPost.id}
ref={
breweryPosts[breweryPosts.length - 1] === breweryPost
? lastBreweryPostRef
: undefined
}
>
<BreweryCard brewery={breweryPost} />
</div>
);
})}
</>
)}
{(isLoading || isLoadingMore) && (
<>
{Array.from({ length: PAGE_SIZE }, (_, i) => (
<LoadingCard key={i} />
))}
</>
)}
</div>
{(isLoading || isLoadingMore) && (
<div className="flex h-32 w-full items-center justify-center">
<Spinner size="sm" />
</div>
)}
{isAtEnd && !isLoading && (
<div className="flex h-20 items-center justify-center text-center">
<div
className="tooltip tooltip-bottom"
data-tip="Scroll back to top of page."
>
<button
type="button"
className="btn-ghost btn-sm btn"
aria-label="Scroll back to top of page."
onClick={() => {
pageRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
>
<FaArrowUp />
</button>
</div>
</div>
)}
</div>
</div>
);
};
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
return (
<>
<div className="flex items-center justify-center bg-base-100">
<div className="my-10 flex w-10/12 flex-col space-y-4">
<header className="my-10">
<div className="space-y-2">
<h1 className="text-6xl font-bold">Breweries</h1>
</div>
</header>
<div className="grid gap-5 md:grid-cols-1 xl:grid-cols-2">
{breweryPosts.map((brewery) => {
return <BreweryCard brewery={brewery} key={brewery.id} />;
})}
</div>
</div>
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps<BreweryPageProps> = async () => {
const breweryPosts = await getAllBreweryPosts();
return {
props: { breweryPosts: JSON.parse(JSON.stringify(breweryPosts)) },
};
};
export default BreweryPage;