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 (
@@ -260,22 +249,11 @@ const ProfilePanel = () => { - - +