mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Add comments pagination, login and register pages
This commit is contained in:
@@ -13,7 +13,7 @@ const CommentCard: React.FC<{
|
||||
}, [comment.createdAt]);
|
||||
|
||||
return (
|
||||
<div className="card-body h-[1/9]">
|
||||
<div className="card-body h-64">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">{comment.postedBy.username}</h3>
|
||||
|
||||
@@ -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<typeof LoginValidationSchema>;
|
||||
const LoginForm = () => {
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit, formState } = useForm<LoginT>({
|
||||
const { register, handleSubmit, formState, reset } = useForm<LoginT>({
|
||||
resolver: zodResolver(LoginValidationSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
@@ -23,18 +25,23 @@ const LoginForm = () => {
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
const [responseError, setResponseError] = useState<string>('');
|
||||
|
||||
const onSubmit: SubmitHandler<LoginT> = 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 (
|
||||
<form className="form-control w-9/12 space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="form-control w-full space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="username">username</FormLabel>
|
||||
@@ -65,8 +72,9 @@ const LoginForm = () => {
|
||||
</FormSegment>
|
||||
</div>
|
||||
|
||||
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
|
||||
<div className="w-full">
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
<button type="submit" className="btn-primary btn w-full">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,9 @@ const Navbar = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="navbar bg-primary">
|
||||
<nav className="navbar bg-primary text-primary-content">
|
||||
<div className="flex-1">
|
||||
<Link className="btn-ghost btn text-3xl normal-case" href="/">
|
||||
<Link className="btn btn-ghost text-3xl normal-case" href="/">
|
||||
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ const Navbar = () => {
|
||||
<span
|
||||
className={`text-lg uppercase ${
|
||||
currentURL === page.slug ? 'font-extrabold' : 'font-semibold'
|
||||
} text-base-content`}
|
||||
} text-primary-content`}
|
||||
>
|
||||
{page.name}
|
||||
</span>
|
||||
@@ -69,7 +69,7 @@ const Navbar = () => {
|
||||
</div>
|
||||
<div className="flex-none lg:hidden">
|
||||
<div className="dropdown-end dropdown">
|
||||
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
||||
<span className="w-10 rounded-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -93,7 +93,7 @@ const Navbar = () => {
|
||||
{pages.map((page) => (
|
||||
<li key={page.slug}>
|
||||
<Link href={page.slug}>
|
||||
<span className="select-none">{page.name}</span>
|
||||
<span className="select-none text-primary-content">{page.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
32
components/ui/alerts/ErrorAlert.tsx
Normal file
32
components/ui/alerts/ErrorAlert.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { FiAlertTriangle } from 'react-icons/fi';
|
||||
|
||||
interface ErrorAlertProps {
|
||||
error: string;
|
||||
setError: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
|
||||
return (
|
||||
<div className="alert alert-error shadow-lg">
|
||||
<div>
|
||||
<FiAlertTriangle className="h-6 w-6" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="btn-ghost btn-sm btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorAlert;
|
||||
@@ -7,7 +7,7 @@ interface FormButtonProps {
|
||||
|
||||
const Button: FunctionComponent<FormButtonProps> = ({ children, type }) => (
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
<button type={type} className="btn btn-primary mt-4 w-full rounded-xl">
|
||||
<button type={type} className="btn-primary btn mt-4 w-full rounded-xl">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,20 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem
|
||||
import useSWR from 'swr';
|
||||
|
||||
const useUser = () => {
|
||||
// check cookies for user
|
||||
const {
|
||||
data: user,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR('/api/users/current', async (url) => {
|
||||
if (!document.cookie) {
|
||||
throw new Error('Not logged in.');
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
||||
import CommentCard from '@/components/BeerById/CommentCard';
|
||||
import Layout from '@/components/ui/Layout';
|
||||
import UserContext from '@/contexts/userContext';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||
@@ -13,32 +14,35 @@ import { BeerPost } from '@prisma/client';
|
||||
import { NextPage, GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
|
||||
interface BeerPageProps {
|
||||
beerPost: BeerPostQueryResult;
|
||||
beerRecommendations: (BeerPost & {
|
||||
brewery: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
beerImages: {
|
||||
id: string;
|
||||
alt: string;
|
||||
url: string;
|
||||
}[];
|
||||
brewery: { id: string; name: string };
|
||||
beerImages: { id: string; alt: string; url: string }[];
|
||||
})[];
|
||||
beerComments: BeerCommentQueryResultArrayT;
|
||||
commentsPageCount: number;
|
||||
}
|
||||
|
||||
const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
beerPost,
|
||||
beerRecommendations,
|
||||
beerComments,
|
||||
commentsPageCount,
|
||||
}) => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const [comments, setComments] = useState(beerComments);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const commentsPageNum = router.query.comments_page
|
||||
? parseInt(router.query.comments_page as string, 10)
|
||||
: 1;
|
||||
|
||||
useEffect(() => {
|
||||
setComments(beerComments);
|
||||
}, [beerComments]);
|
||||
@@ -49,7 +53,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
<title>{beerPost.name}</title>
|
||||
<meta name="description" content={beerPost.description} />
|
||||
</Head>
|
||||
<main>
|
||||
<div>
|
||||
{beerPost.beerImages[0] && (
|
||||
<Image
|
||||
alt={beerPost.beerImages[0].alt}
|
||||
@@ -61,10 +65,10 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
)}
|
||||
|
||||
<div className="my-12 flex w-full items-center justify-center ">
|
||||
<div className="w-10/12 space-y-3">
|
||||
<div className="w-11/12 space-y-3 lg:w-9/12">
|
||||
<BeerInfoHeader beerPost={beerPost} />
|
||||
<div className="mt-4 flex space-x-3">
|
||||
<div className="w-[60%] space-y-3">
|
||||
<div className="mt-4 flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-3">
|
||||
<div className="w-full space-y-3 sm:w-[60%]">
|
||||
<div className="card h-96 bg-base-300">
|
||||
<div className="card-body h-full">
|
||||
{user ? (
|
||||
@@ -78,19 +82,52 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card h-[135rem] bg-base-300">
|
||||
<div className="card bg-base-300 pb-6">
|
||||
{comments.map((comment) => (
|
||||
<CommentCard key={comment.id} comment={comment} />
|
||||
))}
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="btn-group grid w-6/12 grid-cols-2">
|
||||
<Link
|
||||
className={`btn-outline btn ${
|
||||
commentsPageNum === 1
|
||||
? 'btn-disabled pointer-events-none'
|
||||
: 'pointer-events-auto'
|
||||
}`}
|
||||
href={{
|
||||
pathname: `/beers/${beerPost.id}`,
|
||||
query: { comments_page: commentsPageNum - 1 },
|
||||
}}
|
||||
scroll={false}
|
||||
>
|
||||
Next Comments
|
||||
</Link>
|
||||
<Link
|
||||
className={`btn-outline btn ${
|
||||
commentsPageNum === commentsPageCount
|
||||
? 'btn-disabled pointer-events-none'
|
||||
: 'pointer-events-auto'
|
||||
}`}
|
||||
href={{
|
||||
pathname: `/beers/${beerPost.id}`,
|
||||
query: { comments_page: commentsPageNum + 1 },
|
||||
}}
|
||||
scroll={false}
|
||||
>
|
||||
Previous Comments
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[40%]">
|
||||
<div className="sm:w-[40%]">
|
||||
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -98,21 +135,30 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||
export const getServerSideProps: GetServerSideProps<BeerPageProps> = 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 };
|
||||
|
||||
@@ -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<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
|
||||
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">
|
||||
<main className="my-10 flex w-10/12 flex-col space-y-4">
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
|
||||
@@ -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 (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Login</title>
|
||||
<meta name="description" content="Login to your account" />
|
||||
</Head>
|
||||
|
||||
<div className="flex h-full flex-row">
|
||||
<div className="flex h-full w-[40%] flex-col items-center justify-center bg-base-100">
|
||||
<h1>Login</h1>
|
||||
<div className="flex h-full w-[60%] flex-col items-center justify-center bg-base-100">
|
||||
<Image
|
||||
src="https://picsum.photos/1040/1080"
|
||||
alt="Login Image"
|
||||
width={4920}
|
||||
height={4080}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full w-[60%] flex-col items-center justify-center bg-base-300">
|
||||
<LoginForm />
|
||||
<div className="flex h-full w-[40%] flex-col items-center space-y-5 bg-base-300">
|
||||
<div className="mt-44 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>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
16
pages/logout/index.tsx
Normal file
16
pages/logout/index.tsx
Normal file
@@ -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 <div />;
|
||||
};
|
||||
|
||||
export default LogoutPage;
|
||||
165
pages/register/index.tsx
Normal file
165
pages/register/index.tsx
Normal file
@@ -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<RegisterUserProps> = () => {
|
||||
const { reset, register, handleSubmit, formState } = useForm<
|
||||
z.infer<typeof CreateUserValidationSchema>
|
||||
>({
|
||||
resolver: zodResolver(CreateUserValidationSchema),
|
||||
});
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
const [serverResponseError, setServerResponseError] = useState('');
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof CreateUserValidationSchema>) => {
|
||||
try {
|
||||
await sendRegisterUserRequest(data);
|
||||
reset();
|
||||
} catch (error) {
|
||||
setServerResponseError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Something went wrong. We could not register your account.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div
|
||||
className="flex h-full flex-col items-center justify-center space-y-6"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<FaUserCircle className="text-3xl" />
|
||||
<h1 className="text-4xl font-bold">Register</h1>
|
||||
</div>
|
||||
<form className="form-control w-7/12 space-y-5" noValidate>
|
||||
{serverResponseError && (
|
||||
<ErrorAlert error={serverResponseError} setError={setServerResponseError} />
|
||||
)}
|
||||
<div>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="w-[50%]">
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="firstName">First name</FormLabel>
|
||||
<FormError>{errors.firstName?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="firstName"
|
||||
type="text"
|
||||
formValidationSchema={register('firstName')}
|
||||
error={!!errors.firstName}
|
||||
placeholder="first name"
|
||||
/>
|
||||
</FormSegment>
|
||||
</div>
|
||||
|
||||
<div className="w-[50%]">
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="lastName">Last name</FormLabel>
|
||||
<FormError>{errors.lastName?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="lastName"
|
||||
type="text"
|
||||
formValidationSchema={register('lastName')}
|
||||
error={!!errors.lastName}
|
||||
placeholder="last name"
|
||||
/>
|
||||
</FormSegment>
|
||||
</div>
|
||||
</div>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="username">username</FormLabel>
|
||||
<FormError>{errors.username?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="username"
|
||||
type="text"
|
||||
formValidationSchema={register('username')}
|
||||
error={!!errors.username}
|
||||
placeholder="username"
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="email">email</FormLabel>
|
||||
<FormError>{errors.email?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="email"
|
||||
type="email"
|
||||
formValidationSchema={register('email')}
|
||||
error={!!errors.email}
|
||||
placeholder="email"
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="password">password</FormLabel>
|
||||
<FormError>{errors.password?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="password"
|
||||
type="password"
|
||||
formValidationSchema={register('password')}
|
||||
error={!!errors.password}
|
||||
placeholder="password"
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="confirmPassword">confirm password</FormLabel>
|
||||
<FormError>{errors.confirmPassword?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
formValidationSchema={register('confirmPassword')}
|
||||
error={!!errors.confirmPassword}
|
||||
placeholder="confirm password"
|
||||
/>
|
||||
</FormSegment>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="dateOfBirth">Date of birth</FormLabel>
|
||||
<FormError>{errors.dateOfBirth?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
id="dateOfBirth"
|
||||
type="date"
|
||||
formValidationSchema={register('dateOfBirth')}
|
||||
error={!!errors.dateOfBirth}
|
||||
placeholder="date of birth"
|
||||
/>
|
||||
</FormSegment>
|
||||
<Button type="submit">Register User</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterUserPage;
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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');
|
||||
|
||||
35
requests/sendRegisterUserRequest.ts
Normal file
35
requests/sendRegisterUserRequest.ts
Normal file
@@ -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<typeof CreateUserValidationSchema>) {
|
||||
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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user