diff --git a/hooks/useBeerPostSearch.ts b/hooks/useBeerPostSearch.ts new file mode 100644 index 0000000..f029f0d --- /dev/null +++ b/hooks/useBeerPostSearch.ts @@ -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; diff --git a/package-lock.json b/package-lock.json index 6c13661..c7f85de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d674fd3..c6443f4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/api/beers/search.ts b/pages/api/beers/search.ts new file mode 100644 index 0000000..7a42067 --- /dev/null +++ b/pages/api/beers/search.ts @@ -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; +} + +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> +>(); + +router.get(validateRequest({}), search); + +const handler = router.handler(NextConnectOptions); + +export default handler; diff --git a/pages/beers/[id]/index.tsx b/pages/beers/[id]/index.tsx index 13892b2..2412704 100644 --- a/pages/beers/[id]/index.tsx +++ b/pages/beers/[id]/index.tsx @@ -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 = ({ commentsPageCount, likeCount, }) => { - const [comments, setComments] = useState(beerComments); - - useEffect(() => { - setComments(beerComments); - }, [beerComments]); - return ( @@ -65,8 +58,7 @@ const BeerByIdPage: NextPage = ({
@@ -82,7 +74,6 @@ const BeerByIdPage: NextPage = ({ export const getServerSideProps: GetServerSideProps = 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 = 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)), }; diff --git a/pages/beers/search.tsx b/pages/beers/search.tsx new file mode 100644 index 0000000..5eca34b --- /dev/null +++ b/pages/beers/search.tsx @@ -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) => { + 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 ( + +
+
+
+
+ What are you looking for? + +
+
+ +
+ {!showSearchResults ? ( + + ) : ( +
+ {searchResults.map((result) => { + return ; + })} +
+ )} +
+
+
+
+ ); +}; + +export default SearchPage;