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;