- Add log and general settins tab validation
- WIP logo uploading in general settings tab
This commit is contained in:
1 parent
8506291586
commit
df36f873ae
3 files changed
+194
-32
No files matched your search
@@ -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 ? (
|
||||
<>
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
@@ -46,6 +47,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
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)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
@@ -123,6 +125,19 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
>
|
||||
Supported formats: JPG, PNG
|
||||
</Typography>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Button, Box, Stack, Typography } from "@mui/material";
|
||||
import { ConfigBox } from "../../../Pages/Settings/styled";
|
||||
import Checkbox from "../../Inputs/Checkbox";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -10,41 +10,115 @@ import ImageField from "../../Inputs/Image";
|
||||
import { setMode, setTimezone } from "../../../Features/UI/uiSlice";
|
||||
import timezones from "../../../Utils/timezones.json";
|
||||
import Select from "../../Inputs/Select"
|
||||
import { logoImageValidation, publicPageGeneralSettingsValidation } from "../../../Validation/validation"
|
||||
import { buildErrors } from "../../../Validation/error";
|
||||
import { formatBytes } from "../../../Utils/fileUtils";
|
||||
import ProgressUpload from "../../ProgressBars";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
|
||||
const GeneralSettingsPanel = () => {
|
||||
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 = () => {
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Field
|
||||
id="company-name"
|
||||
id="companyName"
|
||||
type="text"
|
||||
label="Company name"
|
||||
value={localData.companyName}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors["companyName"]}
|
||||
/>
|
||||
<Field
|
||||
id="suburl"
|
||||
id="url"
|
||||
type="url"
|
||||
label="suburl"
|
||||
label="SubURL"
|
||||
placeholder={""}
|
||||
value={localData.url}
|
||||
prefix={"http://uptimegenie.com/"}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@@ -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"]}
|
||||
/>
|
||||
|
||||
<ImageField
|
||||
id="update-logo-picture"
|
||||
src={localData.src}
|
||||
loading={true}
|
||||
onChange={handlePicture}
|
||||
id="logo"
|
||||
src={localData.logo?.src??logo?.src}
|
||||
loading={progress.isLoading && progress.value !== 100}
|
||||
onChange={handleLogo}
|
||||
/>
|
||||
{progress.isLoading || progress.value !== 0 || errors["logo"] ? (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={logo?.name}
|
||||
size={logo?.size}
|
||||
progress={progress.value}
|
||||
onClick={removeLogo}
|
||||
error={errors["logo"]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
mt={theme.spacing(10)}
|
||||
gap={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={removeLogo}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdateLogo}
|
||||
disabled={
|
||||
(Object.keys(errors).length !== 0 && errors?.logo) ||
|
||||
progress.value !== 100
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
<Field
|
||||
id="color"
|
||||
label="Color"
|
||||
value={localData.color}
|
||||
error={errors["color"]}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<Select
|
||||
id="theme"
|
||||
label="Theme"
|
||||
value={localData.theme}
|
||||
onChange={(e) => {
|
||||
dispatch(setMode(e.target.value));
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
items={[
|
||||
{ _id: "light", name: "Light" },
|
||||
{ _id: "dark", name: "Dark" },
|
||||
|
||||
@@ -120,6 +120,30 @@ const imageValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const logoImageValidation = 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(800*800)
|
||||
.messages({
|
||||
"number.base": "File size must be a number.",
|
||||
"number.max": "File size must be less than 640000 pixels.",
|
||||
"number.empty": "File size required.",
|
||||
}),
|
||||
});
|
||||
|
||||
const publicPageGeneralSettingsValidation = joi.object({
|
||||
companyName: joi.string().trim().messages({ "string.empty": "Company name is required." }),
|
||||
url: joi.string().trim().messages({ "string.empty": "URL is required." }),
|
||||
timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }),
|
||||
color: joi.string().trim().messages({ "string.empty": "Color is required." }),
|
||||
theme: joi.string().trim().messages({ "string.empty": "Theme is required." })
|
||||
}
|
||||
|
||||
)
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
"string.empty": "TTL is required",
|
||||
@@ -220,4 +244,6 @@ export {
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
publicPageGeneralSettingsValidation,
|
||||
logoImageValidation
|
||||
};
|
||||
Reference in new issue
Block a user