main
Саске Учиха 2 years ago
parent 877a55a3a8
commit f13f9bc99b

@ -10,6 +10,12 @@ Ararat International School - платформа по обучению игры
- Express.js - Express.js
- Mongo DB - Mongo DB
#### V 0.03
- Добавленны UI компоненты: Input, Button, Modal
- Валидация форм регистрации/авторизации
- Модальное окно при ошибке
- LoginForm и RegisterForm заменены на LoginPage и RegisterPage
#### V 0.02 #### V 0.02
- Mobx заменен Redux toolkit - Mobx заменен Redux toolkit

@ -8,18 +8,24 @@
"name": "araratchess", "name": "araratchess",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@react-icons/all-files": "https://github.com/react-icons/react-icons/releases/download/v4.10.1/react-icons-all-files-4.10.1.tgz",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/uniqid": "^5.3.2",
"axios": "^1.4.0", "axios": "^1.4.0",
"body-scroll-lock": "^4.0.0-beta.0",
"framer-motion": "^10.13.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.45.2", "react-hook-form": "^7.45.2",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",
"react-router-dom": "^6.14.2" "react-router-dom": "^6.14.2",
"uniqid": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/body-scroll-lock": "^3.1.0",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/eslint-plugin": "^5.61.0",
@ -427,6 +433,21 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.14", "version": "0.18.14",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.14.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.14.tgz",
@ -972,6 +993,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@react-icons/all-files": {
"version": "4.10.1",
"resolved": "https://github.com/react-icons/react-icons/releases/download/v4.10.1/react-icons-all-files-4.10.1.tgz",
"integrity": "sha512-jm/9akpfUQRYke8YArGwnoTTTcsCNFjzXfC9rUwrtnEDrV3EOXYOF+R5sFrIgHPn+DMp5nEWLlPV4fmnLCEpMQ==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "1.9.5", "version": "1.9.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
@ -1012,6 +1042,12 @@
"axios": "*" "axios": "*"
} }
}, },
"node_modules/@types/body-scroll-lock": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/body-scroll-lock/-/body-scroll-lock-3.1.0.tgz",
"integrity": "sha512-3owAC4iJub5WPqRhxd8INarF2bWeQq1yQHBgYhN0XLBJMpd5ED10RrJ3aKiAwlTyL5wK7RkBD4SZUQz2AAAMdA==",
"dev": true
},
"node_modules/@types/history": { "node_modules/@types/history": {
"version": "4.7.11", "version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
@ -1097,6 +1133,11 @@
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
"dev": true "dev": true
}, },
"node_modules/@types/uniqid": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/@types/uniqid/-/uniqid-5.3.2.tgz",
"integrity": "sha512-/NYoaZpWsnAJDsGYeMNDeG3p3fuUb4AiC7MfKxi5VSu18tXd08w6Ch0fKW94T4FeLXXZwZPoFgHA1O0rDYKyMQ=="
},
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
@ -1469,6 +1510,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/body-scroll-lock": {
"version": "4.0.0-beta.0",
"resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz",
"integrity": "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ=="
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2278,6 +2324,34 @@
"url": "https://www.patreon.com/infusion" "url": "https://www.patreon.com/infusion"
} }
}, },
"node_modules/framer-motion": {
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.13.0.tgz",
"integrity": "sha512-xKhw9VCizmwEHbopOfluaoVunGHSZyMztGbTvsgOYqCjaKu6qtlwWY1J+6GhL41NY1P157JgEikjDm67XCFnvQ==",
"dependencies": {
"tslib": "^2.4.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/framer-motion/node_modules/tslib": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3711,6 +3785,11 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/uniqid": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.4.0.tgz",
"integrity": "sha512-38JRbJ4Fj94VmnC7G/J/5n5SC7Ab46OM5iNtSstB/ko3l1b5g7ALt4qzHFgGciFkyiRNtDXtLNb+VsxtMSE77A=="
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",

@ -10,18 +10,24 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@react-icons/all-files": "https://github.com/react-icons/react-icons/releases/download/v4.10.1/react-icons-all-files-4.10.1.tgz",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/uniqid": "^5.3.2",
"axios": "^1.4.0", "axios": "^1.4.0",
"body-scroll-lock": "^4.0.0-beta.0",
"framer-motion": "^10.13.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.45.2", "react-hook-form": "^7.45.2",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",
"react-router-dom": "^6.14.2" "react-router-dom": "^6.14.2",
"uniqid": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/body-scroll-lock": "^3.1.0",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/eslint-plugin": "^5.61.0",

@ -2,8 +2,8 @@ import { useEffect } from "react"
import RequireAuth from "./components/Auth/RequireAuth" import RequireAuth from "./components/Auth/RequireAuth"
import OnlyUnauthorized from "./components/Auth/OnlyUnauthorized" import OnlyUnauthorized from "./components/Auth/OnlyUnauthorized"
import IndexPage from "./pages/IndexPage" import IndexPage from "./pages/IndexPage"
import LoginForm from "./components/LoginForm" import LoginPage from "./pages/LoginPage"
import RegisterForm from "./components/RegisterForm" import RegisterPage from "./pages/RegisterPage"
import { BrowserRouter, Routes, Route } from 'react-router-dom' import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { checkAuth, userSlice } from "./store/reducers/UserSlice"; import { checkAuth, userSlice } from "./store/reducers/UserSlice";
import { useAppDispatch, useAppSelector } from "./hooks/redux" import { useAppDispatch, useAppSelector } from "./hooks/redux"
@ -32,8 +32,8 @@ function App() {
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path='/' element={<RequireAuth><IndexPage/></RequireAuth>}/> <Route path='/' element={<RequireAuth><IndexPage/></RequireAuth>}/>
<Route path='/login' element={<OnlyUnauthorized><LoginForm/></OnlyUnauthorized>}/> <Route path='/login' element={<OnlyUnauthorized><LoginPage/></OnlyUnauthorized>}/>
<Route path='/register' element={<OnlyUnauthorized><RegisterForm/></OnlyUnauthorized>}/> <Route path='/register' element={<OnlyUnauthorized><RegisterPage/></OnlyUnauthorized>}/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 671 KiB

@ -1,53 +0,0 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import {FC, useEffect} from 'react'
import { useForm, SubmitHandler } from "react-hook-form";
import Logo from '../assets/logo.png'
import { Link, useNavigate } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../hooks/redux';
import { login } from '../store/reducers/UserSlice';
type Form = {
email: string,
password: string,
};
const LoginForm: FC = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const {isAuth} = useAppSelector(state => state.UserSlice);
const { register, handleSubmit, formState: {isSubmitSuccessful} } = useForm<Form>();
useEffect(() => {
if(isSubmitSuccessful) {
if(isAuth) {
navigate('/');
}
}
}, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps
const onSubmit: SubmitHandler<Form> = async (data) => {
await dispatch(login({email: data.email, password: data.password}));
}
return (
<section className="flex items-center h-screen">
<form onSubmit={handleSubmit(onSubmit)} className='bg-white container mx-auto flex flex-col p-10 max-w-2xl'>
<img className='w-52 self-center mb-5' src={ Logo } alt="logo" />
<h1 className='text-2xl font-semibold tracking-wider text-gray-800 capitalize '>Login to your account</h1>
<p className='mt-4 text-gray-500 mb-5'>Let's get you all set up so you can verify your personal account and begin setting up your profile.</p>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="email">Email:</label>
<input id='email' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("email", { required: true })} placeholder='Email'/>
</div>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="password">Password:</label>
<input id='password' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("password", { required: true })} placeholder='Password'/>
</div>
<button className='flex items-center justify-center mt-2 w-full px-6 py-3 text-sm tracking-wide text-white hover:text-[#fbceb1] capitalize transition-colors duration-300 transform bg-gray-800 rounded-md focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50'>Login</button>
<p className='mt-5 text-gray-700'>Dont have account? <Link className='text-[#fbceb1]' to={'/register'}>Sign up here.</Link></p>
</form>
</section>
)
}
export default LoginForm;

@ -0,0 +1,21 @@
import { FC } from 'react'
import Modal from '../UI/Modal';
import { BiErrorCircle } from "@react-icons/all-files/bi/BiErrorCircle";
interface AuthErrorModalProps {
modal: boolean,
setModal: (bool: boolean) => void,
error: string
}
const AuthErrorModal: FC<AuthErrorModalProps> = ({ modal, setModal, error }) => {
return (
<Modal active={modal} setActive={setModal} className='items-center'>
<BiErrorCircle className='text-red-500 text-8xl mb-3'/>
<h2 className='text-4xl dark:text-white font-medium mb-3'>Error!</h2>
<p className='text-lg dark:text-[#c7c7c7] text-center'>{error}</p>
</Modal>
)
}
export default AuthErrorModal;

@ -1,77 +0,0 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import {FC, useEffect} from 'react'
import { useForm, SubmitHandler } from "react-hook-form";
import { useNavigate, Link } from 'react-router-dom';
import Logo from '../assets/logo.png'
import { useAppSelector, useAppDispatch } from '../hooks/redux';
import { registration } from '../store/reducers/UserSlice';
const RegisterForm: FC = () => {
type Form = {
email: string,
name: string,
sname: string,
password: string,
confirm_password: string
};
const navigate = useNavigate();
const dispatch = useAppDispatch();
const {isAuth} = useAppSelector(state => state.UserSlice);
const { register, handleSubmit, watch, formState: {isSubmitSuccessful} } = useForm<Form>();
useEffect(() => {
if(isSubmitSuccessful) {
if(isAuth) {
navigate('/');
}
}
}, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps
const onSubmit: SubmitHandler<Form> = async (data) => {
await dispatch(registration({email: data.email, name: data.name, sname: data.sname, password: data.password}));
}
return (
<section className="flex items-center h-screen">
<form onSubmit={handleSubmit(onSubmit)} className='bg-white container mx-auto flex flex-col p-10 max-w-2xl'>
<img className='w-52 self-center mb-5' src={ Logo } alt="logo" />
<h1 className='text-2xl font-semibold tracking-wider text-gray-800 capitalize '>Register To Get Started</h1>
<p className='mt-4 text-gray-500 mb-5'>Let's get you all set up so you can verify your personal account and begin setting up your profile.</p>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="email">Email:</label>
<input id='email' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("email", { required: true })} type='email' placeholder='Email'/>
</div>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="name">First Name:</label>
<input id='name' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("name", { required: true })} placeholder='First Name'/>
</div>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="sname">Last Name:</label>
<input id='sname' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("sname", { required: true })} type='text' placeholder='Last Name'/>
</div>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="password">Password:</label>
<input id='password' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("password", { required: true })} type='password' placeholder='Password'/>
</div>
<div className="flex flex-col mb-5">
<label className="block text-sm text-gray-600 " htmlFor="password">Confirm Password:</label>
<input id='password' className='block w-full px-5 py-3 mt-2 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-[#fbceb1] focus:ring-[#fbceb1] focus:outline-none focus:ring focus:ring-opacity-40' {...register("confirm_password",
{
required: true,
validate: (val: string) => {
if (watch('password') != val) {
return "Your passwords do no match";
}
},
})} type='password' placeholder='Confirm Password'/>
</div>
<button className='flex items-center justify-center mt-2 w-full px-6 py-3 text-sm tracking-wide text-white hover:text-[#fbceb1] capitalize transition-colors duration-300 transform bg-gray-800 rounded-md focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50'>Register</button>
<p className='mt-5 text-gray-700'>Already have an account ? <Link className='text-[#fbceb1]' to={'/login'}>Log In here.</Link></p>
</form>
</section>
)
}
export default RegisterForm;

@ -0,0 +1,13 @@
import { FC, PropsWithChildren } from 'react'
interface ButtonProps {
className?: string
}
const Button: FC<PropsWithChildren<ButtonProps>> = ({children, className, ...props}) => {
return (
<button className={['flex items-center justify-center w-full px-6 py-3 text-sm tracking-wide text-white hover:text-apricot capitalize transition-colors duration-300 transform bg-gray-800 rounded-md focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50', className].join(' ')} {...props}>{children}</button>
)
}
export default Button;

@ -0,0 +1,32 @@
import { FC } from 'react'
import uniqid from 'uniqid';
import { UseFormRegisterReturn } from 'react-hook-form';
interface InputProps {
className?: string,
wrapperClasses?: string
labelClasses?: string,
label?: string,
name?: string,
error?: string,
placeholder?: string,
type: string,
register?: UseFormRegisterReturn
}
const Input: FC<InputProps> = ({className, wrapperClasses, labelClasses, label, name, error, register, type, ...props}) => {
const uid: string = uniqid();
return (
<div className={['flex flex-col relative', wrapperClasses].join(' ')}>
{label &&
<label className={['block text-sm text-gray-600 mb-2', labelClasses].join(' ')} htmlFor={uid}>{label}</label>
}
<input type={type} id={uid} name={name} {...register} className={['block w-full px-5 py-3 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-apricot focus:ring-apricot focus:outline-none focus:ring focus:ring-opacity-40', className].join(' ')} {...props} />
{error &&
<p className="text-red-600 text-sm absolute -bottom-0.5 translate-y-full">{error}</p>
}
</div>
)
}
export default Input;

@ -0,0 +1,36 @@
import { FC, PropsWithChildren, useEffect, MouseEventHandler } from 'react'
import { motion, AnimatePresence } from "framer-motion";
import { IoMdClose } from "@react-icons/all-files/io/IoMdClose";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
interface ModalProps {
className?: string,
active: boolean,
setActive: (bool: boolean) => void,
}
const Modal: FC<PropsWithChildren<ModalProps>> = ({className, children, active, setActive, ...props}) => {
useEffect(() => {
active ? disableBodyScroll(document.body) : enableBodyScroll(document.body);
}, [active])
const stopProp: MouseEventHandler<HTMLDivElement> = (e) => {
e.stopPropagation();
};
return (
<AnimatePresence>
{active &&
<motion.div className='min-h-full overflow-y-auto fixed w-full h-full bg-black p-3 bg-opacity-80 left-0 top-0 flex justify-center items-center z-30' initial={{ opacity: 0 }} exit={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} onClick={() => setActive(false)}>
<motion.div className={['min-w-[320px] max-w-[500px] max-h-full overflow-y-auto w-full p-8 bg-white dark:bg-[#17212B] flex flex-col rounded-sm relative', className].join(' ')} initial={{ opacity: 0, scale: 0 }} exit={{ opacity: 0, scale: 0 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }} onClick={e=> stopProp(e)} {...props}>
<IoMdClose className='absolute right-3 top-3 text-2xl cursor-pointer dark:text-white' onClick={() => setActive(false)}/>
{children}
</motion.div>
</motion.div>
}
</AnimatePresence>
)
}
export default Modal;

@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import {FC, useEffect, useState} from 'react'
import { useForm, SubmitHandler } from "react-hook-form";
import Logo from '../assets/logo.png'
import { Link, useNavigate } from 'react-router-dom';
import { useAppSelector, useAppDispatch } from '../hooks/redux';
import { login } from '../store/reducers/UserSlice';
import Input from '../components/UI/Input';
import Button from '../components/UI/Button';
import AuthError from '../components/Modals/AuthError';
import { ServerError } from '../models/response/ServerError';
type Form = {
email: string,
password: string,
};
const LoginPage: FC = () => {
const [modal, setModal] = useState<boolean>(false);
const [modalError, setModalError] = useState<string>('');
const navigate = useNavigate();
const dispatch = useAppDispatch();
const {isAuth} = useAppSelector(state => state.UserSlice);
const { register, handleSubmit, formState: {errors, isSubmitSuccessful} } = useForm<Form>();
useEffect(() => {
if(isSubmitSuccessful) {
if(isAuth) {
navigate('/');
}
}
}, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps
const onSubmit: SubmitHandler<Form> = async (data) => {
const response = await dispatch(login({email: data.email, password: data.password}));
const res = response.payload as ServerError;
if(res?.error) {
setModal(true);
setModalError(res.error)
}
}
return (
<section className="flex items-center h-screen">
<form onSubmit={handleSubmit(onSubmit)} className='bg-white container mx-auto flex flex-col p-10 max-w-2xl'>
<img className='w-52 self-center mb-5' src={ Logo } alt="logo" />
<h1 className='text-2xl font-semibold tracking-wider text-gray-800 capitalize '>Login to your account</h1>
<p className='mt-4 text-gray-500 mb-5'>Let's get you all set up so you can verify your personal account and begin setting up your profile.</p>
<Input wrapperClasses='mb-5' type="text" label='Email:' placeholder='Email' error={errors.email?.message} register={register('email', { required: "The field must be filled" })}/>
<Input wrapperClasses='mb-5' type="text" label='Password:' placeholder='Password' error={errors.password?.message} register={register('password', { required: "The field must be filled" })}/>
<Button>Login</Button>
<p className='mt-5 text-gray-700'>Dont have account? <Link className='text-[#fbceb1]' to={'/register'}>Sign up here.</Link></p>
</form>
<AuthError modal={modal} setModal={setModal} error={modalError}/>
</section>
)
}
export default LoginPage;

@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import {FC, useEffect, useState} from 'react'
import { useForm, SubmitHandler } from "react-hook-form";
import { useNavigate, Link } from 'react-router-dom';
import Logo from '../assets/logo.png'
import { useAppSelector, useAppDispatch } from '../hooks/redux';
import { registration } from '../store/reducers/UserSlice';
import { ServerError } from '../models/response/ServerError';
import { EmailValidation, PasswordValidation } from '../utils/ValidationRules';
import Input from '../components/UI/Input';
import Button from '../components/UI/Button';
import AuthError from '../components/Modals/AuthError';
type Form = {
email: string,
name: string,
sname: string,
password: string,
confirm_password: string
};
const RegisterPage: FC = () => {
const [modal, setModal] = useState<boolean>(false);
const [modalError, setModalError] = useState<string>('');
const navigate = useNavigate();
const dispatch = useAppDispatch();
const {isAuth} = useAppSelector(state => state.UserSlice);
const { register, handleSubmit, watch, formState: {errors, isSubmitSuccessful} } = useForm<Form>();
useEffect(() => {
if(isSubmitSuccessful) {
if(isAuth) {
navigate('/');
}
}
}, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps
const onSubmit: SubmitHandler<Form> = async (data) => {
const response = await dispatch(registration({email: data.email, name: data.name, sname: data.sname, password: data.password}));
const res = response.payload as ServerError;
if(res?.error) {
setModal(true);
setModalError(res.error)
}
}
return (
<section className="flex items-center h-screen">
<form onSubmit={handleSubmit(onSubmit)} className='bg-white container mx-auto flex flex-col p-10 max-w-2xl'>
<img className='w-52 self-center mb-5' src={ Logo } alt="logo" />
<h1 className='text-2xl font-semibold tracking-wider text-gray-800 capitalize '>Register To Get Started</h1>
<p className='mt-4 text-gray-500 mb-5'>Let's get you all set up so you can verify your personal account and begin setting up your profile.</p>
<Input wrapperClasses='mb-5' type="email" label='Email:' placeholder='Email' error={errors.email?.message} register={register('email', EmailValidation)}/>
<Input wrapperClasses='mb-5' type="text" label='First Name:' placeholder='First Name' error={errors.name?.message} register={register('name', { required: "The field must be filled" })}/>
<Input wrapperClasses='mb-5' type="text" label='Last Name:' placeholder='Last Name' error={errors.sname?.message} register={register('sname', { required: "The field must be filled" })}/>
<Input wrapperClasses='mb-5' type="password" label='Password:' placeholder='Password' error={errors.password?.message} register={register('password', PasswordValidation)}/>
<Input wrapperClasses='mb-5' type="password" label='Confirm Password:' placeholder='Confirm Password' error={errors.confirm_password?.message} register={register('confirm_password', {
required: "The field must be filled",
validate: (val: string) => {
if (watch('password') != val) {
return "Your passwords do no match";
}
},
})}/>
<Button>Register</Button>
<p className='mt-5 text-gray-700'>Already have an account ? <Link className='text-[#fbceb1]' to={'/login'}>Log In here.</Link></p>
</form>
<AuthError modal={modal} setModal={setModal} error={modalError}/>
</section>
)
}
export default RegisterPage;

@ -39,7 +39,7 @@ export const registration = createAsyncThunk<User, {email: string, name: string,
} catch (error) { } catch (error) {
const err = error as AxiosError; const err = error as AxiosError;
const e = err.response?.data as ServerError const e = err.response?.data as ServerError
return rejectWithValue(e.error); return rejectWithValue(e);
} }
} }
@ -56,7 +56,7 @@ export const login = createAsyncThunk<User, {email: string, password: string}>(
} catch ( error ) { } catch ( error ) {
const err = error as AxiosError; const err = error as AxiosError;
const e = err.response?.data as ServerError const e = err.response?.data as ServerError
return rejectWithValue(e.error); return rejectWithValue(e);
} }
} }
) )

@ -0,0 +1,20 @@
import { RegisterOptions } from "react-hook-form"
const EmailValidation = {
required: "The field must be filled",
pattern: {
value: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<,>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, //eslint-disable-line
message: "Invalid email"
},
}
const PasswordValidation: RegisterOptions = {
required: "The field must be filled",
minLength: {
value: 6,
message: "Password must be more than 6 characters"
},
}
export { EmailValidation, PasswordValidation }

@ -5,7 +5,11 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",
], ],
theme: { theme: {
extend: {}, extend: {
colors: {
'apricot': '#fbceb1',
}
},
}, },
plugins: [], plugins: [],
} }

@ -4,7 +4,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"types": [ "uniqid", "body-scroll-lock" ]
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

Loading…
Cancel
Save