Merge pull request #13 from aaronpo97/search-feat-and-refactor

Add beer search feature and refactor components
This commit is contained in:
Aaron Po
2023-03-27 19:15:14 -04:00
committed by GitHub
19 changed files with 312 additions and 105 deletions

View File

@@ -1,10 +1,9 @@
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, FunctionComponent, useState, useEffect } from 'react';
import { FunctionComponent, useState, useEffect } from 'react';
import { Rating } from 'react-daisyui';
import { useForm, SubmitHandler } from 'react-hook-form';
import { z } from 'zod';
@@ -18,7 +17,6 @@ import FormTextArea from '../ui/forms/FormTextArea';
interface BeerCommentFormProps {
beerPost: BeerPostQueryResult;
setComments: Dispatch<SetStateAction<BeerCommentQueryResultArrayT>>;
}
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => {

View File

@@ -9,14 +9,13 @@ import CommentCard from './CommentCard';
interface BeerPostCommentsSectionProps {
beerPost: BeerPostQueryResult;
setComments: React.Dispatch<React.SetStateAction<BeerCommentQueryResultArrayT>>;
comments: BeerCommentQueryResultArrayT;
commentsPageCount: number;
}
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
beerPost,
setComments,
comments,
commentsPageCount,
}) => {
@@ -30,7 +29,7 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
<div className="card h-96 bg-base-300">
<div className="card-body h-full">
{user ? (
<BeerCommentForm beerPost={beerPost} setComments={setComments} />
<BeerCommentForm beerPost={beerPost} />
) : (
<div className="flex h-full flex-col items-center justify-center">
<span className="text-lg font-bold">Log in to leave a comment.</span>

View File

@@ -0,0 +1,34 @@
import Link from 'next/link';
import { FC } from 'react';
interface PaginationProps {
pageNum: number;
pageCount: number;
}
const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) => {
return (
<div className="btn-group">
<Link
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
scroll={false}
prefetch={true}
>
«
</Link>
<button className="btn">Page {pageNum}</button>
<Link
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
scroll={false}
prefetch={true}
>
»
</Link>
</div>
);
};
export default BeerIndexPaginationBar;

View File

@@ -1,37 +0,0 @@
import { useRouter } from 'next/router';
import { FC } from 'react';
interface PaginationProps {
pageNum: number;
pageCount: number;
}
const Pagination: FC<PaginationProps> = ({ pageCount, pageNum }) => {
const router = useRouter();
return (
<div className="btn-group">
<button
className="btn"
disabled={pageNum <= 1}
onClick={async () =>
router.push({ pathname: '/beers', query: { page_num: pageNum - 1 } })
}
>
«
</button>
<button className="btn">Page {pageNum}</button>
<button
className="btn"
disabled={pageNum >= pageCount}
onClick={async () =>
router.push({ pathname: '/beers', query: { page_num: pageNum + 1 } })
}
>
»
</button>
</div>
);
};
export default Pagination;

View File

@@ -44,7 +44,7 @@ const Navbar = () => {
return (
<nav className="navbar bg-primary text-primary-content">
<div className="flex-1">
<Link className="btn btn-ghost text-3xl normal-case" href="/">
<Link className="btn-ghost btn text-3xl normal-case" href="/">
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
</Link>
</div>
@@ -68,8 +68,8 @@ const Navbar = () => {
</ul>
</div>
<div className="flex-none lg:hidden">
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle">
<div className="dropdown-end dropdown">
<label tabIndex={0} className="btn-ghost btn-circle btn">
<span className="w-10 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,23 +1,38 @@
const Spinner = () => (
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
<svg
aria-hidden="true"
className="w-[100px] animate-spin fill-success text-gray-500"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
);
import { FC } from 'react';
interface SpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg';
}
const Spinner: FC<SpinnerProps> = ({ size = 'md' }) => {
const spinnerWidths: Record<NonNullable<SpinnerProps['size']>, `w-[${number}px]`> = {
xs: 'w-[10px]',
sm: 'w-[20px]',
md: 'w-[100px]',
lg: 'w-[150px]',
};
return (
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
<svg
aria-hidden="true"
className={`${spinnerWidths[size]} animate-spin fill-success text-gray-500`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
);
};
export default Spinner;

View File

@@ -22,7 +22,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
<div className="align-center my-20 flex h-fit flex-col items-center justify-center">
<div className="w-8/12">
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
<Link href={backLink} className="btn btn-ghost btn-sm">
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
<BiArrowBack className="text-xl" />
</Link>
</div>

View File

@@ -8,7 +8,7 @@ export const MAX_AGE = 60 * 60 * 8; // 8 hours
export function setTokenCookie(res: NextApiResponse, token: string) {
const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE,
httpOnly: true,
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',

View File

@@ -0,0 +1,29 @@
import useSWR from 'swr';
import { beerPostQueryResultArraySchema } from '@/services/BeerPost/schema/BeerPostQueryResult';
const useBeerPostSearch = (query: string | undefined) => {
const { data, isLoading, error } = useSWR(
`/api/beers/search?search=${query}`,
async (url) => {
if (!query) return [];
const response = await fetch(url);
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const result = beerPostQueryResultArraySchema.parse(json);
return result;
},
);
return {
searchResults: data,
searchError: error as Error | undefined,
isLoading,
};
};
export default useBeerPostSearch;

View File

@@ -3,15 +3,18 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem
import useSWR from 'swr';
const useUser = () => {
// check cookies for user
const {
data: user,
error,
isLoading,
} = useSWR('/api/users/current', async (url) => {
if (!document.cookie.includes('token')) {
throw new Error('No token cookie found');
}
const response = await fetch(url);
if (!response.ok) {
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
throw new Error(response.statusText);
}
@@ -23,6 +26,7 @@ const useUser = () => {
}
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
console.log(parsedPayload);
if (!parsedPayload.success) {
throw new Error(parsedPayload.error.message);
}

14
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"cookie": "0.5.0",
"date-fns": "^2.29.3",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"multer": "^2.0.0-rc.4",
"multer-storage-cloudinary": "^4.0.0",
"next": "^13.2.1",
@@ -42,6 +43,7 @@
"@types/cookie": "^0.5.1",
"@types/ejs": "^3.1.2",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/multer": "^1.4.7",
"@types/node": "^18.14.1",
"@types/passport-local": "^1.0.35",
@@ -1464,6 +1466,12 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -11034,6 +11042,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",

View File

@@ -23,20 +23,21 @@
"cookie": "0.5.0",
"date-fns": "^2.29.3",
"jsonwebtoken": "^9.0.0",
"multer-storage-cloudinary": "^4.0.0",
"lodash": "^4.17.21",
"multer": "^2.0.0-rc.4",
"next-connect": "^1.0.0-next.3",
"multer-storage-cloudinary": "^4.0.0",
"next": "^13.2.1",
"passport-local": "^1.0.0",
"next-connect": "^1.0.0-next.3",
"passport": "^0.6.0",
"pino-pretty": "^9.3.0",
"passport-local": "^1.0.0",
"pino": "^8.11.0",
"pino-pretty": "^9.3.0",
"react": "18.2.0",
"react-daisyui": "^3.0.3",
"react-dom": "18.2.0",
"react-email": "^1.7.15",
"react-hook-form": "^7.43.2",
"react-icons": "^4.7.1",
"react": "18.2.0",
"sparkpost": "^2.1.4",
"swr": "^2.0.3",
"zod": "^3.20.6"
@@ -46,6 +47,7 @@
"@types/cookie": "^0.5.1",
"@types/ejs": "^3.1.2",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/multer": "^1.4.7",
"@types/node": "^18.14.1",
"@types/passport-local": "^1.0.35",

57
pages/api/beers/search.ts Normal file
View File

@@ -0,0 +1,57 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import DBClient from '@/prisma/DBClient';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
const SearchSchema = z.object({
search: z.string().min(1),
});
interface SearchAPIRequest extends NextApiRequest {
query: z.infer<typeof SearchSchema>;
}
const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
const { search: query } = req.query;
const beers: BeerPostQueryResult[] = await DBClient.instance.beerPost.findMany({
select: {
id: true,
name: true,
ibu: true,
abv: true,
createdAt: true,
description: true,
postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } },
type: { select: { name: true, id: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
},
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
{ type: { name: { contains: query, mode: 'insensitive' } } },
],
},
});
res.status(200).json(beers);
};
const router = createRouter<
SearchAPIRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(validateRequest({}), search);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -17,12 +17,6 @@ interface RegisterUserRequest extends NextApiRequest {
body: z.infer<typeof CreateUserValidationSchema>;
}
const { BASE_URL } = process.env;
if (!BASE_URL) {
throw new ServerError('BASE_URL env variable is not set.', 500);
}
const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
const [usernameTaken, emailTaken] = await Promise.all([
findUserByUsername(req.body.username),

View File

@@ -2,14 +2,11 @@ import { NextPage, GetServerSideProps } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import { useState, useEffect } from 'react';
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
import Layout from '@/components/ui/Layout';
import DBClient from '@/prisma/DBClient';
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
@@ -17,6 +14,8 @@ import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPost } from '@prisma/client';
import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount';
interface BeerPageProps {
beerPost: BeerPostQueryResult;
@@ -36,12 +35,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
commentsPageCount,
likeCount,
}) => {
const [comments, setComments] = useState(beerComments);
useEffect(() => {
setComments(beerComments);
}, [beerComments]);
return (
<Layout>
<Head>
@@ -65,8 +58,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
<BeerPostCommentsSection
beerPost={beerPost}
comments={comments}
setComments={setComments}
comments={beerComments}
commentsPageCount={commentsPageCount}
/>
<div className="md:w-[40%]">
@@ -82,7 +74,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const beerPost = await getBeerPostById(context.params!.id! as string);
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
if (!beerPost) {
@@ -97,19 +88,17 @@ export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (cont
{ id: beerPost.id },
{ pageSize, pageNum: beerCommentPageNum },
);
const numberOfPosts = await DBClient.instance.beerComment.count({
where: { beerPostId: beerPost.id },
});
const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0;
const likeCount = await DBClient.instance.beerPostLike.count({
where: { beerPostId: beerPost.id },
});
const commentCount = await getBeerCommentCount(beerPost.id);
const commentPageCount = commentCount ? Math.ceil(commentCount / pageSize) : 0;
const likeCount = await getBeerPostLikeCount(beerPost.id);
const props = {
beerPost: JSON.parse(JSON.stringify(beerPost)),
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
beerComments: JSON.parse(JSON.stringify(beerComments)),
commentsPageCount: JSON.parse(JSON.stringify(pageCount)),
commentsPageCount: JSON.parse(JSON.stringify(commentPageCount)),
likeCount: JSON.parse(JSON.stringify(likeCount)),
};

View File

@@ -4,7 +4,7 @@ import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts';
import { useRouter } from 'next/router';
import DBClient from '@/prisma/DBClient';
import Layout from '@/components/ui/Layout';
import Pagination from '@/components/BeerIndex/Pagination';
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
import BeerCard from '@/components/BeerIndex/BeerCard';
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
import Head from 'next/head';
@@ -26,16 +26,24 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
<meta name="description" content="Beer posts" />
</Head>
<div className="flex items-center justify-center bg-base-100">
<main className="my-10 flex w-10/12 flex-col space-y-4">
<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">The Biergarten Index</h1>
<h2 className="text-2xl font-bold">
Page {pageNum} of {pageCount}
</h2>
</div>
</header>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{initialBeerPosts.map((post) => {
return <BeerCard post={post} key={post.id} />;
})}
</div>
<div className="flex justify-center">
<Pagination pageNum={pageNum} pageCount={pageCount} />
<BeerIndexPaginationBar pageNum={pageNum} pageCount={pageCount} />
</div>
</main>
</div>
</div>
</Layout>
);

79
pages/beers/search.tsx Normal file
View File

@@ -0,0 +1,79 @@
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
import { useRouter } from 'next/router';
import BeerCard from '@/components/BeerIndex/BeerCard';
import { ChangeEvent, useEffect, useState } from 'react';
import Spinner from '@/components/ui/Spinner';
import debounce from 'lodash/debounce';
import useBeerPostSearch from '@/hooks/useBeerPostSearch';
import FormLabel from '@/components/ui/forms/FormLabel';
const DEBOUNCE_DELAY = 300;
const SearchPage: NextPage = () => {
const router = useRouter();
const querySearch = router.query.search as string | undefined;
const [searchValue, setSearchValue] = useState(querySearch || '');
const { searchResults, isLoading, searchError } = useBeerPostSearch(searchValue);
const debounceSearch = debounce((value: string) => {
router.push({
pathname: '/beers/search',
query: { search: value },
});
}, DEBOUNCE_DELAY);
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setSearchValue(value);
debounceSearch(value);
};
useEffect(() => {
debounce(() => {
if (!querySearch || searchValue) {
return;
}
setSearchValue(searchValue);
}, DEBOUNCE_DELAY)();
}, [querySearch, searchValue]);
const showSearchResults = !isLoading && searchResults && !searchError;
return (
<Layout>
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="h-full w-full space-y-20">
<div className="flex h-[50%] w-full items-center justify-center bg-base-200">
<div className="w-8/12">
<FormLabel htmlFor="search">What are you looking for?</FormLabel>
<input
type="text"
id="search"
className="input-bordered input w-full rounded-lg"
onChange={onChange}
value={searchValue}
/>
</div>
</div>
<div className="flex flex-col items-center justify-center">
{!showSearchResults ? (
<Spinner size="lg" />
) : (
<div className="grid w-8/12 gap-4 md:grid-cols-2 lg:grid-cols-3">
{searchResults.map((result) => {
return <BeerCard key={result.id} post={result} />;
})}
</div>
)}
</div>
</div>
</div>
</Layout>
);
};
export default SearchPage;

View File

@@ -0,0 +1,11 @@
import DBClient from '@/prisma/DBClient';
const getBeerCommentCount = async (beerPostId: string) => {
const count = await DBClient.instance.beerComment.count({
where: { beerPostId },
});
return count;
};
export default getBeerCommentCount;

View File

@@ -0,0 +1,11 @@
import DBClient from '@/prisma/DBClient';
const getBeerPostLikeCount = async (beerPostId: string) => {
const count = await DBClient.instance.beerPostLike.count({
where: { beerPostId },
});
return count;
};
export default getBeerPostLikeCount;