From b85f1f639675c56a3abb126d55a194462c45fa3f Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 22 Jun 2024 18:39:11 -0400 Subject: [PATCH] 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;