diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index aee0fb84b..dbac0bc82 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -12,7 +12,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
-import CreateStatus from "./Pages/Status/CreateStatus";
+import CreateStatus from "./Pages/StatusPage/CreateStatus";
function App() {
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
diff --git a/Client/src/Components/Inputs/ColorPicker/index.jsx b/Client/src/Components/Inputs/ColorPicker/index.jsx
index de63400bc..64b878400 100644
--- a/Client/src/Components/Inputs/ColorPicker/index.jsx
+++ b/Client/src/Components/Inputs/ColorPicker/index.jsx
@@ -21,15 +21,17 @@ import { MuiColorInput } from "mui-color-input";
* >
*
*/
-const ColorPicker = ({ id, value, error, onChange, onBlur }) => {
+const ColorPicker = ({ id, name, value, error, onChange, onBlur }) => {
const theme = useTheme();
return (
onChange({ target: { name, value: color } })}
onBlur={onBlur}
/>
{error && (
@@ -54,7 +56,7 @@ ColorPicker.propTypes = {
value: PropTypes.string,
error: PropTypes.string,
onChange: PropTypes.func.isRequired,
- onBlur: PropTypes.func.isRequired,
+ onBlur: PropTypes.func,
};
export default ColorPicker;
diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx
index 37833f55c..4b8b92aa5 100644
--- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx
+++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import ConfigBox from "../../../Components/ConfigBox";
-import { StatusFormContext } from "../../../Pages/Status/CreateStatusContext";
+import { StatusFormContext } from "../../../Pages/StatusPage/CreateStatusContext";
import { useSelector } from "react-redux";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../Utils/toastUtils";
@@ -22,14 +22,8 @@ import { buildErrors } from "../../../Validation/error";
*/
const ContentPanel = () => {
const theme = useTheme();
- const {
- form,
- setForm,
- errors,
- setErrors,
- handleBlur,
- handelCheckboxChange,
- } = useContext(StatusFormContext);
+ const { form, setForm, errors, setErrors, handleBlur, handelCheckboxChange } =
+ useContext(StatusFormContext);
const [cards, setCards] = useState([]);
const { user, authToken } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState([]);
@@ -80,7 +74,7 @@ const ContentPanel = () => {
const handleServersBlur = () => {
const { error } = publicPageSettingsValidation.validate(
- { "monitors": form.monitors },
+ { monitors: form.monitors },
{
abortEarly: false,
}
@@ -88,7 +82,7 @@ const ContentPanel = () => {
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
- };
+ };
return (
{
form={form}
setForm={setForm}
removeItem={removeCard}
- onBlur= {handleServersBlur}
+ onBlur={handleServersBlur}
/>
{errors["monitors"] && (
diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx
index af0f9c81f..46410e82e 100644
--- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx
+++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx
@@ -12,7 +12,7 @@ import Select from "../../Inputs/Select";
import { logoImageValidation } from "../../../Validation/validation";
import { formatBytes } from "../../../Utils/fileUtils";
import ProgressUpload from "../../ProgressBars";
-import { StatusFormContext } from "../../../Pages/Status/CreateStatusContext";
+import { StatusFormContext } from "../../../Pages/StatusPage/CreateStatusContext";
import ColorPicker from "../../Inputs/ColorPicker";
import Checkbox from "../../Inputs/Checkbox";
@@ -22,8 +22,15 @@ import Checkbox from "../../Inputs/Checkbox";
*/
const GeneralSettingsPanel = () => {
const theme = useTheme();
- const { form, setForm, errors, setErrors, handleBlur, handleChange, handelCheckboxChange } =
- useContext(StatusFormContext);
+ const {
+ form,
+ setForm,
+ errors,
+ setErrors,
+ handleBlur,
+ handleChange,
+ handelCheckboxChange,
+ } = useContext(StatusFormContext);
const [logo, setLogo] = useState(form.logo);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
@@ -213,7 +220,7 @@ const GeneralSettingsPanel = () => {
width: "100%",
maxWidth: "200px",
alignSelf: "center",
- mt: theme.spacing(4)
+ mt: theme.spacing(4),
}}
>
Remove Logo
diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx
index 9fd38b5ef..0a1d2c104 100644
--- a/Client/src/Pages/Account/index.jsx
+++ b/Client/src/Pages/Account/index.jsx
@@ -87,33 +87,6 @@ const Account = ({ open = "profile" }) => {
onKeyDown={handleKeyDown}
onFocus={() => handleFocus(label.toLowerCase())}
tabIndex={index}
- sx={{
- fontSize: 13,
- color: theme.palette.tertiary.contrastText,
- backgroundColor: theme.palette.tertiary.main,
- textTransform: "none",
- minWidth: "fit-content",
- paddingY: theme.spacing(6),
- fontWeight: 400,
- borderBottom: "2px solid transparent",
- borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
- "&:first-child": { borderTopLeftRadius: "8px" },
- "&:last-child": { borderTopRightRadius: "8px", borderRight: 0 },
- "&:focus-visible": {
- color: theme.palette.primary.contrastText,
- borderColor: theme.palette.tertiary.contrastText,
- borderRightColor: theme.palette.primary.lowContrast,
- },
- "&.Mui-selected": {
- backgroundColor: theme.palette.secondary.main,
- color: theme.palette.secondary.contrastText,
- borderColor: theme.palette.secondary.contrastText,
- borderRightColor: theme.palette.primary.lowContrast,
- },
- "&:hover": {
- borderColor: theme.palette.primary.lowContrast,
- },
- }}
/>
))}
diff --git a/Client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/Client/src/Pages/StatusPage/Create/Components/Progress/index.jsx
new file mode 100644
index 000000000..d85225551
--- /dev/null
+++ b/Client/src/Pages/StatusPage/Create/Components/Progress/index.jsx
@@ -0,0 +1,37 @@
+import { Button, Box } from "@mui/material";
+import ProgressUpload from "../../../../../Components/ProgressBars";
+import ImageIcon from "@mui/icons-material/Image";
+
+import { formatBytes } from "../../../../../Utils/fileUtils";
+const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => {
+ if (isLoading) {
+ return (
+ }
+ label={logo?.name}
+ size={formatBytes(logo?.size)}
+ progress={progressValue}
+ onClick={removeLogo}
+ />
+ );
+ }
+
+ if (logo && logoType) {
+ return (
+
+
+
+ );
+ }
+};
+
+export default Progress;
diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
new file mode 100644
index 000000000..763863ec7
--- /dev/null
+++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
@@ -0,0 +1,14 @@
+import { Stack, Typography } from "@mui/material";
+import { TabPanel } from "@mui/lab";
+
+const Content = ({ tabValue }) => {
+ return (
+
+
+ Content
+
+
+ );
+};
+
+export default Content;
diff --git a/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx
new file mode 100644
index 000000000..2dbf3f2b2
--- /dev/null
+++ b/Client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx
@@ -0,0 +1,128 @@
+// Components
+import { Stack, Typography } from "@mui/material";
+import { TabPanel } from "@mui/lab";
+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 ColorPicker from "../../../../../Components/Inputs/ColorPicker";
+import Progress from "../Progress";
+// Utils
+import { useTheme } from "@emotion/react";
+import timezones from "../../../../../Utils/timezones.json";
+
+const TabSettings = ({
+ tabValue,
+ form,
+ handleFormChange,
+ handleImageChange,
+ progress,
+ removeLogo,
+ errors,
+}) => {
+ // Utils
+ const theme = useTheme();
+
+ return (
+
+
+
+
+ Access
+
+ If your status page is ready, you can mark it as published.
+
+
+
+
+
+
+
+
+ Basic Information
+
+ Define company name and the subdomain that your status page points to.
+
+
+
+
+
+
+
+
+
+ Timezone
+
+ Select the timezone that your status page will be displayed in.
+
+
+
+
+
+
+
+
+ Appearance
+
+ Define the default look and feel of your public status page.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TabSettings;
diff --git a/Client/src/Pages/StatusPage/Create/index.jsx b/Client/src/Pages/StatusPage/Create/index.jsx
new file mode 100644
index 000000000..137732cba
--- /dev/null
+++ b/Client/src/Pages/StatusPage/Create/index.jsx
@@ -0,0 +1,165 @@
+// Components
+import { Stack, Tab, Button } from "@mui/material";
+import { TabContext, TabList } from "@mui/lab";
+import Settings from "./Components/Tabs/Settings";
+import Content from "./Components/Tabs/Content";
+
+//Utils
+import { useTheme } from "@emotion/react";
+import { useState, useRef } from "react";
+import { publicPageSettingsValidation } from "../../../Validation/validation";
+import { buildErrors } from "../../../Validation/error";
+
+//Constants
+const TAB_LIST = ["General settings", "Contents"];
+
+const CreateStatusPage = () => {
+ //Redux state
+
+ //Local state
+ const [tab, setTab] = useState(0);
+ const [progress, setProgress] = useState({ value: 0, isLoading: false });
+ const [form, setForm] = useState({
+ isPublished: false,
+ companyName: "",
+ url: "/status/public",
+ logo: null,
+ timezone: "America/Toronto",
+ color: "#4169E1",
+ });
+ const [errors, setErrors] = useState({});
+
+ // Refs
+ const intervalRef = useRef(null);
+
+ //Utils
+ const theme = useTheme();
+
+ // Handlers
+ const handleFormChange = (e) => {
+ let { type, name, value, checked } = e.target;
+
+ // Handle errors
+ const { error } = publicPageSettingsValidation.validate(
+ { [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,
+ }));
+ };
+
+ const handleImageChange = (event) => {
+ 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);
+ };
+
+ 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 });
+ };
+
+ const handleSubmit = () => {
+ let toSubmit = {
+ ...form,
+ logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
+ };
+ const { error } = publicPageSettingsValidation.validate(toSubmit, {
+ abortEarly: false,
+ });
+ console.log(error);
+ setErrors((prev) => {
+ return buildErrors(prev, name, error);
+ });
+ };
+
+ return (
+
+
+ {
+ setTab(TAB_LIST.indexOf(tab));
+ }}
+ >
+ {TAB_LIST.map((tab, idx) => (
+
+ ))}
+
+ {tab === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default CreateStatusPage;
diff --git a/Client/src/Pages/Status/CreateStatus/index.jsx b/Client/src/Pages/StatusPage/CreateStatus/index.jsx
similarity index 100%
rename from Client/src/Pages/Status/CreateStatus/index.jsx
rename to Client/src/Pages/StatusPage/CreateStatus/index.jsx
diff --git a/Client/src/Pages/Status/CreateStatusContext/index.jsx b/Client/src/Pages/StatusPage/CreateStatusContext/index.jsx
similarity index 100%
rename from Client/src/Pages/Status/CreateStatusContext/index.jsx
rename to Client/src/Pages/StatusPage/CreateStatusContext/index.jsx
diff --git a/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx b/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx
new file mode 100644
index 000000000..5af297679
--- /dev/null
+++ b/Client/src/Pages/StatusPage/PublicStatus/Hooks/useStatusPageFetch.jsx
@@ -0,0 +1,11 @@
+import { useEffect, useState } from "react";
+
+const useStatusPageFetch = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [networkError, setNetworkError] = useState(false);
+ const [statusPage, setStatusPage] = useState(undefined);
+
+ return [statusPage, isLoading, networkError];
+};
+
+export { useStatusPageFetch };
diff --git a/Client/src/Pages/StatusPage/PublicStatus/index.jsx b/Client/src/Pages/StatusPage/PublicStatus/index.jsx
new file mode 100644
index 000000000..f3503e4f5
--- /dev/null
+++ b/Client/src/Pages/StatusPage/PublicStatus/index.jsx
@@ -0,0 +1,48 @@
+// Components
+import { Typography, Stack } from "@mui/material";
+import GenericFallback from "../../../Components/GenericFallback";
+import Fallback from "../../../Components/Fallback";
+// Utils
+import { useStatusPageFetch } from "./Hooks/useStatusPageFetch";
+import { useTheme } from "@emotion/react";
+import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+
+const PublicStatus = () => {
+ // Utils
+ const theme = useTheme();
+ const isAdmin = useIsAdmin();
+ const [statusPage, isLoading, networkError] = useStatusPageFetch();
+
+ if (networkError === true) {
+ return (
+
+
+ Network error
+
+ Please check your connection
+
+ );
+ }
+
+ if (!isLoading && typeof statusPage === "undefined") {
+ return (
+
+ );
+ }
+
+ return Content Here;
+};
+
+export default PublicStatus;
diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/StatusPage/index.jsx
similarity index 78%
rename from Client/src/Pages/Status/index.jsx
rename to Client/src/Pages/StatusPage/index.jsx
index 25d5a1410..a9cd91516 100644
--- a/Client/src/Pages/Status/index.jsx
+++ b/Client/src/Pages/StatusPage/index.jsx
@@ -9,34 +9,32 @@ import { useSelector } from "react-redux";
import { logger } from "../../Utils/Logger";
/**
- * The configuration page for public page that contains a general settings and
+ * The configuration page for public page that contains a general settings and
* content tabs, It will display a static page if there is no status page configured
* or the status page if one is already configured
*/
const Status = () => {
const theme = useTheme();
- const navigate = useNavigate();
- const {authToken} = useSelector((state) => state.auth);
+ const navigate = useNavigate();
+ const { authToken } = useSelector((state) => state.auth);
const [initForm, setInitForm] = useState({});
- const STATUS_PAGE = import.meta.env.VITE_STATU_PAGE_URL?? "status-page";
+ const STATUS_PAGE = import.meta.env.VITE_STATU_PAGE_URL ?? "status-page";
useEffect(() => {
const getStatusPage = async () => {
let config = { authToken: authToken, url: STATUS_PAGE };
try {
- let res = await networkService.getStatusPageByUrl(config);
- if(res && res.data)
- setInitForm( res.data.data)
- }catch (error) {
+ let res = await networkService.getStatusPageByUrl(config);
+ if (res && res.data) setInitForm(res.data.data);
+ } catch (error) {
logger.error("Failed to fetch status page", error);
- }
-
+ }
};
getStatusPage();
}, []);
return (
<>
- {Object.keys(initForm).length===0? (
+ {Object.keys(initForm).length === 0 ? (
{
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
borderStyle: "dashed",
- backgroundColor: theme.palette.primary.main,
+ backgroundColor: theme.palette.primary.main,
overflow: "hidden",
},
}}
@@ -76,10 +74,10 @@ const Status = () => {
) : (
-
- )}
+
+ )}
>
- );
+ );
};
export default Status;
diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx
index 420d18804..1c0c5ad08 100644
--- a/Client/src/Routes/index.jsx
+++ b/Client/src/Routes/index.jsx
@@ -29,7 +29,12 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details";
import Incidents from "../Pages/Incidents";
-import Status from "../Pages/Status";
+//Status pages
+import Status from "../Pages/StatusPage";
+import CreateStatus from "../Pages/StatusPage/Create";
+import OldCreateStatus from "../Pages/StatusPage/CreateStatus";
+import PublicStatus from "../Pages/StatusPage/PublicStatus";
+
import Integrations from "../Pages/Integrations";
// Settings
@@ -41,7 +46,6 @@ import Maintenance from "../Pages/Maintenance";
import ProtectedRoute from "../Components/ProtectedRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
-import CreateStatus from "../Pages/Status/CreateStatus";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -111,12 +115,16 @@ const Routes = () => {
}
+ element={}
/>
}
+ element={}
+ />
+ }
/>
({
},
},
},
+ MuiTab: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ fontSize: 13,
+ color: theme.palette.tertiary.contrastText,
+ backgroundColor: theme.palette.tertiary.main,
+ textTransform: "none",
+ minWidth: "fit-content",
+ paddingY: theme.spacing(6),
+ fontWeight: 400,
+ borderBottom: "2px solid transparent",
+ borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
+ "&:first-of-type": { borderTopLeftRadius: "8px" },
+ "&:last-child": { borderTopRightRadius: "8px", borderRight: 0 },
+ "&:focus-visible": {
+ color: theme.palette.primary.contrastText,
+ borderColor: theme.palette.tertiary.contrastText,
+ borderRightColor: theme.palette.primary.lowContrast,
+ },
+ "&.Mui-selected": {
+ backgroundColor: theme.palette.secondary.main,
+ color: theme.palette.secondary.contrastText,
+ borderColor: theme.palette.secondary.contrastText,
+ borderRightColor: theme.palette.primary.lowContrast,
+ },
+ "&:hover": {
+ borderColor: theme.palette.primary.lowContrast,
+ },
+ }),
+ },
+ },
},
shape: {
borderRadius: 2,
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js
index 9c2041e9e..76f4ef606 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -178,20 +178,33 @@ 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(3000000).messages({
- "number.base": "File size must be a number.",
- "number.max": "File size must be less than 3MB.",
- "number.empty": "File size required.",
- }),
-});
+const logoImageValidation = joi
+ .object({
+ type: joi
+ .string()
+ .valid("image/jpeg", "image/png")
+ .allow(null) // Allow null and empty string
+ .messages({
+ "any.only": "Invalid file format.",
+ "string.empty": "File type required.",
+ })
+ .optional(),
+ size: joi
+ .number()
+ .max(3000000)
+ .allow(null) // Allow null and empty string
+ .messages({
+ "number.base": "File size must be a number.",
+ "number.max": "File size must be less than 3MB.",
+ "number.empty": "File size required.",
+ })
+ .optional(),
+ })
+ .allow(null)
+ .optional(); // Make entire object optional
const publicPageSettingsValidation = joi.object({
- publish: joi.bool(),
+ isPublished: joi.bool(),
companyName: joi
.string()
.trim()
@@ -209,7 +222,6 @@ const publicPageSettingsValidation = joi.object({
logo: logoImageValidation,
showUptimePercentage: joi.boolean(),
showBarcode: joi.boolean(),
- showBarcode: joi.boolean(),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({