diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx
index c7805aac9..151914061 100644
--- a/Client/src/Components/Avatar/index.jsx
+++ b/Client/src/Components/Avatar/index.jsx
@@ -1,12 +1,12 @@
import { Avatar as MuiAvatar } from "@mui/material";
import PropTypes from "prop-types";
+import { useSelector } from "react-redux";
+import { useEffect, useState } from "react";
/**
* @component
* @param {Object} props
* @param {string} props.src - Path to image for avatar
- * @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}
@@ -15,29 +15,49 @@ import PropTypes from "prop-types";
*
*/
-const Avatar = ({ src, firstName, lastName, small, sx }) => {
- const borderColor = "#F0F2F4";
- const smallStyle = small ? { width: 32, height: 32 } : {};
+const Avatar = ({ src, small, sx }) => {
+ const { user } = useSelector((state) => state.auth);
+
+ const style = small ? { width: 25, height: 25 } : { width: 64, height: 64 };
+ const border = small ? 1 : 3;
+
+ const [image, setImage] = useState();
+ useEffect(() => {
+ if (user.avatarImage) {
+ setImage(`data:image/png;base64,${user.avatarImage}`);
+ }
+ }, [user?.avatarImage]);
+
return (
- {src === undefined ? `${firstName[0]}${lastName[0]}` : null}
+ {user.firstname?.charAt(0)}{user.lastname?.charAt(0)}
);
};
Avatar.propTypes = {
src: PropTypes.string,
- firstName: PropTypes.string.isRequired,
- lastName: PropTypes.string.isRequired,
small: PropTypes.bool,
sx: PropTypes.object,
};
diff --git a/Client/src/Components/NavBar/index.css b/Client/src/Components/NavBar/index.css
index bebba5136..d412c0a20 100644
--- a/Client/src/Components/NavBar/index.css
+++ b/Client/src/Components/NavBar/index.css
@@ -5,12 +5,6 @@
justify-content: center;
}
-.icon-button-avatar {
- width: 25px;
- height: 25px;
- margin-right: var(--env-var-spacing-1);
-}
-
.icon-button-toggle-title {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
diff --git a/Client/src/Components/NavBar/index.jsx b/Client/src/Components/NavBar/index.jsx
index 8e5fb8996..524c0a179 100644
--- a/Client/src/Components/NavBar/index.jsx
+++ b/Client/src/Components/NavBar/index.jsx
@@ -8,7 +8,7 @@ import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import MenuIcon from "@mui/icons-material/Menu";
import Container from "@mui/material/Container";
-import Avatar from "@mui/material/Avatar";
+import Avatar from "../Avatar";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import { useTheme } from "@mui/material/styles";
@@ -161,12 +161,7 @@ function NavBar() {
sx={{ p: 0 }}
>
-
+
{authState.user.firstname} {authState.user.lastname}
diff --git a/Client/src/Components/ProgressBars/index.css b/Client/src/Components/ProgressBars/index.css
index 8bd3fc966..5ba1939da 100644
--- a/Client/src/Components/ProgressBars/index.css
+++ b/Client/src/Components/ProgressBars/index.css
@@ -17,7 +17,8 @@
padding-left: 2px;
}
.progress-bar-container:has(p.input-error){
- border-color: var(--env-var-color-24);
+ border-color: var(--env-var-color-19);
+ background-color: var(--env-var-color-21);
padding: 8px var(--env-var-spacing-1-plus);
}
.progress-bar-container p.input-error{
diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx
index 2d60683ef..40e1a5a2c 100644
--- a/Client/src/Components/ProgressBars/index.jsx
+++ b/Client/src/Components/ProgressBars/index.jsx
@@ -94,6 +94,9 @@ const ProgressUpload = ({
}
: {
ml: "auto",
+ "&:focus": {
+ outline: "none",
+ },
}
}
>
diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 2208fd646..a56b65919 100644
--- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
-import { useState } from "react";
+import { useRef, useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Divider, Modal, Stack, Typography } from "@mui/material";
import ButtonSpinner from "../../ButtonSpinner";
@@ -16,6 +16,7 @@ import { update } from "../../../Features/Auth/authSlice";
import ImageField from "../../TextFields/Image";
import ImageIcon from "@mui/icons-material/Image";
import ProgressUpload from "../../ProgressBars";
+import { formatBytes } from "../../../Utils/fileUtils";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -29,26 +30,28 @@ const ProfilePanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
+ //redux state
const { user, authToken, isLoading } = useSelector((state) => state.auth);
+
const idToName = {
"edit-first-name": "firstname",
"edit-last-name": "lastname",
- //Disabled for now, will revisit in the future
+ // Disabled for now, will revisit in the future
// "edit-email": "email",
};
+
+ // Local state for form data, errors, and file handling
const [localData, setLocalData] = useState({
firstname: user.firstname,
lastname: user.lastname,
- //Disabled for now, will revisit in the future
- // email: user.email,
- //TODO - upload picture
- profilePicUrl: "placeholder",
+ // email: user.email, // Disabled for now
});
const [errors, setErrors] = useState({});
- const [isOpen, setIsOpen] = useState("");
- const isModalOpen = (name) => isOpen === name;
- const [loading, setLoading] = useState(false);
+ const [file, setFile] = useState();
+ const intervalRef = useRef(null);
+ const [progress, setProgress] = useState({ value: 0, isLoading: false });
+ // Handles input field changes and performs validation
const handleChange = (event) => {
const { value, id } = event.target;
const name = idToName[id];
@@ -57,71 +60,51 @@ const ProfilePanel = () => {
[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;
- });
+ validateField({ [name]: value }, editProfileValidation, name);
};
- //TODO - add setPicture state to localData
- const [picture, setPicture] = useState();
- const [uploadProgress, setUploadProgress] = useState(0);
- const [isUploading, setIsUploading] = useState(false);
+ // Handles image file
const handlePicture = (event) => {
- clearError("picture");
const pic = event.target.files[0];
- console.log(pic);
- //TODO - add setPicture state to localData
- setPicture({ name: pic.name, type: pic.type, size: formatBytes(pic.size) });
- setIsUploading(true);
-
- const { error } = imageValidation.validate(
- {
- type: pic.type,
- size: pic.size,
- },
- { abortEarly: false }
+ let error = validateField(
+ { type: pic.type, size: pic.size },
+ imageValidation
);
+ if (error) return;
- if (error) {
- setErrors((prev) => {
- const updatedErrors = { ...prev };
- updatedErrors["picture"] = error.details[0].message;
- return updatedErrors;
+ setProgress((prev) => ({ ...prev, isLoading: true }));
+ setFile({
+ src: URL.createObjectURL(pic),
+ name: pic.name,
+ size: formatBytes(pic.size),
+ });
+
+ //TODO - potentitally remove once image compression functions are implemented
+ intervalRef.current = setInterval(() => {
+ const buffer = 12;
+ setProgress((prev) => {
+ if (prev.value + buffer >= 100) {
+ clearInterval(intervalRef.current);
+ return { value: 100, isLoading: false };
+ }
+ return { ...prev, value: prev.value + buffer };
});
- return;
- }
-
- setTimeout(() => {
- //TODO - add setPicture state to localData
- setPicture((prev) => ({ ...prev, src: URL.createObjectURL(pic) }));
- }, 1500);
-
- let progress = 0;
- const interval = setInterval(() => {
- progress += 10;
- setUploadProgress(progress);
- if (progress >= 100) {
- clearInterval(interval);
- }
- }, 150);
+ }, 120);
};
- const formatBytes = (bytes) => {
- if (bytes === 0) return "0 Bytes";
- const megabytes = bytes / (1024 * 1024);
- return megabytes.toFixed(2) + " MB";
+
+ // Validates input against provided schema and updates error state
+ const validateField = (toValidate, schema, name = "picture") => {
+ const { error } = schema.validate(toValidate, { abortEarly: false });
+ setErrors((prev) => {
+ const prevErrors = { ...prev };
+ if (error) prevErrors[name] = error.details[0].message;
+ else delete prevErrors[name];
+ return prevErrors;
+ });
+ if (error) return true;
};
+
+ // Clears specific error from errors state
const clearError = (err) => {
setErrors((prev) => {
const updatedErrors = { ...prev };
@@ -129,53 +112,59 @@ const ProfilePanel = () => {
return updatedErrors;
});
};
- const handleCancelUpload = () => {
- //TODO - add setPicture state to localData
- setPicture();
- setUploadProgress(0);
- setIsUploading(false);
- clearError("picture");
- };
- const handleClosePictureModal = () => {
- setIsOpen("");
- setUploadProgress(0);
- setIsUploading(false);
- clearError("picture");
+
+ // Resets picture-related states and clears interval
+ const removePicture = () => {
+ errors["picture"] && clearError("picture");
+ clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
+ setProgress({ value: 0, isLoading: false });
};
- //TODO - revisit once localData is set up properly
- const handleDeletePicture = () => {
- setLoading(true);
- setTimeout(() => {
- //TODO - add setPicture state to localData
- setPicture(null);
- setLoading(false);
- }, 2000);
- };
- //TODO - implement update profile function
- const handleUpdatePicture = () => {
+ // Closes the picture update modal and resets related states
+ const closePictureModal = () => {
+ removePicture();
setIsOpen("");
- setUploadProgress(0);
- setIsUploading(false);
};
- //TODO - implement delete account function
- const handleDeleteAccount = () => {};
- //TODO - implement save profile function
+
+ // Updates profile image displayed on UI
+ const handleUpdatePicture = () => {
+ setProgress({ value: 0, isLoading: false });
+ setLocalData((prev) => ({
+ ...prev,
+ file: file.src,
+ }));
+ setIsOpen("");
+ };
+
+ // Handles form submission to update user profile
const handleSaveProfile = (event) => {
event.preventDefault();
- if (
- localData.firstname === user.firstname &&
- localData.lastname === user.lastname &&
- localData.profilePicUrl === user.profilePicUrl
- ) {
- //TODO - add toast(profile data is unchanged) and maybe disable button
- return;
- }
+ // if (
+ // localData.firstname === user.firstname &&
+ // localData.lastname === user.lastname
+ // ) {
+ // // TODO: Add toast/notification for unchanged profile data
+ // return;
+ // }
dispatch(update({ authToken, localData }));
- //TODO - add toast confirmation
+ // TODO: Add toast/notification for profile update success
};
+ // Removes current profile image from UI
+ const handleDeletePicture = () => {
+ // TODO - implement delete picture function
+ };
+
+ // Initiates the account deletion process
+ const handleDeleteAccount = () => {
+ //TODO - implement delete account function
+ };
+
+ // Modal state and control functions
+ const [isOpen, setIsOpen] = useState("");
+ const isModalOpen = (name) => isOpen === name;
+
return (