diff --git a/README.md b/README.md index c4edfa0..24d0991 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ Ararat International School - платформа по обучению игры - Express.js - Mongo DB +#### V 0.03 +- Добавленны UI компоненты: Input, Button, Modal +- Валидация форм регистрации/авторизации +- Модальное окно при ошибке +- LoginForm и RegisterForm заменены на LoginPage и RegisterPage + #### V 0.02 - Mobx заменен Redux toolkit diff --git a/client/package-lock.json b/client/package-lock.json index d8236a7..4bc7645 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,18 +8,24 @@ "name": "araratchess", "version": "0.0.0", "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", "@types/axios": "^0.14.0", "@types/react-redux": "^7.1.25", "@types/react-router-dom": "^5.3.3", + "@types/uniqid": "^5.3.2", "axios": "^1.4.0", + "body-scroll-lock": "^4.0.0-beta.0", + "framer-motion": "^10.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.2", "react-redux": "^8.1.1", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "uniqid": "^5.4.0" }, "devDependencies": { + "@types/body-scroll-lock": "^3.1.0", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -427,6 +433,21 @@ "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": { "version": "0.18.14", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.14.tgz", @@ -972,6 +993,15 @@ "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": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -1012,6 +1042,12 @@ "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": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", @@ -1097,6 +1133,11 @@ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "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": { "version": "0.0.3", "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_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": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2278,6 +2324,34 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3711,6 +3785,11 @@ "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": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/client/package.json b/client/package.json index b852158..b5c903f 100644 --- a/client/package.json +++ b/client/package.json @@ -10,18 +10,24 @@ "preview": "vite preview" }, "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", "@types/axios": "^0.14.0", "@types/react-redux": "^7.1.25", "@types/react-router-dom": "^5.3.3", + "@types/uniqid": "^5.3.2", "axios": "^1.4.0", + "body-scroll-lock": "^4.0.0-beta.0", + "framer-motion": "^10.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.2", "react-redux": "^8.1.1", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "uniqid": "^5.4.0" }, "devDependencies": { + "@types/body-scroll-lock": "^3.1.0", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 8d77d04..52352b3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,8 +2,8 @@ import { useEffect } from "react" import RequireAuth from "./components/Auth/RequireAuth" import OnlyUnauthorized from "./components/Auth/OnlyUnauthorized" import IndexPage from "./pages/IndexPage" -import LoginForm from "./components/LoginForm" -import RegisterForm from "./components/RegisterForm" +import LoginPage from "./pages/LoginPage" +import RegisterPage from "./pages/RegisterPage" import { BrowserRouter, Routes, Route } from 'react-router-dom' import { checkAuth, userSlice } from "./store/reducers/UserSlice"; import { useAppDispatch, useAppSelector } from "./hooks/redux" @@ -32,8 +32,8 @@ function App() { }/> - }/> - }/> + }/> + }/> ) diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png index 65bd604..44cf409 100644 Binary files a/client/src/assets/logo.png and b/client/src/assets/logo.png differ diff --git a/client/src/components/LoginForm.tsx b/client/src/components/LoginForm.tsx deleted file mode 100644 index ef8ba99..0000000 --- a/client/src/components/LoginForm.tsx +++ /dev/null @@ -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
(); - - useEffect(() => { - if(isSubmitSuccessful) { - if(isAuth) { - navigate('/'); - } - } - }, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps - - const onSubmit: SubmitHandler = async (data) => { - await dispatch(login({email: data.email, password: data.password})); - } - - return ( -
- - logo -

Login to your account

-

Let's get you all set up so you can verify your personal account and begin setting up your profile.

-
- - -
-
- - -
- -

Dont have account? Sign up here.

- -
- ) -} - -export default LoginForm; \ No newline at end of file diff --git a/client/src/components/Modals/AuthError.tsx b/client/src/components/Modals/AuthError.tsx new file mode 100644 index 0000000..ff82cff --- /dev/null +++ b/client/src/components/Modals/AuthError.tsx @@ -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 = ({ modal, setModal, error }) => { + return ( + + +

Error!

+

{error}

+
+ ) +} + +export default AuthErrorModal; \ No newline at end of file diff --git a/client/src/components/RegisterForm.tsx b/client/src/components/RegisterForm.tsx deleted file mode 100644 index e314b02..0000000 --- a/client/src/components/RegisterForm.tsx +++ /dev/null @@ -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
(); - - useEffect(() => { - if(isSubmitSuccessful) { - if(isAuth) { - navigate('/'); - } - } - }, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps - - const onSubmit: SubmitHandler = async (data) => { - await dispatch(registration({email: data.email, name: data.name, sname: data.sname, password: data.password})); - } - - return ( -
- - logo -

Register To Get Started

-

Let's get you all set up so you can verify your personal account and begin setting up your profile.

-
- - -
-
- - -
-
- - -
-
- - -
-
- - { - if (watch('password') != val) { - return "Your passwords do no match"; - } - }, - })} type='password' placeholder='Confirm Password'/> -
- -

Already have an account ? Log In here.

- -
- ) -} - -export default RegisterForm; \ No newline at end of file diff --git a/client/src/components/UI/Button.tsx b/client/src/components/UI/Button.tsx new file mode 100644 index 0000000..c2e5f37 --- /dev/null +++ b/client/src/components/UI/Button.tsx @@ -0,0 +1,13 @@ +import { FC, PropsWithChildren } from 'react' + +interface ButtonProps { + className?: string +} + +const Button: FC> = ({children, className, ...props}) => { + return ( + + ) +} + +export default Button; \ No newline at end of file diff --git a/client/src/components/UI/Input.tsx b/client/src/components/UI/Input.tsx new file mode 100644 index 0000000..b8be90d --- /dev/null +++ b/client/src/components/UI/Input.tsx @@ -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 = ({className, wrapperClasses, labelClasses, label, name, error, register, type, ...props}) => { + const uid: string = uniqid(); + return ( +
+ {label && + + } + + {error && +

{error}

+ } +
+ ) +} + +export default Input; \ No newline at end of file diff --git a/client/src/components/UI/Modal.tsx b/client/src/components/UI/Modal.tsx new file mode 100644 index 0000000..a58b1f7 --- /dev/null +++ b/client/src/components/UI/Modal.tsx @@ -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> = ({className, children, active, setActive, ...props}) => { + + useEffect(() => { + active ? disableBodyScroll(document.body) : enableBodyScroll(document.body); + }, [active]) + + const stopProp: MouseEventHandler = (e) => { + e.stopPropagation(); + }; + + return ( + + {active && + setActive(false)}> + stopProp(e)} {...props}> + setActive(false)}/> + {children} + + + } + + ) +} + +export default Modal; \ No newline at end of file diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx new file mode 100644 index 0000000..1380489 --- /dev/null +++ b/client/src/pages/LoginPage.tsx @@ -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(false); + const [modalError, setModalError] = useState(''); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const {isAuth} = useAppSelector(state => state.UserSlice); + const { register, handleSubmit, formState: {errors, isSubmitSuccessful} } = useForm
(); + + useEffect(() => { + if(isSubmitSuccessful) { + if(isAuth) { + navigate('/'); + } + } + }, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps + + const onSubmit: SubmitHandler = 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 ( +
+ + logo +

Login to your account

+

Let's get you all set up so you can verify your personal account and begin setting up your profile.

+ + + +

Dont have account? Sign up here.

+ + +
+ ) +} + +export default LoginPage; \ No newline at end of file diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx new file mode 100644 index 0000000..695d995 --- /dev/null +++ b/client/src/pages/RegisterPage.tsx @@ -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(false); + const [modalError, setModalError] = useState(''); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const {isAuth} = useAppSelector(state => state.UserSlice); + const { register, handleSubmit, watch, formState: {errors, isSubmitSuccessful} } = useForm
(); + + useEffect(() => { + if(isSubmitSuccessful) { + if(isAuth) { + navigate('/'); + } + } + }, [isSubmitSuccessful]) // eslint-disable-line react-hooks/exhaustive-deps + + const onSubmit: SubmitHandler = 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 ( +
+ + logo +

Register To Get Started

+

Let's get you all set up so you can verify your personal account and begin setting up your profile.

+ + + + + { + if (watch('password') != val) { + return "Your passwords do no match"; + } + }, + })}/> + +

Already have an account ? Log In here.

+ + +
+ ) +} + +export default RegisterPage; \ No newline at end of file diff --git a/client/src/store/reducers/UserSlice.ts b/client/src/store/reducers/UserSlice.ts index ae7674e..db475be 100644 --- a/client/src/store/reducers/UserSlice.ts +++ b/client/src/store/reducers/UserSlice.ts @@ -39,7 +39,7 @@ export const registration = createAsyncThunk( } catch ( error ) { const err = error as AxiosError; const e = err.response?.data as ServerError - return rejectWithValue(e.error); + return rejectWithValue(e); } } ) diff --git a/client/src/utils/ValidationRules.ts b/client/src/utils/ValidationRules.ts new file mode 100644 index 0000000..130d55e --- /dev/null +++ b/client/src/utils/ValidationRules.ts @@ -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 } \ No newline at end of file diff --git a/client/tailwind.config.js b/client/tailwind.config.js index d37737f..f8ed56c 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -5,7 +5,11 @@ export default { "./src/**/*.{js,ts,jsx,tsx}", ], theme: { - extend: {}, + extend: { + colors: { + 'apricot': '#fbceb1', + } + }, }, plugins: [], } diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json index 42872c5..3b0b7a0 100644 --- a/client/tsconfig.node.json +++ b/client/tsconfig.node.json @@ -4,7 +4,8 @@ "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": [ "uniqid", "body-scroll-lock" ] }, "include": ["vite.config.ts"] }