diff --git a/Client/package-lock.json b/Client/package-lock.json
index a5bd33cf9..c3ed59f91 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -15,8 +15,8 @@
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.2",
- "@mui/x-date-pickers": "7.23.2",
+ "@mui/x-data-grid": "7.23.3",
+ "@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -1451,9 +1451,9 @@
}
},
"node_modules/@mui/x-data-grid": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.2.tgz",
- "integrity": "sha512-Xhm4Bh+WiU5MSLFIZ8mE1egHoS0xPEVlwvjYvairPIkxNPubB7f+gtLkuAJ2+m+BYbgO7yDte+Pp7dfjkU+vOA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz",
+ "integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1488,9 +1488,9 @@
}
},
"node_modules/@mui/x-date-pickers": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.2.tgz",
- "integrity": "sha512-Kt9VsEnShaBKiaastTYku66UIWptgc9UMA16d0G/0TkfIsvZrAD3iacQR6HHAXWspaFshdfsRmW2JAoFhzKZsg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
+ "integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -2390,9 +2390,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz",
- "integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==",
+ "version": "18.3.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
+ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4939,16 +4939,15 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5455,9 +5454,9 @@
}
},
"node_modules/react-router": {
- "version": "6.28.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz",
- "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==",
+ "version": "6.28.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
+ "integrity": "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0"
@@ -5470,13 +5469,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.28.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz",
- "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==",
+ "version": "6.28.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.1.tgz",
+ "integrity": "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0",
- "react-router": "6.28.0"
+ "react-router": "6.28.1"
},
"engines": {
"node": ">=14.0.0"
diff --git a/Client/package.json b/Client/package.json
index e5f111c30..adf3b9d34 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -18,8 +18,8 @@
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.2",
- "@mui/x-date-pickers": "7.23.2",
+ "@mui/x-data-grid": "7.23.3",
+ "@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx
index e519a079f..cfc3030b0 100644
--- a/Client/src/Components/BasicTable/index.jsx
+++ b/Client/src/Components/BasicTable/index.jsx
@@ -115,7 +115,7 @@ TablePaginationActions.propTypes = {
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
* @param {number} props.rowsPerPage- Number of rows per page (table).
- *
+ * @param {string} props.emptyMessage - Message to display when there is no data.
* @example
* const data = {
* cols: [
@@ -149,7 +149,7 @@ TablePaginationActions.propTypes = {
*
*/
-const BasicTable = ({ data, paginated, reversed, table }) => {
+const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const dispatch = useDispatch();
@@ -216,7 +216,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
- backgroundColor: theme.palette.background.accent,
+ filter: "brightness(.75)",
+ opacity: 0.75,
+ transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
key={row.id}
@@ -228,6 +230,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
);
})}
+ {displayData.length === 0 && (
+
+
+ {emptyMessage}
+
+
+ )}
@@ -317,6 +329,7 @@ BasicTable.propTypes = {
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
+ emptyMessage: PropTypes.string,
};
export default BasicTable;
diff --git a/Client/src/Pages/Uptime/styled.jsx b/Client/src/Components/ConfigBox/index.jsx
similarity index 81%
rename from Client/src/Pages/Uptime/styled.jsx
rename to Client/src/Components/ConfigBox/index.jsx
index f159d44f2..7650dd47f 100644
--- a/Client/src/Pages/Uptime/styled.jsx
+++ b/Client/src/Components/ConfigBox/index.jsx
@@ -1,12 +1,15 @@
import { Stack, styled } from "@mui/material";
-export const ConfigBox = styled(Stack)(({ theme }) => ({
+const ConfigBox = styled(Stack)(({ theme }) => ({
+ display: "flex",
flexDirection: "row",
+ justifyContent: "space-between",
+ gap: theme.spacing(20),
+ backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
- backgroundColor: theme.palette.background.main,
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
@@ -24,15 +27,12 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
},
- "& h2": {
+ "& h1, & h2": {
color: theme.palette.text.secondary,
- fontSize: 15,
- fontWeight: 600,
- },
- "& h3, & p": {
- color: theme.palette.text.tertiary,
},
"& p": {
- fontSize: 13,
+ color: theme.palette.text.tertiary,
},
}));
+
+export default ConfigBox;
diff --git a/Client/src/Components/IconBox/index.jsx b/Client/src/Components/IconBox/index.jsx
new file mode 100644
index 000000000..8000fbd64
--- /dev/null
+++ b/Client/src/Components/IconBox/index.jsx
@@ -0,0 +1,77 @@
+import { Box, styled } from "@mui/material";
+import PropTypes from "prop-types";
+
+/**
+ * IconBox - A styled box component for rendering icons with consistent sizing and styling
+ *
+ * @component
+ * @param {Object} [props] - Configuration options for the IconBox
+ * @param {number} [props.height=34] - Height of the icon box
+ * @param {number} [props.width=34] - Width of the icon box
+ * @param {number} [props.minWidth=34] - Minimum width of the icon box
+ * @param {number} [props.borderRadius=4] - Border radius of the icon box
+ * @param {number} [props.svgWidth=20] - Width of the SVG icon
+ * @param {number} [props.svgHeight=20] - Height of the SVG icon
+ *
+ * @example
+ * // Basic usage
+ *
+ *
+ *
+ *
+ * @example
+ * // Customized usage
+ *
+ *
+ *
+ *
+ * @returns {React.ReactElement} A styled box containing an icon
+ */
+const IconBox = styled(Box)(
+ ({
+ theme,
+ height = 34,
+ width = 34,
+ minWidth = 34,
+ borderRadius = 4,
+ svgWidth = 20,
+ svgHeight = 20,
+ }) => ({
+ height: height,
+ minWidth: minWidth,
+ width: width,
+ position: "relative",
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.border.dark,
+ borderRadius: borderRadius,
+ backgroundColor: theme.palette.background.accent,
+ "& svg": {
+ position: "absolute",
+ top: "50%",
+ left: "50%",
+ transform: "translate(-50%, -50%)",
+ width: svgWidth,
+ height: svgHeight,
+ "& path": {
+ stroke: theme.palette.text.tertiary,
+ },
+ },
+ })
+);
+
+IconBox.propTypes = {
+ height: PropTypes.number,
+ width: PropTypes.number,
+ minWidth: PropTypes.number,
+ borderRadius: PropTypes.number,
+ svgWidth: PropTypes.number,
+ svgHeight: PropTypes.number,
+};
+
+export default IconBox;
diff --git a/Client/src/Components/Inputs/Radio/index.jsx b/Client/src/Components/Inputs/Radio/index.jsx
index 7a700e2b7..6291485f0 100644
--- a/Client/src/Components/Inputs/Radio/index.jsx
+++ b/Client/src/Components/Inputs/Radio/index.jsx
@@ -42,6 +42,9 @@ const Radio = (props) => {
width: 16,
height: 16,
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
+ "&:not(.Mui-checked)": {
+ boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline
+ },
mt: theme.spacing(0.5),
}}
/>
diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx
index c944955b9..9a1995726 100644
--- a/Client/src/Components/Inputs/Search/index.jsx
+++ b/Client/src/Components/Inputs/Search/index.jsx
@@ -40,6 +40,7 @@ const SearchAdornment = () => {
//TODO keep search state inside of component
const Search = ({
+ label,
id,
options,
filteredBy,
@@ -76,6 +77,14 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
+
+ {label}
+
{
};
ProtectedRoute.propTypes = {
- children: PropTypes.elementType.isRequired,
+ children: PropTypes.element.isRequired,
};
export default ProtectedRoute;
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index a573b3bb0..4636c1739 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -21,6 +21,7 @@ import { useDispatch, useSelector } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import ThemeSwitch from "../ThemeSwitch";
import Avatar from "../Avatar";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
@@ -43,6 +44,7 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import Folder from "../../assets/icons/folder.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
+import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
import "./index.css";
@@ -64,20 +66,31 @@ const menu = [
{ name: "Team", path: "account/team", icon: },
],
},
+ {
+ name: "Settings",
+ icon: ,
+ path: "settings",
+ },
{
name: "Other",
icon: ,
nested: [
- { name: "Settings", path: "settings", icon: },
{ name: "Support", path: "support", icon: },
+ {
+ name: "Discussions",
+ path: "discussions",
+ icon: ,
+ },
{ name: "Docs", path: "docs", icon: },
{ name: "Changelog", path: "changelog", icon: },
],
},
];
+/* TODO this could be a key in nested Path would be the link */
const URL_MAP = {
- support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
+ support: "https://discord.com/invite/NAb6H3UTjK",
+ discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
};
@@ -546,28 +559,35 @@ function Sidebar() {
{authState.user?.role}
-
- openPopup(event, "logout")}
+
+
-
-
-
+ openPopup(event, "logout")}
+ >
+
+
+
+
>
)}
+ * )
+ *
+ * @returns {React.ReactElement} A styled box containing the statistic
+ */
+
+const StatBox = ({ heading, subHeading, sx }) => {
+ const theme = useTheme();
+ return (
+
+ {heading}
+ {subHeading}
+
+ );
+};
+
+StatBox.propTypes = {
+ heading: PropTypes.string.isRequired,
+ subHeading: PropTypes.node.isRequired,
+ sx: PropTypes.object,
+};
+
+export default StatBox;
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index 209dbeaed..bbc6c4305 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -64,7 +64,7 @@ const TeamPanel = () => {
};
fetchTeam();
- }, [user]);
+ }, [authToken]);
useEffect(() => {
let team = members;
@@ -81,8 +81,6 @@ const TeamPanel = () => {
{ id: 1, name: "NAME" },
{ id: 2, name: "EMAIL" },
{ id: 3, name: "ROLE" },
- // FEATURE STILL TO BE IMPLEMENTED
- // { id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
const roles = member.role.map((role) => roleMap[role]).join(",");
@@ -104,45 +102,19 @@ const TeamPanel = () => {
},
{ id: idx + 1, data: member.email },
{
- // TODO - Add select dropdown
id: idx + 2,
data: roles,
},
- // FEATURE STILL TO BE IMPLEMENTED
- // {
- // // TODO - Add delete onClick
- // id: idx + 3,
- // data: (
- //
- //
- //
- // ),
- // },
],
};
}),
};
setTableData(data);
- }, [members, filter]);
+ }, [filter, members, roleMap, theme]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
-
- // RENAME ORGANIZATION
- // const toggleEdit = () => {
- // setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
- // };
- // const handleRename = () => {};
-
- // INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
const handleChange = (event) => {
@@ -226,58 +198,11 @@ const TeamPanel = () => {
},
}}
>
- {/* FEATURE STILL TO BE IMPLEMENTED */}
- {/*
-
- Organization name
-
-
-
- setOrgStates((prev) => ({
- ...prev,
- name: event.target.value,
- }))
- }
- disabled={!orgStates.isEdit}
- sx={{
- color: theme.palette.otherColors.bluishGray,
- "& .Mui-disabled": {
- WebkitTextFillColor: "initial !important",
- },
- "& .Mui-disabled fieldset": {
- borderColor: "transparent !important",
- },
- }}
- inputProps={{
- sx: { textAlign: "end", padding: theme.spacing(4) },
- }}
- />
- : ""}
- onClick={() => toggleEdit()}
- sx={{
- minWidth: 0,
- paddingX: theme.spacing(4),
- ml: orgStates.isEdit ? theme.spacing(4) : 0,
- }}
- />
-
-
- */}
Team members
{
paginated={false}
reversed={true}
table={"team"}
+ emptyMessage={"There are no team members with this role"}
/>
@@ -341,7 +267,7 @@ const TeamPanel = () => {
theme={theme}
>
{
+ const theme = useTheme();
+
+ return (
+
+ );
+};
+
+export default SunAndMoonIcon;
diff --git a/Client/src/Components/ThemeSwitch/index.css b/Client/src/Components/ThemeSwitch/index.css
new file mode 100644
index 000000000..d5db1c1ca
--- /dev/null
+++ b/Client/src/Components/ThemeSwitch/index.css
@@ -0,0 +1,64 @@
+.sun-and-moon > :is(.moon, .sun, .sun-beams) {
+ transform-origin: center;
+}
+
+.theme-toggle .sun-and-moon > .sun-beams {
+ stroke-width: 2px;
+}
+
+.theme-dark .sun-and-moon > .sun {
+ transform: scale(1.75);
+}
+
+.theme-dark .sun-and-moon > .sun-beams {
+ opacity: 0;
+}
+
+.theme-dark .sun-and-moon > .moon > circle {
+ transform: translateX(-7px);
+}
+
+@supports (cx: 1) {
+ .theme-dark .sun-and-moon > .moon > circle {
+ cx: 17;
+ transform: translateX(0);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .sun-and-moon > .sun {
+ transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
+ }
+
+ .sun-and-moon > .sun-beams {
+ transition:
+ transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
+ opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
+ }
+
+ .sun-and-moon .moon > circle {
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ @supports (cx: 1) {
+ .sun-and-moon .moon > circle {
+ transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ }
+
+ .theme-dark .sun-and-moon > .sun {
+ transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
+ transition-duration: 0.25s;
+ transform: scale(1.75);
+ }
+
+ .theme-dark .sun-and-moon > .sun-beams {
+ transition-duration: 0.15s;
+ transform: rotateZ(-25deg);
+ }
+
+ .theme-dark .sun-and-moon > .moon > circle {
+ transition-duration: 0.5s;
+ transition-delay: 0.25s;
+ }
+}
diff --git a/Client/src/Components/ThemeSwitch/index.jsx b/Client/src/Components/ThemeSwitch/index.jsx
new file mode 100644
index 000000000..b751f542a
--- /dev/null
+++ b/Client/src/Components/ThemeSwitch/index.jsx
@@ -0,0 +1,47 @@
+/**
+ * ThemeSwitch Component
+ * Dark and Light Theme Switch
+ * Original Code: https://web.dev/patterns/theming/theme-switch
+ * License: Apache License 2.0
+ * Copyright © Google LLC
+ *
+ * This code has been adapted for use in this project.
+ * Apache License: https://www.apache.org/licenses/LICENSE-2.0
+ */
+
+import { IconButton } from "@mui/material";
+import SunAndMoonIcon from "./SunAndMoonIcon";
+import { useDispatch, useSelector } from "react-redux";
+import { setMode } from "../../Features/UI/uiSlice";
+import "./index.css";
+
+const ThemeSwitch = ({ width = 48, height = 48 }) => {
+ const mode = useSelector((state) => state.ui.mode);
+ const dispatch = useDispatch();
+
+ const toggleTheme = () => {
+ dispatch(setMode(mode === "light" ? "dark" : "light"));
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default ThemeSwitch;
diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx
index e3a019f98..69e081663 100644
--- a/Client/src/Pages/Account/index.jsx
+++ b/Client/src/Pages/Account/index.jsx
@@ -92,6 +92,7 @@ const Account = ({ open = "profile" }) => {
paddingY: theme.spacing(4),
fontWeight: 400,
marginRight: theme.spacing(8),
+ borderBottom: "2px solid transparent",
"&:focus": {
borderBottom: `2px solid ${theme.palette.border.light}`,
},
diff --git a/Client/src/Pages/AdvancedSettings/index.css b/Client/src/Pages/AdvancedSettings/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx
deleted file mode 100644
index 433829f28..000000000
--- a/Client/src/Pages/AdvancedSettings/index.jsx
+++ /dev/null
@@ -1,361 +0,0 @@
-import { useTheme } from "@emotion/react";
-import { Box, Stack, Typography } from "@mui/material";
-import TextInput from "../../Components/Inputs/TextInput";
-import Link from "../../Components/Link";
-import "./index.css";
-import { useDispatch, useSelector } from "react-redux";
-import { createToast } from "../../Utils/toastUtils";
-import PropTypes from "prop-types";
-import LoadingButton from "@mui/lab/LoadingButton";
-import { ConfigBox } from "../Settings/styled";
-import { useNavigate } from "react-router";
-import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
-import { useState, useEffect } from "react";
-import Select from "../../Components/Inputs/Select";
-import { advancedSettingsValidation } from "../../Validation/validation";
-import { buildErrors, hasValidationErrors } from "../../Validation/error";
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
-
-const AdvancedSettings = () => {
- const navigate = useNavigate();
- const isAdmin = useIsAdmin();
- useEffect(() => {
- if (!isAdmin) {
- navigate("/");
- }
- }, [navigate, isAdmin]);
- const [errors, setErrors] = useState({});
- const theme = useTheme();
- const { authToken } = useSelector((state) => state.auth);
- const dispatch = useDispatch();
- const settings = useSelector((state) => state.settings);
- const [localSettings, setLocalSettings] = useState({
- apiBaseUrl: "",
- logLevel: "debug",
- systemEmailHost: "",
- systemEmailPort: "",
- systemEmailAddress: "",
- systemEmailPassword: "",
- jwtTTLNum: 99,
- jwtTTLUnits: "days",
- jwtTTL: "99d",
- dbType: "",
- redisHost: "",
- redisPort: "",
- pagespeedApiKey: "",
- });
-
- const parseJWTTTL = (data) => {
- if (data.jwtTTL) {
- const len = data.jwtTTL.length;
- data.jwtTTLNum = data.jwtTTL.substring(0, len - 1);
- data.jwtTTLUnits = unitItems.filter(
- (itm) => itm._id == data.jwtTTL.substring(len - 1)
- )[0].name;
- }
- };
-
- useEffect(() => {
- const getSettings = async () => {
- const action = await dispatch(getAppSettings({ authToken }));
- if (action.payload.success) {
- parseJWTTTL(action.payload.data);
- setLocalSettings(action.payload.data);
- } else {
- createToast({ body: "Failed to get settings" });
- }
- };
- getSettings();
- }, [authToken, dispatch]);
-
- const logItems = [
- { _id: 1, name: "none" },
- { _id: 2, name: "debug" },
- { _id: 3, name: "error" },
- { _id: 4, name: "warn" },
- ];
-
- const logItemLookup = {
- none: 1,
- debug: 2,
- error: 3,
- warn: 4,
- };
-
- const unitItemLookup = {
- days: "d",
- hours: "h",
- };
- const unitItems = Object.keys(unitItemLookup).map((key) => ({
- _id: unitItemLookup[key],
- name: key,
- }));
-
- const handleLogLevel = (e) => {
- const id = e.target.value;
- const newLogLevel = logItems.find((item) => item._id === id).name;
- setLocalSettings({ ...localSettings, logLevel: newLogLevel });
- };
-
- const handleJWTTTLUnits = (e) => {
- const id = e.target.value;
- const newUnits = unitItems.find((item) => item._id === id).name;
- setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits });
- };
-
- const handleBlur = (event) => {
- const { value, id } = event.target;
- const { error } = advancedSettingsValidation.validate(
- { [id]: value },
- {
- abortEarly: false,
- }
- );
- setErrors((prev) => {
- return buildErrors(prev, id, error);
- });
- };
- const handleChange = (event) => {
- const { value, id } = event.target;
- setLocalSettings({ ...localSettings, [id]: value });
- };
-
- const handleSave = async () => {
- localSettings.jwtTTL =
- localSettings.jwtTTLNum + unitItemLookup[localSettings.jwtTTLUnits];
- if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) {
- return;
- }
- const action = await dispatch(
- updateAppSettings({ settings: localSettings, authToken })
- );
- let body = "";
- if (action.payload.success) {
- parseJWTTTL(action.payload.data);
- setLocalSettings(action.payload.data);
- body = "Settings saved successfully";
- } else {
- body = "Failed to save settings";
- }
- createToast({ body });
- };
-
- return (
-
-
-
-
- Client settings
-
- Modify client settings here.
-
-
-
-
-
-
-
-
-
- Email settings
-
- Set your host email settings here. These settings are used for sending
- system emails.
-
-
-
-
-
-
-
-
-
-
-
- Server settings
-
- Modify server settings here.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- About
-
-
- BlueWave Uptime v1.0.0
-
- Developed by Bluewave Labs.
-
-
-
-
-
-
- Save
-
-
-
-
- );
-};
-
-AdvancedSettings.propTypes = {
- isAdmin: PropTypes.bool,
-};
-export default AdvancedSettings;
diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx
index b36cca7b0..70acbe72d 100644
--- a/Client/src/Pages/Auth/CheckEmail.jsx
+++ b/Client/src/Pages/Auth/CheckEmail.jsx
@@ -5,10 +5,10 @@ import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
-import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import EmailIcon from "../../assets/icons/email.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
+import IconBox from "../../Components/IconBox";
import "./index.css";
const CheckEmail = () => {
@@ -144,9 +144,22 @@ const CheckEmail = () => {
textAlign="center"
>
-
-
-
+
+
+
+
+
Check your email
We sent a password reset link to{" "}
diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx
index 1fd5f1b2f..7fcc19531 100644
--- a/Client/src/Pages/Auth/ForgotPassword.jsx
+++ b/Client/src/Pages/Auth/ForgotPassword.jsx
@@ -6,12 +6,12 @@ import { forgotPassword } from "../../Features/Auth/authSlice";
import { useEffect, useState } from "react";
import { credentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
-import { IconBox } from "./styled";
import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import LoadingButton from "@mui/lab/LoadingButton";
+import IconBox from "../../Components/IconBox";
import "./index.css";
const ForgotPassword = () => {
@@ -146,9 +146,22 @@ const ForgotPassword = () => {
textAlign="center"
>
-
-
-
+
+
+
+
+
Forgot password?
No worries, we'll send you reset instructions.
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
deleted file mode 100644
index a7554a7c1..000000000
--- a/Client/src/Pages/Auth/Login.jsx
+++ /dev/null
@@ -1,562 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { credentials } from "../../Validation/validation";
-import { login } from "../../Features/Auth/authSlice";
-import LoadingButton from "@mui/lab/LoadingButton";
-import { useDispatch, useSelector } from "react-redux";
-import { createToast } from "../../Utils/toastUtils";
-import { networkService } from "../../main";
-import TextInput from "../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
-import Background from "../../assets/Images/background-grid.svg?react";
-import Logo from "../../assets/icons/bwu-icon.svg?react";
-import Mail from "../../assets/icons/mail.svg?react";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import PropTypes from "prop-types";
-import { logger } from "../../Utils/Logger";
-import "./index.css";
-const DEMO = import.meta.env.VITE_APP_DEMO;
-
-/**
- * Displays the initial landing page.
- *
- * @param {Object} props
- * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
- * @returns {JSX.Element}
- */
-const LandingPage = ({ onContinue }) => {
- const theme = useTheme();
-
- return (
- <>
-
-
- Log In
- We are pleased to see you again!
-
-
-
-
-
-
- By continuing, you agree to our{" "}
- {
- window.open(
- "https://bluewavelabs.ca/terms-of-service-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- sx={{
- "&:hover": {
- color: theme.palette.text.tertiary,
- },
- }}
- >
- Terms of Service
- {" "}
- and{" "}
- {
- window.open(
- "https://bluewavelabs.ca/privacy-policy-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- sx={{
- "&:hover": {
- color: theme.palette.text.tertiary,
- },
- }}
- >
- Privacy Policy.
-
-
-
-
- >
- );
-};
-
-LandingPage.propTypes = {
- onContinue: PropTypes.func.isRequired,
-};
-
-/**
- * Renders the first step of the login process.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- Log In
- Enter your email address
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email ? true : false}
- helperText={errors.email}
- ref={inputRef}
- />
-
-
-
-
-
-
- >
- );
-};
-
-StepOne.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-/**
- * Renders the second step of the login process, including a password input field.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const inputRef = useRef(null);
- const authState = useSelector((state) => state.auth);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- const handleNavigate = () => {
- if (form.email !== "" && !errors.email) {
- sessionStorage.setItem("email", form.email);
- }
- navigate("/forgot-password");
- };
-
- return (
- <>
-
-
- Log In
- Enter your password
-
-
- }
- />
-
-
-
- Continue
-
-
-
-
-
- Forgot password?
-
-
- Reset password
-
-
-
- >
- );
-};
-
-StepTwo.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-const Login = () => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
-
- const authState = useSelector((state) => state.auth);
- const { authToken } = authState;
-
- const idMap = {
- "login-email-input": "email",
- "login-password-input": "password",
- };
-
- const [form, setForm] = useState({
- email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
- password: DEMO !== undefined ? "Demouser1!" : "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- if (authToken) {
- navigate("/uptime");
- return;
- }
- networkService
- .doesSuperAdminExist()
- .then((response) => {
- if (response.data.data === false) {
- navigate("/register");
- }
- })
- .catch((error) => {
- logger.error(error);
- });
- }, [authToken, navigate]);
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- setForm((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
- const handleSubmit = async (event) => {
- event.preventDefault();
-
- if (step === 1) {
- const { error } = credentials.validate(
- { email: form.email },
- { abortEarly: false }
- );
- if (error) {
- setErrors((prev) => ({ ...prev, email: error.details[0].message }));
- createToast({ body: error.details[0].message });
- } else {
- setStep(2);
- }
- } else if (step === 2) {
- const { error } = credentials.validate(form, { abortEarly: false });
-
- if (error) {
- // validation errors
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- createToast({
- body:
- error.details && error.details.length > 0
- ? error.details[0].message
- : "Error validating data.",
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/uptime");
- createToast({
- body: "Welcome back! You're successfully logged in.",
- });
- } else {
- if (action.payload) {
- if (action.payload.msg === "Incorrect password")
- setErrors({
- password: "The password you provided does not match our records",
- });
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
- }
- }
- }
- };
-
- return (
-
-
-
-
-
-
- BlueWave Uptime
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.border.light,
- backgroundColor: theme.palette.background.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
- {step === 0 ? (
- setStep(1)} />
- ) : step === 1 ? (
- setStep(0)}
- />
- ) : (
- step === 2 && (
- setStep(1)}
- />
- )
- )}
-
-
- );
-};
-
-export default Login;
diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
index f78193186..ac796e627 100644
--- a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
+++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
@@ -1,4 +1,4 @@
-import { useRef, useEffect } from "react";
+import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import TextInput from "../../../../Components/Inputs/TextInput";
@@ -30,6 +30,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
Log In
@@ -92,4 +93,4 @@ EmailStep.propTypes = {
onChange: PropTypes.func.isRequired,
};
-export default EmailStep;
\ No newline at end of file
+export default EmailStep;
diff --git a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
new file mode 100644
index 000000000..0a5fa8153
--- /dev/null
+++ b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
@@ -0,0 +1,43 @@
+import { Box, Typography, useTheme } from "@mui/material";
+import PropTypes from "prop-types";
+import { useNavigate } from "react-router";
+
+const ForgotPasswordLabel = ({ email, errorEmail }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ const handleNavigate = () => {
+ if (email !== "" && !errorEmail) {
+ sessionStorage.setItem("email", email);
+ }
+ navigate("/forgot-password");
+ };
+
+ return (
+
+
+ Forgot password?
+
+
+ Reset password
+
+
+ );
+};
+
+ForgotPasswordLabel.proptype = {
+ email: PropTypes.string.isRequired,
+ emailError: PropTypes.string.isRequired,
+};
+
+export default ForgotPasswordLabel;
diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
index 34df4959d..b7384ba98 100644
--- a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
+++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
@@ -1,5 +1,4 @@
import { useRef, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -22,7 +21,6 @@ import PropTypes from "prop-types";
*/
const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
- const navigate = useNavigate();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
@@ -32,13 +30,6 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
}
}, []);
- const handleNavigate = () => {
- if (form.email !== "" && !errors.email) {
- sessionStorage.setItem("email", form.email);
- }
- navigate("/forgot-password");
- };
-
return (
<>
{
-
-
- Forgot password?
-
-
- Reset password
-
-
>
);
@@ -156,4 +121,4 @@ PasswordStep.propTypes = {
onBack: PropTypes.func.isRequired,
};
-export default PasswordStep
\ No newline at end of file
+export default PasswordStep;
diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx
index 1b3a420ea..890c87eb7 100644
--- a/Client/src/Pages/Auth/Login/Login.jsx
+++ b/Client/src/Pages/Auth/Login/Login.jsx
@@ -11,8 +11,10 @@ import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/bwu-icon.svg?react";
import { logger } from "../../../Utils/Logger";
import "../index.css";
-import EmailStep from "./Components/EmailStep";
-import PasswordStep from "./Components/PasswordStep";
+import EmailStep from "./Components/EmailStep";
+import PasswordStep from "./Components/PasswordStep";
+import ThemeSwitch from "../../../Components/ThemeSwitch";
+import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -42,7 +44,7 @@ const Login = () => {
useEffect(() => {
if (authToken) {
- navigate("/monitors");
+ navigate("/uptime");
return;
}
networkService
@@ -108,7 +110,7 @@ const Login = () => {
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
- navigate("/monitors");
+ navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
});
@@ -174,6 +176,7 @@ const Login = () => {
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
+ rowGap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
@@ -205,6 +208,13 @@ const Login = () => {
/>
)
)}
+
+
+
+
);
diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
index c380425c9..2c4ef0106 100644
--- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -4,10 +4,10 @@ import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
+import IconBox from "../../Components/IconBox";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -80,9 +80,22 @@ const NewPasswordConfirmed = () => {
textAlign="center"
>
-
-
-
+
+
+
+
+
Password reset
Your password has been successfully reset. Click below to log in magically.
@@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => {