From 80261a713bfaf78f4556610602e9813366c43959 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 13 Feb 2023 10:56:09 -0500 Subject: [PATCH] Add comments pagination, login and register pages --- components/BeerById/CommentCard.tsx | 2 +- components/Login/LoginForm.tsx | 16 +- components/ui/Navbar.tsx | 10 +- components/ui/alerts/ErrorAlert.tsx | 32 ++++ components/ui/forms/Button.tsx | 2 +- hooks/useUser.ts | 6 + pages/beers/[id].tsx | 84 +++++++-- pages/beers/index.tsx | 5 + pages/login/index.tsx | 29 ++- pages/logout/index.tsx | 16 ++ pages/register/index.tsx | 165 ++++++++++++++++++ prisma/seed/create/createNewUsers.ts | 2 +- requests/sendLoginUserRequest.ts | 3 + requests/sendRegisterUserRequest.ts | 35 ++++ .../User/schema/CreateUserValidationSchema.ts | 78 +++++---- services/User/schema/GetUserSchema.ts | 6 +- 16 files changed, 422 insertions(+), 69 deletions(-) create mode 100644 components/ui/alerts/ErrorAlert.tsx create mode 100644 pages/logout/index.tsx create mode 100644 pages/register/index.tsx create mode 100644 requests/sendRegisterUserRequest.ts diff --git a/components/BeerById/CommentCard.tsx b/components/BeerById/CommentCard.tsx index 5af3e49..7d5712b 100644 --- a/components/BeerById/CommentCard.tsx +++ b/components/BeerById/CommentCard.tsx @@ -13,7 +13,7 @@ const CommentCard: React.FC<{ }, [comment.createdAt]); return ( -
+

{comment.postedBy.username}

diff --git a/components/Login/LoginForm.tsx b/components/Login/LoginForm.tsx index 08288df..722bc35 100644 --- a/components/Login/LoginForm.tsx +++ b/components/Login/LoginForm.tsx @@ -2,8 +2,10 @@ import sendLoginUserRequest from '@/requests/sendLoginUserRequest'; import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRouter } from 'next/router'; +import { useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; +import ErrorAlert from '../ui/alerts/ErrorAlert'; import FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; import FormLabel from '../ui/forms/FormLabel'; @@ -13,7 +15,7 @@ import FormTextInput from '../ui/forms/FormTextInput'; type LoginT = z.infer; const LoginForm = () => { const router = useRouter(); - const { register, handleSubmit, formState } = useForm({ + const { register, handleSubmit, formState, reset } = useForm({ resolver: zodResolver(LoginValidationSchema), defaultValues: { username: '', @@ -23,18 +25,23 @@ const LoginForm = () => { const { errors } = formState; + const [responseError, setResponseError] = useState(''); + const onSubmit: SubmitHandler = async (data) => { try { const response = await sendLoginUserRequest(data); router.push(`/users/${response.id}`); } catch (error) { - console.error(error); + if (error instanceof Error) { + setResponseError(error.message); + reset(); + } } }; return ( -
+
username @@ -65,8 +72,9 @@ const LoginForm = () => {
+ {responseError && }
-
diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index 150d54c..9b81836 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -42,9 +42,9 @@ const Navbar = () => { ]; return ( -
-
); }; @@ -98,21 +135,30 @@ const BeerByIdPage: NextPage = ({ export const getServerSideProps: GetServerSideProps = async (context) => { const beerPost = await getBeerPostById(context.params!.id! as string); + const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1; + if (!beerPost) { return { notFound: true }; } const { type, brewery, id } = beerPost; + const beerRecommendations = await getBeerRecommendations({ type, brewery, id }); + + const pageSize = 5; const beerComments = await getAllBeerComments( { id: beerPost.id }, - { pageSize: 9, pageNum: 1 }, + { pageSize, pageNum: beerCommentPageNum }, ); - const beerRecommendations = await getBeerRecommendations({ type, brewery, id }); + const numberOfPosts = await DBClient.instance.beerComment.count({ + where: { beerPostId: beerPost.id }, + }); + const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0; const props = { beerPost: JSON.parse(JSON.stringify(beerPost)), beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)), beerComments: JSON.parse(JSON.stringify(beerComments)), + commentsPageCount: JSON.parse(JSON.stringify(pageCount)), }; return { props }; diff --git a/pages/beers/index.tsx b/pages/beers/index.tsx index 6a05ee4..24361a9 100644 --- a/pages/beers/index.tsx +++ b/pages/beers/index.tsx @@ -7,6 +7,7 @@ import Layout from '@/components/ui/Layout'; import Pagination from '@/components/BeerIndex/Pagination'; import BeerCard from '@/components/BeerIndex/BeerCard'; import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; +import Head from 'next/head'; interface BeerPageProps { initialBeerPosts: BeerPostQueryResult[]; @@ -20,6 +21,10 @@ const BeerPage: NextPage = ({ initialBeerPosts, pageCount }) => { const pageNum = parseInt(query.page_num as string, 10) || 1; return ( + + Beer + +
diff --git a/pages/login/index.tsx b/pages/login/index.tsx index b564eea..24ae7d3 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -4,6 +4,10 @@ import { useRouter } from 'next/router'; import Layout from '@/components/ui/Layout'; import useUser from '@/hooks/useUser'; import LoginForm from '@/components/Login/LoginForm'; +import Image from 'next/image'; + +import { FaUserCircle } from 'react-icons/fa'; +import Head from 'next/head'; const LoginPage: NextPage = () => { const { user } = useUser(); @@ -19,12 +23,29 @@ const LoginPage: NextPage = () => { return ( + + Login + + +
-
-

Login

+
+ Login Image
-
- +
+
+
+ +

Login

+
+ +
diff --git a/pages/logout/index.tsx b/pages/logout/index.tsx new file mode 100644 index 0000000..0cf2d44 --- /dev/null +++ b/pages/logout/index.tsx @@ -0,0 +1,16 @@ +import { NextPage } from 'next'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +const LogoutPage: NextPage = () => { + const router = useRouter(); + + useEffect(() => { + document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + router.reload(); + router.push('/'); + }, [router]); + return
; +}; + +export default LogoutPage; diff --git a/pages/register/index.tsx b/pages/register/index.tsx new file mode 100644 index 0000000..c65ba97 --- /dev/null +++ b/pages/register/index.tsx @@ -0,0 +1,165 @@ +import ErrorAlert from '@/components/ui/alerts/ErrorAlert'; +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 Layout from '@/components/ui/Layout'; +import sendRegisterUserRequest from '@/requests/sendRegisterUserRequest'; +import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { NextPage } from 'next'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { FaUserCircle } from 'react-icons/fa'; +import { z } from 'zod'; + +interface RegisterUserProps {} + +const RegisterUserPage: NextPage = () => { + const { reset, register, handleSubmit, formState } = useForm< + z.infer + >({ + resolver: zodResolver(CreateUserValidationSchema), + }); + + const { errors } = formState; + + const [serverResponseError, setServerResponseError] = useState(''); + + const onSubmit = async (data: z.infer) => { + try { + await sendRegisterUserRequest(data); + reset(); + } catch (error) { + setServerResponseError( + error instanceof Error + ? error.message + : 'Something went wrong. We could not register your account.', + ); + } + }; + + return ( + +
+
+ +

Register

+
+ + {serverResponseError && ( + + )} +
+
+
+ + First name + {errors.firstName?.message} + + + + +
+ +
+ + Last name + {errors.lastName?.message} + + + + +
+
+ + username + {errors.username?.message} + + + + + + email + {errors.email?.message} + + + + + + password + {errors.password?.message} + + + + + + confirm password + {errors.confirmPassword?.message} + + + + + + Date of birth + {errors.dateOfBirth?.message} + + + + + +
+ +
+
+ ); +}; + +export default RegisterUserPage; diff --git a/prisma/seed/create/createNewUsers.ts b/prisma/seed/create/createNewUsers.ts index fc189f4..9af1101 100644 --- a/prisma/seed/create/createNewUsers.ts +++ b/prisma/seed/create/createNewUsers.ts @@ -18,7 +18,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfUsers; i++) { - const randomValue = crypto.randomBytes(2).toString('hex'); + const randomValue = crypto.randomBytes(4).toString('hex'); const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); const username = `${firstName[0]}.${lastName}.${randomValue}`; diff --git a/requests/sendLoginUserRequest.ts b/requests/sendLoginUserRequest.ts index f664c79..7be9e64 100644 --- a/requests/sendLoginUserRequest.ts +++ b/requests/sendLoginUserRequest.ts @@ -17,6 +17,9 @@ const sendLoginUserRequest = async (data: { username: string; password: string } throw new Error('API response validation failed'); } + if (!parsed.data.success) { + throw new Error(parsed.data.message); + } const parsedPayload = BasicUserInfoSchema.safeParse(parsed.data.payload); if (!parsedPayload.success) { throw new Error('API response payload validation failed'); diff --git a/requests/sendRegisterUserRequest.ts b/requests/sendRegisterUserRequest.ts new file mode 100644 index 0000000..7e106b4 --- /dev/null +++ b/requests/sendRegisterUserRequest.ts @@ -0,0 +1,35 @@ +import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema'; +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +async function sendRegisterUserRequest(data: z.infer) { + const response = await fetch('/api/users/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + const json = await response.json(); + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('API response validation failed.'); + } + + if (!parsed.data.success) { + throw new Error(parsed.data.message); + } + + const parsedPayload = GetUserSchema.safeParse(parsed.data.payload); + + if (!parsedPayload.success) { + throw new Error('API response payload validation failed.'); + } + + return parsedPayload.data; +} + +export default sendRegisterUserRequest; diff --git a/services/User/schema/CreateUserValidationSchema.ts b/services/User/schema/CreateUserValidationSchema.ts index e576656..850f815 100644 --- a/services/User/schema/CreateUserValidationSchema.ts +++ b/services/User/schema/CreateUserValidationSchema.ts @@ -2,36 +2,52 @@ import sub from 'date-fns/sub'; import { z } from 'zod'; const minimumDateOfBirth = sub(new Date(), { years: 19 }); -const CreateUserValidationSchema = z.object({ - email: z.string().email({ message: 'Email must be a valid email address.' }), - // use special characters, numbers, and uppercase letters - password: z - .string() - .min(8, { message: 'Password must be at least 8 characters.' }) - .refine((password) => /[A-Z]/.test(password), { - message: 'Password must contain at least one uppercase letter.', - }) - .refine((password) => /[0-9]/.test(password), { - message: 'Password must contain at least one number.', - }) - .refine((password) => /[^a-zA-Z0-9]/.test(password), { - message: 'Password must contain at least one special character.', - }), - - firstName: z.string().min(1, { message: 'First name must not be empty.' }), - lastName: z.string().min(1, { message: 'Last name must not be empty.' }), - username: z - .string() - .min(1, { message: 'Username must not be empty.' }) - .max(20, { message: 'Username must be less than 20 characters.' }), - dateOfBirth: z.string().refine( - (dateOfBirth) => { - const parsedDateOfBirth = new Date(dateOfBirth); - - return parsedDateOfBirth <= minimumDateOfBirth; - }, - { message: 'You must be at least 19 years old to register.' }, - ), -}); +const CreateUserValidationSchema = z + .object({ + email: z.string().email({ message: 'Email must be a valid email address.' }), + // use special characters, numbers, and uppercase letters + password: z + .string() + .min(8, { message: 'Password must be at least 8 characters.' }) + .refine((password) => /[A-Z]/.test(password), { + message: 'Password must contain at least one uppercase letter.', + }) + .refine((password) => /[0-9]/.test(password), { + message: 'Password must contain at least one number.', + }) + .refine((password) => /[^a-zA-Z0-9]/.test(password), { + message: 'Password must contain at least one special character.', + }), + confirmPassword: z.string(), + firstName: z + .string() + .min(1, { message: 'First name must not be empty.' }) + .max(20, { message: 'First name must be less than 20 characters.' }) + .refine((firstName) => /^[a-zA-Z]+$/.test(firstName), { + message: 'First name must only contain letters.', + }), + lastName: z + .string() + .min(1, { message: 'Last name must not be empty.' }) + .max(20, { message: 'Last name must be less than 20 characters.' }) + .refine((lastName) => /^[a-zA-Z]+$/.test(lastName), { + message: 'Last name must only contain letters.', + }), + username: z + .string() + .min(1, { message: 'Username must not be empty.' }) + .max(20, { message: 'Username must be less than 20 characters.' }), + dateOfBirth: z.string().refine( + (dateOfBirth) => { + const parsedDateOfBirth = new Date(dateOfBirth); + return parsedDateOfBirth <= minimumDateOfBirth; + }, + { message: 'You must be at least 19 years old to register.' }, + ), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match.', + path: ['confirmPassword'], + }); export default CreateUserValidationSchema; diff --git a/services/User/schema/GetUserSchema.ts b/services/User/schema/GetUserSchema.ts index 529f824..2619334 100644 --- a/services/User/schema/GetUserSchema.ts +++ b/services/User/schema/GetUserSchema.ts @@ -3,12 +3,12 @@ import { z } from 'zod'; const GetUserSchema = z.object({ id: z.string().uuid(), username: z.string(), - createdAt: z.date().or(z.string()), - updatedAt: z.date().or(z.string()).optional(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date().optional(), email: z.string().email(), firstName: z.string(), lastName: z.string(), - dateOfBirth: z.date().or(z.string()), + dateOfBirth: z.coerce.date(), }); export default GetUserSchema;