mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #13 from aaronpo97/search-feat-and-refactor
Add beer search feature and refactor components
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
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 { useRouter } from 'next/router';
|
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 { 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';
|
||||||
@@ -18,7 +17,6 @@ import FormTextArea from '../ui/forms/FormTextArea';
|
|||||||
|
|
||||||
interface BeerCommentFormProps {
|
interface BeerCommentFormProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: BeerPostQueryResult;
|
||||||
setComments: Dispatch<SetStateAction<BeerCommentQueryResultArrayT>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => {
|
const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost }) => {
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import CommentCard from './CommentCard';
|
|||||||
|
|
||||||
interface BeerPostCommentsSectionProps {
|
interface BeerPostCommentsSectionProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: BeerPostQueryResult;
|
||||||
setComments: React.Dispatch<React.SetStateAction<BeerCommentQueryResultArrayT>>;
|
|
||||||
comments: BeerCommentQueryResultArrayT;
|
comments: BeerCommentQueryResultArrayT;
|
||||||
commentsPageCount: number;
|
commentsPageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
||||||
beerPost,
|
beerPost,
|
||||||
setComments,
|
|
||||||
comments,
|
comments,
|
||||||
commentsPageCount,
|
commentsPageCount,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -30,7 +29,7 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({
|
|||||||
<div className="card h-96 bg-base-300">
|
<div className="card h-96 bg-base-300">
|
||||||
<div className="card-body h-full">
|
<div className="card-body h-full">
|
||||||
{user ? (
|
{user ? (
|
||||||
<BeerCommentForm beerPost={beerPost} setComments={setComments} />
|
<BeerCommentForm beerPost={beerPost} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
<span className="text-lg font-bold">Log in to leave a comment.</span>
|
||||||
|
|||||||
34
components/BeerIndex/BeerIndexPaginationBar.tsx
Normal file
34
components/BeerIndex/BeerIndexPaginationBar.tsx
Normal 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;
|
||||||
@@ -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;
|
|
||||||
@@ -44,7 +44,7 @@ const Navbar = () => {
|
|||||||
return (
|
return (
|
||||||
<nav className="navbar bg-primary text-primary-content">
|
<nav className="navbar bg-primary text-primary-content">
|
||||||
<div className="flex-1">
|
<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>
|
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,8 +68,8 @@ const Navbar = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<div className="dropdown dropdown-end">
|
<div className="dropdown-end dropdown">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
||||||
<span className="w-10 rounded-full">
|
<span className="w-10 rounded-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
const Spinner = () => (
|
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">
|
<div role="status" className="flex flex-col items-center justify-center rounded-3xl">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-[100px] animate-spin fill-success text-gray-500"
|
className={`${spinnerWidths[size]} animate-spin fill-success text-gray-500`}
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -19,5 +33,6 @@ const Spinner = () => (
|
|||||||
<span className="sr-only">Loading...</span>
|
<span className="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
|||||||
@@ -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="align-center my-20 flex h-fit flex-col items-center justify-center">
|
||||||
<div className="w-8/12">
|
<div className="w-8/12">
|
||||||
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
|
<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" />
|
<BiArrowBack className="text-xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const MAX_AGE = 60 * 60 * 8; // 8 hours
|
|||||||
export function setTokenCookie(res: NextApiResponse, token: string) {
|
export function setTokenCookie(res: NextApiResponse, token: string) {
|
||||||
const cookie = serialize(TOKEN_NAME, token, {
|
const cookie = serialize(TOKEN_NAME, token, {
|
||||||
maxAge: MAX_AGE,
|
maxAge: MAX_AGE,
|
||||||
httpOnly: true,
|
httpOnly: false,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
|||||||
29
hooks/useBeerPostSearch.ts
Normal file
29
hooks/useBeerPostSearch.ts
Normal 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;
|
||||||
@@ -3,15 +3,18 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const useUser = () => {
|
const useUser = () => {
|
||||||
// check cookies for user
|
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSWR('/api/users/current', async (url) => {
|
} = useSWR('/api/users/current', async (url) => {
|
||||||
|
if (!document.cookie.includes('token')) {
|
||||||
|
throw new Error('No token cookie found');
|
||||||
|
}
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ const useUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
||||||
|
console.log(parsedPayload);
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error(parsedPayload.error.message);
|
throw new Error(parsedPayload.error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"cookie": "0.5.0",
|
"cookie": "0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "^13.2.1",
|
"next": "^13.2.1",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.14.1",
|
"@types/node": "^18.14.1",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
@@ -1464,6 +1466,12 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
||||||
@@ -11034,6 +11042,12 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"@types/mdast": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -23,20 +23,21 @@
|
|||||||
"cookie": "0.5.0",
|
"cookie": "0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
"lodash": "^4.17.21",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "^13.2.1",
|
"next": "^13.2.1",
|
||||||
"passport-local": "^1.0.0",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"pino-pretty": "^9.3.0",
|
"passport-local": "^1.0.0",
|
||||||
"pino": "^8.11.0",
|
"pino": "^8.11.0",
|
||||||
|
"pino-pretty": "^9.3.0",
|
||||||
|
"react": "18.2.0",
|
||||||
"react-daisyui": "^3.0.3",
|
"react-daisyui": "^3.0.3",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-email": "^1.7.15",
|
"react-email": "^1.7.15",
|
||||||
"react-hook-form": "^7.43.2",
|
"react-hook-form": "^7.43.2",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.7.1",
|
||||||
"react": "18.2.0",
|
|
||||||
"sparkpost": "^2.1.4",
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"zod": "^3.20.6"
|
"zod": "^3.20.6"
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.14.1",
|
"@types/node": "^18.14.1",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
|
|||||||
57
pages/api/beers/search.ts
Normal file
57
pages/api/beers/search.ts
Normal 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;
|
||||||
@@ -17,12 +17,6 @@ interface RegisterUserRequest extends NextApiRequest {
|
|||||||
body: z.infer<typeof CreateUserValidationSchema>;
|
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 registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
||||||
const [usernameTaken, emailTaken] = await Promise.all([
|
const [usernameTaken, emailTaken] = await Promise.all([
|
||||||
findUserByUsername(req.body.username),
|
findUserByUsername(req.body.username),
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import { NextPage, GetServerSideProps } from 'next';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
|
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
|
||||||
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
|
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
|
||||||
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
|
|
||||||
import DBClient from '@/prisma/DBClient';
|
|
||||||
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
||||||
@@ -17,6 +14,8 @@ import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
|||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { BeerPost } from '@prisma/client';
|
import { BeerPost } from '@prisma/client';
|
||||||
|
import getBeerPostLikeCount from '@/services/BeerPostLike/getBeerPostLikeCount';
|
||||||
|
import getBeerCommentCount from '@/services/BeerComment/getBeerCommentCount';
|
||||||
|
|
||||||
interface BeerPageProps {
|
interface BeerPageProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: BeerPostQueryResult;
|
||||||
@@ -36,12 +35,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
commentsPageCount,
|
commentsPageCount,
|
||||||
likeCount,
|
likeCount,
|
||||||
}) => {
|
}) => {
|
||||||
const [comments, setComments] = useState(beerComments);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setComments(beerComments);
|
|
||||||
}, [beerComments]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Head>
|
<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">
|
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
|
||||||
<BeerPostCommentsSection
|
<BeerPostCommentsSection
|
||||||
beerPost={beerPost}
|
beerPost={beerPost}
|
||||||
comments={comments}
|
comments={beerComments}
|
||||||
setComments={setComments}
|
|
||||||
commentsPageCount={commentsPageCount}
|
commentsPageCount={commentsPageCount}
|
||||||
/>
|
/>
|
||||||
<div className="md:w-[40%]">
|
<div className="md:w-[40%]">
|
||||||
@@ -82,7 +74,6 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
||||||
const beerPost = await getBeerPostById(context.params!.id! as string);
|
const beerPost = await getBeerPostById(context.params!.id! as string);
|
||||||
|
|
||||||
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
|
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
|
||||||
|
|
||||||
if (!beerPost) {
|
if (!beerPost) {
|
||||||
@@ -97,19 +88,17 @@ export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (cont
|
|||||||
{ id: beerPost.id },
|
{ id: beerPost.id },
|
||||||
{ pageSize, pageNum: beerCommentPageNum },
|
{ pageSize, pageNum: beerCommentPageNum },
|
||||||
);
|
);
|
||||||
const numberOfPosts = await DBClient.instance.beerComment.count({
|
|
||||||
where: { beerPostId: beerPost.id },
|
const commentCount = await getBeerCommentCount(beerPost.id);
|
||||||
});
|
|
||||||
const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0;
|
const commentPageCount = commentCount ? Math.ceil(commentCount / pageSize) : 0;
|
||||||
const likeCount = await DBClient.instance.beerPostLike.count({
|
const likeCount = await getBeerPostLikeCount(beerPost.id);
|
||||||
where: { beerPostId: beerPost.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
||||||
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
||||||
beerComments: JSON.parse(JSON.stringify(beerComments)),
|
beerComments: JSON.parse(JSON.stringify(beerComments)),
|
||||||
commentsPageCount: JSON.parse(JSON.stringify(pageCount)),
|
commentsPageCount: JSON.parse(JSON.stringify(commentPageCount)),
|
||||||
likeCount: JSON.parse(JSON.stringify(likeCount)),
|
likeCount: JSON.parse(JSON.stringify(likeCount)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import Layout from '@/components/ui/Layout';
|
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 BeerCard from '@/components/BeerIndex/BeerCard';
|
||||||
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@@ -26,16 +26,24 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
|
|||||||
<meta name="description" content="Beer posts" />
|
<meta name="description" content="Beer posts" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex items-center justify-center bg-base-100">
|
<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">
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{initialBeerPosts.map((post) => {
|
{initialBeerPosts.map((post) => {
|
||||||
return <BeerCard post={post} key={post.id} />;
|
return <BeerCard post={post} key={post.id} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Pagination pageNum={pageNum} pageCount={pageCount} />
|
<BeerIndexPaginationBar pageNum={pageNum} pageCount={pageCount} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
79
pages/beers/search.tsx
Normal file
79
pages/beers/search.tsx
Normal 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;
|
||||||
11
services/BeerComment/getBeerCommentCount.ts
Normal file
11
services/BeerComment/getBeerCommentCount.ts
Normal 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;
|
||||||
11
services/BeerPostLike/getBeerPostLikeCount.ts
Normal file
11
services/BeerPostLike/getBeerPostLikeCount.ts
Normal 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;
|
||||||
Reference in New Issue
Block a user