diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx index 9d258eaad..d1404e94c 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -14,8 +14,9 @@ import { checkImage } from "../../../Utils/fileUtils"; * @returns {JSX.Element} The rendered component. */ -const ImageField = ({ id, src, loading, onChange }) => { +const ImageField = ({ id, src, loading, onChange, error }) => { const theme = useTheme(); + const error_border_style = error? {borderColor: theme.palette.error.main}: {} const [isDragging, setIsDragging] = useState(false); const handleDragEnter = () => { @@ -28,7 +29,7 @@ const ImageField = ({ id, src, loading, onChange }) => { return ( <> {!checkImage(src) || loading ? ( - <> + <> { borderColor: theme.palette.primary.main, backgroundColor: "hsl(215, 87%, 51%, 0.05)", }, + ...error_border_style }} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} @@ -62,7 +64,7 @@ const ImageField = ({ id, src, loading, onChange }) => { cursor: "pointer", maxWidth: "500px", minHeight: "175px", - zIndex: 1 + zIndex: 1, }, "& fieldset": { padding: 0, @@ -79,7 +81,7 @@ const ImageField = ({ id, src, loading, onChange }) => { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - zIndex:0, + zIndex: 0, width: "100%", }} > @@ -114,7 +116,7 @@ const ImageField = ({ id, src, loading, onChange }) => { > (maximum size: 3MB) - + { > Supported formats: JPG, PNG + {error && ( + + {error} + + )} ) : ( { const theme = useTheme(); const dispatch = useDispatch(); const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); + const [logo, setLogo] = useState(); + const [progress, setProgress] = useState({ value: 0, isLoading: false }); + const intervalRef = useRef(null); // Local state for form data, errors, and file handling const [localData, setLocalData] = useState({ companyName: "", url: "", timezone: "America/Toronto", - color: "#4169E1", + color: "#4169E1", theme: "light", //which fields matching below? publish: false, - file: null + logo: null, }); - const handleChange = (event, appendedId) => { + // Clears specific error from errors state + const clearError = (err) => { + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (updatedErrors[err]) delete updatedErrors[err]; + return updatedErrors; + }); + }; + const removeLogo = () => { + errors["logo"] && clearError("logo"); + setLogo({}); + // interrupt interval if image upload is canceled prior to completing the process + clearInterval(intervalRef.current); + setProgress({ value: 0, isLoading: false }); + }; + + // Updates profile image displayed on UI + const handleUpdateLogo = () => { + setProgress({ value: 0, isLoading: false }); + setLocalData((prev) => ({ + ...prev, + logo: logo.src, + })); + }; + + const handleChange = (event) => { event.preventDefault(); const { value, id } = event.target; - let name = appendedId ?? idMap[id] ?? id; setLocalData((prev) => ({ - ...prev, - [name]: value, - })); - } - + ...prev, + [id]: value, + })); + }; const handleSubmit = () => {}; - const handleBlur = () => {}; + const handleBlur = (event) => { + event.preventDefault(); + const { value, id } = event.target; + const { error } = publicPageGeneralSettingsValidation.validate( + { [id]: value }, + { + abortEarly: false, + } + ); - const handlePicture = (event) => { + setErrors((prev) => { + return buildErrors(prev, id, error); + }); + }; + + const validateField = (toValidate, schema, name = "logo") => { + const { error } = schema.validate(toValidate, { abortEarly: false }); + setErrors((prev) => { + let prevErrors = { ...prev }; + if (error) prevErrors[name] = error?.details[0].message; + else delete prevErrors[name]; + return prevErrors; + }); + if (error) return true; + }; + + const handleLogo = (event) => { const pic = event.target.files[0]; + let error = validateField({ type: pic?.type, size: pic?.size }, logoImageValidation); + if (error) return; + + setProgress((prev) => ({ ...prev, isLoading: true })); + setLogo({ + src: URL.createObjectURL(pic), + name: pic.name, + size: formatBytes(pic.size), + }); + + 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); }; return ( @@ -79,7 +153,7 @@ const GeneralSettingsPanel = () => { id="published-to-public" label={`Published and visible to the public`} isChecked={localData.publish} - value={localData.publish} + value={"localData.publish"} onChange={handleChange} onBlur={handleBlur} /> @@ -97,19 +171,24 @@ const GeneralSettingsPanel = () => { @@ -128,29 +207,71 @@ const GeneralSettingsPanel = () => { id="display-timezone" label="Display timezone" value={localData.timezone} - onChange={(e) => { - dispatch(setTimezone({ timezone: e.target.value })); - }} + onChange={handleChange} + onBlur={handleBlur} items={timezones} + error={errors["display-timezone"]} /> + + {progress.isLoading || progress.value !== 0 || errors["logo"] ? ( + } + label={logo?.name} + size={logo?.size} + progress={progress.value} + onClick={removeLogo} + error={errors["logo"]} + /> + ) : ( + "" + )} + + + +