Move next js project to archive (#207)

This commit is contained in:
Aaron Po
2026-04-20 02:30:25 -04:00
committed by GitHub
parent 92ec16ce93
commit d47e3ed7f0
347 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { NextPage } from 'next';
import Head from 'next/head';
const NotFound: NextPage = () => {
return (
<>
<Head>
<title>404 Page Not Found</title>
<meta name="description" content="404 Page Not Found" />
</Head>
<div className="mx-2 flex h-dvh flex-col items-center justify-center text-center lg:mx-0">
<h1 className="text-3xl font-bold lg:text-5xl">404: Not Found</h1>
<h2 className="text-lg font-bold">
Sorry, the page you are looking for does not exist.
</h2>
</div>
</>
);
};
export default NotFound;

View File

@@ -0,0 +1,21 @@
import { NextPage } from 'next';
import Head from 'next/head';
const ServerErrorPage: NextPage = () => {
return (
<>
<Head>
<title>500 Internal Server Error</title>
<meta name="description" content="500 Internal Server Error" />
</Head>
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
<h1 className="text-2xl font-bold lg:text-4xl">500: Something Went Wrong</h1>
<h2 className="text-lg font-bold">
Please try again later or contact us if the problem persists.
</h2>
</div>
</>
);
};
export default ServerErrorPage;

View File

@@ -0,0 +1,49 @@
import { AuthProvider } from '@/contexts/UserContext';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import { themeChange } from 'theme-change';
import { Analytics } from '@vercel/analytics/react';
import { Nunito_Sans } from 'next/font/google';
import Head from 'next/head';
import Layout from '@/components/ui/Layout';
import CustomToast from '@/components/ui/CustomToast';
const font = Nunito_Sans({ subsets: ['latin-ext'] });
const App = ({ Component, pageProps }: AppProps) => {
useEffect(() => {
themeChange(false);
}, []);
return (
<>
<style jsx global>
{`
html {
font-family: ${font.style.fontFamily};
}
`}
</style>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
</Head>
<AuthProvider>
<Layout>
<CustomToast>
<Component {...pageProps} />
</CustomToast>
</Layout>
</AuthProvider>
<Analytics />
</>
);
};
export default App;

View File

@@ -0,0 +1,32 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="favicon/favicon-16x16.png"
/>
<link rel="manifest" href="favicon/site.webmanifest" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -0,0 +1,43 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { CommentRequest } from '@/controllers/comments/types';
import {
checkIfBeerCommentOwner,
deleteBeerPostComment,
editBeerPostComment,
} from '@/controllers/comments/beer-comments';
const router = createRouter<
CommentRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router
.delete(
validateRequest({
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBeerCommentOwner,
deleteBeerPostComment,
)
.put(
validateRequest({
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,
checkIfBeerCommentOwner,
editBeerPostComment,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,39 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
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 CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import {
createBeerPostComment,
getAllBeerPostComments,
} from '@/controllers/comments/beer-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
const router = createRouter<
// @TODO: Fix this any type
any,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({
bodySchema: CreateCommentValidationSchema,
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createBeerPostComment,
);
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeerPostComments,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,31 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { createRouter } from 'next-connect';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
import { UploadImagesRequest } from '@/controllers/images/types';
import { processBeerImageData } from '@/controllers/images/beer-images';
const router = createRouter<
UploadImagesRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
// @ts-expect-error
uploadMiddlewareMultiple,
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
processBeerImageData,
);
const handler = router.handler(NextConnectOptions);
export default handler;
export const config = { api: { bodyParser: false } };

View File

@@ -0,0 +1,44 @@
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import { EditBeerPostRequest } from '@/controllers/posts/beer-posts/types';
import {
checkIfBeerPostOwner,
editBeerPost,
deleteBeerPost,
} from '@/controllers/posts/beer-posts';
import EditBeerPostValidationSchema from '@/services/posts/beer-post/schema/EditBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
const router = createRouter<
EditBeerPostRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router
.put(
validateRequest({
bodySchema: EditBeerPostValidationSchema,
querySchema: z.object({ postId: z.string() }),
}),
getCurrentUser,
checkIfBeerPostOwner,
editBeerPost,
)
.delete(
validateRequest({ querySchema: z.object({ postId: z.string() }) }),
getCurrentUser,
checkIfBeerPostOwner,
deleteBeerPost,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,33 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { NextApiResponse } from 'next';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import {
sendBeerPostLikeRequest,
getBeerPostLikeCount,
} from '@/controllers/likes/beer-posts-likes';
import { LikeRequest } from '@/controllers/likes/types';
const router = createRouter<
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBeerPostLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerPostLikeCount,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,24 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { checkIfBeerPostIsLiked } from '@/controllers/likes/beer-posts-likes';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
checkIfBeerPostIsLiked,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBeerPostRecommendations } from '@/controllers/posts/beer-posts';
import { GetBeerRecommendationsRequest } from '@/controllers/posts/beer-posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetBeerRecommendationsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getBeerPostRecommendations,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { createRouter } from 'next-connect';
import CreateBeerPostValidationSchema from '@/services/posts/beer-post/schema/CreateBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { createBeerPost } from '@/controllers/posts/beer-posts';
import { CreateBeerPostRequest } from '@/controllers/posts/beer-posts/types';
const router = createRouter<
CreateBeerPostRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: CreateBeerPostValidationSchema }),
getCurrentUser,
createBeerPost,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBeerPosts } from '@/controllers/posts/beer-posts';
import { GetAllBeerPostsRequest } from '@/controllers/posts/beer-posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetAllBeerPostsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema,
}),
getBeerPosts,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,67 @@
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/posts/beer-post/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: z.infer<typeof BeerPostQueryResult>[] =
await DBClient.instance.beerPost.findMany({
select: {
id: true,
name: true,
ibu: true,
abv: true,
createdAt: true,
updatedAt: true,
description: true,
postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } },
style: { select: { name: true, id: true, description: true } },
beerImages: {
select: {
alt: true,
path: true,
caption: true,
id: true,
createdAt: true,
updatedAt: true,
},
},
},
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
{ style: { 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

@@ -0,0 +1,29 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getAllBeersByBeerStyle } from '@/controllers/posts/beer-styles';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface GetAllBeersByBeerStyleRequest extends NextApiRequest {
query: { page_size: string; page_num: string; id: string };
}
const router = createRouter<
GetAllBeersByBeerStyleRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeersByBeerStyle,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,42 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import {
checkIfBeerStyleCommentOwner,
deleteBeerStyleComment,
editBeerStyleComment,
} from '@/controllers/comments/beer-style-comments';
import { CommentRequest } from '@/controllers/comments/types';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
CommentRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router
.delete(
validateRequest({
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBeerStyleCommentOwner,
deleteBeerStyleComment,
)
.put(
validateRequest({
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,
checkIfBeerStyleCommentOwner,
editBeerStyleComment,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,36 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
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 CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import { createComment, getAll } from '@/controllers/comments/beer-style-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
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: CreateCommentValidationSchema,
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createComment,
);
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAll,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,22 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBeerStyle } from '@/controllers/posts/beer-styles';
import { GetBeerStyleByIdRequest } from '@/controllers/posts/beer-styles/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetBeerStyleByIdRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerStyle,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,34 @@
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { NextApiResponse } from 'next';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import {
getBeerStyleLikeCountRequest,
sendBeerStyleLikeRequest,
} from '@/controllers/likes/beer-style-likes';
import { LikeRequest } from '@/controllers/likes/types';
const router = createRouter<
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBeerStyleLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerStyleLikeCountRequest,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,23 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { checkIfBeerStyleIsLiked } from '@/controllers/likes/beer-style-likes';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
checkIfBeerStyleIsLiked,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,24 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { createBeerStyle } from '@/controllers/posts/beer-styles';
import { CreateBeerStyleRequest } from '@/controllers/posts/beer-styles/types';
import CreateBeerStyleValidationSchema from '@/services/posts/beer-style-post/schema/CreateBeerStyleValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
CreateBeerStyleRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: CreateBeerStyleValidationSchema }),
getCurrentUser,
createBeerStyle,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,26 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBeerStyles } from '@/controllers/posts/beer-styles';
import { GetAllPostsRequest } from '@/controllers/posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetAllPostsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema,
}),
getBeerStyles,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,25 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getAllBeersByBrewery } from '@/controllers/posts/breweries';
import { GetAllPostsByConnectedPostId } from '@/controllers/posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetAllPostsByConnectedPostId,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeersByBrewery,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,44 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import {
checkIfBreweryCommentOwner,
deleteBreweryPostComment,
editBreweryPostComment,
} from '@/controllers/comments/brewery-comments';
import { CommentRequest } from '@/controllers/comments/types';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
CommentRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router
.delete(
validateRequest({
querySchema: z.object({ commentId: z.string().cuid(), postId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBreweryCommentOwner,
deleteBreweryPostComment,
)
.put(
validateRequest({
querySchema: z.object({ commentId: z.string().cuid(), postId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,
checkIfBreweryCommentOwner,
editBreweryPostComment,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,37 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
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 CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import { createComment, getAll } from '@/controllers/comments/brewery-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
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: CreateCommentValidationSchema,
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createComment,
);
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAll,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,31 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { createRouter } from 'next-connect';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
import { UploadImagesRequest } from '@/controllers/images/types';
import { processBreweryImageData } from '@/controllers/images/brewery-images';
const router = createRouter<
UploadImagesRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
// @ts-expect-error
uploadMiddlewareMultiple,
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
processBreweryImageData,
);
const handler = router.handler(NextConnectOptions);
export default handler;
export const config = { api: { bodyParser: false } };

View File

@@ -0,0 +1,25 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import {
checkIfBreweryPostOwner,
editBreweryPost,
deleteBreweryPost,
} from '@/controllers/posts/breweries';
import { EditBreweryPostRequest } from '@/controllers/posts/breweries/types';
const router = createRouter<
EditBreweryPostRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(getCurrentUser, checkIfBreweryPostOwner, editBreweryPost);
router.delete(getCurrentUser, checkIfBreweryPostOwner, deleteBreweryPost);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,33 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import {
sendBreweryPostLikeRequest,
getBreweryPostLikeCount,
} from '@/controllers/likes/brewery-post-likes';
import { LikeRequest } from '@/controllers/likes/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBreweryPostLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBreweryPostLikeCount,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,24 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBreweryPostLikeStatus } from '@/controllers/likes/brewery-post-likes';
import { LikeRequest } from '@/controllers/likes/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBreweryPostLikeStatus,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { createRouter } from 'next-connect';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import CreateBreweryPostSchema from '@/services/posts/brewery-post/schema/CreateBreweryPostSchema';
import { CreateBreweryPostRequest } from '@/controllers/posts/breweries/types';
import { createBreweryPost } from '@/controllers/posts/breweries';
const router = createRouter<
CreateBreweryPostRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: CreateBreweryPostSchema }),
getCurrentUser,
createBreweryPost,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,23 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBreweryPosts } from '@/controllers/posts/breweries';
import { GetBreweryPostsRequest } from '@/controllers/posts/breweries/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetBreweryPostsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: PaginatedQueryResponseSchema }),
getBreweryPosts,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,25 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getMapBreweryPosts } from '@/controllers/posts/breweries';
import { GetBreweryPostsRequest } from '@/controllers/posts/breweries/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetBreweryPostsRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: PaginatedQueryResponseSchema }),
getMapBreweryPosts,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,30 @@
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 { followUser } from '@/controllers/users/profile';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
query: { id: string };
}
const router = createRouter<
GetUserFollowInfoRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
getCurrentUser,
followUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,26 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getUserFollowers } from '@/controllers/users/profile';
import { GetUserFollowInfoRequest } from '@/controllers/users/profile/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
GetUserFollowInfoRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getUserFollowers,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,30 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getUsersFollowed } from '@/controllers/users/profile';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string };
}
const router = createRouter<
GetUserFollowInfoRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getUsersFollowed,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,41 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { editUserInfo, deleteAccount } from '@/controllers/users/auth';
import { checkIfUserCanEditUser } from '@/controllers/users/profile';
import { EditUserRequest } from '@/controllers/users/profile/types';
import EditUserSchema from '@/services/users/auth/schema/EditUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
EditUserRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
getCurrentUser,
validateRequest({
bodySchema: EditUserSchema,
querySchema: z.object({ id: z.string().cuid() }),
}),
checkIfUserCanEditUser,
editUserInfo,
);
router.delete(
getCurrentUser,
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
}),
checkIfUserCanEditUser,
deleteAccount,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,27 @@
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { checkIfUserIsFollowedBySessionUser } from '@/controllers/users/profile';
import { GetUserFollowInfoRequest } from '@/controllers/users/profile/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
const router = createRouter<
GetUserFollowInfoRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
getCurrentUser,
checkIfUserIsFollowedBySessionUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { GetAllPostsByConnectedPostId } from '@/controllers/posts/types';
import { getBeerPostsByUserId } from '@/controllers/posts/beer-posts';
const router = createRouter<
GetAllPostsByConnectedPostId,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getBeerPostsByUserId,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,25 @@
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { getBreweryPostsByUserId } from '@/controllers/posts/breweries';
import { GetAllPostsByConnectedPostId } from '@/controllers/posts/types';
const router = createRouter<
GetAllPostsByConnectedPostId,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getBreweryPostsByUserId,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,27 @@
import { singleUploadMiddleware } from '@/config/multer/uploadMiddleware';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { checkIfUserCanUpdateProfile, updateAvatar } from '@/controllers/users/profile';
import { UpdateProfileRequest } from '@/controllers/users/profile/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
UpdateProfileRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
getCurrentUser,
checkIfUserCanUpdateProfile,
// @ts-expect-error
singleUploadMiddleware,
updateAvatar,
);
const handler = router.handler();
export default handler;
export const config = { api: { bodyParser: false } };

View File

@@ -0,0 +1,25 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { checkIfUserCanUpdateProfile, updateProfile } from '@/controllers/users/profile';
import { UpdateProfileRequest } from '@/controllers/users/profile/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
UpdateProfileRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
getCurrentUser,
checkIfUserCanUpdateProfile,
validateRequest({ bodySchema: z.object({ bio: z.string().max(1000) }) }),
updateProfile,
);
const handler = router.handler();
export default handler;

View File

@@ -0,0 +1,23 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { CheckEmailRequest } from '@/controllers/users/auth/types';
import { checkEmail } from '@/controllers/users/auth';
const router = createRouter<
CheckEmailRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: z.object({ email: z.string().email() }) }),
checkEmail,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,24 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { CheckUsernameRequest } from '@/controllers/users/auth/types';
import { checkUsername } from '@/controllers/users/auth';
const router = createRouter<
CheckUsernameRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
validateRequest({ querySchema: z.object({ username: z.string() }) }),
checkUsername,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,27 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { TokenValidationRequest } from '@/controllers/users/auth/types';
import { confirmUser } from '@/controllers/users/auth';
import TokenValidationSchema from '@/services/users/auth/schema/TokenValidationSchema';
const router = createRouter<
TokenValidationRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: TokenValidationSchema }),
confirmUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,18 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { sendCurrentUser } from '@/controllers/users/auth';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(getCurrentUser, sendCurrentUser);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { updatePassword } from '@/controllers/users/auth';
import { UpdatePasswordRequest } from '@/controllers/users/auth/types';
import { UpdatePasswordSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
UpdatePasswordRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
validateRequest({ bodySchema: UpdatePasswordSchema }),
getCurrentUser,
updatePassword,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,22 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { resetPassword } from '@/controllers/users/auth';
import { ResetPasswordRequest } from '@/controllers/users/auth/types';
const router = createRouter<
ResetPasswordRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: z.object({ email: z.string().email() }) }),
resetPassword,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,24 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import { createRouter } from 'next-connect';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { authenticateUser, loginUser } from '@/controllers/users/auth';
import LoginValidationSchema from '@/services/users/auth/schema/LoginValidationSchema';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: LoginValidationSchema }),
authenticateUser,
loginUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,16 @@
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 { logoutUser } from '@/controllers/users/auth';
const router = createRouter<
NextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.all(logoutUser);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,25 @@
import { NextApiResponse } from 'next';
import { z } from 'zod';
import { createRouter } from 'next-connect';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { registerUser } from '@/controllers/users/auth';
import { RegisterUserRequest } from '@/controllers/users/auth/types';
import { CreateUserValidationSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
const router = createRouter<
RegisterUserRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({
bodySchema: CreateUserValidationSchema,
}),
registerUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,18 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import { resendConfirmation } from '@/controllers/users/auth';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(getCurrentUser, resendConfirmation);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,65 @@
import { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
import EditBeerPostForm from '@/components/EditBeerPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
import { getBeerPostById } from '@/services/posts/beer-post';
interface EditPageProps {
beerPost: z.infer<typeof BeerPostQueryResult>;
}
const EditBeerPostPage: NextPage<EditPageProps> = ({ beerPost }) => {
const pageTitle = `Edit \u201c${beerPost.name}\u201d`;
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageTitle} />
</Head>
<FormPageLayout
headingText={pageTitle}
headingIcon={BiBeer}
backLink={`/beers/${beerPost.id}`}
backLinkText={`Back to "${beerPost.name}"`}
>
<EditBeerPostForm
previousValues={{
name: beerPost.name,
abv: beerPost.abv,
ibu: beerPost.ibu,
description: beerPost.description,
id: beerPost.id,
}}
/>
</FormPageLayout>
</>
);
};
export default EditBeerPostPage;
export const getServerSideProps = withPageAuthRequired<EditPageProps>(
async (context, session) => {
const beerPostId = context.params?.id as string;
const beerPost = await getBeerPostById({ beerPostId });
const { id: userId } = session;
if (!beerPost) {
return { notFound: true };
}
const isBeerPostOwner = beerPost.postedBy.id === userId;
return isBeerPostOwner
? { props: { beerPost: JSON.parse(JSON.stringify(beerPost)) } }
: { redirect: { destination: `/beers/${beerPostId}`, permanent: false } };
},
);

View File

@@ -0,0 +1,114 @@
import { NextPage, GetServerSideProps } from 'next';
import Head from 'next/head';
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
import { z } from 'zod';
import 'react-responsive-carousel/lib/styles/carousel.min.css';
import { Carousel } from 'react-responsive-carousel';
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import { Tab } from '@headlessui/react';
import dynamic from 'next/dynamic';
import { CldImage } from 'next-cloudinary';
import { getBeerPostById } from '@/services/posts/beer-post';
const [BeerInfoHeader, BeerPostCommentsSection, BeerRecommendations] = [
dynamic(() => import('@/components/BeerById/BeerInfoHeader')),
dynamic(() => import('@/components/BeerById/BeerPostCommentsSection')),
dynamic(() => import('@/components/BeerById/BeerRecommendations')),
];
interface BeerPageProps {
beerPost: z.infer<typeof BeerPostQueryResult>;
}
const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
const isDesktop = useMediaQuery('(min-width: 1024px)');
return (
<>
<Head>
<title>{beerPost.name}</title>
<meta name="description" content={beerPost.description} />
</Head>
<div className="mt-10">
<Carousel
className="w-full"
useKeyboardArrows
autoPlay
interval={10000}
infiniteLoop
showThumbs={false}
>
{beerPost.beerImages.length
? beerPost.beerImages.map((image, index) => (
<div key={image.id} id={`image-${index}}`} className="w-full">
<CldImage
alt={image.alt}
src={image.path}
height={1080}
crop="fill"
width={1920}
className="h-96 w-full object-cover lg:h-[42rem]"
/>
</div>
))
: Array.from({ length: 1 }).map((_, i) => (
<div className="h-96 lg:h-[42rem]" key={i} />
))}
</Carousel>
<main 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">
<BeerInfoHeader beerPost={beerPost} />
{isDesktop ? (
<div className="mt-4 flex flex-row space-x-3 space-y-0">
<div className="w-[60%]">
<BeerPostCommentsSection beerPost={beerPost} />
</div>
<div className="w-[40%]">
<BeerRecommendations beerPost={beerPost} />
</div>
</div>
) : (
<Tab.Group>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Comments</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Other Beers</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel>
<BeerPostCommentsSection beerPost={beerPost} />
</Tab.Panel>
<Tab.Panel>
<BeerRecommendations beerPost={beerPost} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</div>
</main>
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const beerPost = await getBeerPostById({
beerPostId: context.params?.id as string,
});
if (!beerPost) {
return { notFound: true };
}
const props = {
beerPost: JSON.parse(JSON.stringify(beerPost)),
};
return { props };
};
export default BeerByIdPage;

View File

@@ -0,0 +1,101 @@
import { NextPage } from 'next';
import BeerCard from '@/components/BeerIndex/BeerCard';
import Head from 'next/head';
import { MutableRefObject, useRef } from 'react';
import { useInView } from 'react-intersection-observer';
import Spinner from '@/components/ui/Spinner';
import useBeerPosts from '@/hooks/data-fetching/beer-posts/useBeerPosts';
import { FaArrowUp } from 'react-icons/fa';
import LoadingCard from '@/components/ui/LoadingCard';
const BeerPage: NextPage = () => {
const PAGE_SIZE = 20;
const { beerPosts, setSize, size, isLoading, isLoadingMore, isAtEnd } = useBeerPosts({
pageSize: PAGE_SIZE,
});
const { ref: lastBeerPostRef } = useInView({
onChange: (visible) => {
if (!visible || isAtEnd) return;
setSize(size + 1);
},
});
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
return (
<>
<Head>
<title>Beers | The Biergarten App</title>
<meta
name="description"
content="Find beers made by breweries near you and around the world."
/>
</Head>
<div className="mt-10 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 App</h1>
<h2 className="text-2xl font-bold lg:text-4xl">Beers</h2>
</div>
</header>
<div className="grid gap-6 xl:grid-cols-2">
{!!beerPosts.length && !isLoading && (
<>
{beerPosts.map((beerPost, i) => {
return (
<div
key={beerPost.id}
ref={beerPosts.length === i + 1 ? lastBeerPostRef : undefined}
>
<BeerCard post={beerPost} />
</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 btn-ghost btn-sm"
aria-label="Scroll back to top of page."
onClick={() => {
pageRef.current?.scrollIntoView({
behavior: 'smooth',
});
}}
>
<FaArrowUp />
</button>
</div>
</div>
)}
</div>
</div>
</>
);
};
export default BeerPage;

View File

@@ -0,0 +1,76 @@
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/data-fetching/beer-posts/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) || '';
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(querySearch);
}, DEBOUNCE_DELAY)();
}, [querySearch, searchValue]);
const showSearchResults = !isLoading && searchResults && !searchError;
return (
<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 input-bordered 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>
);
};
export default SearchPage;

View File

@@ -0,0 +1,77 @@
import { NextPage, GetServerSideProps } from 'next';
import Head from 'next/head';
import { z } from 'zod';
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import { Tab } from '@headlessui/react';
import BeerStyleHeader from '@/components/BeerStyleById/BeerStyleHeader';
import BeerStyleQueryResult from '@/services/posts/beer-style-post/schema/BeerStyleQueryResult';
import BeerStyleCommentSection from '@/components/BeerStyleById/BeerStyleCommentSection';
import BeerStyleBeerSection from '@/components/BeerStyleById/BeerStyleBeerSection';
import { getBeerStyleByIdService } from '@/services/posts/beer-style-post';
interface BeerStylePageProps {
beerStyle: z.infer<typeof BeerStyleQueryResult>;
}
const BeerStyleByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
const isDesktop = useMediaQuery('(min-width: 1024px)');
return (
<>
<Head>
<title>{beerStyle.name}</title>
<meta name="description" content={beerStyle.description} />
</Head>
<>
<main className="mb-12 mt-10 flex w-full items-center justify-center">
<div className="mt-10 w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
<BeerStyleHeader beerStyle={beerStyle} />
{isDesktop ? (
<div className="mt-4 flex flex-row space-x-3 space-y-0">
<div className="w-[60%]">
<BeerStyleCommentSection beerStyle={beerStyle} />
</div>
<div className="w-[40%]">
<BeerStyleBeerSection beerStyle={beerStyle} />
</div>
</div>
) : (
<Tab.Group>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Comments</Tab>
<Tab className="tab uppercase ui-selected:tab-active">
Beers in this Style
</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel>
<BeerStyleCommentSection beerStyle={beerStyle} />
</Tab.Panel>
<Tab.Panel>
<BeerStyleBeerSection beerStyle={beerStyle} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</div>
</main>
</>
</>
);
};
export default BeerStyleByIdPage;
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const id = params!.id as string;
const beerStyle = await getBeerStyleByIdService({ beerStyleId: id });
if (!beerStyle) {
return { notFound: true };
}
return { props: { beerStyle: JSON.parse(JSON.stringify(beerStyle)) } };
};

View File

@@ -0,0 +1,101 @@
import { NextPage } from 'next';
import Head from 'next/head';
import { MutableRefObject, useRef } from 'react';
import { FaArrowUp } from 'react-icons/fa';
import { useInView } from 'react-intersection-observer';
import BeerStyleCard from '@/components/BeerStyleIndex/BeerStyleCard';
import SmLoadingCard from '@/components/ui/SmLoadingCard';
import Spinner from '@/components/ui/Spinner';
import useBeerStyles from '@/hooks/data-fetching/beer-styles/useBeerStyles';
const BeerStylePage: NextPage = () => {
const PAGE_SIZE = 20;
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const { beerStyles, isLoading, isLoadingMore, isAtEnd, size, setSize } = useBeerStyles({
pageSize: PAGE_SIZE,
});
const { ref: lastBeerStyleRef } = useInView({
onChange: (visible) => {
if (!visible || isAtEnd) return;
setSize(size + 1);
},
});
return (
<>
<Head>
<title>Beer Styles | The Biergarten App</title>
<meta
name="description"
content="Find beers made by breweries near you and around the world."
/>
</Head>
<div className="mt-10 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 App</h1>
<h2 className="text-2xl font-bold lg:text-4xl">Styles</h2>
</div>
</header>
<div className="grid gap-6 xl:grid-cols-2">
{!!beerStyles.length && !isLoading && (
<>
{beerStyles.map((beerStyle, i) => {
return (
<div
key={beerStyle.id}
ref={beerStyles.length === i + 1 ? lastBeerStyleRef : undefined}
>
<BeerStyleCard beerStyle={beerStyle} />
</div>
);
})}
</>
)}
{(isLoading || isLoadingMore) && (
<>
{Array.from({ length: PAGE_SIZE }, (_, i) => (
<SmLoadingCard 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 btn-ghost btn-sm"
aria-label="Scroll back to top of page."
onClick={() => {
pageRef.current?.scrollIntoView({
behavior: 'smooth',
});
}}
>
<FaArrowUp />
</button>
</div>
</div>
)}
</div>
</div>
</>
);
};
export default BeerStylePage;

View File

@@ -0,0 +1,47 @@
import CreateBeerPostForm from '@/components/CreateBeerPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import DBClient from '@/prisma/DBClient';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import { BeerStyle } from '@prisma/client';
import { NextPage } from 'next';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
import { getBreweryPostByIdService } from '@/services/posts/brewery-post';
interface CreateBeerPageProps {
brewery: z.infer<typeof BreweryPostQueryResult>;
styles: BeerStyle[];
}
const CreateBeerPost: NextPage<CreateBeerPageProps> = ({ brewery, styles }) => {
return (
<FormPageLayout
headingText="Create a new beer"
headingIcon={BiBeer}
backLink="/beers"
backLinkText="Back to beers"
>
<CreateBeerPostForm brewery={brewery} styles={styles} />
</FormPageLayout>
);
};
export const getServerSideProps = withPageAuthRequired<CreateBeerPageProps>(
async (context) => {
const id = context.params?.id as string;
const breweryPost = await getBreweryPostByIdService({ breweryPostId: id });
const beerStyles = await DBClient.instance.beerStyle.findMany();
return {
props: {
brewery: JSON.parse(JSON.stringify(breweryPost)),
styles: JSON.parse(JSON.stringify(beerStyles)),
},
};
},
);
export default CreateBeerPost;

View File

@@ -0,0 +1,57 @@
import EditBreweryPostForm from '@/components/EditBreweryPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import { getBreweryPostByIdService } from '@/services/posts/brewery-post';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import { NextPage } from 'next';
import Head from 'next/head';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
interface EditPageProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
}
const EditBreweryPostPage: NextPage<EditPageProps> = ({ breweryPost }) => {
const pageTitle = `Edit \u201c${breweryPost.name}\u201d`;
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageTitle} />
</Head>
<FormPageLayout
headingText={pageTitle}
headingIcon={BiBeer}
backLink={`/breweries/${breweryPost.id}`}
backLinkText={`Back to "${breweryPost.name}"`}
>
<EditBreweryPostForm breweryPost={breweryPost} />
</FormPageLayout>
</>
);
};
export default EditBreweryPostPage;
export const getServerSideProps = withPageAuthRequired<EditPageProps>(
async (context, session) => {
const breweryPostId = context.params?.id as string;
const breweryPost = await getBreweryPostByIdService({ breweryPostId });
const { id: userId } = session;
if (!breweryPost) {
return { notFound: true };
}
const isBreweryPostOwner = breweryPost.postedBy.id === userId;
return isBreweryPostOwner
? { props: { breweryPost: JSON.parse(JSON.stringify(breweryPost)) } }
: { redirect: { destination: `/breweries/${breweryPostId}`, permanent: false } };
},
);

View File

@@ -0,0 +1,127 @@
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import { GetServerSideProps, NextPage } from 'next';
import { z } from 'zod';
import Head from 'next/head';
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
import { Carousel } from 'react-responsive-carousel';
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import { Tab } from '@headlessui/react';
import dynamic from 'next/dynamic';
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
import { CldImage } from 'next-cloudinary';
import { getBreweryPostByIdService } from '@/services/posts/brewery-post';
const [BreweryInfoHeader, BreweryBeersSection, BreweryCommentsSection, BreweryPostMap] = [
dynamic(() => import('@/components/BreweryById/BreweryInfoHeader')),
dynamic(() => import('@/components/BreweryById/BreweryBeerSection')),
dynamic(() => import('@/components/BreweryById/BreweryCommentsSection')),
dynamic(() => import('@/components/BreweryById/BreweryPostMap')),
];
interface BreweryPageProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
mapboxToken: string;
}
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost, mapboxToken }) => {
const [longitude, latitude] = breweryPost.location.coordinates;
const isDesktop = useMediaQuery('(min-width: 1024px)');
return (
<>
<Head>
<title>{breweryPost.name}</title>
<meta name="description" content={breweryPost.description} />
</Head>
<div className="mt-10">
<Carousel
className="w-full"
useKeyboardArrows
autoPlay
interval={10000}
infiniteLoop
showThumbs={false}
>
{breweryPost.breweryImages.length
? breweryPost.breweryImages.map((image, index) => (
<div key={image.id} id={`image-${index}}`} className="w-full">
<CldImage
alt={image.alt}
src={image.path}
crop="fill"
height={1080}
width={1920}
className="h-96 w-full object-cover lg:h-[42rem]"
/>
</div>
))
: Array.from({ length: 1 }).map((_, i) => (
<div className="h-96 lg:h-[42rem]" key={i} />
))}
</Carousel>
<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} />
{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">
<BreweryPostMap
coordinates={{ latitude, longitude }}
token={mapboxToken}
/>
<BreweryBeersSection breweryPost={breweryPost} />
</div>
</div>
) : (
<>
<BreweryPostMap
coordinates={{ latitude, longitude }}
token={mapboxToken}
/>
<Tab.Group>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab-md tab w-1/2 uppercase ui-selected:tab-active">
Comments
</Tab>
<Tab className="tab-md tab 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 breweryPost={breweryPost} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</>
)}
</div>
</div>
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps<BreweryPageProps> = async (
context,
) => {
const breweryPost = await getBreweryPostByIdService({
breweryPostId: context.params?.id as string,
});
const mapboxToken = MAPBOX_ACCESS_TOKEN;
return !breweryPost
? { notFound: true }
: { props: { breweryPost: JSON.parse(JSON.stringify(breweryPost)), mapboxToken } };
};
export default BreweryByIdPage;

View File

@@ -0,0 +1,40 @@
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import { NextPage } from 'next';
import Head from 'next/head';
import { FaBeer } from 'react-icons/fa';
import CreateBreweryPostForm from '@/components/BreweryPost/CreateBreweryPostForm';
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
interface CreateBreweryPageProps {
mapboxAccessToken: string;
}
const CreateBreweryPage: NextPage<CreateBreweryPageProps> = ({ mapboxAccessToken }) => {
return (
<>
<Head>
<title>Create Brewery</title>
</Head>
<div className="flex w-full flex-col items-center justify-center">
<div className="w-full">
<FormPageLayout
backLink="/breweries"
backLinkText="Back to Breweries"
headingText="Create Brewery"
headingIcon={FaBeer}
>
<CreateBreweryPostForm mapboxAccessToken={mapboxAccessToken} />
</FormPageLayout>
</div>
</div>
</>
);
};
export default CreateBreweryPage;
export const getServerSideProps = withPageAuthRequired<CreateBreweryPageProps>(
async () => ({ props: { mapboxAccessToken: MAPBOX_ACCESS_TOKEN } }),
);

View File

@@ -0,0 +1,134 @@
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/data-fetching/brewery-posts/useBreweryPosts';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import { NextPage } from 'next';
import Head from 'next/head';
import { useContext, MutableRefObject, useRef } from 'react';
import Link from 'next/link';
import { FaPlus, FaArrowUp, FaMap } from 'react-icons/fa';
import { useInView } from 'react-intersection-observer';
import { z } from 'zod';
interface BreweryPageProps {
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
}
const BreweryPage: NextPage<BreweryPageProps> = () => {
const PAGE_SIZE = 20;
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 (
<>
<Head>
<title>Breweries</title>
<meta
name="description"
content="Find breweries near you and around the world."
/>
</Head>
<div className="mt-10 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 className="space-y-2">
<div>
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten App</h1>
<h2 className="text-2xl font-bold lg:text-4xl">Breweries</h2>
</div>
</div>
<div className="flex flex-col">
{!!user && (
<div
className="tooltip tooltip-left"
data-tip="Create a new brewery post"
>
<Link href="/breweries/create" className="btn btn-ghost btn-sm">
<FaPlus className="text-lg" />
</Link>
</div>
)}
<div className="tooltip tooltip-left" data-tip="View map">
<Link className="btn btn-ghost btn-sm" href="/breweries/map">
<FaMap className="text-lg" />
</Link>
</div>
</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 btn-ghost btn-sm"
aria-label="Scroll back to top of page."
onClick={() => {
pageRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}}
>
<FaArrowUp />
</button>
</div>
</div>
)}
</div>
</div>
</>
);
};
export default BreweryPage;

View File

@@ -0,0 +1,132 @@
import { GetServerSideProps, NextPage } from 'next';
import { useEffect, useMemo, useState } from 'react';
import Map, { Marker, Popup } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import LocationMarker from '@/components/ui/LocationMarker';
import Link from 'next/link';
import Head from 'next/head';
import useGeolocation from '@/hooks/utilities/useGeolocation';
import BreweryPostMapQueryResult from '@/services/posts/brewery-post/schema/BreweryPostMapQueryResult';
import { z } from 'zod';
import useBreweryMapPagePosts from '@/hooks/data-fetching/brewery-posts/useBreweryMapPagePosts';
import ControlPanel from '@/components/ui/maps/ControlPanel';
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
type MapStyles = Record<'light' | 'dark', `mapbox://styles/mapbox/${string}`>;
interface BreweryMapPageProps {
token: string;
}
const BreweryMapPage: NextPage<BreweryMapPageProps> = ({ token }) => {
const [popupInfo, setPopupInfo] = useState<z.infer<
typeof BreweryPostMapQueryResult
> | null>(null);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
setTheme(localStorage.getItem('theme') === 'dark' ? 'dark' : 'light');
}, []);
const { breweries } = useBreweryMapPagePosts({ pageSize: 50 });
const mapStyles: MapStyles = {
light: 'mapbox://styles/mapbox/light-v10',
dark: 'mapbox://styles/mapbox/dark-v10',
};
const pins = useMemo(
() => (
<>
{breweries.map((brewery) => {
const [longitude, latitude] = brewery.location.coordinates;
return (
<Marker
key={brewery.id}
latitude={latitude}
longitude={longitude}
onClick={(e) => {
e.originalEvent.stopPropagation();
setPopupInfo(brewery);
}}
>
<LocationMarker size="md" color="blue" />
</Marker>
);
})}
</>
),
[breweries],
);
const { coords, error: geoError } = useGeolocation();
const userLocationPin = useMemo(
() =>
coords && !geoError ? (
<Marker latitude={coords.latitude} longitude={coords.longitude}>
<LocationMarker size="lg" color="red" />
</Marker>
) : null,
[coords, geoError],
);
return (
<>
<Head>
<title>Brewery Map | The Biergarten App</title>
<meta
name="description"
content="Find breweries near you and around the world."
/>
</Head>
<div className="h-dvh">
<Map
// center the map on North America
initialViewState={{ zoom: 3, latitude: 48.3544, longitude: -99.9981 }}
style={{ width: '100%', height: '100%' }}
mapStyle={mapStyles[theme]}
mapboxAccessToken={token}
scrollZoom
>
<ControlPanel />
{pins}
{userLocationPin}
{popupInfo && (
<Popup
anchor="bottom"
longitude={popupInfo.location.coordinates[0]}
latitude={popupInfo.location.coordinates[1]}
onClose={() => setPopupInfo(null)}
>
<div className="flex flex-col text-black">
<Link
className="link-hover link text-base font-bold"
href={`/breweries/${popupInfo.id}`}
>
{popupInfo.name}
</Link>
<p className="text-base">
{popupInfo.location.city}
{popupInfo.location.stateOrProvince
? `, ${popupInfo.location.stateOrProvince}`
: ''}
{popupInfo.location.country ? `, ${popupInfo.location.country}` : ''}
</p>
</div>
</Popup>
)}
</Map>
</div>
</>
);
};
export default BreweryMapPage;
export const getServerSideProps: GetServerSideProps<BreweryMapPageProps> = async () => ({
props: { token: MAPBOX_ACCESS_TOKEN },
});

View File

@@ -0,0 +1,60 @@
import { NextPage } from 'next';
import { CldImage } from 'next-cloudinary';
import Head from 'next/head';
const keywords = [
'beer',
'bier',
'biergarten',
'brewery',
'brew',
'drink',
'alcohol',
'brews',
'breweries',
'craft beer',
'beer enthusiast',
'beer tasting',
'beer culture',
'beer connoisseur',
'beer reviews',
'beer community',
'beer events',
'brewpubs',
'beer aficionado',
'beer types',
'beer selection',
'beer recommendations',
'beer ratings',
'beer pairing',
'beer recipes',
];
const description = `An app for beer lovers to share their favourite brews and breweries with like-minded people online.`;
const Home: NextPage = () => {
return (
<>
<Head>
<title>The Biergarten App</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
</Head>
<div className="relative flex h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-9/12 flex-col space-y-3 text-base-content">
<h1 className="text-5xl font-extrabold lg:text-8xl">The Biergarten App</h1>
<p className="font-bold lg:text-3xl">{description}</p>
</div>
</div>
</>
);
};
export default Home;

View File

@@ -0,0 +1,54 @@
import { NextPage } from 'next';
import LoginForm from '@/components/Login/LoginForm';
import { FaUserCircle } from 'react-icons/fa';
import Head from 'next/head';
import Link from 'next/link';
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
import { CldImage } from 'next-cloudinary';
const LoginPage: NextPage = () => {
useRedirectWhenLoggedIn();
return (
<>
<Head>
<title>Login</title>
<meta name="description" content="Login to your account" />
</Head>
<div className="relative flex h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-9/12 flex-col items-center text-center text-base-content lg:w-6/12">
<div className="mb-8 flex w-full flex-col items-center space-y-2 text-center">
<FaUserCircle className="text-6xl text-primary-content" />
<h1 className="text-6xl font-extrabold">Login</h1>
</div>
<div className="w-full">
<LoginForm />
</div>
<div className="mt-5 flex w-full flex-col space-y-1 text-center font-bold italic">
<Link className="link-hover link text-lg" href="/register">
Don&apos;t have an account?
</Link>
<Link className="link-hover link text-lg" href="/users/forgot-password">
Forgot your password?{' '}
</Link>
</div>
</div>
</div>
</>
);
};
export default LoginPage;

View File

@@ -0,0 +1,43 @@
import RegisterUserForm from '@/components/RegisterUserForm';
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
import { NextPage } from 'next';
import { CldImage } from 'next-cloudinary';
import Head from 'next/head';
import { FaUserCircle } from 'react-icons/fa';
const RegisterUserPage: NextPage = () => {
useRedirectWhenLoggedIn();
return (
<>
<Head>
<title>Register User</title>
<meta name="description" content="Register a new user" />
</Head>
<div className="relative flex min-h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-10/12 flex-col items-center pb-20 text-center text-base-content lg:w-6/12">
<div className="mb-8 mt-20 flex w-full flex-col items-center space-y-2 text-center">
<FaUserCircle className="text-6xl text-primary-content" />
<h1 className="text-6xl font-extrabold">Register</h1>
</div>
<div className="w-full">
<RegisterUserForm />
</div>
</div>
</div>
</>
);
};
export default RegisterUserPage;

View File

@@ -0,0 +1,48 @@
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
import Head from 'next/head';
import { FC } from 'react';
import { z } from 'zod';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import UserHeader from '@/components/UserPage/UserHeader';
import { findUserByIdService } from '@/services/users/auth';
interface UserInfoPageProps {
user: z.infer<typeof GetUserSchema>;
}
const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isDesktop = useMediaQuery('(min-width: 1024px)');
const title = `${user.username} | The Biergarten App`;
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content="User information" />
</Head>
<>
<main className="mb-12 mt-16 flex w-full flex-col items-center justify-center">
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
<UserHeader user={user} />
</div>
</main>
</>
</>
);
};
export default UserInfoPage;
export const getServerSideProps = withPageAuthRequired<UserInfoPageProps>(
async (context) => {
const { id } = context.params!;
const user = await findUserByIdService({ userId: id as string });
return user
? { props: { user: JSON.parse(JSON.stringify(user)) } }
: { notFound: true };
},
);

View File

@@ -0,0 +1,181 @@
import { useContext, useEffect } from 'react';
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import toast from 'react-hot-toast';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import createErrorToast from '@/util/createErrorToast';
import UserContext from '@/contexts/UserContext';
import UserAvatar from '@/components/Account/UserAvatar';
import UpdateProfileForm from '@/components/Account/UpdateProfileForm';
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
import UpdateProfileSchema from '@/services/users/auth/schema/UpdateProfileSchema';
import sendUpdateUserAvatarRequest from '@/requests/users/profile/sendUpdateUserAvatarRequest';
import sendUpdateUserProfileRequest from '@/requests/users/profile/sendUpdateUserProfileRequest';
import Spinner from '@/components/ui/Spinner';
const ProfilePage: NextPage = () => {
const { user, mutate: mutateUser } = useContext(UserContext);
const {
register,
handleSubmit,
setValue,
formState: { errors, isSubmitting },
watch,
} = useForm<z.infer<typeof UpdateProfileSchema>>({
resolver: zodResolver(UpdateProfileSchema),
});
useEffect(() => {
if (!user || !user.bio) {
return;
}
setValue('bio', user.bio);
}, [user, setValue]);
const router = useRouter();
const onSubmit: SubmitHandler<z.infer<typeof UpdateProfileSchema>> = async (data) => {
try {
const loadingToast = toast.loading('Updating profile...');
if (!user) {
throw new Error('User is not logged in.');
}
if (data.userAvatar instanceof FileList && data.userAvatar.length === 1) {
await sendUpdateUserAvatarRequest({
userId: user.id,
file: data.userAvatar[0],
});
}
await sendUpdateUserProfileRequest({
userId: user.id,
bio: data.bio,
});
toast.remove(loadingToast);
await mutateUser!();
await router.push(`/users/${user.id}`);
toast.success('Profile updated.');
} catch (error) {
createErrorToast(error);
}
};
const { followingCount } = useGetUsersFollowedByUser({
userId: user?.id,
});
const { followerCount } = useGetUsersFollowingUser({
userId: user?.id,
});
const getUserAvatarPath = () => {
const watchedInput = watch('userAvatar');
if (
!(
watchedInput instanceof FileList &&
watchedInput.length === 1 &&
watchedInput[0].type.startsWith('image/')
)
) {
return '';
}
const [avatar] = watchedInput;
return URL.createObjectURL(avatar);
};
return (
<>
<Head>
<title>The Biergarten App || Update Your Profile</title>
<meta name="description" content="Update your user profile." />
</Head>
<div className="my-20 flex flex-col items-center justify-center">
{user ? (
<div className="w-10/12 lg:w-7/12">
<div className="card">
<div className="card-body">
<div className="my-10 flex flex-col items-center justify-center">
<div className="my-2 h-40 xl:my-5 xl:h-52">
<UserAvatar
user={{
...user,
userAvatar:
// Render the user's avatar if they have one and then switch to the preview it's being updated.
user.userAvatar && !getUserAvatarPath()
? user.userAvatar
: {
id: 'preview',
alt: 'User Avatar',
caption: 'User Avatar',
path: getUserAvatarPath(),
createdAt: new Date(),
updatedAt: new Date(),
},
}}
/>
</div>
<div className="text-center">
<h2 className="my-1 text-2xl font-bold xl:text-3xl">
{user.username}
</h2>
<div className="flex space-x-3 font-bold xl:text-lg">
<span>{followingCount} Following</span>
<span>{followerCount} Followers</span>
</div>
</div>
<div>
<p className="hyphens-auto text-center xl:text-lg">
{watch('bio') ? (
<span className="hyphens-manual">{watch('bio')}</span>
) : (
<span className="italic">Your bio will appear here.</span>
)}
</p>
</div>
</div>
<UpdateProfileForm
handleSubmit={handleSubmit}
onSubmit={onSubmit}
errors={errors}
isSubmitting={isSubmitting}
register={register}
user={user}
/>
</div>
</div>
</div>
) : (
<div className="flex h-96 items-center justify-center">
<Spinner />
</div>
)}
</div>
</>
);
};
export default ProfilePage;
export const getServerSideProps: GetServerSideProps = withPageAuthRequired();

View File

@@ -0,0 +1,76 @@
import withPageAuthRequired from '@/util/withPageAuthRequired';
import { NextPage } from 'next';
import { Tab } from '@headlessui/react';
import Head from 'next/head';
import AccountInfo from '@/components/Account/AccountInfo';
import { useContext, useReducer } from 'react';
import UserContext from '@/contexts/UserContext';
import Security from '@/components/Account/Security';
import DeleteAccount from '@/components/Account/DeleteAccount';
import accountPageReducer from '@/reducers/accountPageReducer';
import UserAvatar from '@/components/Account/UserAvatar';
import UserPosts from '@/components/Account/UserPosts';
import UpdateProfileLink from '@/components/Account/UpdateProfileLink';
const AccountPage: NextPage = () => {
const { user } = useContext(UserContext);
const [pageState, dispatch] = useReducer(accountPageReducer, {
accountInfoOpen: false,
securityOpen: false,
deleteAccountOpen: false,
});
if (!user) return null;
return (
<>
<Head>
<title>Your Account | The Biergarten App</title>
<meta
name="description"
content="Your account page. Here you can view your account information, change your settings, and view your posts."
/>
</Head>
<div className="mt-10 flex min-h-dvh flex-col items-center">
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-8/12">
<div className="flex flex-col items-center space-y-3">
<div className="mb-1 h-28 w-28">
<UserAvatar user={user} />
</div>
<div className="flex flex-col items-center space-y-1">
<p className="text-3xl font-bold">Hello, {user!.username}!</p>
<p className="text-lg">Welcome to your account page.</p>
</div>
</div>
<div className="h-full w-full">
<Tab.Group>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Account</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Your Posts</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel className="mt-3 h-full space-y-3">
<UpdateProfileLink />
<AccountInfo pageState={pageState} dispatch={dispatch} />
<Security pageState={pageState} dispatch={dispatch} />
<DeleteAccount pageState={pageState} dispatch={dispatch} />
</Tab.Panel>
<Tab.Panel className="h-full">
<UserPosts />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
</div>
</>
);
};
export default AccountPage;
export const getServerSideProps = withPageAuthRequired();

View File

@@ -0,0 +1,73 @@
import useConfirmUser from '@/hooks/auth/useConfirmUser';
import createErrorToast from '@/util/createErrorToast';
import Head from 'next/head';
import { FC, useState } from 'react';
import { toast } from 'react-hot-toast';
const ConfirmUserPage: FC = () => {
const { needsToLogin, tokenInvalid } = useConfirmUser();
const [confirmationResent, setConfirmationResent] = useState(false);
const onClick = async () => {
const resentConfirmationLoadingToast = toast.loading(
'Resending your confirmation email.',
);
try {
const response = await fetch('/api/users/resend-confirmation', {
method: 'POST',
});
if (!response.ok) {
throw new Error('Something went wrong.');
}
toast.remove(resentConfirmationLoadingToast);
toast.success('Sent a new confirmation email.');
setConfirmationResent(true);
} catch (err) {
createErrorToast(err);
}
};
return (
<>
<Head>
<title>Confirm User | The Biergarten App</title>
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-4">
{needsToLogin && (
<p className="text-center text-xl font-bold">
Please login to confirm your account.
</p>
)}
{!needsToLogin && tokenInvalid && !confirmationResent && (
<>
<p className="text-center text-2xl font-bold">
Your confirmation token is invalid or is expired.
</p>
<button
className="btn btn-outline btn-sm normal-case"
onClick={onClick}
type="button"
>
Click here to request a new token.
</button>
</>
)}
{!needsToLogin && tokenInvalid && confirmationResent && (
<>
<p className="text-center text-2xl font-bold">
Resent your confirmation link.
</p>
<p className="font-xl text-center">Please check your email.</p>
</>
)}
</div>
</>
);
};
export default ConfirmUserPage;

View File

@@ -0,0 +1,47 @@
import Spinner from '@/components/ui/Spinner';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import UserContext from '@/contexts/UserContext';
import { GetServerSideProps, NextPage } from 'next';
import { useContext } from 'react';
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import Head from 'next/head';
const ProtectedPage: NextPage = () => {
const { user, isLoading } = useContext(UserContext);
const currentTime = new Date().getHours();
const isMorning = currentTime >= 3 && currentTime < 12;
const isAfternoon = currentTime >= 12 && currentTime < 18;
const isEvening = currentTime >= 18 || currentTime < 3;
const isDesktop = useMediaQuery('(min-width: 768px)');
return (
<>
<Head>
<title>Hello! | The Biergarten App</title>
</Head>
<div className="h-dvh flex flex-col items-center justify-center space-y-3 bg-base-100 text-center">
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
{user && !isLoading && (
<>
<h1 className="text-2xl font-bold lg:text-7xl">
Good {isMorning && 'morning'}
{isAfternoon && 'afternoon'}
{isEvening && 'evening'}
{`, ${user?.firstName}!`}
</h1>
<h2 className="text-xl font-bold lg:text-4xl">
Welcome to the Biergarten App!
</h2>
</>
)}
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps = withPageAuthRequired();
export default ProtectedPage;

View File

@@ -0,0 +1,89 @@
import Button from '@/components/ui/forms/Button';
import FormError from '@/components/ui/forms/FormError';
import FormInfo from '@/components/ui/forms/FormInfo';
import FormLabel from '@/components/ui/forms/FormLabel';
import FormSegment from '@/components/ui/forms/FormSegment';
import FormTextInput from '@/components/ui/forms/FormTextInput';
import { BaseCreateUserSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import createErrorToast from '@/util/createErrorToast';
import { zodResolver } from '@hookform/resolvers/zod';
import { NextPage } from 'next';
import { SubmitHandler, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useRouter } from 'next/router';
import { FaUserCircle } from 'react-icons/fa';
import { sendForgotPasswordRequest } from '@/requests/users/auth';
interface ForgotPasswordPageProps {}
const ForgotPasswordPage: NextPage<ForgotPasswordPageProps> = () => {
const { register, handleSubmit, formState, reset } = useForm({
resolver: zodResolver(BaseCreateUserSchema.pick({ email: true })),
defaultValues: { email: '' },
});
const router = useRouter();
const { errors } = formState;
const onSubmit: SubmitHandler<{ email: string }> = async (data) => {
try {
const loadingToast = toast.loading('Sending reset link...');
await sendForgotPasswordRequest({ email: data.email });
reset();
toast.dismiss(loadingToast);
toast.success('Password reset link sent!');
router.push('/');
} catch (error) {
createErrorToast(error);
}
};
return (
<div className="flex h-full flex-col items-center">
<div className="mt-64 flex w-10/12 flex-col space-y-3 text-center xl:w-6/12">
<div className="space-y-1">
<div className="my-2 flex flex-col items-center justify-center">
<FaUserCircle className="text-3xl" />
<h1 className="text-3xl font-bold">Forgot Your Password?</h1>
</div>
<p className="xl:text-lg">
Enter your email address below, and we will send you a link to reset your
password.
</p>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="form-control space-y-3"
noValidate
>
<div>
<FormInfo>
<FormLabel htmlFor="email">Email</FormLabel>
<FormError>{errors.email?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextInput
id="email"
type="email"
formValidationSchema={register('email')}
disabled={formState.isSubmitting}
error={!!errors.email}
placeholder="Email"
/>
</FormSegment>
</div>
<div>
<Button type="submit" isSubmitting={formState.isSubmitting}>
Send Reset Link
</Button>
</div>
</form>
</div>
</div>
);
};
export default ForgotPasswordPage;

View File

@@ -0,0 +1,43 @@
import { setLoginSession } from '@/config/auth/session';
import { verifyResetPasswordToken } from '@/config/jwt';
import ServerError from '@/config/util/ServerError';
import { findUserByIdService } from '@/services/users/auth';
import { GetServerSideProps, NextApiResponse, NextPage } from 'next';
const TokenExpiredPage: NextPage = () => {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold">Token Expired</h1>
<p className="text-lg">
Your link to reset your password has expired or is invalid.
</p>
</div>
</div>
);
};
export default TokenExpiredPage;
export const getServerSideProps: GetServerSideProps = async (context) => {
try {
const token = context.query.token as string | undefined;
if (!token) {
throw new ServerError('Token not provided', 400);
}
const { id } = await verifyResetPasswordToken(token as string);
const user = await findUserByIdService({ userId: id as string });
if (!user) {
throw new ServerError('User not found', 404);
}
await setLoginSession(context.res as NextApiResponse, user);
return { redirect: { destination: '/users/account', permanent: false } };
} catch (error) {
return { props: {} };
}
};