Refactored input elements with their respective components and added validation

This commit is contained in:
Daniel Cojocea committed 2024-06-22 18:39:11 -04:00
1 parent 952e6f07c1
commit b85f1f6396
6 files changed
+347 -121

No files matched your search

+4 -1
View File
@@ -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
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
*/
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;
@@ -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 = () => {
<form className="edit-password-form" noValidate spellCheck="false">
<div className="edit-password-form__wrapper">
<AnnouncementsDualButtonWithIcon
icon={<InfoOutlinedIcon style={{ fill: theme.palette.secondary.main }} />}
icon={
<InfoOutlinedIcon
style={{ fill: theme.palette.secondary.main }}
/>
}
subject="SSO login"
body="Since you logged in via SSO, you cannot reset or modify your password."
/>
@@ -52,64 +127,45 @@ const PasswordPanel = () => {
<div className="edit-password-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Current Password
Current password
</Typography>
</Stack>
<FormControl sx={{ flex: 1 }}>
<OutlinedInput
<Stack>
<PasswordTextField
id="edit-current-password"
value="RandomPasswordLol"
type={showPassword ? "text" : "password"}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword((show) => !show)}
sx={{
width: "30px",
height: "30px",
"&:focus": {
outline: "none",
},
}}
>
{!showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
></OutlinedInput>
</FormControl>
label={null}
placeholder="Enter your current password"
autoComplete="current-password"
visibility={showPassword}
setVisibility={setShowPassword}
/>
</Stack>
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography variant="h4" component="h1">
Password
New password
</Typography>
</Stack>
<FormControl sx={{ flex: 1 }}>
<OutlinedInput
id="edit-password"
value="RandomPasswordLol"
type={showPassword ? "text" : "password"}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword((show) => !show)}
sx={{
width: "30px",
height: "30px",
"&:focus": {
outline: "none",
},
}}
>
{!showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
></OutlinedInput>
</FormControl>
<Stack>
<PasswordTextField
id="edit-new-password"
label={null}
placeholder="Enter your new password"
autoComplete="new-password"
visibility={showPassword}
setVisibility={setShowPassword}
onChange={handleChange}
error={errors["edit-new-password"] ? true : false}
/>
{errors["edit-new-password"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-new-password"]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-password-form__wrapper">
<Stack>
@@ -117,6 +173,28 @@ const PasswordPanel = () => {
Confirm new password
</Typography>
</Stack>
<Stack>
<PasswordTextField
id="edit-confirm-password"
label={null}
placeholder="Reenter your new password"
autoComplete="new-password"
visibility={showPassword}
setVisibility={setShowPassword}
onChange={handleChange}
error={errors["edit-confirm-password"] ? true : false}
/>
{errors["edit-confirm-password"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-confirm-password"]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-password-form__wrapper">
<Stack></Stack>
<Box sx={{ flex: 1 }}>
<AnnouncementsDualButtonWithIcon
icon={<WarningAmberOutlinedIcon style={{ fill: "#f79009" }} />}
@@ -133,6 +211,7 @@ const PasswordPanel = () => {
onClick={handleSavePassword}
isLoading={isLoading}
loadingText="Saving..."
disabled={Object.keys(errors).length !== 0 && true}
sx={{
paddingX: "40px",
}}
@@ -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
</Typography>
</Stack>
{/* TODO - use existing textfield components */}
<TextField
id="edit-first-name"
placeholder="Enter your first name"
sx={{
flex: 1,
}}
/>
<Stack>
<StringTextField
id="edit-first-name"
label={null}
placeholder="Enter your first name"
autoComplete="given-name"
onChange={handleChange}
error={errors["edit-first-name"] ? true : false}
/>
{errors["edit-first-name"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-first-name"]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
@@ -77,14 +145,23 @@ const ProfilePanel = () => {
Last Name
</Typography>
</Stack>
{/* TODO - use existing textfield components */}
<TextField
id="edit-last-name"
placeholder="Enter your last name"
sx={{
flex: 1,
}}
/>
<Stack>
<StringTextField
id="edit-last-name"
label={null}
placeholder="Enter your last name"
autoComplete="family-name"
onChange={handleChange}
error={errors["edit-last-name"] ? true : false}
/>
{errors["edit-last-name"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-last-name"]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
@@ -95,14 +172,23 @@ const ProfilePanel = () => {
After updating, you'll receive a confirmation email.
</Typography>
</Stack>
{/* TODO - use existing textfield components */}
<TextField
id="edit-email"
placeholder="Enter your email"
sx={{
flex: 1,
}}
/>
<Stack>
<EmailTextField
id="edit-email"
label={null}
placeholder="Enter your email"
autoComplete="email"
onChange={handleChange}
error={errors["edit-email"] ? true : false}
/>
{errors["edit-email"] ? (
<Typography variant="h5" component="p" className="input-error">
{errors["edit-email"]}
</Typography>
) : (
""
)}
</Stack>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
@@ -114,12 +200,14 @@ const ProfilePanel = () => {
</Typography>
</Stack>
<Stack className="row-stack" direction="row" alignItems="center">
{/* TODO - Use Avatar component instead of @mui */}
{/* TODO - Update placeholder values with redux data */}
<Avatar
alt="Remy Sharp"
src="/static/images/avatar/2.jpg"
className="icon-button-avatar"
style={{ width: "64px", height: "64px" }}
firstName="Jackie"
lastName="Dawn"
sx={{
width: "64px", height: "64px", border: "none", mr: "8px"
}}
/>
<ButtonSpinner
level="tertiary"
@@ -152,6 +240,7 @@ const ProfilePanel = () => {
onClick={handleSaveProfile}
isLoading={isLoading}
loadingText="Saving..."
disabled={Object.keys(errors).length !== 0 && true}
sx={{
paddingX: "40px",
}}
@@ -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 = ({
<div className="password-text-field">
<TextField
onChange={onChange}
type="password"
type={visibility ? "text" : "password"}
autoComplete={autoComplete}
error={error}
className="password-text-field-input"
@@ -58,26 +64,42 @@ const PasswordTextField = ({
InputProps={{
endAdornment: (
<InputAdornment position="end">
<div className="icon-holder">
{error && showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{error && !showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{!error && showIcon && (
<HelpOutlineIcon
className="text-field-icon"
style={{ width: "16px", height: "16px" }}
/>
)}
</div>
{visibility !== undefined ? (
<IconButton
aria-label="toggle password visibility"
onClick={() => setVisibility((show) => !show)}
sx={{
width: "30px",
height: "30px",
"&:focus": {
outline: "none",
},
}}
>
{!visibility ? <VisibilityOff /> : <Visibility />}
</IconButton>
) : (
<div className="icon-holder">
{error && showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{error && !showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{!error && showIcon && (
<HelpOutlineIcon
className="text-field-icon"
style={{ width: "16px", height: "16px" }}
/>
)}
</div>
)}
</InputAdornment>
),
}}
+41 -8
View File
@@ -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;
}
+3 -3
View File
@@ -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;