diff --git a/Client/src/Components/ProgressBars/index.css b/Client/src/Components/ProgressBars/index.css index 37317228f..8bd3fc966 100644 --- a/Client/src/Components/ProgressBars/index.css +++ b/Client/src/Components/ProgressBars/index.css @@ -1,6 +1,6 @@ -.progress-bar-container{ - background-color: #fafafa; - padding: var(--env-var-spacing-1-plus); +.progress-bar-container { + background-color: #fafafa; + padding: var(--env-var-spacing-1-plus); } .progress-bar-container h2.MuiTypography-root, .progress-bar-container p.MuiTypography-root { @@ -10,9 +10,22 @@ .progress-bar-container p.MuiTypography-root { opacity: 0.6; } -.progress-bar-container p.MuiTypography-root:has(span){ - font-size: 12px; +.progress-bar-container p.MuiTypography-root:has(span) { + font-size: 12px; } .progress-bar-container p.MuiTypography-root span { - padding-left: 2px; -} \ No newline at end of file + padding-left: 2px; +} +.progress-bar-container:has(p.input-error){ + border-color: var(--env-var-color-24); + padding: 8px var(--env-var-spacing-1-plus); +} +.progress-bar-container p.input-error{ + color: var(--env-var-color-24); + opacity: 0.8; +} +.progress-bar-container:has(p.input-error)>.MuiStack-root>svg{ + fill: var(--env-var-color-24); + width: 20px; + height: 20px; +} diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index 211777080..2d60683ef 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -9,6 +9,7 @@ import { Typography, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import "./index.css"; /** @@ -18,10 +19,18 @@ import "./index.css"; * @param {string} props.size - The size information for the progress item. * @param {number} props.progress - The current progress value (0-100). * @param {function} props.onClick - The function to handle click events on the remove button. + * @param {string} props.error - Error message to display if there's an error (optional). * @returns {JSX.Element} The rendered component. */ -const ProgressUpload = ({ icon, label, size, progress = 0, onClick }) => { +const ProgressUpload = ({ + icon, + label, + size, + progress = 0, + onClick, + error, +}) => { const theme = useTheme(); return ( { border: `solid 1px ${theme.palette.otherColors.graishWhite}`, }} > - - {icon ? ( + + {error ? ( + + ) : icon ? ( { ) : ( "" )} - - - {label} + {error ? ( + + {error} - - {size} - - + ) : ( + + + {error ? error : label} + + {!error && size} + + )} { /> - - - - - - {progress} - % - - + {!error ? ( + + + + + + {progress} + % + + + ) : ( + "" + )} ); }; @@ -107,6 +137,7 @@ ProgressUpload.propTypes = { size: PropTypes.string.isRequired, // Size information for the progress item progress: PropTypes.number.isRequired, // Current progress value (0-100) onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button + error: PropTypes.string, // Error message to display if there's an error (optional) }; export default ProgressUpload; diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index dd6398911..2208fd646 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -7,7 +7,10 @@ import Button from "../../Button"; import EmailTextField from "../../TextFields/Email/EmailTextField"; import StringTextField from "../../TextFields/Text/TextField"; import Avatar from "../../Avatar"; -import { editProfileValidation } from "../../../Validation/validation"; +import { + editProfileValidation, + imageValidation, +} from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { update } from "../../../Features/Auth/authSlice"; import ImageField from "../../TextFields/Image"; @@ -76,12 +79,30 @@ const ProfilePanel = () => { const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); 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 } + ); + + if (error) { + setErrors((prev) => { + const updatedErrors = { ...prev }; + updatedErrors["picture"] = error.details[0].message; + return updatedErrors; + }); + return; + } + setTimeout(() => { //TODO - add setPicture state to localData setPicture((prev) => ({ ...prev, src: URL.createObjectURL(pic) })); @@ -98,20 +119,28 @@ const ProfilePanel = () => { }; const formatBytes = (bytes) => { if (bytes === 0) return "0 Bytes"; - const k = 1024; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " MB"; + const megabytes = bytes / (1024 * 1024); + return megabytes.toFixed(2) + " MB"; + }; + const clearError = (err) => { + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (updatedErrors[err]) delete updatedErrors[err]; + return updatedErrors; + }); }; const handleCancelUpload = () => { //TODO - add setPicture state to localData - setPicture(null); + setPicture(); setUploadProgress(0); setIsUploading(false); + clearError("picture"); }; const handleClosePictureModal = () => { setIsOpen(""); setUploadProgress(0); setIsUploading(false); + clearError("picture"); }; //TODO - revisit once localData is set up properly @@ -353,7 +382,6 @@ const ProfilePanel = () => { aria-describedby="update-profile-picture" open={isModalOpen("picture")} onClose={handleClosePictureModal} - disablePortal > { size={picture?.size} progress={uploadProgress} onClick={handleCancelUpload} + error={errors["picture"]} /> ) : ( "" diff --git a/Client/src/Components/TextFields/Image/index.css b/Client/src/Components/TextFields/Image/index.css index 48b1a82ff..96a730bf0 100644 --- a/Client/src/Components/TextFields/Image/index.css +++ b/Client/src/Components/TextFields/Image/index.css @@ -1,13 +1,27 @@ .image-field-wrapper h2.MuiTypography-root, -.image-field-wrapper p.MuiTypography-root { +.image-field-wrapper p.MuiTypography-root, +.image-field-wrapper + p.MuiTypography-root, +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { color: var(--env-var-color-2); +} +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { + font-weight: 600; +} +.image-field-wrapper h2.MuiTypography-root, +.MuiStack-root:has(#modal-update-picture) button, +.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { font-size: var(--env-var-font-size-medium); } .image-field-wrapper h2.MuiTypography-root { margin-top: 10px; } +.image-field-wrapper + p.MuiTypography-root { + margin-top: 8px; +} +.image-field-wrapper + p.MuiTypography-root, .image-field-wrapper p.MuiTypography-root { - opacity: 0.8; + opacity: 0.6; + font-size: var(--env-var-font-size-small-plus); } .image-field-wrapper h2.MuiTypography-root span { color: var(--env-var-color-3); diff --git a/Client/src/Components/TextFields/Image/index.jsx b/Client/src/Components/TextFields/Image/index.jsx index 5a3a35089..ae0a50c7d 100644 --- a/Client/src/Components/TextFields/Image/index.jsx +++ b/Client/src/Components/TextFields/Image/index.jsx @@ -27,77 +27,78 @@ const ImageField = ({ id, picture, onChange }) => { return ( <> {!picture ? ( - - + - - + - - - - Click to upload or drag and drop - - - (max. 800x400px) - - - + + + + + Click to upload or drag and drop + + (maximum size: 3MB) + + + Supported formats: JPG, PNG + ) : ( { borderRadius: "50%", overflow: "hidden", backgroundImage: `url(${picture})`, - backgroundSize: "cover" + backgroundSize: "cover", }} - > - + > )} diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 4aea7e48e..00903fdb7 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -150,7 +150,23 @@ const createMonitorValidation = joi.object({ }), }); +const imageValidation = joi.object({ + type: joi.string().valid("image/jpeg", "image/png").messages({ + "any.only": "Invalid file format.", + "string.empty": "File type required.", + }), + size: joi + .number() + .max(3 * 1024 * 1024) + .messages({ + "number.base": "File size must be a number.", + "number.max": "File size must be less than 3 MB.", + "number.empty": "File size required.", + }), +}); + export { + imageValidation, createMonitorValidation, registerValidation, loginValidation,