From b85f1f639675c56a3abb126d55a194462c45fa3f Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 22 Jun 2024 18:39:11 -0400 Subject: [PATCH 1/4] Refactored input elements with their respective components and added validation --- Client/src/Components/Avatar/index.jsx | 5 +- .../ProfileSettings/PasswordPanel.jsx | 179 +++++++++++++----- .../ProfileSettings/ProfilePanel.jsx | 163 ++++++++++++---- .../TextFields/Password/PasswordTextField.jsx | 66 ++++--- Client/src/Pages/Settings/index.css | 49 ++++- Client/src/index.css | 6 +- 6 files changed, 347 insertions(+), 121 deletions(-) diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx index d881de7bb..c7805aac9 100644 --- a/Client/src/Components/Avatar/index.jsx +++ b/Client/src/Components/Avatar/index.jsx @@ -8,13 +8,14 @@ import PropTypes from "prop-types"; * @param {string} props.firstName - The users first name * @param {string} props.lastName - The users last name * @param {boolean} props.small - Specifies if avatar should be large + * @param {Object} [props.sx] - Additional styles to apply to the button. * @returns {JSX.Element} * @example * // Render a red label * */ -const Avatar = ({ src, firstName, lastName, small }) => { +const Avatar = ({ src, firstName, lastName, small, sx }) => { const borderColor = "#F0F2F4"; const smallStyle = small ? { width: 32, height: 32 } : {}; return ( @@ -25,6 +26,7 @@ const Avatar = ({ src, firstName, lastName, small }) => { display: "inline-flex", border: `0.2rem solid ${borderColor}`, ...smallStyle, + ...sx }} > {src === undefined ? `${firstName[0]}${lastName[0]}` : null} @@ -37,6 +39,7 @@ Avatar.propTypes = { firstName: PropTypes.string.isRequired, lastName: PropTypes.string.isRequired, small: PropTypes.bool, + sx: PropTypes.object, }; export default Avatar; diff --git a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx index 3297c8e86..372417298 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx @@ -17,6 +17,7 @@ import Visibility from "@mui/icons-material/Visibility"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; import ButtonSpinner from "../../ButtonSpinner"; +import PasswordTextField from "../../TextFields/Password/PasswordTextField"; /** * PasswordPanel component manages the form for editing password. @@ -31,6 +32,76 @@ const PasswordPanel = () => { const [isLoading, setIsLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); + //for testing, will tweak when I implement redux slice + const [localData, setLocalData] = useState({ + "edit-current-password": { + value: "", + //TBD + type: "", + }, + "edit-new-password": { + value: "", + type: "password", + }, + "edit-confirm-password": { + value: "", + type: "confirm", + }, + }); + const [errors, setErrors] = useState({}); + + const handleChange = (event) => { + const { value, id } = event.target; + setLocalData((prev) => ({ + ...prev, + [id]: { + ...prev[id], + value: 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; + } + + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (error === "") { + delete updatedErrors[id]; + } else { + updatedErrors[id] = error; + } + return updatedErrors; + }); + }; + //TODO - implement save password function const handleSavePassword = () => { setIsLoading(true); @@ -44,7 +115,11 @@ const PasswordPanel = () => {
} + icon={ + + } subject="SSO login" body="Since you logged in via SSO, you cannot reset or modify your password." /> @@ -52,64 +127,45 @@ const PasswordPanel = () => {
- Current Password + Current password - - + - setShowPassword((show) => !show)} - sx={{ - width: "30px", - height: "30px", - "&:focus": { - outline: "none", - }, - }} - > - {!showPassword ? : } - - - } - > - + label={null} + placeholder="Enter your current password" + autoComplete="current-password" + visibility={showPassword} + setVisibility={setShowPassword} + /> +
- Password + New password - - - setShowPassword((show) => !show)} - sx={{ - width: "30px", - height: "30px", - "&:focus": { - outline: "none", - }, - }} - > - {!showPassword ? : } - - - } - > - + + + {errors["edit-new-password"] ? ( + + {errors["edit-new-password"]} + + ) : ( + "" + )} +
@@ -117,6 +173,28 @@ const PasswordPanel = () => { Confirm new password + + + {errors["edit-confirm-password"] ? ( + + {errors["edit-confirm-password"]} + + ) : ( + "" + )} + +
+
+ } @@ -133,6 +211,7 @@ const PasswordPanel = () => { onClick={handleSavePassword} isLoading={isLoading} loadingText="Saving..." + disabled={Object.keys(errors).length !== 0 && true} sx={{ paddingX: "40px", }} diff --git a/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx index d06b42761..1c668bb97 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx @@ -1,17 +1,12 @@ import { useTheme } from "@emotion/react"; import { useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; -import { - Avatar, - Box, - Divider, - Modal, - Stack, - TextField, - Typography, -} from "@mui/material"; +import { Box, Divider, Modal, Stack, Typography } from "@mui/material"; import ButtonSpinner from "../../ButtonSpinner"; import Button from "../../Button"; +import EmailTextField from "../../TextFields/Email/EmailTextField"; +import StringTextField from "../../TextFields/Text/TextField"; +import Avatar from "../../Avatar"; /** * ProfilePanel component displays a form for editing user profile information @@ -26,6 +21,70 @@ const ProfilePanel = () => { //TODO - use redux loading state //!! - currently all loading buttons are tied to the same state const [isLoading, setIsLoading] = useState(false); + + //for testing, will tweak when I implement redux slice + const [localData, setLocalData] = useState({ + "edit-first-name": { + value: "", + type: "name", + }, + "edit-last-name": { + value: "", + type: "name", + }, + "edit-email": { + value: "", + type: "email", + }, + }); + const [errors, setErrors] = useState({}); + + const handleChange = (event) => { + const { value, id } = event.target; + setLocalData((prev) => ({ + ...prev, + [id]: { + ...prev[id], + value: 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; + } + + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (error === "") { + delete updatedErrors[id]; + } else { + updatedErrors[id] = error; + } + return updatedErrors; + }); + }; + //TODO - implement delete profile picture function const handleDeletePicture = () => { setIsLoading(true); @@ -62,14 +121,23 @@ const ProfilePanel = () => { First Name - {/* TODO - use existing textfield components */} - + + + {errors["edit-first-name"] ? ( + + {errors["edit-first-name"]} + + ) : ( + "" + )} +
@@ -77,14 +145,23 @@ const ProfilePanel = () => { Last Name - {/* TODO - use existing textfield components */} - + + + {errors["edit-last-name"] ? ( + + {errors["edit-last-name"]} + + ) : ( + "" + )} +
@@ -95,14 +172,23 @@ const ProfilePanel = () => { After updating, you'll receive a confirmation email. - {/* TODO - use existing textfield components */} - + + + {errors["edit-email"] ? ( + + {errors["edit-email"]} + + ) : ( + "" + )} +
@@ -114,12 +200,14 @@ const ProfilePanel = () => { - {/* TODO - Use Avatar component instead of @mui */} + {/* TODO - Update placeholder values with redux data */} { onClick={handleSaveProfile} isLoading={isLoading} loadingText="Saving..." + disabled={Object.keys(errors).length !== 0 && true} sx={{ paddingX: "40px", }} diff --git a/Client/src/Components/TextFields/Password/PasswordTextField.jsx b/Client/src/Components/TextFields/Password/PasswordTextField.jsx index b0c18de8d..d9c30367a 100644 --- a/Client/src/Components/TextFields/Password/PasswordTextField.jsx +++ b/Client/src/Components/TextFields/Password/PasswordTextField.jsx @@ -1,9 +1,11 @@ import "./passwordTextField.css"; import React, { useState } from "react"; -import { InputAdornment, TextField } from "@mui/material"; +import { IconButton, InputAdornment, TextField } from "@mui/material"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import { useTheme } from "@mui/material"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import Visibility from "@mui/icons-material/Visibility"; /** * @component @@ -16,6 +18,8 @@ import { useTheme } from "@mui/material"; * @param {"error" | "help"} [props.icon] - The type of icon to display (error or help) (optional) * @param {string} props.helperText - The helper text displayed below the text field (optional) * @param {boolean} props.error - A flag indicating if the text field has an error (required) + * @param {boolean} props.visibility - A flag indicating if the password visibility is toggled (optional) + * @param {function} props.setVisibility - Function to toggle password visibility (optional) * @returns {JSX.Element} - Renders the password text field component with dynamic icon display */ const PasswordTextField = ({ @@ -28,6 +32,8 @@ const PasswordTextField = ({ icon, helperText, error, + visibility, + setVisibility, }) => { const theme = useTheme(); @@ -48,7 +54,7 @@ const PasswordTextField = ({
-
- {error && showIcon && ( - - )} - {error && !showIcon && ( - - )} - {!error && showIcon && ( - - )} -
+ {visibility !== undefined ? ( + setVisibility((show) => !show)} + sx={{ + width: "30px", + height: "30px", + "&:focus": { + outline: "none", + }, + }} + > + {!visibility ? : } + + ) : ( +
+ {error && showIcon && ( + + )} + {error && !showIcon && ( + + )} + {!error && showIcon && ( + + )} +
+ )} ), }} diff --git a/Client/src/Pages/Settings/index.css b/Client/src/Pages/Settings/index.css index c966a9031..6748239fc 100644 --- a/Client/src/Pages/Settings/index.css +++ b/Client/src/Pages/Settings/index.css @@ -64,8 +64,9 @@ margin-top: var(--env-var-spacing-4); } .edit-team-form__wrapper.compact { - margin-top: 30px; - color: var(--env-var-color-29); + margin-top: var(--env-var-spacing-3); + justify-content: flex-start; + align-items: center; } .edit-profile-form__wrapper input, .edit-password-form__wrapper input, @@ -81,7 +82,8 @@ .announcement-content-subject { margin-bottom: 2px; } -#edit-password + div > button > svg, +#edit-new-password + div > button > svg, +#edit-confirm-password + div > button > svg, #edit-current-password + div > button > svg { color: var(--env-var-color-29); } @@ -95,10 +97,12 @@ border-color: var(--env-var-color-27); background-color: var(--lighter-env-var-color-27); } -.MuiBox-root > .announcement-tile .announcement-content-body, -.MuiBox-root > .announcement-tile .announcement-close svg { +.MuiBox-root > .announcement-tile .announcement-content-body { color: var(--env-var-color-28); - fill: var(--env-var-color-28) !important; +} +.MuiBox-root > .announcement-tile .announcement-close svg, +.edit-password-form__wrapper > .announcement-tile .announcement-close svg { + display: none; } #org-name-flex-container .Mui-disabled { color: var(--env-var-color-2) !important; @@ -116,8 +120,7 @@ padding-left: 20px; } .edit-team-form__wrapper td { - font-size: (--env-var-font-size-medium); - color: var(--env-var-color-29); + color: var(--env-var-color-25); } #select-actions, @@ -155,3 +158,33 @@ min-height: 22px; border-radius: 50%; } + +/* for testing, will remove later */ +@media only screen and (max-width: 1600px) { + .settings { + max-width: calc(90vw - 250px) !important; + } + .edit-profile-form__wrapper .MuiStack-root:not(.row-stack), + .edit-password-form__wrapper .MuiStack-root:not(.row-stack), + .edit-organization-form__wrapper .MuiStack-root:not(.row-stack) { + margin-right: 30px; + } +} + +.settings p.MuiTypography-root.input-error{ + color: var(--env-var-color-24); + opacity: 0.8; +} +.settings .email-text-field, +.settings .password-text-field { + margin: 0; +} + +.edit-profile-form__wrapper .MuiFormControl-root, +.edit-password-form__wrapper .MuiFormControl-root{ + width: 100%; +} +.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root), +.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root){ + margin: 0; +} \ No newline at end of file diff --git a/Client/src/index.css b/Client/src/index.css index e682b2a09..dbace21e4 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -3,9 +3,9 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; + /* color-scheme: light dark; color: rgba(255, 255, 255, 0.87); - background-color: #242424; + background-color: #242424; */ font-synthesis: none; text-rendering: optimizeLegibility; @@ -45,7 +45,7 @@ --env-var-color-27: #fecf60; --lighter-env-var-color-27: rgba(254, 207, 96, 0.1); --env-var-color-28: #f79009; - --env-var-color-29: #667085; + --env-var-color-29: #D0D5DD; --env-var-radius-1: 4px; --env-var-radius-2: 8px; From 75f28eaf45e57e936fc06525ff1013e5f178f4af Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sun, 23 Jun 2024 17:41:40 -0400 Subject: [PATCH 2/4] Integrated joi validation --- .../ProfileSettings/PasswordPanel.jsx | 135 ++++++++---------- .../ProfileSettings/ProfilePanel.jsx | 87 +++++------ .../TextFields/Password/PasswordTextField.jsx | 1 + Client/src/Pages/Settings/index.css | 6 + Client/src/Validation/validation.js | 73 ++++++++++ 5 files changed, 175 insertions(+), 127 deletions(-) diff --git a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx index 372417298..3646b5aed 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx @@ -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 ( - +
{ autoComplete="current-password" visibility={showPassword} setVisibility={setShowPassword} + onChange={handleChange} + error={errors[idToName["edit-current-password"]] ? true : false} /> + {errors[idToName["edit-current-password"]] ? ( + + {errors[idToName["edit-current-password"]]} + + ) : ( + "" + )}
@@ -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"]] ? ( - {errors["edit-new-password"]} + {errors[idToName["edit-new-password"]]} ) : ( "" @@ -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"]] ? ( - {errors["edit-confirm-password"]} + {errors[idToName["edit-confirm-password"]]} ) : ( "" @@ -208,7 +197,7 @@ const PasswordPanel = () => { { 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"]] ? ( - {errors["edit-first-name"]} + {errors[idToName["edit-first-name"]]} ) : ( "" @@ -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"]] ? ( - {errors["edit-last-name"]} + {errors[idToName["edit-last-name"]]} ) : ( "" @@ -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"]] ? ( - {errors["edit-email"]} + {errors[idToName["edit-email"]]} ) : ( "" @@ -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", }} /> setVisibility((show) => !show)} + tabIndex={-1} sx={{ width: "30px", height: "30px", diff --git a/Client/src/Pages/Settings/index.css b/Client/src/Pages/Settings/index.css index 6748239fc..4a0854cb2 100644 --- a/Client/src/Pages/Settings/index.css +++ b/Client/src/Pages/Settings/index.css @@ -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 { diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 74233a029..d80de04f5 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -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, }; From e6ea59dee565928b4390bec7bbc7ed1fa345b653 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sun, 23 Jun 2024 18:42:33 -0400 Subject: [PATCH 3/4] Adjusted form fields to lowercase as per backend requirements --- .../Components/TabPanels/ProfileSettings/ProfilePanel.jsx | 8 ++++---- Client/src/Validation/validation.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx index 536d89ae2..c407b9a0b 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx @@ -25,13 +25,13 @@ const ProfilePanel = () => { //for testing, will tweak when I implement redux slice const idToName = { - "edit-first-name": "firstName", - "edit-last-name": "lastName", + "edit-first-name": "firstname", + "edit-last-name": "lastname", "edit-email": "email", }; const [localData, setLocalData] = useState({ - firstName: "", - lastName: "", + firstname: "", + lastname: "", email: "", }); const [errors, setErrors] = useState({}); diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index d80de04f5..209607d9a 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -64,11 +64,11 @@ const newPasswordValidation = joi.object({ }); const editProfileValidation = joi.object({ - firstName: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({ + 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({ + 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.", }), From bb42bfc00a5cb9d03b715a9dd0881c5f2078ce5d Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Mon, 24 Jun 2024 11:43:07 -0400 Subject: [PATCH 4/4] Adjusted password form field namings --- .../TabPanels/ProfileSettings/PasswordPanel.jsx | 11 ++++++----- Client/src/Validation/validation.js | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx index 3646b5aed..56eb9a12a 100644 --- a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx @@ -24,13 +24,14 @@ const PasswordPanel = () => { //for testing, will tweak when I implement redux slice const idToName = { - "edit-current-password": "currentPassword", - "edit-new-password": "password", + "edit-current-password": "password", + //TBD - form field naming + "edit-new-password": "newpassword", "edit-confirm-password": "confirm", }; const [localData, setLocalData] = useState({ - currentPassword: "", password: "", + newpassword: "", confirm: "", }); const [errors, setErrors] = useState({}); @@ -45,7 +46,7 @@ const PasswordPanel = () => { const validation = editPasswordValidation.validate( { [name]: value }, - { abortEarly: false, context: { password: localData.password } } + { abortEarly: false, context: { newpassword: localData.newpassword } } ); setErrors((prev) => { @@ -66,7 +67,7 @@ const PasswordPanel = () => { const { error } = editPasswordValidation.validate(localData, { abortEarly: false, - context: { password: localData.password }, + context: { newpassword: localData.newpassword }, }); if (error) { diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 209607d9a..c2249fc93 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -84,12 +84,12 @@ const editProfileValidation = joi.object({ const editPasswordValidation = joi.object({ // TBD - validation for current password ? - currentPassword : joi + password : joi .string().trim() .messages({ "string.empty": "*Current password is required.", }), - password: joi + newpassword: joi .string() .trim() .min(8)