feat: Update Register page to match new Login design for consistent UX

This commit is contained in:
karenvicent
2025-08-06 17:17:44 -04:00
parent 37a3028540
commit 786dd427bc
6 changed files with 393 additions and 264 deletions

View File

@@ -30,7 +30,17 @@ const LanguageSelector = () => {
value={language}
onChange={handleChange}
size="small"
sx={{ minWidth: 80 }}
sx={{
minWidth: 80,
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
"& .MuiSelect-icon": {
alignSelf: "center",
},
}}
>
{languages.map((lang) => {
let parsedLang = lang === "en" ? "gb" : lang;
@@ -47,11 +57,17 @@ const LanguageSelector = () => {
<MenuItem
key={lang}
value={lang}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
justifyContent="center"
>
<Box
component="span"

View File

@@ -1,15 +1,11 @@
// 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";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import Background from "../../../assets/Images/background-grid.svg?react";
import AuthPageWrapper from "../components/AuthPageWrapper";
// Utils
import { login } from "../../../Features/Auth/authSlice";
import { useNavigate } from "react-router-dom";
@@ -50,12 +46,10 @@ const Login = () => {
[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) {
@@ -64,7 +58,6 @@ const Login = () => {
setErrors(formErrors);
return;
}
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
@@ -89,150 +82,69 @@ const Login = () => {
}
}
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
position="relative"
backgroundColor={theme.palette.primary.main}
sx={{ overflow: "hidden" }}
<AuthPageWrapper
welcome={t("auth.login.welcome")}
heading={t("auth.login.heading")}
>
<Box
sx={{
position: "absolute",
top: 0,
left: "0%",
transform: "translate(-40%, -40%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Box
sx={{
position: "absolute",
bottom: 0,
right: 0,
transform: "translate(45%, 55%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<AuthHeader hideLogo={true} />
<Stack
backgroundColor={theme.palette.primary.main}
component="form"
width="100%"
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
sx={{
borderRadius: theme.spacing(8),
boxShadow: theme.palette.tertiary.cardShadow,
margin: "auto",
alignItems: "center",
gap: theme.spacing(10),
padding: theme.spacing(20),
zIndex: 1,
position: "relative",
width: {
sm: "60%",
md: "50%",
lg: "40%",
xl: "30%",
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<Box
mb={theme.spacing(10)}
mt={theme.spacing(5)}
>
<Box
sx={{
width: { xs: 60, sm: 80, md: 90 },
}}
/>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Stack
mb={theme.spacing(12)}
textAlign="center"
>
<Typography
variant="h1"
mb={theme.spacing(2)}
>
{t("auth.login.welcome")}
</Typography>
<Typography variant="h1">{t("auth.login.heading")}</Typography>
</Stack>
<Stack
component="form"
width="100%"
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
sx={{
width: {
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
name="password"
label={t("auth.common.inputs.password.label")}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
endAdornment={<PasswordEndAdornment />}
/>
<Button
variant="contained"
color="accent"
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Login
</Button>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
href="/forgot-password"
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
href="/register"
<TextInput
type="password"
name="password"
label={t("auth.common.inputs.password.label")}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
endAdornment={<PasswordEndAdornment />}
/>
<Button
variant="contained"
color="accent"
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Login
</Button>
</Stack>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
href="/forgot-password"
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
href="/register"
/>
</AuthPageWrapper>
);
};

View File

@@ -1,10 +1,10 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import AuthHeader from "../components/AuthHeader";
import TextInput from "../../../Components/Inputs/TextInput";
import Check from "../../../Components/Check/Check";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import PasswordTooltip from "../components/PasswordTooltip";
// Utils
import { useTheme } from "@emotion/react";
@@ -16,6 +16,7 @@ import { useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { register } from "../../../Features/Auth/authSlice";
import AuthPageWrapper from "../components/AuthPageWrapper";
import { createToast } from "../../../Utils/toastUtils";
import PropTypes from "prop-types";
@@ -195,59 +196,38 @@ const Register = ({ superAdminExists }) => {
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
<AuthPageWrapper
heading={t("auth.registration.heading.user")}
welcome={t("auth.registration.welcome")}
>
<AuthHeader />
<Stack
margin="auto"
component="form"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
padding={theme.spacing(8)}
gap={theme.spacing(8)}
onSubmit={onSubmit}
sx={{
width: {
sm: "80%",
},
}}
>
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
<Typography variant="h2">
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</Typography>
<Stack
component="form"
width="100%"
maxWidth={600}
alignSelf="center"
justifyContent="center"
border={1}
borderRadius={theme.spacing(5)}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.primary.main}
padding={theme.spacing(12)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
direction={{ xs: "column", lg: "row" }}
justifyContent="space-between"
gap={theme.spacing(4)}
>
<Typography variant="h1">
{superAdminExists
? t("auth.registration.heading.user")
: t("auth.registration.heading.superAdmin")}
</Typography>
<Typography>
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</Typography>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextInput
name="firstName"
sx={{ flex: 1 }}
label={t("auth.common.inputs.firstName.label")}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.firstName.placeholder")}
autoComplete="given-name"
@@ -259,6 +239,9 @@ const Register = ({ superAdminExists }) => {
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
sx={{ flex: 1 }}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.lastName.placeholder")}
autoComplete="family-name"
@@ -267,82 +250,76 @@ const Register = ({ superAdminExists }) => {
error={errors.lastName ? true : false}
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onPasswordChange}
error={errors.password && errors.password[0] ? true : false}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
} // Other errors are related to required password conditions and are visualized below the input
/>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
text={t("auth.common.inputs.password.rules.length.highlighted")}
variant={feedback.length}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
text={t("auth.common.inputs.password.rules.special.highlighted")}
variant={feedback.special}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
text={t("auth.common.inputs.password.rules.number.highlighted")}
variant={feedback.number}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.uppercase.beginning")}
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
variant={feedback.uppercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.lowercase.beginning")}
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
variant={feedback.lowercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
text={t("auth.common.inputs.password.rules.match.highlighted")}
variant={feedback.confirm}
/>
</Stack>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{ width: "30%", alignSelf: "flex-end" }}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
<TextInput
type="email"
name="email"
gap={theme.spacing(4)}
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<PasswordTooltip
feedback={feedback}
form={form}
>
<Box>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onPasswordChange}
error={errors.password && errors.password[0] ? true : false}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
}
/>
</Box>
</PasswordTooltip>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
marginBottom={theme.spacing(4)}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{
width: "100%",
alignSelf: "center",
fontWeight: 700,
mt: theme.spacing(10),
}}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
</AuthPageWrapper>
);
};

View File

@@ -0,0 +1,107 @@
import Background from "../../../assets/Images/background-grid.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import AuthHeader from "../components/AuthHeader";
import { useTheme } from "@mui/material/styles";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import PropTypes from "prop-types";
const AuthPageWrapper = ({ children, heading, welcome }) => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
position="relative"
backgroundColor={theme.palette.primary.main}
sx={{ overflow: "hidden" }}
>
<AuthHeader hideLogo={true} />
<Box
sx={{
position: "absolute",
top: 0,
left: "0%",
transform: "translate(-40%, -40%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Box
sx={{
position: "absolute",
bottom: 0,
right: 0,
transform: "translate(45%, 55%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
backgroundColor={theme.palette.primary.main}
sx={{
borderRadius: theme.spacing(8),
boxShadow: theme.palette.tertiary.cardShadow,
margin: "auto",
alignItems: "center",
gap: theme.spacing(10),
padding: theme.spacing(20),
zIndex: 1,
position: "relative",
width: {
sm: "60%",
md: "50%",
lg: "40%",
xl: "30%",
},
}}
>
<Box
mb={theme.spacing(10)}
mt={theme.spacing(5)}
>
<Box
sx={{
width: { xs: 60, sm: 70, md: 80 },
}}
/>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Stack
mb={theme.spacing(4)}
textAlign="center"
>
<Typography
variant="h1"
mb={theme.spacing(2)}
>
{welcome}
</Typography>
<Typography variant="h1">{heading}</Typography>
</Stack>
{children}
</Stack>
</Stack>
);
};
export default AuthPageWrapper;
AuthPageWrapper.propTypes = {
children: PropTypes.node,
heading: PropTypes.node,
welcome: PropTypes.node,
};

View File

@@ -0,0 +1,116 @@
import Check from "../../../Components/Check/Check";
import Stack from "@mui/material/Stack";
import { Tooltip, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const PasswordTooltip = ({ feedback, form, children }) => {
const theme = useTheme();
const { t } = useTranslation();
const hasPassword = form.password.length > 0;
const hasInvalidFeedback = Object.values(feedback).some(
(status) => status !== "success"
);
const showPasswordTooltip = hasPassword && hasInvalidFeedback;
return (
<Tooltip
placement="right"
arrow
open={showPasswordTooltip}
title={
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.length.beginning") +
" " +
t("auth.common.inputs.password.rules.length.highlighted")
}
variant={feedback.length}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.special.beginning") +
" " +
t("auth.common.inputs.password.rules.special.highlighted")
}
variant={feedback.special}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.number.beginning") +
" " +
t("auth.common.inputs.password.rules.number.highlighted")
}
variant={feedback.number}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.uppercase.beginning") +
" " +
t("auth.common.inputs.password.rules.uppercase.highlighted")
}
variant={feedback.uppercase}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.lowercase.beginning") +
" " +
t("auth.common.inputs.password.rules.lowercase.highlighted")
}
variant={feedback.lowercase}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.match.beginning") +
" " +
t("auth.common.inputs.password.rules.match.highlighted")
}
variant={feedback.confirm}
/>
</Stack>
}
slotProps={{
tooltip: {
sx: {
backgroundColor: theme.palette.tertiary.background,
border: `0.5px solid ${theme.palette.primary.lowContrast}90`,
borderRadius: theme.spacing(4),
color: theme.palette.primary.contrastText,
width: "auto",
maxWidth: { xs: "25vw", md: "none" },
whiteSpace: { xs: "normal", md: "nowrap" },
paddingTop: theme.spacing(8),
px: theme.spacing(8),
},
},
arrow: {
sx: {
color: theme.palette.tertiary.background,
},
},
}}
>
{children}
</Tooltip>
);
};
PasswordTooltip.propTypes = {
feedback: PropTypes.shape({
length: PropTypes.string.isRequired,
special: PropTypes.string,
number: PropTypes.string,
uppercase: PropTypes.string,
lowercase: PropTypes.string,
confirm: PropTypes.string,
}),
form: PropTypes.shape({
password: PropTypes.string.isRequired,
}),
children: PropTypes.node,
};
export default PasswordTooltip;

View File

@@ -87,7 +87,7 @@
"highlighted": "one lower character"
},
"match": {
"beginning": "Confirm password and password",
"beginning": "Passwords",
"highlighted": "must match"
},
"number": {
@@ -195,7 +195,8 @@
"termsAndPolicies": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"toasts": {
"success": "Welcome! Your account was created successfully."
}
},
"welcome": "Welcome to Checkmate!"
}
},
"avgCpuTemperature": "Average CPU Temperature",