}
+ icon={
+
+ }
subject="SSO login"
body="Since you logged in via SSO, you cannot reset or modify your password."
/>
@@ -52,64 +108,54 @@ 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}
+ onChange={handleChange}
+ error={errors[idToName["edit-current-password"]] ? true : false}
+ />
+ {errors[idToName["edit-current-password"]] ? (
+
+ {errors[idToName["edit-current-password"]]}
+
+ ) : (
+ ""
+ )}
+
- Password
+ New password
-
-
- setShowPassword((show) => !show)}
- sx={{
- width: "30px",
- height: "30px",
- "&:focus": {
- outline: "none",
- },
- }}
- >
- {!showPassword ? : }
-
-
- }
- >
-
+
+
+ {errors[idToName["edit-new-password"]] ? (
+
+ {errors[idToName["edit-new-password"]]}
+
+ ) : (
+ ""
+ )}
+
@@ -117,6 +163,28 @@ const PasswordPanel = () => {
Confirm new password
+
+
+ {errors[idToName["edit-confirm-password"]] ? (
+
+ {errors[idToName["edit-confirm-password"]]}
+
+ ) : (
+ ""
+ )}
+
+
+
+
}
@@ -130,9 +198,10 @@ const PasswordPanel = () => {
{
//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 idToName = {
+ "edit-first-name": "firstname",
+ "edit-last-name": "lastname",
+ "edit-email": "email",
+ };
+ const [localData, setLocalData] = useState({
+ firstname: "",
+ lastname: "",
+ email: "",
+ });
+ const [errors, setErrors] = useState({});
+
+ const handleChange = (event) => {
+ const { value, id } = event.target;
+ const name = idToName[id];
+ setLocalData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ const validation = editProfileValidation.validate(
+ { [name]: value },
+ { abortEarly: false }
+ );
+
+ setErrors((prev) => {
+ const updatedErrors = { ...prev };
+
+ if (validation.error) {
+ updatedErrors[name] = validation.error.details[0].message;
+ } else {
+ delete updatedErrors[name];
+ }
+ return updatedErrors;
+ });
+ };
+
//TODO - implement delete profile picture function
const handleDeletePicture = () => {
setIsLoading(true);
@@ -62,14 +97,23 @@ const ProfilePanel = () => {
First Name
- {/* TODO - use existing textfield components */}
-
+
+
+ {errors[idToName["edit-first-name"]] ? (
+
+ {errors[idToName["edit-first-name"]]}
+
+ ) : (
+ ""
+ )}
+
@@ -77,14 +121,23 @@ const ProfilePanel = () => {
Last Name
- {/* TODO - use existing textfield components */}
-
+
+
+ {errors[idToName["edit-last-name"]] ? (
+
+ {errors[idToName["edit-last-name"]]}
+
+ ) : (
+ ""
+ )}
+
@@ -95,14 +148,23 @@ const ProfilePanel = () => {
After updating, you'll receive a confirmation email.
- {/* TODO - use existing textfield components */}
-
+
+
+ {errors[idToName["edit-email"]] ? (
+
+ {errors[idToName["edit-email"]]}
+
+ ) : (
+ ""
+ )}
+
@@ -114,12 +176,17 @@ 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..5a6fc959d 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)}
+ tabIndex={-1}
+ 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..4a0854cb2 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,39 @@
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 .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 {
+ 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/Validation/validation.js b/Client/src/Validation/validation.js
index 74233a029..c2249fc93 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 ?
+ password : joi
+ .string().trim()
+ .messages({
+ "string.empty": "*Current password is required.",
+ }),
+ newpassword: 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,
};
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;