diff --git a/client/src/Pages/Auth/Login/Components/EmailStep.jsx b/client/src/Pages/Auth/Login/Components/EmailStep.jsx deleted file mode 100644 index f3f56ec26..000000000 --- a/client/src/Pages/Auth/Login/Components/EmailStep.jsx +++ /dev/null @@ -1,106 +0,0 @@ -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"; -import { useTranslation } from "react-i18next"; - -/** - * 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); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - {t("auth.login.heading")} - {t("auth.login.subheadings.stepOne")} - - - - - - - - - - ); -}; - -EmailStep.propTypes = { - form: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, -}; - -export default EmailStep; diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx deleted file mode 100644 index fe8c688ac..000000000 --- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import PropTypes from "prop-types"; -import { useNavigate } from "react-router"; -import { Trans, useTranslation } from "react-i18next"; - -const ForgotPasswordLabel = ({ email, errorEmail }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const handleNavigate = () => { - if (email !== "" && !errorEmail) { - sessionStorage.setItem("email", email); - } - navigate("/forgot-password"); - }; - - return ( - - - - ), - }} - /> - - - ); -}; - -ForgotPasswordLabel.propTypes = { - email: PropTypes.string, - errorEmail: PropTypes.string, -}; - -export default ForgotPasswordLabel; diff --git a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/client/src/Pages/Auth/Login/Components/PasswordStep.jsx deleted file mode 100644 index 5aa56fbb0..000000000 --- a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useRef, useEffect } from "react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -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"; -import { useTranslation } from "react-i18next"; -/** - * 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 inputRef = useRef(null); - const authState = useSelector((state) => state.auth); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - {t("auth.login.heading")} - {t("auth.login.subheadings.stepTwo")} - - - } - /> - - - - - - - - ); -}; - -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; diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx deleted file mode 100644 index d2be3af35..000000000 --- a/client/src/Pages/Auth/Login/Login.jsx +++ /dev/null @@ -1,266 +0,0 @@ -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { loginCredentials } 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/checkmate-icon.svg?react"; -import "../index.css"; -import EmailStep from "./Components/EmailStep"; -import PasswordStep from "./Components/PasswordStep"; -import ThemeSwitch from "../../../Components/ThemeSwitch"; -import ForgotPasswordLabel from "./Components/ForgotPasswordLabel"; -import LanguageSelector from "../../../Components/LanguageSelector"; -import { Trans, useTranslation } from "react-i18next"; - -const DEMO = import.meta.env.VITE_APP_DEMO; - -/** - * Displays the login page. - */ - -const Login = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const navigate = useNavigate(); - 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("/uptime"); - } - }, [authToken, navigate]); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idMap[id]; - const lowerCasedValue = - name === idMap["login-email-input"] ? value?.toLowerCase() || value : value; - setForm((prev) => ({ - ...prev, - [name]: lowerCasedValue, - })); - - const { error } = loginCredentials.validate( - { [name]: lowerCasedValue }, - { 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 } = loginCredentials.validate( - { email: form.email }, - { abortEarly: false } - ); - if (error) { - const errorMessage = error.details[0].message; - const translatedMessage = errorMessage.startsWith("auth") - ? t(errorMessage) // Localization keys are in validation.js - : errorMessage; // FIXME: Potential untranslated string - setErrors((prev) => ({ ...prev, email: translatedMessage })); - createToast({ body: translatedMessage }); - } else { - setStep(1); - } - } else if (step === 1) { - const { error } = loginCredentials.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.startsWith("auth") - ? t(error.details[0].message) // Localization keys are in validation.js - : error.details[0].message // FIXME: Potential untranslated string - : t("auth.common.errors.validation"), - }); - } else { - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/uptime"); - createToast({ - body: t("auth.login.toasts.success"), - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: t("auth.common.fields.password.errors.incorrect"), - }); - // dispatch errors - createToast({ - body: t("auth.login.toasts.incorrectPassword"), - }); - } else { - // unknown errors - createToast({ - body: t("common.toasts.unknownError"), - }); - } - } - } - } - }; - - return ( - - - - - - - - {t("common.appName")} - - - - - - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - {step === 0 ? ( - - ) : ( - step === 1 && ( - setStep(0)} - /> - ) - )} - - - {/* Registration link */} - - - navigate("/register")} - /> - ), - }} - /> - - - - - ); -}; - -export default Login; diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx new file mode 100644 index 000000000..04155a9d6 --- /dev/null +++ b/client/src/Pages/Auth/Login/index.jsx @@ -0,0 +1,162 @@ +// Components +import Stack from "@mui/material/Stack"; +import AuthHeader from "../components/AuthHeader"; +import Button from "@mui/material/Button"; +import TextInput from "../../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import { loginCredentials } from "../../../Validation/validation"; +import TextLink from "../components/TextLink"; + +// Utils +import { login } from "../../../Features/Auth/authSlice"; +import { useNavigate } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../Utils/toastUtils"; + +const Login = () => { + // Local state + const [form, setForm] = useState({ + email: "", + password: "", + }); + const [errors, setErrors] = useState({ + email: "", + password: "", + }); + + // Hooks + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + // Handlers + const onChange = (e) => { + const { name, value } = e.target; + const updatedForm = { ...form, [name]: value }; + const { error } = loginCredentials.validate({ [name]: value }); + setForm(updatedForm); + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + + const onSubmit = async (e) => { + e.preventDefault(); + const toSubmit = { ...form }; + const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); + + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return; + } + + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.login.toasts.success"), + }); + } else { + if (action.payload) { + if (action.payload.msg === "Incorrect password") + setErrors({ + password: t("auth.login.errors.password.incorrect"), + }); + // dispatch errors + createToast({ + body: t("auth.login.toasts.incorrectPassword"), + }); + } else { + // unknown errors + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + + return ( + + + + + + } + /> + + + + + + + ); +}; + +export default Login;