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,