diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 82c8203a8..a248dd5b0 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -5,7 +5,7 @@ import { useDispatch } from "react-redux"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; import NotFound from "./Pages/NotFound"; -import Login from "./Pages/Auth/Login"; +import Login from "./Pages/Auth/Login/Login"; import Register from "./Pages/Auth/Register/Register"; import Account from "./Pages/Account"; import Monitors from "./Pages/Monitors/Home"; diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx deleted file mode 100644 index 677637302..000000000 --- a/Client/src/Pages/Auth/Login.jsx +++ /dev/null @@ -1,446 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { useNavigate } from "react-router-dom"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { credentials } from "../../Validation/validation"; -import { login } from "../../Features/Auth/authSlice"; -import LoadingButton from "@mui/lab/LoadingButton"; -import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../Utils/toastUtils"; -import { networkService } from "../../main"; -import TextInput from "../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import Background from "../../assets/Images/background-grid.svg?react"; -import Logo from "../../assets/icons/bwu-icon.svg?react"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import PropTypes from "prop-types"; -import { logger } from "../../Utils/Logger"; -import "./index.css"; -const DEMO = import.meta.env.VITE_APP_DEMO; - -/** - * Renders the first step of the login process. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -const StepOne = ({ form, errors, onSubmit, onChange }) => { - const theme = useTheme(); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - Log In - Enter your email address - - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email ? true : false} - helperText={errors.email} - ref={inputRef} - /> - - - - - - - ); -}; - -StepOne.propTypes = { - form: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, -}; - -/** - * Renders the second step of the login process, including a password input field. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const inputRef = useRef(null); - const authState = useSelector((state) => state.auth); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - const handleNavigate = () => { - if (form.email !== "" && !errors.email) { - sessionStorage.setItem("email", form.email); - } - navigate("/forgot-password"); - }; - - return ( - <> - - - Log In - Enter your password - - - } - /> - - - - Continue - - - - - - Forgot password? - - - Reset password - - - - - ); -}; - -StepTwo.propTypes = { - form: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onBack: PropTypes.func.isRequired, -}; - -const Login = () => { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const theme = useTheme(); - - const authState = useSelector((state) => state.auth); - const { authToken } = authState; - - const idMap = { - "login-email-input": "email", - "login-password-input": "password", - }; - - const [form, setForm] = useState({ - email: DEMO !== undefined ? "uptimedemo@demo.com" : "", - password: DEMO !== undefined ? "Demouser1!" : "", - }); - const [errors, setErrors] = useState({}); - const [step, setStep] = useState(0); - - useEffect(() => { - if (authToken) { - navigate("/monitors"); - return; - } - networkService - .doesSuperAdminExist() - .then((response) => { - if (response.data.data === false) { - navigate("/register"); - } - }) - .catch((error) => { - logger.error(error); - }); - }, [authToken, navigate]); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idMap[id]; - setForm((prev) => ({ - ...prev, - [name]: value, - })); - - const { error } = credentials.validate({ [name]: value }, { abortEarly: false }); - - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - if (step === 0) { - const { error } = credentials.validate( - { email: form.email }, - { abortEarly: false } - ); - if (error) { - setErrors((prev) => ({ ...prev, email: error.details[0].message })); - createToast({ body: error.details[0].message }); - } else { - setStep(1); - } - } else if (step === 1) { - const { error } = credentials.validate(form, { abortEarly: false }); - - if (error) { - // validation errors - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ - body: - error.details && error.details.length > 0 - ? error.details[0].message - : "Error validating data.", - }); - } else { - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/monitors"); - createToast({ - body: "Welcome back! You're successfully logged in.", - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: "The password you provided does not match our records", - }); - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } - } - } - }; - - return ( - - - - - - - BlueWave Uptime - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.border.light, - backgroundColor: theme.palette.background.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - {step === 0 ? ( - - ) : ( - step === 1 && ( - setStep(0)} - /> - ) - )} - - - ); -}; - -export default Login; diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx new file mode 100644 index 000000000..f78193186 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx @@ -0,0 +1,95 @@ +import { useRef, useEffect } from "react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import PropTypes from "prop-types"; + +/** + * Renders the email step of the login process which includes an email field. + * + * @param {Object} props + * @param {Object} props.form - Form state object. + * @param {Object} props.errors - Object containing form validation errors. + * @param {Function} props.onSubmit - Callback function to handle form submission. + * @param {Function} props.onChange - Callback function to handle form input changes. + * @param {Function} props.onBack - Callback function to handle "Back" button click. + * @returns {JSX.Element} + */ +const EmailStep = ({ form, errors, onSubmit, onChange }) => { + const theme = useTheme(); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <> + + + Log In + Enter your email address + + + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email ? true : false} + helperText={errors.email} + ref={inputRef} + /> + + + + + + + ); +}; + +EmailStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default EmailStep; \ No newline at end of file diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx new file mode 100644 index 000000000..34df4959d --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx @@ -0,0 +1,159 @@ +import { useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { useSelector } from "react-redux"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import PropTypes from "prop-types"; + +/** + * Renders the password step of the login process, including a password input field. + * + * @param {Object} props + * @param {Object} props.form - Form state object. + * @param {Object} props.errors - Object containing form validation errors. + * @param {Function} props.onSubmit - Callback function to handle form submission. + * @param {Function} props.onChange - Callback function to handle form input changes. + * @param {Function} props.onBack - Callback function to handle "Back" button click. + * @returns {JSX.Element} + */ +const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const inputRef = useRef(null); + const authState = useSelector((state) => state.auth); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + const handleNavigate = () => { + if (form.email !== "" && !errors.email) { + sessionStorage.setItem("email", form.email); + } + navigate("/forgot-password"); + }; + + return ( + <> + + + Log In + Enter your password + + + } + /> + + + + Continue + + + + + + Forgot password? + + + Reset password + + + + + ); +}; + +PasswordStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, +}; + +export default PasswordStep \ No newline at end of file diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx new file mode 100644 index 000000000..1b3a420ea --- /dev/null +++ b/Client/src/Pages/Auth/Login/Login.jsx @@ -0,0 +1,213 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { credentials } from "../../../Validation/validation"; +import { login } from "../../../Features/Auth/authSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../main"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import Logo from "../../../assets/icons/bwu-icon.svg?react"; +import { logger } from "../../../Utils/Logger"; +import "../index.css"; +import EmailStep from "./Components/EmailStep"; +import PasswordStep from "./Components/PasswordStep"; + +const DEMO = import.meta.env.VITE_APP_DEMO; + +/** + * Displays the login page. + */ + +const Login = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const theme = useTheme(); + + const authState = useSelector((state) => state.auth); + const { authToken } = authState; + + const idMap = { + "login-email-input": "email", + "login-password-input": "password", + }; + + const [form, setForm] = useState({ + email: DEMO !== undefined ? "uptimedemo@demo.com" : "", + password: DEMO !== undefined ? "Demouser1!" : "", + }); + const [errors, setErrors] = useState({}); + const [step, setStep] = useState(0); + + useEffect(() => { + if (authToken) { + navigate("/monitors"); + return; + } + networkService + .doesSuperAdminExist() + .then((response) => { + if (response.data.data === false) { + navigate("/register"); + } + }) + .catch((error) => { + logger.error(error); + }); + }, [authToken, navigate]); + + const handleChange = (event) => { + const { value, id } = event.target; + const name = idMap[id]; + setForm((prev) => ({ + ...prev, + [name]: value, + })); + + const { error } = credentials.validate({ [name]: value }, { abortEarly: false }); + + setErrors((prev) => { + const prevErrors = { ...prev }; + if (error) prevErrors[name] = error.details[0].message; + else delete prevErrors[name]; + return prevErrors; + }); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + + if (step === 0) { + const { error } = credentials.validate( + { email: form.email }, + { abortEarly: false } + ); + if (error) { + setErrors((prev) => ({ ...prev, email: error.details[0].message })); + createToast({ body: error.details[0].message }); + } else { + setStep(1); + } + } else if (step === 1) { + const { error } = credentials.validate(form, { abortEarly: false }); + + if (error) { + // validation errors + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ + body: + error.details && error.details.length > 0 + ? error.details[0].message + : "Error validating data.", + }); + } else { + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/monitors"); + createToast({ + body: "Welcome back! You're successfully logged in.", + }); + } else { + if (action.payload) { + if (action.payload.msg === "Incorrect password") + setErrors({ + password: "The password you provided does not match our records", + }); + // dispatch errors + createToast({ + body: action.payload.msg, + }); + } else { + // unknown errors + createToast({ + body: "Unknown error.", + }); + } + } + } + } + }; + + return ( + + + + + + + BlueWave Uptime + + .MuiStack-root": { + border: 1, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, + padding: { + xs: theme.spacing(12), + sm: theme.spacing(20), + }, + }, + }} + > + {step === 0 ? ( + + ) : ( + step === 1 && ( + setStep(0)} + /> + ) + )} + + + ); +}; + +export default Login;