diff --git a/Client/index.html b/Client/index.html
index 2b6c6b546..3407fb5cd 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -1,16 +1,14 @@
-
+
+
+
+
+
+ BlueWave Uptime
+
-
-
-
-
- BlueWave Uptime
-
-
-
-
-
-
-
+
+
+
+
diff --git a/Client/public/bluewave_favicon.ico b/Client/public/bluewave_favicon.ico
new file mode 100644
index 000000000..487946975
Binary files /dev/null and b/Client/public/bluewave_favicon.ico differ
diff --git a/Client/public/vite.svg b/Client/public/vite.svg
deleted file mode 100644
index e7b8dfb1b..000000000
--- a/Client/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx
index 6fb8ea612..0b2fe56fe 100644
--- a/Client/src/Components/Inputs/Field/index.jsx
+++ b/Client/src/Components/Inputs/Field/index.jsx
@@ -12,7 +12,7 @@ import Visibility from "@mui/icons-material/Visibility";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import Button from "../../Button";
import "./index.css";
-import { useState } from "react";
+import { forwardRef, useState } from "react";
/**
* @param {Object} props
@@ -30,140 +30,151 @@ import { useState } from "react";
* @param {function} props.onChange - Function called on input change.
* @param {string} [props.error] - Error message to display for the input field.
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
+ * @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
*/
-const Field = ({
- type = "text",
- id,
- label,
- https,
- isRequired,
- isOptional,
- optionalLabel,
- hasCopy,
- autoComplete,
- placeholder,
- value,
- onChange,
- error,
- disabled,
-}) => {
- const theme = useTheme();
+const Field = forwardRef(
+ (
+ {
+ type = "text",
+ id,
+ label,
+ https,
+ isRequired,
+ isOptional,
+ optionalLabel,
+ hasCopy,
+ autoComplete,
+ placeholder,
+ value,
+ onChange,
+ error,
+ disabled,
+ },
+ ref
+ ) => {
+ const theme = useTheme();
- // TODO - are we using this feature anywhere ?
- const [copy, setCopy] = useState(false);
- const handleCopy = () => {
- setCopy(true);
- setTimeout(() => setCopy(false), 1000);
- };
+ // TODO - are we using this feature anywhere ?
+ const [copy, setCopy] = useState(false);
+ const handleCopy = () => {
+ setCopy(true);
+ setTimeout(() => setCopy(false), 1000);
+ };
- const [isVisible, setVisible] = useState(false);
+ const [isVisible, setVisible] = useState(false);
- return (
-
- {label && (
-
- {label}
- {isRequired ? * : ""}
- {isOptional ? (
-
- {optionalLabel || "(optional)"}
-
- ) : (
- ""
- )}
-
- )}
-
-
- {https ? "https" : "http"}://
-
-
- ),
- endAdornment:
- type === "password" ? (
-
- setVisible((show) => !show)}
- tabIndex={-1}
- sx={{
- color: theme.palette.section.borderColor,
- padding: `calc(${theme.gap.xs} / 2)`,
- "&:focus": {
- outline: "none",
- },
- "& .MuiTouchRipple-root": {
- pointerEvents: "none",
- display: "none",
- },
- }}
- >
- {!isVisible ? : }
-
-
+ return (
+
+ {label && (
+
+ {label}
+ {isRequired ? * : ""}
+ {isOptional ? (
+
+ {optionalLabel || "(optional)"}
+
) : (
- hasCopy && (
-
- }
- onClick={handleCopy}
+ ""
+ )}
+
+ )}
+
+
+ {https ? "https" : "http"}://
+
+
+ ),
+ endAdornment:
+ type === "password" ? (
+
+ setVisible((show) => !show)}
+ tabIndex={-1}
sx={{
- borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
- lineHeight: 0,
+ color: theme.palette.section.borderColor,
+ padding: `calc(${theme.gap.xs} / 2)`,
+ "&:focus": {
+ outline: "none",
+ },
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
- />
+ >
+ {!isVisible ? : }
+
- )
- ),
- }}
- />
- {error && (
-
- {error}
-
- )}
-
- );
-};
+ ) : (
+ hasCopy && (
+
+ }
+ onClick={handleCopy}
+ sx={{
+ borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
+ lineHeight: 0,
+ "& .MuiTouchRipple-root": {
+ pointerEvents: "none",
+ display: "none",
+ },
+ }}
+ />
+
+ )
+ ),
+ }}
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+ }
+);
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css
index 595c2b67a..048700db6 100644
--- a/Client/src/Layouts/HomeLayout/index.css
+++ b/Client/src/Layouts/HomeLayout/index.css
@@ -1,6 +1,3 @@
-.home-layout * {
- box-sizing: border-box;
-}
.home-layout {
display: grid;
position: relative;
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
index d40cb078b..2868b5d75 100644
--- a/Client/src/Pages/Auth/Login.jsx
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -1,18 +1,221 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
-
-import "./index.css";
-import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
-import Button from "../../Components/Button";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import axiosInstance from "../../Utils/axiosConfig";
+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 } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
+import Button from "../../Components/Button";
+import axiosInstance from "../../Utils/axiosConfig";
import Field from "../../Components/Inputs/Field";
-import { Checkbox, FormControlLabel, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import Mail from "../../assets/icons/mail.svg?react";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+
+import "./index.css";
+
+/**
+ * Displays the initial landing page.
+ *
+ * @param {Object} props
+ * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
+ * @returns {JSX.Element}
+ */
+const LandingPage = ({ onContinue }) => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+ Log In
+ We are pleased to see you again!
+
+
+ }
+ onClick={onContinue}
+ sx={{
+ width: "100%",
+ "& svg": {
+ mr: theme.gap.small,
+ },
+ }}
+ />
+
+
+
+ By continuing, you agree to our{" "}
+ Terms of Service and{" "}
+ Privacy Policy.
+
+
+
+ >
+ );
+};
+
+/**
+ * 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, onBack }) => {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Log In
+ Enter your email address
+
+
+
+
+
+ }
+ onClick={onBack}
+ sx={{
+ mb: theme.gap.medium,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.xs,
+ },
+ }}
+ />
+
+
+
+ >
+ );
+};
+
+/**
+ * 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);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Log In
+ Enter your password
+
+
+
+
+
+ }
+ onClick={onBack}
+ sx={{
+ mb: theme.gap.medium,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.xs,
+ },
+ }}
+ />
+
+
+
+
+ Forgot password?
+
+ navigate("/forgot-password")}
+ >
+ Reset password
+
+
+
+ >
+ );
+};
const Login = () => {
const dispatch = useDispatch();
@@ -29,6 +232,7 @@ const Login = () => {
password: "",
});
const [errors, setErrors] = useState({});
+ const [step, setStep] = useState(0);
useEffect(() => {
axiosInstance
@@ -43,47 +247,6 @@ const Login = () => {
});
}, [navigate]);
- const handleSubmit = async (e) => {
- e.preventDefault();
-
- 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) {
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
- }
- }
- };
-
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
@@ -105,91 +268,136 @@ const Login = () => {
});
};
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+
+ if (step === 1) {
+ 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(2);
+ }
+ } else if (step === 2) {
+ 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 {
+ setErrors({
+ password: "The password you provided does not match our records",
+ });
+ if (action.payload) {
+ // dispatch errors
+ createToast({
+ body: action.payload.msg,
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: "Unknown error.",
+ });
+ }
+ }
+ }
+ }
+ };
+
return (
-
+
-
-
+ )
+ )}
+
+
+ Don't have an account? —
+ {
+ navigate("/register");
+ }}
+ sx={{ userSelect: "none" }}
+ >
+ Sign Up
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css
index 4f2767f54..f762c1a24 100644
--- a/Client/src/Pages/Auth/index.css
+++ b/Client/src/Pages/Auth/index.css
@@ -2,7 +2,6 @@
/* SHARED */
/* ////// */
.register-page,
-.login-page,
.set-new-password-page,
.password-confirmed-page,
.forgot-password-page,
@@ -12,7 +11,6 @@
height: var(--env-var-height-1);
}
.register-page h1.MuiTypography-root,
-.login-page h1.MuiTypography-root,
.set-new-password-page h1.MuiTypography-root,
.password-confirmed-page h1.MuiTypography-root,
.forgot-password-page h1.MuiTypography-root,
@@ -24,8 +22,6 @@
.register-page p.MuiTypography-root,
.register-page span,
.register-page button,
-.login-page p.MuiTypography-root,
-.login-page span,
.login-page button,
.set-new-password-page p.MuiTypography-root,
.set-new-password-page button,
@@ -38,13 +34,11 @@
.check-email-page span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
-.register-page p.MuiTypography-root,
-.login-page p.MuiTypography-root {
+.register-page p.MuiTypography-root {
color: var(--env-var-color-2);
opacity: 0.8;
}
.register-page button:not(.MuiIconButton-root),
-.login-page button:not(.MuiIconButton-root),
.set-new-password-page button:not(.MuiIconButton-root),
.password-confirmed-page button:not(.MuiIconButton-root),
.forgot-password-page button:not(.MuiIconButton-root),
@@ -54,12 +48,10 @@
line-height: 0;
}
-.register-page svg rect,
-.login-page svg rect {
+.register-page svg rect {
fill: none;
}
-.register-form,
-.login-form {
+.register-form {
margin-top: 95px;
}
@@ -77,7 +69,6 @@
color: var(--env-var-color-2);
}
-.login-page .MuiFormControlLabel-root .MuiButtonBase-root,
.set-new-password-page button:not(.MuiIconButton-root) svg,
.password-confirmed-page button:not(.MuiIconButton-root) svg,
.forgot-password-page button svg,
@@ -108,19 +99,6 @@
cursor: pointer;
font-weight: 600;
}
-.login-page .MuiFormControlLabel-root {
- margin: 0;
-}
-.login-page .MuiFormControlLabel-root span:not(.Mui-checked) svg {
- fill: var(--env-var-color-4);
-}
-.login-page .MuiFormControlLabel-root svg {
- margin-bottom: 1px;
-}
-.login-page .MuiFormControlLabel-root .MuiTypography-root {
- color: var(--env-var-color-5);
- user-select: none;
-}
/* /////////// */
/* Check Email */
@@ -128,3 +106,70 @@
.check-email-page span.MuiTypography-root {
font-weight: 600;
}
+
+/* */
+.login-page {
+ height: 100vh;
+}
+.login-page h1 {
+ font-size: 37px;
+ font-weight: 600;
+ color: var(--env-var-color-3);
+}
+.login-page button:not(.MuiIconButton-root),
+.login-page .field h3.MuiTypography-root,
+.login-page .field input,
+.login-page .field .input-error,
+.login-page p,
+.login-page span {
+ font-size: var(--env-var-font-size-medium-plus);
+}
+.login-page p {
+ color: var(--env-var-color-2-light);
+}
+.login-page .field h3.MuiTypography-root,
+.login-page .field input {
+ color: var(--env-var-color-5);
+}
+
+.login-page p + span {
+ color: var(--env-var-color-3);
+ opacity: 0.8;
+ cursor: pointer;
+ transition: opacity 300ms ease-in;
+}
+.login-page p > span {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ cursor: pointer;
+ transition: all 200ms;
+}
+.login-page p > span:hover {
+ color: var(--env-var-color-2);
+ text-underline-offset: 4px;
+}
+.login-page p + span:hover {
+ opacity: 1;
+}
+.login-page button:not(.MuiIconButton-root) {
+ user-select: none;
+ border-radius: var(--env-var-radius-2);
+ line-height: 1;
+}
+.login-page button:not(.MuiIconButton-root),
+.login-page .field .MuiInputBase-root:has(input) {
+ height: 38px;
+}
+.login-page .field svg {
+ width: 24px;
+ height: 24px;
+}
+
+.login-page > .MuiStack-root:nth-of-type(1) {
+ height: var(--env-var-nav-bar-height);
+}
+.login-page .background-pattern-svg {
+ top: 14%;
+ width: 100%;
+ max-width: 800px;
+}
diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
index 0d3026c78..e0311d4e7 100644
--- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
+++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
@@ -18,7 +18,7 @@
width: 90px;
}
-.MuiInputAdornment-outlined .MuiIconButton-root svg {
+.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
width: 20px;
}
diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js
index 72250bebb..c065798d2 100644
--- a/Client/src/Utils/Theme.js
+++ b/Client/src/Utils/Theme.js
@@ -98,6 +98,7 @@ const theme = createTheme({
large: "24px",
xl: "40px",
xxl: "60px",
+ triplexl: "120px"
},
alert: {
info: {
diff --git a/Client/src/assets/icons/bwu-icon.svg b/Client/src/assets/icons/bwu-icon.svg
new file mode 100644
index 000000000..80ab50ad9
--- /dev/null
+++ b/Client/src/assets/icons/bwu-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/Client/src/assets/icons/mail.svg b/Client/src/assets/icons/mail.svg
new file mode 100644
index 000000000..75c22b111
--- /dev/null
+++ b/Client/src/assets/icons/mail.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/Client/src/index.css b/Client/src/index.css
index e6b23d257..5693ddaa5 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -1,5 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
+* {
+ box-sizing: border-box;
+}
+
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
@@ -20,6 +24,7 @@
--env-var-color-0: #fff;
--env-var-color-1: #101828;
--env-var-color-2: #475467;
+ --env-var-color-2-light: #838c99;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;