Files
Checkmate/Client/src/Pages/StatusPage/Create/index.jsx
T

253 lines
6.4 KiB
React
Raw Normal View History

2025-02-02 21:26:48 -08:00
// Components
2025-02-03 15:06:49 -08:00
import { Stack, Button, Typography } from "@mui/material";
import Tabs from "./Components/Tabs";
import GenericFallback from "../../../Components/GenericFallback";
import SkeletonLayout from "./Components/Skeleton";
2025-02-02 21:26:48 -08:00
//Utils
import { useTheme } from "@emotion/react";
2025-02-05 10:57:28 -08:00
import { useState, useEffect, useRef, useCallback } from "react";
2025-02-03 15:06:49 -08:00
import { statusPageValidation } from "../../../Validation/validation";
2025-02-02 21:26:48 -08:00
import { buildErrors } from "../../../Validation/error";
2025-02-03 15:06:49 -08:00
import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
import { useCreateStatusPage } from "./Hooks/useCreateStatusPage";
import { createToast } from "../../../Utils/toastUtils";
2025-02-04 13:06:42 -08:00
import { useNavigate } from "react-router-dom";
2025-02-05 10:57:28 -08:00
import { useLocation } from "react-router-dom";
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch";
2025-02-10 13:49:01 -08:00
import { useParams } from "react-router-dom";
2025-02-05 10:57:28 -08:00
2025-02-02 21:26:48 -08:00
//Constants
const TAB_LIST = ["General settings", "Contents"];
2025-02-03 15:06:49 -08:00
const ERROR_TAB_MAPPING = [
["companyName", "url", "timezone", "color", "isPublished", "logo"],
["monitors", "showUptimePercentage", "showCharts"],
];
2025-02-02 21:26:48 -08:00
const CreateStatusPage = () => {
2025-02-10 13:49:01 -08:00
const { url } = useParams();
2025-02-02 21:26:48 -08:00
//Local state
const [tab, setTab] = useState(0);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const [form, setForm] = useState({
isPublished: false,
companyName: "",
2025-02-10 13:49:01 -08:00
url: url ?? Math.floor(Math.random() * 1000000).toFixed(0),
2025-02-04 13:06:42 -08:00
logo: undefined,
2025-02-02 21:26:48 -08:00
timezone: "America/Toronto",
color: "#4169E1",
2025-02-10 13:49:01 -08:00
type: "uptime",
2025-02-03 15:06:49 -08:00
monitors: [],
showCharts: true,
showUptimePercentage: true,
2025-02-02 21:26:48 -08:00
});
const [errors, setErrors] = useState({});
2025-02-03 15:06:49 -08:00
const [selectedMonitors, setSelectedMonitors] = useState([]);
2025-02-02 21:26:48 -08:00
// Refs
const intervalRef = useRef(null);
2025-02-05 10:57:28 -08:00
// Setup
const location = useLocation();
2025-02-10 13:49:01 -08:00
const isCreate = location.pathname === "/status/uptime/create";
2025-02-05 10:57:28 -08:00
2025-02-02 21:26:48 -08:00
//Utils
const theme = useTheme();
2025-02-03 15:06:49 -08:00
const [monitors, isLoading, networkError] = useMonitorsFetch();
2025-02-05 10:57:28 -08:00
const [createStatusPage, createStatusIsLoading, createStatusPageNetworkError] =
useCreateStatusPage(isCreate);
2025-02-04 13:06:42 -08:00
const navigate = useNavigate();
2025-02-10 13:49:01 -08:00
2025-02-05 10:57:28 -08:00
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
2025-02-10 13:49:01 -08:00
useStatusPageFetch(isCreate, url);
2025-02-02 21:26:48 -08:00
// Handlers
const handleFormChange = (e) => {
let { type, name, value, checked } = e.target;
// Handle errors
2025-02-03 15:06:49 -08:00
const { error } = statusPageValidation.validate(
2025-02-02 21:26:48 -08:00
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, name, error);
});
//Handle checkbox
if (type === "checkbox") {
setForm((prev) => ({
...prev,
[name]: checked,
}));
return;
}
// Handle other inputs
setForm((prev) => ({
...prev,
[name]: value,
}));
};
2025-02-04 13:06:42 -08:00
const handleImageChange = useCallback((event) => {
2025-02-02 21:26:48 -08:00
const img = event.target?.files?.[0];
const newLogo = {
src: URL.createObjectURL(img),
name: img.name,
type: img.type,
size: img.size,
};
setForm((prev) => ({
...prev,
logo: newLogo,
}));
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);
2025-02-04 13:06:42 -08:00
}, []);
2025-02-02 21:26:48 -08:00
const removeLogo = () => {
setForm((prev) => ({
...prev,
logo: undefined,
}));
// interrupt interval if image upload is canceled prior to completing the process
clearInterval(intervalRef.current);
setProgress({ value: 0, isLoading: false });
};
2025-02-03 15:06:49 -08:00
const handleSubmit = async () => {
2025-02-02 21:26:48 -08:00
let toSubmit = {
...form,
logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
};
2025-02-03 15:06:49 -08:00
const { error } = statusPageValidation.validate(toSubmit, {
2025-02-02 21:26:48 -08:00
abortEarly: false,
});
2025-02-03 15:06:49 -08:00
if (typeof error === "undefined") {
const success = await createStatusPage({ form });
if (success) {
2025-02-10 16:15:48 -08:00
createToast({
body: isCreate
? "Status page created successfully"
: "Status page updated successfully",
});
2025-02-10 13:49:01 -08:00
navigate(`/status/uptime/${form.url}`);
2025-02-03 15:06:49 -08:00
}
return;
}
const newErrors = {};
error?.details?.forEach((err) => {
newErrors[err.path[0]] = err.message;
2025-02-02 21:26:48 -08:00
});
2025-02-03 15:06:49 -08:00
setErrors((prev) => ({ ...prev, ...newErrors }));
const errorTabs = Object.keys(newErrors).map((err) => {
return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err));
});
// If there's an error in the current tab, don't change the tab
if (errorTabs.some((errorTab) => errorTab === tab)) {
return;
}
2025-02-10 13:49:01 -08:00
// If we get -1, there's an unknown error
if (errorTabs[0] === -1) {
createToast({ body: "Unknown error" });
return;
}
2025-02-03 15:06:49 -08:00
// Otherwise go to tab with error
setTab(errorTabs[0]);
2025-02-02 21:26:48 -08:00
};
2025-02-05 10:57:28 -08:00
// If we are configuring, populate fields
useEffect(() => {
if (isCreate) return;
if (typeof statusPage === "undefined") {
return;
}
let newLogo = undefined;
if (statusPage.logo) {
newLogo = {
src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
name: "logo",
type: statusPage.logo.contentType,
size: null,
};
}
setForm((prev) => {
return {
...prev,
companyName: statusPage?.companyName,
isPublished: statusPage?.isPublished,
timezone: statusPage?.timezone,
monitors: statusPageMonitors.map((monitor) => monitor._id),
color: statusPage?.color,
logo: newLogo,
};
});
setSelectedMonitors(statusPageMonitors);
}, [isCreate, statusPage, statusPageMonitors]);
2025-02-03 15:06:49 -08:00
if (networkError === true) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
if (isLoading) return <SkeletonLayout />;
2025-02-05 10:57:28 -08:00
// Load fields
2025-02-02 21:26:48 -08:00
return (
<Stack gap={theme.spacing(10)}>
2025-02-03 15:06:49 -08:00
<Tabs
form={form}
errors={errors}
monitors={monitors}
selectedMonitors={selectedMonitors}
setSelectedMonitors={setSelectedMonitors}
handleFormChange={handleFormChange}
handleImageChange={handleImageChange}
progress={progress}
removeLogo={removeLogo}
tab={tab}
setTab={setTab}
TAB_LIST={TAB_LIST}
2025-02-10 13:49:01 -08:00
isCreate={isCreate}
2025-02-03 15:06:49 -08:00
/>
2025-02-02 21:26:48 -08:00
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
>
Save
</Button>
</Stack>
</Stack>
);
};
export default CreateStatusPage;