Restructure codebase to use src directory

This commit is contained in:
Aaron William Po
2023-04-11 23:32:06 -04:00
parent 90f2cc2c0c
commit 08422fe24e
141 changed files with 6 additions and 4 deletions

22
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,22 @@
// create a 404 next js page using tailwind
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
import Head from 'next/head';
const NotFound: NextPage = () => {
return (
<Layout>
<Head>
<title>404 Page Not Found</title>
<meta name="description" content="404 Page Not Found" />
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-4">
<h1 className="text-7xl font-bold">Error: 404</h1>
<h2 className="text-xl font-bold">Page Not Found</h2>
</div>
</Layout>
);
};
export default NotFound;

20
src/pages/500.tsx Normal file
View File

@@ -0,0 +1,20 @@
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
import Head from 'next/head';
const ServerErrorPage: NextPage = () => {
return (
<Layout>
<Head>
<title>500 Internal Server Error</title>
<meta name="description" content="500 Internal Server Error" />
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-4">
<h1 className="text-7xl font-bold">Error: 500</h1>
<h2 className="text-xl font-bold">Internal Server Error</h2>
</div>
</Layout>
);
};
export default ServerErrorPage;

29
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,29 @@
import UserContext from '@/contexts/userContext';
import useUser from '@/hooks/useUser';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import { Space_Grotesk } from 'next/font/google';
const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
});
export default function App({ Component, pageProps }: AppProps) {
const { user, isLoading, error, mutate } = useUser();
return (
<>
<style jsx global>
{`
html {
font-family: ${spaceGrotesk.style.fontFamily};
}
`}
</style>
<UserContext.Provider value={{ user, isLoading, error, mutate }}>
<Component {...pageProps} />
</UserContext.Provider>
</>
);
}

13
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -0,0 +1,16 @@
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
interface AccountPageProps {}
const AccountPage: NextPage<AccountPageProps> = () => {
return (
<Layout>
<div>
<h1>Account Page</h1>
</div>
</Layout>
);
};
export default AccountPage;

View File

@@ -0,0 +1,60 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface DeleteCommentRequest extends UserExtendedNextApiRequest {
query: { id: string };
}
const deleteComment = async (
req: DeleteCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const user = req.user!;
const comment = await DBClient.instance.beerComment.findUnique({
where: { id },
});
if (!comment) {
throw new ServerError('Comment not found', 404);
}
if (comment.postedById !== user.id) {
throw new ServerError('You are not authorized to delete this comment', 403);
}
await DBClient.instance.beerComment.delete({
where: { id },
});
res.status(200).json({
success: true,
message: 'Comment deleted successfully',
statusCode: 200,
});
};
const router = createRouter<
DeleteCommentRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.delete(
validateRequest({
querySchema: z.object({ id: z.string().uuid() }),
}),
getCurrentUser,
deleteComment,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,102 @@
import DBClient from '@/prisma/DBClient';
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
interface CreateCommentRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof BeerCommentValidationSchema>;
query: { id: string };
}
interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string };
}
const createComment = async (
req: CreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { content, rating } = req.body;
const beerPostId = req.query.id;
const newBeerComment: z.infer<typeof BeerCommentQueryResult> =
await createNewBeerComment({
content,
rating,
beerPostId,
userId: req.user!.id,
});
res.status(201).json({
message: 'Beer comment created successfully',
statusCode: 201,
payload: newBeerComment,
success: true,
});
};
const getAll = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const beerPostId = req.query.id;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;
const comments = await getAllBeerComments(
{ id: beerPostId },
{ pageSize: parseInt(page_size, 10), pageNum: parseInt(page_num, 10) },
);
const pageCount = await DBClient.instance.beerComment.count({ where: { beerPostId } });
res.setHeader('X-Total-Count', pageCount);
res.status(200).json({
message: 'Beer comments fetched successfully',
statusCode: 200,
payload: comments,
success: true,
});
};
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: BeerCommentValidationSchema,
querySchema: z.object({ id: z.string().uuid() }),
}),
getCurrentUser,
createComment,
);
router.get(
validateRequest({
querySchema: z.object({
id: z.string().uuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
}),
getAll,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,98 @@
import DBClient from '@/prisma/DBClient';
import { BeerImage } from '@prisma/client';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import { createRouter, expressWrapper } from 'next-connect';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import multer from 'multer';
import cloudinaryConfig from '@/config/cloudinary';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import ServerError from '@/config/util/ServerError';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
const { storage } = cloudinaryConfig;
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
const { mimetype } = file;
const isImage = mimetype.startsWith('image/');
if (!isImage) {
cb(null, false);
}
cb(null, true);
};
const uploadMiddleware = expressWrapper(
multer({ storage, fileFilter, limits: { files: 3 } }).array('images'),
);
const BeerPostImageValidationSchema = z.object({
caption: z.string(),
alt: z.string(),
});
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
files?: Express.Multer.File[];
query: { id: string };
body: z.infer<typeof BeerPostImageValidationSchema>;
}
const processImageData = async (
req: UploadBeerPostImagesRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { files, user, body } = req;
if (!files || !files.length) {
throw new ServerError('No images uploaded', 400);
}
const beerImagePromises: Promise<BeerImage>[] = [];
files.forEach((file) => {
beerImagePromises.push(
DBClient.instance.beerImage.create({
data: {
alt: body.alt,
postedBy: { connect: { id: user!.id } },
beerPost: { connect: { id: req.query.id } },
path: file.path,
caption: body.caption,
},
}),
);
});
const beerImages = await Promise.all(beerImagePromises);
res.status(200).json({
success: true,
message: `Successfully uploaded ${beerImages.length} image${
beerImages.length > 1 ? 's' : ''
}`,
statusCode: 200,
});
};
const router = createRouter<
UploadBeerPostImagesRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
// @ts-expect-error
uploadMiddleware,
validateRequest({ bodySchema: BeerPostImageValidationSchema }),
processImageData,
);
const handler = router.handler(NextConnectOptions);
export default handler;
export const config = { api: { bodyParser: false } };

View File

@@ -0,0 +1,90 @@
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import editBeerPostById from '@/services/BeerPost/editBeerPostById';
import EditBeerPostValidationSchema from '@/services/BeerPost/schema/EditBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter, NextHandler } from 'next-connect';
import { z } from 'zod';
import ServerError from '@/config/util/ServerError';
import DBClient from '@/prisma/DBClient';
interface BeerPostRequest extends UserExtendedNextApiRequest {
query: { id: string };
}
interface EditBeerPostRequest extends BeerPostRequest {
body: z.infer<typeof EditBeerPostValidationSchema>;
}
const checkIfBeerPostOwner = async (
req: BeerPostRequest,
res: NextApiResponse,
next: NextHandler,
) => {
const { user, query } = req;
const { id } = query;
const beerPost = await getBeerPostById(id);
if (!beerPost) {
throw new ServerError('Beer post not found', 404);
}
if (beerPost.postedBy.id !== user!.id) {
throw new ServerError('You cannot edit that beer post.', 403);
}
next();
};
const editBeerPost = async (
req: EditBeerPostRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const {
body,
query: { id },
} = req;
await editBeerPostById(id, body);
res.status(200).json({
message: 'Beer post updated successfully',
success: true,
statusCode: 200,
});
};
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
const {
query: { id },
} = req;
const deleted = await DBClient.instance.beerPost.delete({
where: { id },
});
if (!deleted) {
throw new ServerError('Beer post not found', 404);
}
res.status(200).json({
message: 'Beer post deleted successfully',
success: true,
statusCode: 200,
});
};
const router = createRouter<
EditBeerPostRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(getCurrentUser, checkIfBeerPostOwner, editBeerPost);
router.delete(getCurrentUser, checkIfBeerPostOwner, deleteBeerPost);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { createRouter } from 'next-connect';
import createNewBeerPost from '@/services/BeerPost/createNewBeerPost';
import CreateBeerPostValidationSchema from '@/services/BeerPost/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';
interface CreateBeerPostRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof CreateBeerPostValidationSchema>;
}
const createBeerPost = async (
req: CreateBeerPostRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { name, description, typeId, abv, ibu, breweryId } = req.body;
const newBeerPost = await createNewBeerPost({
name,
description,
abv,
ibu,
typeId,
breweryId,
userId: req.user!.id,
});
res.status(201).json({
message: 'Beer post created successfully',
statusCode: 201,
payload: newBeerPost,
success: true,
});
};
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,58 @@
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
import DBClient from '@/prisma/DBClient';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
const SearchSchema = z.object({
search: z.string().min(1),
});
interface SearchAPIRequest extends NextApiRequest {
query: z.infer<typeof SearchSchema>;
}
const search = async (req: SearchAPIRequest, res: NextApiResponse) => {
const { search: query } = req.query;
const beers: z.infer<typeof beerPostQueryResult>[] =
await DBClient.instance.beerPost.findMany({
select: {
id: true,
name: true,
ibu: true,
abv: true,
createdAt: true,
description: true,
postedBy: { select: { username: true, id: true } },
brewery: { select: { name: true, id: true } },
type: { select: { name: true, id: true } },
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
},
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ brewery: { name: { contains: query, mode: 'insensitive' } } },
{ type: { name: { contains: query, mode: 'insensitive' } } },
],
},
});
res.status(200).json(beers);
};
const router = createRouter<
SearchAPIRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(validateRequest({}), search);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,56 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import { verifyConfirmationToken } from '@/config/jwt';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import ServerError from '@/config/util/ServerError';
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 updateUserToBeConfirmedById from '@/services/User/updateUserToBeConfirmedById';
const ConfirmUserValidationSchema = z.object({ token: z.string() });
interface ConfirmUserRequest extends UserExtendedNextApiRequest {
query: z.infer<typeof ConfirmUserValidationSchema>;
}
const confirmUser = async (req: ConfirmUserRequest, res: NextApiResponse) => {
const { token } = req.query;
const user = req.user!;
const { id } = verifyConfirmationToken(token);
if (user.id !== id) {
throw new ServerError('Could not confirm user.', 401);
}
if (user.isAccountVerified) {
throw new ServerError('User is already verified.', 400);
}
await updateUserToBeConfirmedById(id);
res.status(200).json({
message: 'User confirmed successfully.',
statusCode: 200,
success: true,
});
};
const router = createRouter<
ConfirmUserRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({ querySchema: ConfirmUserValidationSchema }),
confirmUser,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,27 @@
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';
const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResponse) => {
const { user } = req;
res.status(200).json({
message: `Currently logged in as ${user!.username}`,
statusCode: 200,
success: true,
payload: user,
});
};
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,51 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import passport from 'passport';
import { createRouter, expressWrapper } from 'next-connect';
import localStrat from '@/config/auth/localStrat';
import { setLoginSession } from '@/config/auth/session';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
const router = createRouter<
UserExtendedNextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
validateRequest({ bodySchema: LoginValidationSchema }),
expressWrapper(async (req, res, next) => {
passport.initialize();
passport.use(localStrat);
passport.authenticate(
'local',
{ session: false },
(error: unknown, token: z.infer<typeof GetUserSchema>) => {
if (error) {
next(error);
return;
}
req.user = token;
next();
},
)(req, res, next);
}),
async (req, res) => {
const user = req.user!;
await setLoginSession(res, user);
res.status(200).json({
message: 'Login successful.',
payload: user,
statusCode: 200,
success: true,
});
},
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,28 @@
import { getLoginSession } from '@/config/auth/session';
import { removeTokenCookie } from '@/config/auth/cookie';
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 ServerError from '@/config/util/ServerError';
const router = createRouter<
NextApiRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.all(async (req, res) => {
const session = await getLoginSession(req);
if (!session) {
throw new ServerError('You are not logged in.', 400);
}
removeTokenCookie(res);
res.redirect('/');
});
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,65 @@
import { setLoginSession } from '@/config/auth/session';
import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
import ServerError from '@/config/util/ServerError';
import { createRouter } from 'next-connect';
import createNewUser from '@/services/User/createNewUser';
import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import findUserByUsername from '@/services/User/findUserByUsername';
import findUserByEmail from '@/services/User/findUserByEmail';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import sendConfirmationEmail from '@/services/User/sendConfirmationEmail';
interface RegisterUserRequest extends NextApiRequest {
body: z.infer<typeof CreateUserValidationSchema>;
}
const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
const [usernameTaken, emailTaken] = await Promise.all([
findUserByUsername(req.body.username),
findUserByEmail(req.body.email),
]);
if (usernameTaken) {
throw new ServerError(
'Could not register a user with that username as it is already taken.',
409,
);
}
if (emailTaken) {
throw new ServerError(
'Could not register a user with that email as it is already taken.',
409,
);
}
const user = await createNewUser(req.body);
await setLoginSession(res, {
id: user.id,
username: user.username,
});
await sendConfirmationEmail(user);
res.status(200).json({
success: true,
statusCode: 200,
message: 'User registered successfully.',
payload: user,
});
};
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,66 @@
import { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Layout from '@/components/ui/Layout';
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import EditBeerPostForm from '@/components/EditBeerPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
interface EditPageProps {
beerPost: z.infer<typeof beerPostQueryResult>;
}
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
const pageTitle = `Edit "${beerPost.name}"`;
return (
<Layout>
<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>
</Layout>
);
};
export default EditPage;
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,122 @@
import { NextPage, GetServerSideProps } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
import Layout from '@/components/ui/Layout';
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { BeerPost } from '@prisma/client';
import { z } from 'zod';
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
import { Carousel } from 'react-responsive-carousel';
import useMediaQuery from '@/hooks/useMediaQuery';
import { Tab } from '@headlessui/react';
interface BeerPageProps {
beerPost: z.infer<typeof beerPostQueryResult>;
beerRecommendations: (BeerPost & {
brewery: { id: string; name: string };
beerImages: { id: string; alt: string; url: string }[];
})[];
}
const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }) => {
const isMd = useMediaQuery('(min-width: 768px)');
return (
<>
<Head>
<title>{beerPost.name}</title>
<meta name="description" content={beerPost.description} />
</Head>
<Layout>
<div>
<Carousel
className="w-full"
useKeyboardArrows
autoPlay
interval={10000}
infiniteLoop
showThumbs={false}
>
{beerPost.beerImages.map((image, index) => (
<div key={image.id} id={`image-${index}}`} className="w-full">
<Image
alt={image.alt}
src={image.path}
height={1080}
width={1920}
className="h-[42rem] w-full object-cover"
/>
</div>
))}
</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">
<BeerInfoHeader beerPost={beerPost} />
{isMd ? (
<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 beerRecommendations={beerRecommendations} />
</div>
</div>
) : (
<Tab.Group>
<Tab.List className="card flex flex-row bg-base-300">
<Tab className="ui-selected:bg-gray w-1/2 p-3 uppercase">
Comments
</Tab>
<Tab className="ui-selected:bg-gray w-1/2 p-3 uppercase">
Recommendations
</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel>
<BeerPostCommentsSection beerPost={beerPost} />
</Tab.Panel>
<Tab.Panel>
<BeerRecommendations beerRecommendations={beerRecommendations} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</div>
</div>
</div>
</Layout>
</>
);
};
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const beerPost = await getBeerPostById(context.params!.id! as string);
if (!beerPost) {
return { notFound: true };
}
const { type, brewery, id } = beerPost;
const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
const props = {
beerPost: JSON.parse(JSON.stringify(beerPost)),
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
};
return { props };
};
export default BeerByIdPage;

View File

@@ -0,0 +1,45 @@
import CreateBeerPostForm from '@/components/CreateBeerPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import Layout from '@/components/ui/Layout';
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
import DBClient from '@/prisma/DBClient';
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { BeerType } from '@prisma/client';
import { NextPage } from 'next';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
interface CreateBeerPageProps {
breweries: z.infer<typeof BreweryPostQueryResult>[];
types: BeerType[];
}
const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
return (
<Layout>
<FormPageLayout
headingText="Create a new beer"
headingIcon={BiBeer}
backLink="/beers"
backLinkText="Back to beers"
>
<CreateBeerPostForm breweries={breweries} types={types} />
</FormPageLayout>
</Layout>
);
};
export const getServerSideProps = withPageAuthRequired<CreateBeerPageProps>(async () => {
const breweryPosts = await getAllBreweryPosts();
const beerTypes = await DBClient.instance.beerType.findMany();
return {
props: {
breweries: JSON.parse(JSON.stringify(breweryPosts)),
types: JSON.parse(JSON.stringify(beerTypes)),
},
};
});
export default Create;

78
src/pages/beers/index.tsx Normal file
View File

@@ -0,0 +1,78 @@
import { GetServerSideProps, NextPage } from 'next';
import getAllBeerPosts from '@/services/BeerPost/getAllBeerPosts';
import { useRouter } from 'next/router';
import DBClient from '@/prisma/DBClient';
import Layout from '@/components/ui/Layout';
import BeerIndexPaginationBar from '@/components/BeerIndex/BeerIndexPaginationBar';
import BeerCard from '@/components/BeerIndex/BeerCard';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import Head from 'next/head';
import { z } from 'zod';
import Link from 'next/link';
import UserContext from '@/contexts/userContext';
import { useContext } from 'react';
interface BeerPageProps {
initialBeerPosts: z.infer<typeof beerPostQueryResult>[];
pageCount: number;
}
const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
const router = useRouter();
const { query } = router;
const { user } = useContext(UserContext);
const pageNum = parseInt(query.page_num as string, 10) || 1;
return (
<Layout>
<Head>
<title>Beer</title>
<meta name="description" content="Beer posts" />
</Head>
<div className="flex items-center justify-center bg-base-100">
<div className="my-10 flex w-10/12 flex-col space-y-4">
<header className="my-10 flex justify-between">
<div className="space-y-2">
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
<h2 className="text-2xl font-bold">
Page {pageNum} of {pageCount}
</h2>
</div>
{!!user && (
<div>
<Link href="/beers/create" className="btn-primary btn">
Create a new beer post
</Link>
</div>
)}
</header>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{initialBeerPosts.map((post) => {
return <BeerCard post={post} key={post.id} />;
})}
</div>
<div className="flex justify-center">
<BeerIndexPaginationBar pageNum={pageNum} pageCount={pageCount} />
</div>
</div>
</div>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
const { query } = context;
const pageNumber = parseInt(query.page_num as string, 10) || 1;
const pageSize = 12;
const numberOfPosts = await DBClient.instance.beerPost.count();
const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0;
const beerPosts = await getAllBeerPosts(pageNumber, pageSize);
return {
props: { initialBeerPosts: JSON.parse(JSON.stringify(beerPosts)), pageCount },
};
};
export default BeerPage;

View File

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

View File

@@ -0,0 +1,29 @@
import Layout from '@/components/ui/Layout';
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import { GetServerSideProps, NextPage } from 'next';
import { z } from 'zod';
interface BreweryPageProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
}
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
return (
<Layout>
<h1 className="text-3xl font-bold underline">{breweryPost.name}</h1>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps<BreweryPageProps> = async (
context,
) => {
const breweryPost = await getBreweryPostById(context.params!.id! as string);
return !breweryPost
? { notFound: true }
: { props: { breweryPost: JSON.parse(JSON.stringify(breweryPost)) } };
};
export default BreweryByIdPage;

View File

@@ -0,0 +1,70 @@
import { GetServerSideProps, NextPage } from 'next';
import Link from 'next/link';
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import Layout from '@/components/ui/Layout';
import { FC } from 'react';
import Image from 'next/image';
import { z } from 'zod';
interface BreweryPageProps {
breweryPosts: z.infer<typeof BreweryPostQueryResult>[];
}
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
brewery,
}) => {
return (
<div className="card bg-base-300" key={brewery.id}>
<figure className="card-image h-96">
{brewery.breweryImages.length > 0 && (
<Image
src={brewery.breweryImages[0].path}
alt={brewery.name}
width="1029"
height="110"
/>
)}
</figure>
<div className="card-body space-y-3">
<div>
<h2 className="text-3xl font-bold">
<Link href={`/breweries/${brewery.id}`}>{brewery.name}</Link>
</h2>
<h3 className="text-xl font-semibold">{brewery.location}</h3>
</div>
</div>
</div>
);
};
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
return (
<Layout>
<div className="flex items-center justify-center bg-base-100">
<div className="my-10 flex w-10/12 flex-col space-y-4">
<header className="my-10">
<div className="space-y-2">
<h1 className="text-6xl font-bold">Breweries</h1>
</div>
</header>
<div className="grid gap-5 md:grid-cols-1 xl:grid-cols-2">
{breweryPosts.map((brewery) => {
return <BreweryCard brewery={brewery} key={brewery.id} />;
})}
</div>
</div>
</div>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps<BreweryPageProps> = async () => {
const breweryPosts = await getAllBreweryPosts();
return {
props: { breweryPosts: JSON.parse(JSON.stringify(breweryPosts)) },
};
};
export default BreweryPage;

12
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,12 @@
import Layout from '@/components/ui/Layout';
import { NextPage } from 'next';
const Home: NextPage = () => {
return (
<Layout>
<div></div>
</Layout>
);
};
export default Home;

56
src/pages/login/index.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { NextPage } from 'next';
import Layout from '@/components/ui/Layout';
import LoginForm from '@/components/Login/LoginForm';
import Image from 'next/image';
import { FaUserCircle } from 'react-icons/fa';
import Head from 'next/head';
import Link from 'next/link';
import useRedirectWhenLoggedIn from '@/hooks/useRedirectIfLoggedIn';
const LoginPage: NextPage = () => {
useRedirectWhenLoggedIn();
return (
<Layout>
<Head>
<title>Login</title>
<meta name="description" content="Login to your account" />
</Head>
<div className="flex h-full flex-row">
<div className="hidden h-full flex-col items-center justify-center bg-base-100 lg:flex lg:w-[55%]">
<Image
src="https://picsum.photos/5000/5000"
alt="Login Image"
width={4920}
height={4080}
className="h-full w-full object-cover"
/>
</div>
<div className="flex h-full w-full flex-col items-center justify-center bg-base-300 lg:w-[45%]">
<div className="w-10/12 space-y-5 sm:w-9/12">
<div className=" flex flex-col items-center space-y-2">
<FaUserCircle className="text-3xl" />
<h1 className="text-4xl font-bold">Login</h1>
</div>
<LoginForm />
<div className="mt-3 flex flex-col items-center space-y-1">
<Link href="/register" className="text-primary-500 link-hover link italic">
Don&apos;t have an account?
</Link>
<Link
href="/reset-password"
className="text-primary-500 link-hover link italic"
>
Forgot password?
</Link>
</div>
</div>
</div>
</div>
</Layout>
);
};
export default LoginPage;

View File

@@ -0,0 +1,31 @@
import RegisterUserForm from '@/components/RegisterUserForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import Layout from '@/components/ui/Layout';
import useRedirectWhenLoggedIn from '@/hooks/useRedirectIfLoggedIn';
import { NextPage } from 'next';
import Head from 'next/head';
import { BiUser } from 'react-icons/bi';
const RegisterUserPage: NextPage = () => {
useRedirectWhenLoggedIn();
return (
<Layout>
<Head>
<title>Register User</title>
<meta name="description" content="Register a new user" />
</Head>
<FormPageLayout
headingText="Register User"
headingIcon={BiUser}
backLink="/"
backLinkText="Back to home"
>
<RegisterUserForm />
</FormPageLayout>
</Layout>
);
};
export default RegisterUserPage;

View File

@@ -0,0 +1,40 @@
import Layout from '@/components/ui/Layout';
import Spinner from '@/components/ui/Spinner';
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
import UserContext from '@/contexts/userContext';
import { GetServerSideProps, NextPage } from 'next';
import { useContext } from 'react';
const ProtectedPage: NextPage = () => {
const { user, isLoading } = useContext(UserContext);
const currentTime = new Date().getHours();
const isMorning = currentTime > 4 && currentTime < 12;
const isAfternoon = currentTime > 12 && currentTime < 18;
const isEvening = (currentTime > 18 && currentTime < 24) || currentTime < 4;
return (
<Layout>
<div className="flex h-full flex-col items-center justify-center space-y-3">
{isLoading && <Spinner size="xl" />}
{user && (
<>
<h1 className="text-7xl font-bold">
Good {isMorning && 'morning'}
{isAfternoon && 'afternoon'}
{isEvening && 'evening'}
{`, ${user?.firstName}!`}
</h1>
<h2 className="text-4xl font-bold">Welcome to the Biergarten App!</h2>
</>
)}
</div>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps = withPageAuthRequired();
export default ProtectedPage;