From 2f80a9a834677dc88b5351ddaf20444a088726aa Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 16:35:47 -0500 Subject: [PATCH 01/28] create ImageUpload component and extract logic from profilepanel --- Client/src/Components/ImageUpload/index.jsx | 143 ++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 Client/src/Components/ImageUpload/index.jsx diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx new file mode 100644 index 000000000..d8656e0b7 --- /dev/null +++ b/Client/src/Components/ImageUpload/index.jsx @@ -0,0 +1,143 @@ +import { useState, useRef } from "react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import ImageField from "../Inputs/Image"; +import ProgressUpload from "../ProgressBars"; +import { formatBytes } from "../../Utils/fileUtils"; +import { imageValidation } from "../../Validation/validation"; +import ImageIcon from "@mui/icons-material/Image"; +import GenericDialog from "../Dialog/genericDialog"; + +/** + * ImageUpload component handles the uploading and updating of the profile picture. + * + * @param {Object} props - Component props. + * @param {boolean} props.open - Controls the visibility of the modal. + * @param {Function} props.onClose - Callback to close the modal. + * @param {Function} props.onUpdate - Callback to update the profile picture. + * @param {string} props.currentImage - The current profile image URL or base64 string. + * @returns {JSX.Element} + */ +const ImageUpload = ({ open, onClose, onUpdate, currentImage }) => { + const [file, setFile] = useState(); + const [progress, setProgress] = useState({ value: 0, isLoading: false }); + const [errors, setErrors] = useState({}); + const intervalRef = useRef(null); + + // Handles image file selection + const handlePicture = (event) => { + const pic = event.target.files[0]; + let error = validateField({ type: pic.type, size: pic.size }, imageValidation); + if (error) return; + + setProgress((prev) => ({ ...prev, isLoading: true })); + setFile({ + src: URL.createObjectURL(pic), + name: pic.name, + size: formatBytes(pic.size), + delete: false, + }); + + // Simulate upload progress + 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 }; + }); + }, 120); + }; + + // 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; + }; + + // Resets picture-related states and clears interval + const removePicture = () => { + errors["picture"] && setErrors((prev) => ({ ...prev, picture: undefined })); + setFile({ delete: true }); + clearInterval(intervalRef.current); + setProgress({ value: 0, isLoading: false }); + }; + + // Updates the profile picture and closes the modal + const handleUpdatePicture = () => { + setProgress({ value: 0, isLoading: false }); + onUpdate(file.src); // Pass the new image URL to the parent component + onClose(); // Close the modal + }; + + return ( + + + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( + } + label={file?.name} + size={file?.size} + progress={progress.value} + onClick={removePicture} + error={errors["picture"]} + /> + ) : ( + "" + )} + + + + + + ); +}; + +export default ImageUpload; \ No newline at end of file From 2670375eb3050416045b41588c5bcbdb52f477de Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 16:39:24 -0500 Subject: [PATCH 02/28] update profilepanel to use the imageupload component --- .../TabPanels/Account/ProfilePanel.jsx | 207 +++--------------- 1 file changed, 30 insertions(+), 177 deletions(-) diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index e20cb13a4..707a6d035 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -1,30 +1,18 @@ import { useTheme } from "@emotion/react"; -import { useRef, useState } from "react"; +import { useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, Button, Divider, Stack, Typography } from "@mui/material"; import Avatar from "../../Avatar"; import TextInput from "../../Inputs/TextInput"; -import ImageField from "../../Inputs/Image"; -import { credentials, imageValidation } from "../../../Validation/validation"; +import { credentials } from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice"; -import ImageIcon from "@mui/icons-material/Image"; -import ProgressUpload from "../../ProgressBars"; -import { formatBytes } from "../../../Utils/fileUtils"; import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import LoadingButton from "@mui/lab/LoadingButton"; import { GenericDialog } from "../../Dialog/genericDialog"; -import Dialog from "../../Dialog"; - -/** - * ProfilePanel component displays a form for editing user profile information - * and allows for actions like updating profile picture, credentials, - * and deleting account. - * - * @returns {JSX.Element} - */ +import ImageUpload from "../../ImageUpload"; // Import the new ImageUpload component const ProfilePanel = () => { const theme = useTheme(); @@ -32,30 +20,25 @@ const ProfilePanel = () => { const SPACING_GAP = theme.spacing(12); - //redux state + // 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 - // "edit-email": "email", }; - // Local state for form data, errors, and file handling + // Local state for form data and errors const [localData, setLocalData] = useState({ firstName: user.firstName, lastName: user.lastName, - // email: user.email, // Disabled for now }); const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); - const intervalRef = useRef(null); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); + const [isOpen, setIsOpen] = useState(""); // Handles input field changes and performs validation const handleChange = (event) => { - errors["unchanged"] && clearError("unchanged"); + errors["unchanged"] && setErrors((prev) => ({ ...prev, unchanged: undefined })); const { value, id } = event.target; const name = idToName[id]; setLocalData((prev) => ({ @@ -66,33 +49,6 @@ const ProfilePanel = () => { validateField({ [name]: value }, credentials, name); }; - // Handles image file - const handlePicture = (event) => { - const pic = event.target.files[0]; - let error = validateField({ type: pic.type, size: pic.size }, imageValidation); - if (error) return; - - setProgress((prev) => ({ ...prev, isLoading: true })); - setFile({ - src: URL.createObjectURL(pic), - name: pic.name, - size: formatBytes(pic.size), - delete: false, - }); - - //TODO - potentitally remove, will revisit in the future - 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 }; - }); - }, 120); - }; - // Validates input against provided schema and updates error state const validateField = (toValidate, schema, name = "picture") => { const { error } = schema.validate(toValidate, { abortEarly: false }); @@ -102,51 +58,6 @@ const ProfilePanel = () => { else delete prevErrors[name]; return prevErrors; }); - if (error) return true; - }; - - // Clears specific error from errors state - const clearError = (err) => { - setErrors((prev) => { - const updatedErrors = { ...prev }; - if (updatedErrors[err]) delete updatedErrors[err]; - return updatedErrors; - }); - }; - - // Resets picture-related states and clears interval - const removePicture = () => { - errors["picture"] && clearError("picture"); - setFile({ delete: true }); - clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process - setProgress({ value: 0, isLoading: false }); - }; - - // Opens the picture update modal - const openPictureModal = () => { - setIsOpen("picture"); - setFile({ delete: localData.deleteProfileImage }); - }; - - // Closes the picture update modal and resets related states - const closePictureModal = () => { - errors["picture"] && clearError("picture"); - setFile(); //reset file - clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process - setProgress({ value: 0, isLoading: false }); - setIsOpen(""); - }; - - // Updates profile image displayed on UI - const handleUpdatePicture = () => { - setProgress({ value: 0, isLoading: false }); - setLocalData((prev) => ({ - ...prev, - file: file.src, - deleteProfileImage: false, - })); - setIsOpen(""); - errors["unchanged"] && clearError("unchanged"); }; // Handles form submission to update user profile @@ -177,13 +88,21 @@ const ProfilePanel = () => { } }; - // Removes current profile image from UI + // Handles updating the profile picture + const handleUpdatePicture = (newImage) => { + setLocalData((prev) => ({ + ...prev, + file: newImage, + deleteProfileImage: false, + })); + }; + + // Handles deleting the profile picture const handleDeletePicture = () => { setLocalData((prev) => ({ ...prev, deleteProfileImage: true, })); - errors["unchanged"] && clearError("unchanged"); }; // Initiates the account deletion process @@ -194,12 +113,10 @@ const ProfilePanel = () => { dispatch(clearUptimeMonitorState()); } else { if (action.payload) { - // dispatch errors createToast({ body: action.payload.msg, }); } else { - // unknown errors createToast({ body: "Unknown error.", }); @@ -207,10 +124,6 @@ const ProfilePanel = () => { } }; - // Modal state and control functions - const [isOpen, setIsOpen] = useState(""); - const isModalOpen = (name) => isOpen === name; - return ( { direction="row" gap={SPACING_GAP} > - {/* This 0.9 is a bit magic numbering, refactor */} First name @@ -317,16 +229,16 @@ const ProfilePanel = () => { sx={{ marginRight: "8px" }} /> @@ -390,8 +302,8 @@ const ProfilePanel = () => { )} - { isLoading={isLoading} /> - - - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( - } - label={file?.name} - size={file?.size} - progress={progress.value} - onClick={removePicture} - error={errors["picture"]} - /> - ) : ( - "" - )} - - - - - + {/* Image Upload Modal */} + setIsOpen("")} + onUpdate={handleUpdatePicture} + currentImage={user?.avatarImage ? `data:image/png;base64,${user.avatarImage}` : ""} + /> ); }; -ProfilePanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default ProfilePanel; +export default ProfilePanel; \ No newline at end of file From edd98bfaa84d6751ebf66b4292d15a3f29310a46 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 16:46:34 -0500 Subject: [PATCH 03/28] pass theme prop to GenericDialog in ImageUpload component --- Client/src/Components/ImageUpload/index.jsx | 244 +++++++++--------- .../TabPanels/Account/ProfilePanel.jsx | 1 + 2 files changed, 120 insertions(+), 125 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index d8656e0b7..c509228c8 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -5,139 +5,133 @@ import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; import { imageValidation } from "../../Validation/validation"; import ImageIcon from "@mui/icons-material/Image"; -import GenericDialog from "../Dialog/genericDialog"; +import {GenericDialog} from "../Dialog/genericDialog"; -/** - * ImageUpload component handles the uploading and updating of the profile picture. - * - * @param {Object} props - Component props. - * @param {boolean} props.open - Controls the visibility of the modal. - * @param {Function} props.onClose - Callback to close the modal. - * @param {Function} props.onUpdate - Callback to update the profile picture. - * @param {string} props.currentImage - The current profile image URL or base64 string. - * @returns {JSX.Element} - */ -const ImageUpload = ({ open, onClose, onUpdate, currentImage }) => { - const [file, setFile] = useState(); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const [errors, setErrors] = useState({}); - const intervalRef = useRef(null); +const ImageUpload = ({ open, onClose, onUpdate, currentImage, theme }) => { + const [file, setFile] = useState(); + const [progress, setProgress] = useState({ value: 0, isLoading: false }); + const [errors, setErrors] = useState({}); + const intervalRef = useRef(null); - // Handles image file selection - const handlePicture = (event) => { - const pic = event.target.files[0]; - let error = validateField({ type: pic.type, size: pic.size }, imageValidation); - if (error) return; + // Handles image file selection + const handlePicture = (event) => { + const pic = event.target.files[0]; + let error = validateField({ type: pic.type, size: pic.size }, imageValidation); + if (error) return; - setProgress((prev) => ({ ...prev, isLoading: true })); - setFile({ - src: URL.createObjectURL(pic), - name: pic.name, - size: formatBytes(pic.size), - delete: false, - }); + setProgress((prev) => ({ ...prev, isLoading: true })); + setFile({ + src: URL.createObjectURL(pic), + name: pic.name, + size: formatBytes(pic.size), + delete: false, + }); - // Simulate upload progress - 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 }; - }); - }, 120); - }; + // Simulate upload progress + 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 }; + }); + }, 120); + }; - // 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; - }; + // 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; + }; - // Resets picture-related states and clears interval - const removePicture = () => { - errors["picture"] && setErrors((prev) => ({ ...prev, picture: undefined })); - setFile({ delete: true }); - clearInterval(intervalRef.current); - setProgress({ value: 0, isLoading: false }); - }; + // Resets picture-related states and clears interval + const removePicture = () => { + errors["picture"] && setErrors((prev) => ({ ...prev, picture: undefined })); + setFile({ delete: true }); + clearInterval(intervalRef.current); + setProgress({ value: 0, isLoading: false }); + }; - // Updates the profile picture and closes the modal - const handleUpdatePicture = () => { - setProgress({ value: 0, isLoading: false }); - onUpdate(file.src); // Pass the new image URL to the parent component - onClose(); // Close the modal - }; + // Updates the profile picture and closes the modal + const handleUpdatePicture = () => { + setProgress({ value: 0, isLoading: false }); + onUpdate(file.src); // Pass the new image URL to the parent component + onClose(); // Close the modal + }; - return ( - - - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( - } - label={file?.name} - size={file?.size} - progress={progress.value} - onClick={removePicture} - error={errors["picture"]} - /> - ) : ( - "" - )} - - - - - - ); + return ( + + + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( + } + label={file?.name} + size={file?.size} + progress={progress.value} + onClick={removePicture} + error={errors["picture"]} + /> + ) : ( + "" + )} + + + + + + ); }; export default ImageUpload; \ No newline at end of file diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 707a6d035..090bf7289 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -321,6 +321,7 @@ const ProfilePanel = () => { onClose={() => setIsOpen("")} onUpdate={handleUpdatePicture} currentImage={user?.avatarImage ? `data:image/png;base64,${user.avatarImage}` : ""} + theme={theme} /> ); From 223cad00b25c50518ebadc192f762a07b96f96db Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 16:58:49 -0500 Subject: [PATCH 04/28] resolve prop validation errors in GenericDialog and ImageField --- Client/src/Components/ImageUpload/index.jsx | 9 +++++---- Client/src/Components/TabPanels/Account/ProfilePanel.jsx | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index c509228c8..3fd86cb06 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -79,18 +79,19 @@ const ImageUpload = ({ open, onClose, onUpdate, currentImage, theme }) => { isLoading={false} > + /> {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( } diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 090bf7289..38c26fb28 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -309,11 +309,14 @@ const ProfilePanel = () => { description={ "If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action." } + onClose={() => setIsOpen("")} onCancel={() => setIsOpen("")} confirmationButtonLabel={"Delete account"} onConfirm={handleDeleteAccount} isLoading={isLoading} - /> + > + <> + {/* Image Upload Modal */} Date: Mon, 10 Feb 2025 18:30:45 -0500 Subject: [PATCH 05/28] add logic to handle base64 images and placeholders --- Client/src/Components/ImageUpload/index.jsx | 39 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 3fd86cb06..e235fb528 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Box, Button, Stack, Typography } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import ImageField from "../Inputs/Image"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; @@ -7,12 +7,47 @@ import { imageValidation } from "../../Validation/validation"; import ImageIcon from "@mui/icons-material/Image"; import {GenericDialog} from "../Dialog/genericDialog"; -const ImageUpload = ({ open, onClose, onUpdate, currentImage, theme }) => { +const isValidBase64Image = (data) => { + return /^[A-Za-z0-9+/=]+$/.test(data); + }; + +const ImageUpload = ({ + open, + onClose, + onUpdate, + currentImage, + theme, + shouldRender = true, + alt = "Uploaded Image", + width = "auto", + height = "auto", + minWidth = "auto", + minHeight = "auto", + maxWidth = "auto", + maxHeight = "auto", + placeholder, + sx + }) => { const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); const [errors, setErrors] = useState({}); const intervalRef = useRef(null); + // Handle base64 and placeholder logic + let imageSrc = currentImage; + + if (typeof file?.src !== "undefined") { + imageSrc = file.src; + } else if (typeof currentImage !== "undefined" && isValidBase64Image(currentImage)) { + imageSrc = `data:image/png;base64,${currentImage}`; + } else if (typeof placeholder !== "undefined") { + imageSrc = placeholder; + } + + if (shouldRender === false) { + return null; + } + // Handles image file selection const handlePicture = (event) => { const pic = event.target.files[0]; From e9afb903e9770705900bbba412ef12aad3b11ab3 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 18:34:48 -0500 Subject: [PATCH 06/28] update the rendering logic to use the new props --- Client/src/Components/ImageUpload/index.jsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index e235fb528..bf4f8aa0e 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Button, Stack } from "@mui/material"; +import { Box, Button, Stack } from "@mui/material"; import ImageField from "../Inputs/Image"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; @@ -113,6 +113,18 @@ const ImageUpload = ({ onConfirm={handleUpdatePicture} isLoading={false} > + Date: Mon, 10 Feb 2025 18:39:00 -0500 Subject: [PATCH 07/28] replace and comment image component usage for testing --- Client/src/Components/Image/index.jsx | 128 +++++++++--------- Client/src/Components/StatBox/index.jsx | 5 +- .../DistributedUptime/CreateStatus/index.jsx | 5 +- .../Components/ControlsHeader/index.jsx | 5 +- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/Client/src/Components/Image/index.jsx b/Client/src/Components/Image/index.jsx index 8f30f8a9e..92707de66 100644 --- a/Client/src/Components/Image/index.jsx +++ b/Client/src/Components/Image/index.jsx @@ -1,72 +1,72 @@ -import { Box } from "@mui/material"; -import PropTypes from "prop-types"; +// import { Box } from "@mui/material"; +// import PropTypes from "prop-types"; -const isValidBase64Image = (data) => { - return /^[A-Za-z0-9+/=]+$/.test(data); -}; +// const isValidBase64Image = (data) => { +// return /^[A-Za-z0-9+/=]+$/.test(data); +// }; -const Image = ({ - shouldRender = true, - src, - alt, - width = "auto", - height = "auto", - minWidth = "auto", - minHeight = "auto", - maxWidth = "auto", - maxHeight = "auto", - base64, - placeholder, - sx, -}) => { - if (shouldRender === false) { - return null; - } +// const Image = ({ +// shouldRender = true, +// src, +// alt, +// width = "auto", +// height = "auto", +// minWidth = "auto", +// minHeight = "auto", +// maxWidth = "auto", +// maxHeight = "auto", +// base64, +// placeholder, +// sx, +// }) => { +// if (shouldRender === false) { +// return null; +// } - if (typeof src !== "undefined" && typeof base64 !== "undefined") { - console.warn("base64 takes precedence over src and overwrites it"); - } +// if (typeof src !== "undefined" && typeof base64 !== "undefined") { +// console.warn("base64 takes precedence over src and overwrites it"); +// } - if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { - src = `data:image/png;base64,${base64}`; - } +// if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { +// src = `data:image/png;base64,${base64}`; +// } - if ( - typeof src === "undefined" && - typeof base64 === "undefined" && - typeof placeholder !== "undefined" - ) { - src = placeholder; - } +// if ( +// typeof src === "undefined" && +// typeof base64 === "undefined" && +// typeof placeholder !== "undefined" +// ) { +// src = placeholder; +// } - return ( - - ); -}; +// return ( +// +// ); +// }; -Image.propTypes = { - shouldRender: PropTypes.bool, - src: PropTypes.string, - alt: PropTypes.string.isRequired, - width: PropTypes.string, - height: PropTypes.string, - minWidth: PropTypes.string, - minHeight: PropTypes.string, - maxWidth: PropTypes.string, - maxHeight: PropTypes.string, - base64: PropTypes.string, - sx: PropTypes.object, -}; +// Image.propTypes = { +// shouldRender: PropTypes.bool, +// src: PropTypes.string, +// alt: PropTypes.string.isRequired, +// width: PropTypes.string, +// height: PropTypes.string, +// minWidth: PropTypes.string, +// minHeight: PropTypes.string, +// maxWidth: PropTypes.string, +// maxHeight: PropTypes.string, +// base64: PropTypes.string, +// sx: PropTypes.object, +// }; -export default Image; +// export default Image; diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 58fdb7ebf..30086a221 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,5 +1,6 @@ import { Stack, Typography } from "@mui/material"; -import Image from "../Image"; +//import Image from "../Image"; +import ImageUpload from "../ImageUpload"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; @@ -106,7 +107,7 @@ const StatBox = ({ }} > {img && ( - { gap={theme.spacing(18)} alignItems="center" > - Logo - {"Company Date: Mon, 10 Feb 2025 18:51:30 -0500 Subject: [PATCH 08/28] remove box material from imageupload component --- Client/src/Components/ImageUpload/index.jsx | 22 +-------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index bf4f8aa0e..f9de1d80c 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Box, Button, Stack } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import ImageField from "../Inputs/Image"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; @@ -18,15 +18,7 @@ const ImageUpload = ({ currentImage, theme, shouldRender = true, - alt = "Uploaded Image", - width = "auto", - height = "auto", - minWidth = "auto", - minHeight = "auto", - maxWidth = "auto", - maxHeight = "auto", placeholder, - sx }) => { const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); @@ -113,18 +105,6 @@ const ImageUpload = ({ onConfirm={handleUpdatePicture} isLoading={false} > - Date: Mon, 10 Feb 2025 21:32:30 -0500 Subject: [PATCH 09/28] revert back image component and usage --- Client/src/Components/Image/index.jsx | 128 +++++++++--------- Client/src/Components/StatBox/index.jsx | 5 +- .../DistributedUptime/CreateStatus/index.jsx | 5 +- .../Components/ControlsHeader/index.jsx | 5 +- 4 files changed, 70 insertions(+), 73 deletions(-) diff --git a/Client/src/Components/Image/index.jsx b/Client/src/Components/Image/index.jsx index 92707de66..8f30f8a9e 100644 --- a/Client/src/Components/Image/index.jsx +++ b/Client/src/Components/Image/index.jsx @@ -1,72 +1,72 @@ -// import { Box } from "@mui/material"; -// import PropTypes from "prop-types"; +import { Box } from "@mui/material"; +import PropTypes from "prop-types"; -// const isValidBase64Image = (data) => { -// return /^[A-Za-z0-9+/=]+$/.test(data); -// }; +const isValidBase64Image = (data) => { + return /^[A-Za-z0-9+/=]+$/.test(data); +}; -// const Image = ({ -// shouldRender = true, -// src, -// alt, -// width = "auto", -// height = "auto", -// minWidth = "auto", -// minHeight = "auto", -// maxWidth = "auto", -// maxHeight = "auto", -// base64, -// placeholder, -// sx, -// }) => { -// if (shouldRender === false) { -// return null; -// } +const Image = ({ + shouldRender = true, + src, + alt, + width = "auto", + height = "auto", + minWidth = "auto", + minHeight = "auto", + maxWidth = "auto", + maxHeight = "auto", + base64, + placeholder, + sx, +}) => { + if (shouldRender === false) { + return null; + } -// if (typeof src !== "undefined" && typeof base64 !== "undefined") { -// console.warn("base64 takes precedence over src and overwrites it"); -// } + if (typeof src !== "undefined" && typeof base64 !== "undefined") { + console.warn("base64 takes precedence over src and overwrites it"); + } -// if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { -// src = `data:image/png;base64,${base64}`; -// } + if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { + src = `data:image/png;base64,${base64}`; + } -// if ( -// typeof src === "undefined" && -// typeof base64 === "undefined" && -// typeof placeholder !== "undefined" -// ) { -// src = placeholder; -// } + if ( + typeof src === "undefined" && + typeof base64 === "undefined" && + typeof placeholder !== "undefined" + ) { + src = placeholder; + } -// return ( -// -// ); -// }; + return ( + + ); +}; -// Image.propTypes = { -// shouldRender: PropTypes.bool, -// src: PropTypes.string, -// alt: PropTypes.string.isRequired, -// width: PropTypes.string, -// height: PropTypes.string, -// minWidth: PropTypes.string, -// minHeight: PropTypes.string, -// maxWidth: PropTypes.string, -// maxHeight: PropTypes.string, -// base64: PropTypes.string, -// sx: PropTypes.object, -// }; +Image.propTypes = { + shouldRender: PropTypes.bool, + src: PropTypes.string, + alt: PropTypes.string.isRequired, + width: PropTypes.string, + height: PropTypes.string, + minWidth: PropTypes.string, + minHeight: PropTypes.string, + maxWidth: PropTypes.string, + maxHeight: PropTypes.string, + base64: PropTypes.string, + sx: PropTypes.object, +}; -// export default Image; +export default Image; diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 30086a221..58fdb7ebf 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,6 +1,5 @@ import { Stack, Typography } from "@mui/material"; -//import Image from "../Image"; -import ImageUpload from "../ImageUpload"; +import Image from "../Image"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; @@ -107,7 +106,7 @@ const StatBox = ({ }} > {img && ( - { gap={theme.spacing(18)} alignItems="center" > - - Date: Mon, 10 Feb 2025 22:08:04 -0500 Subject: [PATCH 10/28] add drag and drop event handlers to imageupload --- Client/src/Components/ImageUpload/index.jsx | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index f9de1d80c..ff1e5a689 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Button, Stack } from "@mui/material"; +import { Box, Button, Stack } from "@mui/material"; import ImageField from "../Inputs/Image"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; @@ -23,6 +23,7 @@ const ImageUpload = ({ const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); const [errors, setErrors] = useState({}); + const [isDragging, setIsDragging] = useState(false); const intervalRef = useRef(null); // Handle base64 and placeholder logic @@ -40,6 +41,15 @@ const ImageUpload = ({ return null; } + const handleDragEnter = () => setIsDragging(true); + const handleDragLeave = () => setIsDragging(false); + const handleDrop = (event) => { + event.preventDefault(); + setIsDragging(false); + handlePicture(event); + }; + + // Handles image file selection const handlePicture = (event) => { const pic = event.target.files[0]; @@ -105,6 +115,27 @@ const ImageUpload = ({ onConfirm={handleUpdatePicture} isLoading={false} > + Date: Mon, 10 Feb 2025 22:12:23 -0500 Subject: [PATCH 11/28] replace the usage of imagefield --- Client/src/Components/ImageUpload/index.jsx | 27 ++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index ff1e5a689..a1498d8f6 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,6 +1,5 @@ import { useState, useRef } from "react"; import { Box, Button, Stack } from "@mui/material"; -import ImageField from "../Inputs/Image"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; import { imageValidation } from "../../Validation/validation"; @@ -136,20 +135,20 @@ const ImageUpload = ({ onDragLeave={handleDragLeave} onDrop={handleDrop} /> - + style={{ + opacity: 0, + cursor: "pointer", + width: "100%", + height: "175px", + zIndex: 1, + position: "absolute", + }} + /> + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( } From c8b1fb7faebcab4f43cbc535a8c65aceda916e92 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 22:18:16 -0500 Subject: [PATCH 12/28] display upload icon and message in drag drop field --- Client/src/Components/ImageUpload/index.css | 18 +++++++++ Client/src/Components/ImageUpload/index.jsx | 44 ++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Client/src/Components/ImageUpload/index.css diff --git a/Client/src/Components/ImageUpload/index.css b/Client/src/Components/ImageUpload/index.css new file mode 100644 index 000000000..7492f8636 --- /dev/null +++ b/Client/src/Components/ImageUpload/index.css @@ -0,0 +1,18 @@ +.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 { + font-size: var(--env-var-font-size-small-plus); +} diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index a1498d8f6..e45a75198 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,10 +1,13 @@ import { useState, useRef } from "react"; -import { Box, Button, Stack } from "@mui/material"; +import { Box, Button, Stack, IconButton, Typography } from "@mui/material"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; import { imageValidation } from "../../Validation/validation"; import ImageIcon from "@mui/icons-material/Image"; import {GenericDialog} from "../Dialog/genericDialog"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import "./index.css"; + const isValidBase64Image = (data) => { return /^[A-Za-z0-9+/=]+$/.test(data); @@ -134,7 +137,7 @@ const ImageUpload = ({ onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDrop={handleDrop} - /> + > + + + + + + + Click to upload + {" "} + or drag and drop + + + (maximum size: 3MB) + + + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( Date: Mon, 10 Feb 2025 22:37:51 -0500 Subject: [PATCH 13/28] replace input with textfield material --- Client/src/Components/ImageUpload/index.jsx | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index e45a75198..3ea99a92f 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Box, Button, Stack, IconButton, Typography } from "@mui/material"; +import { Box, Button, Stack, IconButton, TextField , Typography } from "@mui/material"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; import { imageValidation } from "../../Validation/validation"; @@ -108,6 +108,7 @@ const ImageUpload = ({ return ( e.preventDefault()} > - Date: Mon, 10 Feb 2025 22:40:48 -0500 Subject: [PATCH 14/28] add validation for files --- Client/src/Components/ImageUpload/index.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 3ea99a92f..83d777172 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -55,17 +55,20 @@ const ImageUpload = ({ // Handles image file selection const handlePicture = (event) => { const pic = event.target.files[0]; + if (!pic) return; + + // Validate file type and size let error = validateField({ type: pic.type, size: pic.size }, imageValidation); if (error) return; - - setProgress((prev) => ({ ...prev, isLoading: true })); + + setProgress({ value: 0, isLoading: true }); setFile({ src: URL.createObjectURL(pic), name: pic.name, size: formatBytes(pic.size), delete: false, }); - + // Simulate upload progress intervalRef.current = setInterval(() => { const buffer = 12; @@ -78,6 +81,7 @@ const ImageUpload = ({ }); }, 120); }; + // Validates input against provided schema and updates error state const validateField = (toValidate, schema, name = "picture") => { From 419d982322005877139cc714328a8ec49acc5f36 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Mon, 10 Feb 2025 23:50:25 -0500 Subject: [PATCH 15/28] fix preview image display issue --- Client/src/Components/ImageUpload/index.jsx | 178 +++++++++++--------- 1 file changed, 102 insertions(+), 76 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 83d777172..f936dc15a 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -6,6 +6,7 @@ import { imageValidation } from "../../Validation/validation"; import ImageIcon from "@mui/icons-material/Image"; import {GenericDialog} from "../Dialog/genericDialog"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import { checkImage } from "../../Utils/fileUtils"; import "./index.css"; @@ -115,7 +116,7 @@ const ImageUpload = ({ id="modal-update-picture" open={open} onClose={onClose} - theme={theme} // Pass the theme prop + theme={theme} title={"Upload Image"} description={"Select an image to upload."} confirmationButtonLabel={"Update"} @@ -123,85 +124,109 @@ const ImageUpload = ({ isLoading={false} > e.preventDefault()} - > - e.preventDefault()} +> + + {/* ✅ FILE INPUT FIXED - Ensuring it covers entire upload area */} + - - + + {!checkImage(file?.src || currentImage) || progress.isLoading ? ( + <> + + + + + + + Click to upload + {" "} + or drag and drop + + + (maximum size: 3MB) + + + + ) : ( + - - - - - Click to upload - {" "} - or drag and drop - - - (maximum size: 3MB) - - - + /> + )} + - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( + + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( } label={file?.name} @@ -210,15 +235,14 @@ const ImageUpload = ({ onClick={removePicture} error={errors["picture"]} /> - ) : ( - "" - )} - + > - - + + + + ); }; From b3599c117f20853c54a3ffbc8579a72ffa009824 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Tue, 11 Feb 2025 00:07:32 -0500 Subject: [PATCH 16/28] remove imagefield component and replace the usages with imageupload --- Client/src/Components/ImageUpload/index.jsx | 282 +++++++++--------- Client/src/Components/Inputs/Image/index.css | 18 -- Client/src/Components/Inputs/Image/index.jsx | 175 ----------- .../Create/Components/Tabs/Settings.jsx | 4 +- 4 files changed, 141 insertions(+), 338 deletions(-) delete mode 100644 Client/src/Components/Inputs/Image/index.css delete mode 100644 Client/src/Components/Inputs/Image/index.jsx diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index f936dc15a..3732d920d 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -113,160 +113,156 @@ const ImageUpload = ({ return ( - e.preventDefault()} -> - - {/* ✅ FILE INPUT FIXED - Ensuring it covers entire upload area */} - - - {!checkImage(file?.src || currentImage) || progress.isLoading ? ( - <> - - - - - - - Click to upload - {" "} - or drag and drop - - - (maximum size: 3MB) - - - - ) : ( + id="modal-update-picture" + open={open} + onClose={onClose} + theme={theme} + title={"Upload Image"} + description={"Select an image to upload."} + confirmationButtonLabel={"Update"} + onConfirm={handleUpdatePicture} + isLoading={false} + > e.preventDefault()} + > + - )} - + {!checkImage(file?.src || currentImage) || progress.isLoading ? ( + <> + + + + + + + Click to upload + {" "} + or drag and drop + + + (maximum size: 3MB) + + + + ) : ( + + )} + - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( - } - label={file?.name} - size={file?.size} - progress={progress.value} - onClick={removePicture} - error={errors["picture"]} - /> - ) : null} - - - - - - + {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( + } + label={file?.name} + size={file?.size} + progress={progress.value} + onClick={removePicture} + error={errors["picture"]} + /> + ) : null} + + + + + ); }; diff --git a/Client/src/Components/Inputs/Image/index.css b/Client/src/Components/Inputs/Image/index.css deleted file mode 100644 index 7492f8636..000000000 --- a/Client/src/Components/Inputs/Image/index.css +++ /dev/null @@ -1,18 +0,0 @@ -.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 { - font-size: var(--env-var-font-size-small-plus); -} diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx deleted file mode 100644 index 971f17c00..000000000 --- a/Client/src/Components/Inputs/Image/index.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useState } from "react"; -import PropTypes from "prop-types"; -import { Box, IconButton, Stack, TextField, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import "./index.css"; -import { checkImage } from "../../../Utils/fileUtils"; - -/** - * @param {Object} props - The component props. - * @param {string} props.id - The unique identifier for the input field. - * @param {string} props.src - The URL of the image to display. - * @param {function} props.onChange - The function to handle file input change. - * @param {boolean} props.isRound - Whether the shape of the image to display is round. - * @param {string} props.maxSize - Custom message for the max uploaded file size - * @returns {JSX.Element} The rendered component. - */ - -const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize }) => { - const theme = useTheme(); - const error_border_style = error ? { borderColor: theme.palette.error.main } : {}; - - const roundShape = isRound ? { borderRadius: "50%" } : {}; - - const [isDragging, setIsDragging] = useState(false); - const handleDragEnter = () => { - setIsDragging(true); - }; - const handleDragLeave = () => { - setIsDragging(false); - }; - - return ( - <> - {!checkImage(src) || loading ? ( - <> - - - - - - - - - Click to upload - {" "} - or drag and drop - - - (maximum size: {maxSize ?? "3MB"}) - - - - - Supported formats: JPG, PNG - - {error && ( - - {error} - - )} - - ) : ( - - - - )} - - ); -}; - -ImageField.propTypes = { - id: PropTypes.string.isRequired, - src: PropTypes.string, - onChange: PropTypes.func.isRequired, - isRound: PropTypes.bool, - maxSize: PropTypes.string, -}; - -export default ImageField; diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index e4fd56ebb..28b5e5998 100644 --- a/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -5,7 +5,7 @@ import ConfigBox from "../../../../../Components/ConfigBox"; import Checkbox from "../../../../../Components/Inputs/Checkbox"; import TextInput from "../../../../../Components/Inputs/TextInput"; import Select from "../../../../../Components/Inputs/Select"; -import ImageField from "../../../../../Components/Inputs/Image"; +import ImageUpload from "../../../../../Components/ImageUpload"; import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; import Progress from "../Progress"; @@ -101,7 +101,7 @@ const TabSettings = ({ - - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( } From 5b97b1995a3ff04710502a6322253d59da1b4fb9 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Tue, 11 Feb 2025 01:59:05 -0500 Subject: [PATCH 18/28] set drag and drop event handlers correctly --- Client/src/Components/ImageUpload/index.jsx | 31 ++++++++++--------- .../TabPanels/Account/ProfilePanel.jsx | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 9324708ff..3c3388eab 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -13,15 +13,7 @@ const isValidBase64Image = (data) => { return /^[A-Za-z0-9+/=]+$/.test(data); }; -const ImageUpload = ({ - open, - onClose, - onUpdate, - currentImage, - theme, - shouldRender = true, - placeholder, - }) => { +const ImageUpload = ({ open, onClose, onUpdate, currentImage, theme, shouldRender = true, placeholder,}) => { const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); const [errors, setErrors] = useState({}); @@ -43,13 +35,24 @@ const ImageUpload = ({ return null; } - const handleDragEnter = () => setIsDragging(true); - const handleDragLeave = () => setIsDragging(false); - const handleDrop = (event) => { + const handleDragEnter = (event) => { + event.preventDefault(); + setIsDragging(true); +}; + +const handleDragLeave = (event) => { event.preventDefault(); setIsDragging(false); - handlePicture(event); - }; +}; + +const handleDrop = (event) => { + event.preventDefault(); + setIsDragging(false); + + if (event.dataTransfer.files.length > 0) { + handlePicture({ target: { files: event.dataTransfer.files } }); + } +}; // Handles image file selection const handlePicture = (event) => { diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 38c26fb28..e58bb3bc0 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -12,7 +12,7 @@ import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import LoadingButton from "@mui/lab/LoadingButton"; import { GenericDialog } from "../../Dialog/genericDialog"; -import ImageUpload from "../../ImageUpload"; // Import the new ImageUpload component +import ImageUpload from "../../ImageUpload"; const ProfilePanel = () => { const theme = useTheme(); From eedfdd366b94095ed1cdaf3f6d28582a3fa6eaa9 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Tue, 11 Feb 2025 02:02:00 -0500 Subject: [PATCH 19/28] add prop to handle validation error --- Client/src/Components/ImageUpload/index.jsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 3c3388eab..bdaeaaf78 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -91,12 +91,16 @@ const handleDrop = (event) => { 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; + const prevErrors = { ...prev }; + if (error) { + prevErrors[name] = error.details[0].message; + if (onError) onError(error.details[0].message); + } else { + delete prevErrors[name]; + } + return prevErrors; }); - if (error) return true; + return !!error; }; // Resets picture-related states and clears interval From 808cc9240bd029deb76b4b1dd802fdd245e8f199 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Tue, 11 Feb 2025 02:13:35 -0500 Subject: [PATCH 20/28] added support for props to standardize API --- Client/src/Components/ImageUpload/index.jsx | 55 +++++++++++++-------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index bdaeaaf78..45265ec9c 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -13,7 +13,8 @@ const isValidBase64Image = (data) => { return /^[A-Za-z0-9+/=]+$/.test(data); }; -const ImageUpload = ({ open, onClose, onUpdate, currentImage, theme, shouldRender = true, placeholder,}) => { +const ImageUpload = ({ open, onClose, onUpdate, value, currentImage = value, theme, shouldRender = true, placeholder, maxSize, acceptedTypes, previewSize = 150, onError,}) => { + const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); const [errors, setErrors] = useState({}); @@ -61,31 +62,45 @@ const handleDrop = (event) => { event.target.value = ""; setFile(null); - - // Validate file type and size + + if (maxSize && pic.size > maxSize) { + const errorMsg = `File size exceeds ${formatBytes(maxSize)}`; + setErrors({ picture: errorMsg }); + if (onError) onError(errorMsg); + return; + } + + if (acceptedTypes && !acceptedTypes.includes(pic.type)) { + const errorMsg = `File type not supported. Allowed: ${acceptedTypes.join(", ")}`; + setErrors({ picture: errorMsg }); + if (onError) onError(errorMsg); + return; + } + + // Validate file type and size using schema let error = validateField({ type: pic.type, size: pic.size }, imageValidation); if (error) return; - + setProgress({ value: 0, isLoading: true }); setFile({ - src: URL.createObjectURL(pic), - name: pic.name, - size: formatBytes(pic.size), - delete: false, + src: URL.createObjectURL(pic), + name: pic.name, + size: formatBytes(pic.size), + delete: false, }); - + // Simulate upload progress 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 }; - }); + const buffer = 12; + setProgress((prev) => { + if (prev.value + buffer >= 100) { + clearInterval(intervalRef.current); + return { value: 100, isLoading: false }; + } + return { ...prev, value: prev.value + buffer }; + }); }, 120); - }; + }; // Validates input against provided schema and updates error state const validateField = (toValidate, schema, name = "picture") => { @@ -215,8 +230,8 @@ const handleDrop = (event) => { ) : ( Date: Wed, 12 Feb 2025 19:31:54 -0500 Subject: [PATCH 21/28] remove index.css file --- Client/src/Components/ImageUpload/index.css | 18 ------------------ Client/src/Components/ImageUpload/index.jsx | 1 - 2 files changed, 19 deletions(-) delete mode 100644 Client/src/Components/ImageUpload/index.css diff --git a/Client/src/Components/ImageUpload/index.css b/Client/src/Components/ImageUpload/index.css deleted file mode 100644 index 7492f8636..000000000 --- a/Client/src/Components/ImageUpload/index.css +++ /dev/null @@ -1,18 +0,0 @@ -.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 { - font-size: var(--env-var-font-size-small-plus); -} diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 45265ec9c..2e90918e1 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -7,7 +7,6 @@ import ImageIcon from "@mui/icons-material/Image"; import {GenericDialog} from "../Dialog/genericDialog"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import { checkImage } from "../../Utils/fileUtils"; -import "./index.css"; const isValidBase64Image = (data) => { return /^[A-Za-z0-9+/=]+$/.test(data); From 1368ffa13e715cd97994dca722e10b57ca979048 Mon Sep 17 00:00:00 2001 From: Vishnu Sreekumaran Nair <200557136@student.georgianc.on.ca> Date: Wed, 12 Feb 2025 21:31:26 -0500 Subject: [PATCH 22/28] remove internal error handling from imageupload component --- Client/src/Components/ImageUpload/index.jsx | 90 ++++++++++--------- .../TabPanels/Account/ProfilePanel.jsx | 18 ++-- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/Client/src/Components/ImageUpload/index.jsx b/Client/src/Components/ImageUpload/index.jsx index 2e90918e1..a8d060276 100644 --- a/Client/src/Components/ImageUpload/index.jsx +++ b/Client/src/Components/ImageUpload/index.jsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { Box, Button, Stack, IconButton, TextField , Typography } from "@mui/material"; +import { Box, Button, Stack, IconButton , Typography } from "@mui/material"; import ProgressUpload from "../ProgressBars"; import { formatBytes } from "../../Utils/fileUtils"; import { imageValidation } from "../../Validation/validation"; @@ -7,29 +7,34 @@ import ImageIcon from "@mui/icons-material/Image"; import {GenericDialog} from "../Dialog/genericDialog"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import { checkImage } from "../../Utils/fileUtils"; +import { useTheme } from "@mui/material/styles"; const isValidBase64Image = (data) => { return /^[A-Za-z0-9+/=]+$/.test(data); }; -const ImageUpload = ({ open, onClose, onUpdate, value, currentImage = value, theme, shouldRender = true, placeholder, maxSize, acceptedTypes, previewSize = 150, onError,}) => { +const ImageUpload = ({ open, onClose, onUpdate, shouldRender = true, placeholder, maxSize, acceptedTypes, previewSize = 150, setErrors, errors}) => { const [file, setFile] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const [errors, setErrors] = useState({}); const [isDragging, setIsDragging] = useState(false); const intervalRef = useRef(null); - + const theme = useTheme(); + const UPLOAD_PROGRESS_INCREMENT = 12; // Controls the speed of upload progress + const UPLOAD_PROGRESS_INTERVAL = 120; // Controls how often progress updates (milliseconds) + const UPLOAD_COMPLETE = 100; // Represents the max progress percentage + // Handle base64 and placeholder logic - let imageSrc = currentImage; + let imageSrc = placeholder || ""; // Default to placeholder or empty string - if (typeof file?.src !== "undefined") { - imageSrc = file.src; - } else if (typeof currentImage !== "undefined" && isValidBase64Image(currentImage)) { - imageSrc = `data:image/png;base64,${currentImage}`; - } else if (typeof placeholder !== "undefined") { - imageSrc = placeholder; - } + if (file?.src) { + imageSrc = file.src; // Use uploaded file's preview + } else if (placeholder && isValidBase64Image(placeholder)) { + imageSrc = `data:image/png;base64,${placeholder}`; // Convert base64 string if valid + } else { + imageSrc = ""; // Ensure it's an empty string instead of "undefined" + } + if (shouldRender === false) { return null; @@ -64,14 +69,14 @@ const handleDrop = (event) => { if (maxSize && pic.size > maxSize) { const errorMsg = `File size exceeds ${formatBytes(maxSize)}`; - setErrors({ picture: errorMsg }); + if (setErrors) setErrors((prev) => ({ ...prev, picture: errorMsg })); if (onError) onError(errorMsg); return; } if (acceptedTypes && !acceptedTypes.includes(pic.type)) { const errorMsg = `File type not supported. Allowed: ${acceptedTypes.join(", ")}`; - setErrors({ picture: errorMsg }); + if (setErrors) setErrors((prev) => ({ ...prev, picture: errorMsg })); if (onError) onError(errorMsg); return; } @@ -90,30 +95,31 @@ const handleDrop = (event) => { // Simulate upload progress intervalRef.current = setInterval(() => { - const buffer = 12; setProgress((prev) => { - if (prev.value + buffer >= 100) { + const nextValue = prev.value + UPLOAD_PROGRESS_INCREMENT; + if (nextValue >= UPLOAD_COMPLETE) { clearInterval(intervalRef.current); - return { value: 100, isLoading: false }; + return { value: UPLOAD_COMPLETE, isLoading: false }; } - return { ...prev, value: prev.value + buffer }; + return { ...prev, value: nextValue }; }); - }, 120); - }; + }, UPLOAD_PROGRESS_INTERVAL); + }; // 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; - if (onError) onError(error.details[0].message); - } else { - delete prevErrors[name]; - } - return prevErrors; - }); + if (setErrors) { + setErrors((prev) => { + const prevErrors = { ...prev }; + if (error) { + prevErrors[name] = error.details[0].message; + } else { + delete prevErrors[name]; + } + return prevErrors; + }); + } return !!error; }; @@ -127,8 +133,10 @@ const handleDrop = (event) => { // Updates the profile picture and closes the modal const handleUpdatePicture = () => { + if (file?.src) { + onUpdate(file.src); + } setProgress({ value: 0, isLoading: false }); - onUpdate(file.src); // Pass the new image URL to the parent component onClose(); // Close the modal }; @@ -137,11 +145,11 @@ const handleDrop = (event) => { id="modal-update-picture" open={open} onClose={onClose} - theme={theme} title={"Upload Image"} description={"Select an image to upload."} confirmationButtonLabel={"Update"} onConfirm={handleUpdatePicture} + theme={theme} isLoading={false} > { }} /> - {!checkImage(file?.src || currentImage) || progress.isLoading ? ( + {!checkImage(imageSrc) || progress.isLoading ? ( <> { width: `${previewSize}px`, height: `${previewSize}px`, overflow: "hidden", - backgroundImage: `url(${file?.src || currentImage})`, + backgroundImage: imageSrc ? `url(${imageSrc})` : "none", backgroundSize: "cover", backgroundPosition: "center", borderRadius: "50%", @@ -244,21 +252,21 @@ const handleDrop = (event) => { )} - {progress.isLoading || progress.value !== 0 || errors["picture"] ? ( + {progress.isLoading || progress.value !== 0 || errors?.picture ? ( } label={file?.name} - size={file?.size} + size={file?.size || "0 KB"} progress={progress.value} onClick={removePicture} - error={errors["picture"]} + error={errors?.picture} /> ) : null}