mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-27 12:19:13 -06:00
Integrated joi validation
This commit is contained in:
@@ -2,22 +2,12 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import React, { useState } from "react";
|
||||
import AnnouncementsDualButtonWithIcon from "../../Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import { Box, Divider, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import PasswordTextField from "../../TextFields/Password/PasswordTextField";
|
||||
import { editPasswordValidation } from "../../../Validation/validation";
|
||||
|
||||
/**
|
||||
* PasswordPanel component manages the form for editing password.
|
||||
@@ -33,86 +23,76 @@ const PasswordPanel = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
//for testing, will tweak when I implement redux slice
|
||||
const idToName = {
|
||||
"edit-current-password": "currentPassword",
|
||||
"edit-new-password": "password",
|
||||
"edit-confirm-password": "confirm",
|
||||
};
|
||||
const [localData, setLocalData] = useState({
|
||||
"edit-current-password": {
|
||||
value: "",
|
||||
//TBD
|
||||
type: "",
|
||||
},
|
||||
"edit-new-password": {
|
||||
value: "",
|
||||
type: "password",
|
||||
},
|
||||
"edit-confirm-password": {
|
||||
value: "",
|
||||
type: "confirm",
|
||||
},
|
||||
currentPassword: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[id]: {
|
||||
...prev[id],
|
||||
value: value,
|
||||
},
|
||||
[name]: value,
|
||||
}));
|
||||
validateField(id, value);
|
||||
};
|
||||
|
||||
const validateField = (id, value) => {
|
||||
let error = "";
|
||||
switch (localData[id].type) {
|
||||
case "password":
|
||||
error =
|
||||
value.trim() === ""
|
||||
? "*This field is required."
|
||||
: value.length < 8
|
||||
? "*Password must be at least 8 characters long."
|
||||
: !/[A-Z]/.test(value)
|
||||
? "*Password must contain at least one uppercase letter."
|
||||
: !/\d/.test(value)
|
||||
? "*Password must contain at least one number."
|
||||
: !/[!@#$%^&*]/.test(value)
|
||||
? "*Password must contain at least one symbol."
|
||||
: "";
|
||||
break;
|
||||
case "confirm":
|
||||
error =
|
||||
value.trim() === ""
|
||||
? "*This field is required."
|
||||
: value !== localData["edit-new-password"].value
|
||||
? "*Passwords do not match."
|
||||
: "";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const validation = editPasswordValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: localData.password } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error === "") {
|
||||
delete updatedErrors[id];
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
updatedErrors[id] = error;
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
//TODO - implement save password function
|
||||
const handleSavePassword = () => {
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
const { error } = editPasswordValidation.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { password: localData.password },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
//TODO - submit logic
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel value="1">
|
||||
<form className="edit-password-form" noValidate spellCheck="false">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="edit-password-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<AnnouncementsDualButtonWithIcon
|
||||
icon={
|
||||
@@ -138,7 +118,16 @@ const PasswordPanel = () => {
|
||||
autoComplete="current-password"
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-current-password"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-current-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-current-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
@@ -156,11 +145,11 @@ const PasswordPanel = () => {
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors["edit-new-password"] ? true : false}
|
||||
error={errors[idToName["edit-new-password"]] ? true : false}
|
||||
/>
|
||||
{errors["edit-new-password"] ? (
|
||||
{errors[idToName["edit-new-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors["edit-new-password"]}
|
||||
{errors[idToName["edit-new-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
@@ -182,11 +171,11 @@ const PasswordPanel = () => {
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors["edit-confirm-password"] ? true : false}
|
||||
error={errors[idToName["edit-confirm-password"]] ? true : false}
|
||||
/>
|
||||
{errors["edit-confirm-password"] ? (
|
||||
{errors[idToName["edit-confirm-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors["edit-confirm-password"]}
|
||||
{errors[idToName["edit-confirm-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
@@ -208,7 +197,7 @@ const PasswordPanel = () => {
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSavePassword}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
|
||||
@@ -7,6 +7,7 @@ import Button from "../../Button";
|
||||
import EmailTextField from "../../TextFields/Email/EmailTextField";
|
||||
import StringTextField from "../../TextFields/Text/TextField";
|
||||
import Avatar from "../../Avatar";
|
||||
import { editProfileValidation } from "../../../Validation/validation";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -23,63 +24,38 @@ const ProfilePanel = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
//for testing, will tweak when I implement redux slice
|
||||
const idToName = {
|
||||
"edit-first-name": "firstName",
|
||||
"edit-last-name": "lastName",
|
||||
"edit-email": "email",
|
||||
};
|
||||
const [localData, setLocalData] = useState({
|
||||
"edit-first-name": {
|
||||
value: "",
|
||||
type: "name",
|
||||
},
|
||||
"edit-last-name": {
|
||||
value: "",
|
||||
type: "name",
|
||||
},
|
||||
"edit-email": {
|
||||
value: "",
|
||||
type: "email",
|
||||
},
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[id]: {
|
||||
...prev[id],
|
||||
value: value,
|
||||
},
|
||||
[name]: value,
|
||||
}));
|
||||
validateField(id, value);
|
||||
};
|
||||
|
||||
const validateField = (id, value) => {
|
||||
let error = "";
|
||||
switch (localData[id].type) {
|
||||
case "name":
|
||||
error =
|
||||
value.trim() === ""
|
||||
? "*This field is required."
|
||||
: /\d/.test(value)
|
||||
? "*Name is invalid."
|
||||
: "";
|
||||
break;
|
||||
case "email":
|
||||
error =
|
||||
value.trim() === ""
|
||||
? "*This field is required."
|
||||
: !/^\S+@\S+\.\S+$/.test(value)
|
||||
? "*Email is invalid."
|
||||
: "";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const validation = editProfileValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error === "") {
|
||||
delete updatedErrors[id];
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
updatedErrors[id] = error;
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
@@ -128,11 +104,11 @@ const ProfilePanel = () => {
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
error={errors["edit-first-name"] ? true : false}
|
||||
error={errors[idToName["edit-first-name"]] ? true : false}
|
||||
/>
|
||||
{errors["edit-first-name"] ? (
|
||||
{errors[idToName["edit-first-name"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors["edit-first-name"]}
|
||||
{errors[idToName["edit-first-name"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
@@ -152,11 +128,11 @@ const ProfilePanel = () => {
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
onChange={handleChange}
|
||||
error={errors["edit-last-name"] ? true : false}
|
||||
error={errors[idToName["edit-last-name"]] ? true : false}
|
||||
/>
|
||||
{errors["edit-last-name"] ? (
|
||||
{errors[idToName["edit-last-name"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors["edit-last-name"]}
|
||||
{errors[idToName["edit-last-name"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
@@ -179,11 +155,11 @@ const ProfilePanel = () => {
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={handleChange}
|
||||
error={errors["edit-email"] ? true : false}
|
||||
error={errors[idToName["edit-email"]] ? true : false}
|
||||
/>
|
||||
{errors["edit-email"] ? (
|
||||
{errors[idToName["edit-email"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors["edit-email"]}
|
||||
{errors[idToName["edit-email"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
@@ -206,7 +182,10 @@ const ProfilePanel = () => {
|
||||
firstName="Jackie"
|
||||
lastName="Dawn"
|
||||
sx={{
|
||||
width: "64px", height: "64px", border: "none", mr: "8px"
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
border: "none",
|
||||
mr: "8px",
|
||||
}}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
|
||||
@@ -68,6 +68,7 @@ const PasswordTextField = ({
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setVisibility((show) => !show)}
|
||||
tabIndex={-1}
|
||||
sx={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
|
||||
@@ -171,9 +171,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings .MuiStack-root:has(p.MuiTypography-root.input-error){
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settings p.MuiTypography-root.input-error{
|
||||
color: var(--env-var-color-24);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
}
|
||||
.settings .email-text-field,
|
||||
.settings .password-text-field {
|
||||
|
||||
@@ -63,9 +63,82 @@ const newPasswordValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const editProfileValidation = joi.object({
|
||||
firstName: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*First name is required.",
|
||||
"string.pattern.base": "*First name must contain only letters.",
|
||||
}),
|
||||
lastName: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*Last name is required.",
|
||||
"string.pattern.base": "*Last name must contain only letters.",
|
||||
}),
|
||||
email: joi
|
||||
.string()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.messages({
|
||||
"string.empty": "*Email is required.",
|
||||
"string.email": "*Invalid email address.",
|
||||
}),
|
||||
});
|
||||
|
||||
const editPasswordValidation = joi.object({
|
||||
// TBD - validation for current password ?
|
||||
currentPassword : joi
|
||||
.string().trim()
|
||||
.messages({
|
||||
"string.empty": "*Current password is required.",
|
||||
}),
|
||||
password: joi
|
||||
.string()
|
||||
.trim()
|
||||
.min(8)
|
||||
.messages({
|
||||
"string.empty": "*Password is required.",
|
||||
"string.min": "*Password must be at least 8 characters long.",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one uppercase letter."
|
||||
);
|
||||
}
|
||||
if (!/[a-z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one lowercase letter."
|
||||
);
|
||||
}
|
||||
if (!/\d/.test(value)) {
|
||||
return helpers.message("*Password must contain at least one number.");
|
||||
}
|
||||
if (!/[!@#$%^&*]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one special character."
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}),
|
||||
confirm: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "*Password confirmation is required.",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
const { password } = helpers.prefs.context;
|
||||
if (value !== password) {
|
||||
return helpers.message("*Passwords do not match.");
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
registerValidation,
|
||||
loginValidation,
|
||||
recoveryValidation,
|
||||
newPasswordValidation,
|
||||
editPasswordValidation,
|
||||
editProfileValidation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user