diff --git a/Client/package-lock.json b/Client/package-lock.json index 2a7b9b7e1..44e276f63 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -18,7 +18,7 @@ "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "@reduxjs/toolkit": "2.2.5", - "axios": "1.7.2", + "axios": "1.7.4", "chart.js": "^4.4.3", "dayjs": "1.11.11", "joi": "17.13.1", @@ -29,7 +29,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.12.7", + "recharts": "2.13.0-alpha.4", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, @@ -2655,9 +2655,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5509,15 +5509,15 @@ } }, "node_modules/recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.13.0-alpha.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz", + "integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -5540,12 +5540,6 @@ "decimal.js-light": "^2.4.1" } }, - "node_modules/recharts/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", diff --git a/Client/package.json b/Client/package.json index f711bff9a..769e1f4a6 100644 --- a/Client/package.json +++ b/Client/package.json @@ -20,7 +20,7 @@ "@mui/x-data-grid": "7.3.2", "@mui/x-date-pickers": "7.3.2", "@reduxjs/toolkit": "2.2.5", - "axios": "1.7.2", + "axios": "1.7.4", "chart.js": "^4.4.3", "dayjs": "1.11.11", "joi": "17.13.1", @@ -31,7 +31,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.12.7", + "recharts": "2.13.0-alpha.4", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx index 4f4351508..2a9f70a08 100644 --- a/Client/src/Components/Avatar/index.jsx +++ b/Client/src/Components/Avatar/index.jsx @@ -3,6 +3,27 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useEffect, useState } from "react"; +/** + * Generates a color based on the input string. + * @param {string} string - The input string to generate the color from. + * @returns {string} + */ +const stringToColor = (string) => { + let hash = 0; + let i; + for (i = 0; i < string.length; i += 1) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = "#"; + for (i = 0; i < 3; i += 1) { + const value = (hash >> (i * 8)) & 0xff; + color += `00${value.toString(16)}`.slice(-2); + } + + return color; +}; + /** * @component * @param {Object} props @@ -18,7 +39,7 @@ import { useEffect, useState } from "react"; const Avatar = ({ src, small, sx }) => { const { user } = useSelector((state) => state.auth); - const style = small ? { width: 25, height: 25 } : { width: 64, height: 64 }; + const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; const border = small ? 1 : 3; const [image, setImage] = useState(); @@ -35,8 +56,9 @@ const Avatar = ({ src, small, sx }) => { src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg" } sx={{ - fontSize: small ? "13px" : "22px", + fontSize: small ? "16px" : "22px", fontWeight: 400, + backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`), display: "inline-flex", "&::before": { content: `""`, @@ -52,7 +74,8 @@ const Avatar = ({ src, small, sx }) => { ...sx, }} > - {user.firstName?.charAt(0)}{user.lastName?.charAt(0)} + {user.firstName?.charAt(0)} + {user.lastName?.charAt(0)} ); }; diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css index fd1f75b53..14adc440c 100644 --- a/Client/src/Components/BasicTable/index.css +++ b/Client/src/Components/BasicTable/index.css @@ -72,7 +72,7 @@ } .MuiPaper-root + .MuiPagination-root { - margin-top: 35px; + margin-top: 24px; border: solid 1px var(--env-var-color-16); border-radius: var(--env-var-radius-1); padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2); diff --git a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx new file mode 100644 index 000000000..f7fc0d3d8 --- /dev/null +++ b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx @@ -0,0 +1,50 @@ +import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts"; + +import PropTypes from "prop-types"; + +const MonitorDetails60MinChart = ({ data }) => { + const labelStyle = { + fontSize: "10px", + fill: "#475467", + }; + + const color = { + true: "var(--env-var-color-23)", + false: "var(--env-var-color-24)", + undefined: "var(--env-var-color-33)", + }; + return ( + + + {data.map((check, index) => ( + + ))} + + + + + + + + + ); +}; + +MonitorDetails60MinChart.propTypes = { + data: PropTypes.array.isRequired, +}; + +export default MonitorDetails60MinChart; diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 91657f542..c37e467f6 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -28,15 +28,6 @@ const CustomToolTip = ({ active, payload, label }) => { }; const MonitorDetailsAreaChart = ({ checks }) => { - // SQUASH ERROR, NOT PERMANENT SOLUTION - const error = console.error; - console.error = (...args) => { - if (/defaultProps/.test(args[0])) return; - error(...args); - }; - - // END SQUASH ERROR, NOT PERMANENT SOLUTION - const formatDate = (timestamp) => { const date = new Date(timestamp); return date.toLocaleTimeString("en-US", { diff --git a/Client/src/Components/Check/Check.jsx b/Client/src/Components/Check/Check.jsx index 1b246566f..645e0b5e2 100644 --- a/Client/src/Components/Check/Check.jsx +++ b/Client/src/Components/Check/Check.jsx @@ -46,7 +46,7 @@ const Check = ({ text, variant = "info", outlined = false }) => { }; Check.propTypes = { - text: PropTypes.string.isRequired, + text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, variant: PropTypes.oneOf(["info", "error", "success"]), outlined: PropTypes.bool, }; diff --git a/Client/src/Components/Check/check.css b/Client/src/Components/Check/check.css index cb24be466..f1432d2bc 100644 --- a/Client/src/Components/Check/check.css +++ b/Client/src/Components/Check/check.css @@ -1,4 +1,4 @@ -.check span.MuiTypography-root { +.check > span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); color: var(--env-var-color-2); opacity: 0.8; diff --git a/Client/src/Components/NavBar/index.css b/Client/src/Components/NavBar/index.css deleted file mode 100644 index fcedc53a2..000000000 --- a/Client/src/Components/NavBar/index.css +++ /dev/null @@ -1,34 +0,0 @@ -/* NavBar Component Styles*/ -.MuiToolbar-root { - min-height: var(--env-var-nav-bar-height) !important; -} - -.icon-button-toggle-title { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); -} - -.icon-button-toggle-pic { - width: 10px; - height: 5px; -} - -#icon-button { - -webkit-transition: none; - transition: none; - outline: none; - box-shadow: none; - background-color: transparent; -} - -#menu-appbar svg { - width: 16px; - height: 16px; - color: var(--env-var-color-25); -} - -#bw-uptime-logo-dashboard { - width: fit-content; - height: 16px; - margin-left: 15px; -} diff --git a/Client/src/Components/NavBar/index.jsx b/Client/src/Components/NavBar/index.jsx deleted file mode 100644 index 95123d975..000000000 --- a/Client/src/Components/NavBar/index.jsx +++ /dev/null @@ -1,203 +0,0 @@ -import "./index.css"; -import { cloneElement, useState } from "react"; -import AppBar from "@mui/material/AppBar"; -import Box from "@mui/material/Box"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import Typography from "@mui/material/Typography"; -import Menu from "@mui/material/Menu"; -import Avatar from "../Avatar"; -import Tooltip from "@mui/material/Tooltip"; -import MenuItem from "@mui/material/MenuItem"; -import { useTheme } from "@mui/material/styles"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import { clearAuthState } from "../../Features/Auth/authSlice"; -import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; - -import { useDispatch, useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import LockSvg from "../../assets/icons/lock.svg?react"; -import UserSvg from "../../assets/icons/user.svg?react"; -import TeamSvg from "../../assets/icons/user-two.svg?react"; -import LogoutSvg from "../../assets/icons/logout.svg?react"; -import { Stack, useScrollTrigger } from "@mui/material"; -import axiosIntance from "../../Utils/axiosConfig"; -import axios from "axios"; - -const icons = { - Profile: , - Team: , - Password: , - Logout: , -}; - -function AddBorderOnScroll(props) { - const { children, window } = props; - const trigger = useScrollTrigger({ - target: window ? window() : undefined, - disableHysteresis: true, - threshold: 0, - }); - - return ( - - {children} - - ); -} - -/** - * NavBar component - * - * A responsive navigation bar component with a user menu. - * - * @component - * @example - * return ( - * - * ) - */ -function NavBar() { - const theme = useTheme(); - const [anchorElUser, setAnchorElUser] = useState(null); - const dispatch = useDispatch(); - const navigate = useNavigate(); - const authState = useSelector((state) => state.auth); - - // Initialize settings and update based on user role - let settings = ["Profile", "Password", "Team", "Logout"]; - if (authState.user?.role && !authState.user.role.includes("admin")) { - settings = ["Profile", "Password", "Logout"]; - } - - /** - * Handles opening the user menu. - * - * @param {React.MouseEvent} event - The event triggered by clicking the user menu button. - */ - const handleOpenUserMenu = (event) => { - setAnchorElUser(event.currentTarget); - }; - - /** - * Handles logging out the user - * - */ - const logout = async () => { - // Clear auth state - dispatch(clearAuthState()); - dispatch(clearUptimeMonitorState()); - // Make request to BE to remove JWT from user - await axiosIntance.post( - "/auth/logout", - { email: authState.user.email }, - { - headers: { - Authorization: `Bearer ${authState.authToken}`, - "Content-Type": "application/json", - }, - } - ); - navigate("/login"); - }; - - /** - * Handles closing the user menu. - */ - const handleCloseUserMenu = (setting) => { - setAnchorElUser(null); - switch (setting) { - case "Profile": - navigate("/account/profile"); - break; - case "Team": - navigate("/account/team"); - break; - case "Password": - navigate("/account/password"); - break; - case "Logout": - logout(); - break; - default: - break; - } - }; - - return ( - - - - - - - - {authState.user?.firstName} {authState.user?.lastName} - - - - - - - {settings.map((setting) => ( - handleCloseUserMenu(setting)} - sx={{ width: "150px" }} - > - {icons[setting]} - - {setting} - - - ))} - - - - ); -} - -export default NavBar; diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css index 6fa9d2134..4840043c3 100644 --- a/Client/src/Components/Sidebar/index.css +++ b/Client/src/Components/Sidebar/index.css @@ -18,11 +18,14 @@ aside span.MuiTypography-root { color: var(--env-var-color-5); line-height: 1; } -aside .MuiStack-root:nth-last-child(2) { +aside .MuiStack-root:nth-last-child(3) { margin-top: auto; +} +aside .MuiStack-root:last-child, +aside .MuiStack-root:nth-last-child(3) { position: relative; } -aside .MuiStack-root:nth-last-child(2):before { +aside .MuiStack-root:last-child:before { content: ""; position: absolute; top: -10px; @@ -31,5 +34,35 @@ aside .MuiStack-root:nth-last-child(2):before { border-top: solid 1px var(--env-var-color-6); } aside .MuiStack-root:last-child { - margin-bottom: 10px; + margin-top: 15px; +} + +.sidebar-menu { + margin-top: -20px; +} +.sidebar-menu .MuiPaper-root { + box-shadow: var(--env-var-shadow-1); + border: solid 1px var(--env-var-color-6); + border-radius: var(--env-var-radius-1); + gap: 1px; +} +.sidebar-menu .MuiList-root { + min-width: 100px; + width: 150px; + padding-bottom: 0; +} +.sidebar-menu li.MuiButtonBase-root:last-child { + border-top: solid 1px var(--env-var-color-6); + padding: 12px 16px; +} +.sidebar-menu li.MuiButtonBase-root { + min-height: fit-content; +} +.sidebar-menu .MuiList-root svg { + width: 16px; + height: 16px; +} +.sidebar-menu span { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-2); } diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 3b2a32394..5a5b1d980 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -1,7 +1,17 @@ -import { Stack, Typography } from "@mui/material"; +import { useState } from "react"; +import { Menu, MenuItem, Stack, Tooltip, Typography } from "@mui/material"; import { useLocation, useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; - +import { useDispatch, useSelector } from "react-redux"; +import { clearAuthState } from "../../Features/Auth/authSlice"; +import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { createToast } from "../../Utils/toastUtils"; +import axiosInstance from "../../Utils/axiosConfig"; +import Avatar from "../Avatar"; +import LockSvg from "../../assets/icons/lock.svg?react"; +import UserSvg from "../../assets/icons/user.svg?react"; +import TeamSvg from "../../assets/icons/user-two.svg?react"; +import LogoutSvg from "../../assets/icons/logout.svg?react"; import BWULogo from "../../assets/Images/bwl-logo.svg?react"; import Support from "../../assets/icons/support.svg?react"; import StatusPages from "../../assets/icons/status-pages.svg?react"; @@ -11,16 +21,10 @@ import Incidents from "../../assets/icons/incidents.svg?react"; import Integrations from "../../assets/icons/integrations.svg?react"; import PageSpeed from "../../assets/icons/page-speed.svg?react"; import Settings from "../../assets/icons/settings.svg?react"; +import Arrow from "../../assets/icons/down-arrow.svg?react"; import "./index.css"; -/** - * @component - * Sidebar component serves as a sidebar containing a menu. - * - * @returns {JSX.Element} The JSX element representing the Sidebar component. - */ - const menu = [ { name: "Monitors", path: "monitors", icon: }, { name: "Incidents", path: "incidents", icon: }, @@ -32,10 +36,94 @@ const menu = [ { name: "Settings", path: "settings", icon: }, ]; +const icons = { + Profile: , + Team: , + Password: , + Logout: , +}; + +/** + * @component + * Sidebar component serves as a sidebar containing a menu. + * + * @returns {JSX.Element} The JSX element representing the Sidebar component. + */ + function Sidebar() { const theme = useTheme(); const navigate = useNavigate(); const location = useLocation(); + const dispatch = useDispatch(); + const [anchorElUser, setAnchorElUser] = useState(null); + const authState = useSelector((state) => state.auth); + + // Initialize settings and update based on user role + let settings = ["Profile", "Password", "Team", "Logout"]; + if (authState.user?.role && !authState.user.role.includes("admin")) { + settings = ["Profile", "Password", "Logout"]; + } + + /** + * Handles opening the user menu. + * + * @param {React.MouseEvent} event - The event triggered by clicking the user menu button. + */ + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + /** + * Handles logging out the user + * + */ + const logout = async () => { + try { + // Make request to BE to remove JWT from user + await axiosInstance.post( + "/auth/logout", + { email: authState.user.email }, + { + headers: { + Authorization: `Bearer ${authState.authToken}`, + "Content-Type": "application/json", + }, + } + ); + + // Clear auth state + dispatch(clearAuthState()); + dispatch(clearUptimeMonitorState()); + navigate("/login"); + } catch (error) { + createToast({ + body: error.message, + }); + } + }; + + /** + * Handles closing the user menu. + */ + const handleCloseUserMenu = (setting) => { + setAnchorElUser(null); + switch (setting) { + case "Profile": + navigate("/account/profile"); + break; + case "Team": + navigate("/account/team"); + break; + case "Password": + navigate("/account/password"); + break; + case "Logout": + logout(); + break; + default: + break; + } + }; return ( @@ -48,6 +136,8 @@ function Sidebar() { key={item.path} direction="row" alignItems="center" + py={theme.gap.small} + px={theme.gap.medium} gap={theme.gap.small} borderRadius={`${theme.shape.borderRadius}px`} onClick={() => @@ -59,12 +149,66 @@ function Sidebar() { ) : navigate(`/${item.path}`) } - sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }} > {item.icon} {item.name} ))} + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + + + + {settings.map((setting) => ( + handleCloseUserMenu(setting)}> + {icons[setting]} + + {setting} + + + ))} + ); } diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index a30c295d9..6da315fc4 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -20,7 +20,11 @@ export const register = createAsyncThunk( if (error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -33,7 +37,11 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => { if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } }); @@ -72,7 +80,11 @@ export const update = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -91,7 +103,11 @@ export const deleteUser = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -106,7 +122,11 @@ export const forgotPassword = createAsyncThunk( if (error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -128,7 +148,11 @@ export const setNewPassword = createAsyncThunk( if (error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 4377edb35..6af825a37 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -25,7 +25,11 @@ export const createPageSpeed = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -40,7 +44,11 @@ export const getPageSpeedMonitors = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -63,7 +71,11 @@ export const getPageSpeedByUserId = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -94,7 +106,11 @@ export const updatePageSpeed = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -115,7 +131,11 @@ export const deletePageSpeed = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index e69e6d41d..27d616e1e 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -25,7 +25,11 @@ export const createUptimeMonitor = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -40,7 +44,11 @@ export const getUptimeMonitors = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -63,7 +71,11 @@ export const getUptimeMonitorsByUserId = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -94,7 +106,11 @@ export const updateUptimeMonitor = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); @@ -115,7 +131,11 @@ export const deleteUptimeMonitor = createAsyncThunk( if (error.response && error.response.data) { return thunkApi.rejectWithValue(error.response.data); } - return thunkApi.rejectWithValue(error.message); + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } } ); diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index 048700db6..a62016a60 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -1,55 +1,34 @@ -.home-layout { - display: grid; - position: relative; - grid-template-areas: - "aside header" - "aside main"; - grid-template-columns: var(--env-var-side-bar-width) auto; - grid-template-rows: var(--env-var-nav-bar-height) auto; +#root:has(.home-layout) { background-color: var(--env-var-color-30); +} + +.home-layout { + display: flex; + position: relative; + gap: var(--env-var-spacing-2); min-height: 100vh; + max-width: 1400px; + margin: 0 auto; + padding: var(--env-var-spacing-2); } .home-layout aside { - grid-area: aside; - position: fixed; - top: 0; - height: 100%; - width: var(--env-var-side-bar-width); + position: sticky; + top: var(--env-var-spacing-2); + left: 0; + + height: calc(100vh - var(--env-var-spacing-2) * 2); + max-width: var(--env-var-side-bar-width); + flex: 1; + + border: 1px solid var(--color-border-0); + border-radius: var(--env-var-radius-1); background-color: var(--env-var-color-8); - border-right: 1px solid var(--color-border-0); + padding: var(--env-var-spacing-1) var(--env-var-spacing-1-plus); } -.home-layout header { - grid-area: header; - background-color: var(--env-var-color-30); -} + .home-layout > div { - height: 100%; - grid-area: main; - padding: var(--env-var-spacing-2) calc(var(--env-var-spacing-4) * 2); - padding-bottom: var(--env-var-spacing-4); - max-width: 100%; -} -.home-layout > div:not(:has([class*="fallback"])) { - max-width: 1500px; -} - -@media (max-width: 600px) { - .home-layout { - grid-template-areas: - "header header" - "main main"; - grid-template-columns: 1fr; - grid-template-rows: 64px auto; - } - - aside { - display: none; - visibility: hidden; - } - - .home-layout > div { - padding: var(--env-var-spacing-1); - } + min-height: calc(100vh - var(--env-var-spacing-2) * 2); + flex: 1; } diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index 007fb7d05..81cb2fd9c 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,4 +1,3 @@ -import NavBar from "../../Components/NavBar"; import Sidebar from "../../Components/Sidebar"; import { Outlet } from "react-router"; import { Box } from "@mui/material"; @@ -9,7 +8,6 @@ const HomeLayout = () => { return ( - ); diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index db08ec3a2..82f37a969 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -29,7 +29,7 @@ const Account = ({ open = "profile" }) => { if (!user.role.includes("admin")) tabList = ["Profile", "Password"]; return ( - + { const theme = useTheme(); @@ -79,28 +79,71 @@ const CheckEmail = () => { } }; + const handleNavigate = () => { + sessionStorage.removeItem("email"); + navigate("/login"); + }; + return ( -
- + -
- - - - Check your email - - - We sent a password reset link to{" "} - - {email || "username@email.com"} + + + BlueWave Uptime + + .MuiStack-root": { + border: 1, + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.otherColors.graishWhite, + backgroundColor: theme.palette.otherColors.white, + padding: { + xs: theme.gap.large, + sm: theme.gap.xl, + }, + }, + }} + > + + + + Check your email + + We sent a password reset link to{" "} + + {email || "username@email.com"} + - - - -
+ + + Go back to — + + Log In + + + ); }; diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx index 86db72076..c51417c91 100644 --- a/Client/src/Pages/Auth/ForgotPassword.jsx +++ b/Client/src/Pages/Auth/ForgotPassword.jsx @@ -1,18 +1,17 @@ -import "./index.css"; -import background from "../../assets/Images/background_pattern_decorative.png"; -import Logomark from "../../assets/icons/key.svg?react"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import { useState } from "react"; -import { credentials } from "../../Validation/validation"; -import { useNavigate } from "react-router-dom"; -import Field from "../../Components/Inputs/Field"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; import { createToast } from "../../Utils/toastUtils"; import { useDispatch, useSelector } from "react-redux"; import { forgotPassword } from "../../Features/Auth/authSlice"; +import { useEffect, useState } from "react"; +import { credentials } from "../../Validation/validation"; +import { useNavigate } from "react-router-dom"; +import Field from "../../Components/Inputs/Field"; import ButtonSpinner from "../../Components/ButtonSpinner"; -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Button from "../../Components/Button"; +import Logo from "../../assets/icons/bwu-icon.svg?react"; +import Key from "../../assets/icons/key.svg?react"; +import background from "../../assets/Images/background_pattern_decorative.png"; +import "./index.css"; const ForgotPassword = () => { const navigate = useNavigate(); @@ -25,9 +24,10 @@ const ForgotPassword = () => { email: "", }); - const idMap = { - "forgot-password-email-input": "email", - }; + useEffect(() => { + const email = sessionStorage.getItem("email"); + email && setForm({ email: sessionStorage.getItem("email") }); + }, []); const handleSubmit = async (event) => { event.preventDefault(); @@ -40,11 +40,9 @@ const ForgotPassword = () => { error.details && error.details.length > 0 ? error.details[0].message : "Error validating data."; - setErrors(err); + setErrors({ email: err }); createToast({ - variant: "info", body: err, - hasIcon: false, }); } else { const action = await dispatch(forgotPassword(form)); @@ -71,72 +69,111 @@ const ForgotPassword = () => { }; const handleChange = (event) => { - const { value, id } = event.target; - const name = idMap[id]; - setForm((prev) => ({ - ...prev, - [name]: value, - })); + const { value } = event.target; + setForm({ email: value }); const { error } = credentials.validate( - { [name]: value }, + { email: value }, { abortEarly: false } ); - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); + if (error) setErrors({ email: error.details[0].message }); + else delete errors.email; + }; + + const handleNavigate = () => { + sessionStorage.removeItem("email"); + navigate("/login"); }; return ( -
- + -
- - - - Forgot password? - - - No worries, we'll send you reset instructions. - + + + BlueWave Uptime + + .MuiStack-root": { + border: 1, + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.otherColors.graishWhite, + backgroundColor: theme.palette.otherColors.white, + padding: { + xs: theme.gap.large, + sm: theme.gap.xl, + }, + }, + }} + > + + + + Forgot password? + + No worries, we'll send you reset instructions. + + + + + + + + - - - -
+ + + Go back to — + + Log In + + + ); }; diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index eef972ec4..ad5fcdbce 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -28,7 +28,11 @@ const LandingPage = ({ onContinue }) => { return ( <> - + Log In We are pleased to see you again! @@ -82,7 +86,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Log In Enter your email address @@ -154,9 +158,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { } }, []); + const handleNavigate = () => { + if (form.email !== "" && !errors.email) { + sessionStorage.setItem("email", form.email); + } + navigate("/forgot-password"); + }; + return ( <> - + Log In Enter your password @@ -209,7 +220,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { component="span" ml={theme.gap.xs} sx={{ userSelect: "none" }} - onClick={() => navigate("/forgot-password")} + onClick={handleNavigate} > Reset password @@ -329,10 +340,9 @@ const Login = () => { return ( - background pattern { maxWidth={600} flex={1} justifyContent="center" - p={theme.gap.xl} - pb={theme.gap.triplexl} + px={{ xs: theme.gap.large, lg: theme.gap.xl }} + pb={theme.gap.xl} mx="auto" sx={{ "& > .MuiStack-root": { diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx index f73e5edcf..be6aeb9b5 100644 --- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -1,14 +1,14 @@ -import "./index.css"; -import background from "../../assets/Images/background_pattern_decorative.png"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react"; -import Button from "../../Components/Button"; -import { Stack, Typography } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router"; import { useDispatch } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Button from "../../Components/Button"; +import background from "../../assets/Images/background_pattern_decorative.png"; +import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react"; +import Logo from "../../assets/icons/bwu-icon.svg?react"; +import "./index.css"; const NewPasswordConfirmed = () => { const theme = useTheme(); @@ -22,39 +22,77 @@ const NewPasswordConfirmed = () => { }; return ( -
- + -
- - - - Password reset - - - Your password has been successfully reset. Click below to log in - magically. - - - + + + BlueWave Uptime + + .MuiStack-root": { + border: 1, + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.otherColors.graishWhite, + backgroundColor: theme.palette.otherColors.white, + padding: { + xs: theme.gap.large, + sm: theme.gap.xl, + }, + }, + }} + > + + + + Password reset + + Your password has been successfully reset. Click below to log in + magically. + +
+
+ + Go back to — + + Log In + + +
); }; diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 77b3dd2fc..30b439f16 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -31,7 +31,11 @@ const LandingPage = ({ isAdmin, onSignup }) => { return ( <> - + Sign Up @@ -87,7 +91,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your personal details @@ -107,18 +111,16 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { />
- - - +
@@ -129,7 +131,6 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { img={} onClick={onBack} sx={{ - mb: theme.gap.medium, px: theme.gap.ml, "& svg.MuiSvgIcon-root": { mr: theme.gap.xs, @@ -173,7 +174,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your email address @@ -202,7 +203,6 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { img={} onClick={onBack} sx={{ - mb: theme.gap.medium, px: theme.gap.ml, "& svg.MuiSvgIcon-root": { mr: theme.gap.xs, @@ -246,7 +246,7 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Create your password @@ -267,23 +267,29 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { />
- - - + - + + Must be at least 8 + characters long + + } variant={ errors?.password === "Password is required" ? "error" @@ -295,7 +301,12 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { } /> + Must contain one + special character and a number + + } variant={ errors?.password === "Password is required" ? "error" @@ -309,7 +320,14 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { } /> + + Must contain at least + {" "} + one upper and lower character + + } variant={ errors?.password === "Password is required" ? "error" @@ -330,7 +348,6 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { img={} onClick={onBack} sx={{ - mb: theme.gap.medium, px: theme.gap.ml, "& svg.MuiSvgIcon-root": { mr: theme.gap.xs, @@ -513,10 +530,9 @@ const Register = ({ isAdmin }) => { return ( - background pattern { maxWidth={600} flex={1} justifyContent="center" - p={theme.gap.xl} - pb={theme.gap.triplexl} + px={{ xs: theme.gap.large, lg: theme.gap.xl }} + pb={theme.gap.xl} mx="auto" sx={{ "& > .MuiStack-root": { diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx index ce95fb7a5..fab448ffa 100644 --- a/Client/src/Pages/Auth/SetNewPassword.jsx +++ b/Client/src/Pages/Auth/SetNewPassword.jsx @@ -1,20 +1,19 @@ -import "./index.css"; -import background from "../../assets/Images/background_pattern_decorative.png"; -import LockIcon from "../../assets/icons/lock-button-icon.svg?react"; -import Check from "../../Components/Check/Check"; -import ButtonSpinner from "../../Components/ButtonSpinner"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import { useDispatch, useSelector } from "react-redux"; +import { setNewPassword } from "../../Features/Auth/authSlice"; +import { createToast } from "../../Utils/toastUtils"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; import { useParams } from "react-router-dom"; import { useState } from "react"; import { credentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; +import Check from "../../Components/Check/Check"; +import ButtonSpinner from "../../Components/ButtonSpinner"; import Field from "../../Components/Inputs/Field"; -import { useDispatch, useSelector } from "react-redux"; -import { setNewPassword } from "../../Features/Auth/authSlice"; -import { createToast } from "../../Utils/toastUtils"; -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import Button from "../../Components/Button"; +import LockIcon from "../../assets/icons/lock-button-icon.svg?react"; +import background from "../../assets/Images/background_pattern_decorative.png"; +import Logo from "../../assets/icons/bwu-icon.svg?react"; +import "./index.css"; const SetNewPassword = () => { const navigate = useNavigate(); @@ -105,100 +104,158 @@ const SetNewPassword = () => { }; return ( -
- + -
- - - - Set new password - - - Your new password must be different to previously used passwords. - - - - - - - - - - + + + BlueWave Uptime + + .MuiStack-root": { + border: 1, + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.otherColors.graishWhite, + backgroundColor: theme.palette.otherColors.white, + padding: { + xs: theme.gap.large, + sm: theme.gap.xl, + }, + }, + }} + > + + + + Set new password + + Your new password must be different to previously used passwords. + + + + + + +
+ + + + + Must be at least 8 + characters long + + } + variant={ + errors?.password === "Password is required" + ? "error" + : form.password === "" + ? "info" + : form.password.length < 8 + ? "error" + : "success" + } + /> + + Must contain one + special character and a number + + } + variant={ + errors?.password === "Password is required" + ? "error" + : form.password === "" + ? "info" + : !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test( + form.password + ) + ? "error" + : "success" + } + /> + + + Must contain at least + {" "} + one upper and lower character + + } + variant={ + errors?.password === "Password is required" + ? "error" + : form.password === "" + ? "info" + : !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password) + ? "error" + : "success" + } + /> + +
-
+
+ + Go back to — + navigate("/login")} + sx={{ userSelect: "none" }} + > + Log In + + +
); }; diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css index 577f27799..979ae191c 100644 --- a/Client/src/Pages/Auth/index.css +++ b/Client/src/Pages/Auth/index.css @@ -1,80 +1,3 @@ -/* ////// */ -/* SHARED */ -/* ////// */ -.set-new-password-page, -.password-confirmed-page, -.forgot-password-page, -.check-email-page { - display: flex; - justify-content: center; - height: var(--env-var-height-1); -} -.set-new-password-page h1.MuiTypography-root, -.password-confirmed-page h1.MuiTypography-root, -.forgot-password-page h1.MuiTypography-root, -.check-email-page h1.MuiTypography-root { - font-size: var(--env-var-font-size-large); - color: var(--env-var-color-1); - font-weight: 600; -} -.set-new-password-page p.MuiTypography-root, -.set-new-password-page button, -.password-confirmed-page p.MuiTypography-root, -.password-confirmed-page button, -.forgot-password-page p.MuiTypography-root, -.forgot-password-page button, -.check-email-page p.MuiTypography-root, -.check-email-page button, -.check-email-page span.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} -.set-new-password-page button:not(.MuiIconButton-root), -.password-confirmed-page button:not(.MuiIconButton-root), -.forgot-password-page button:not(.MuiIconButton-root), -.check-email-page button:not(.MuiIconButton-root) { - height: 34px; - border-radius: var(--env-var-radius-2); - line-height: 0; -} - -.set-new-password-form, -.password-confirmed-form, -.forgot-password-form, -.check-email-form { - margin-top: 65px; - width: var(--env-var-width-2); -} -.set-new-password-page p.MuiTypography-root, -.password-confirmed-page p.MuiTypography-root, -.forgot-password-page p.MuiTypography-root, -.check-email-page p.MuiTypography-root { - color: var(--env-var-color-2); -} - -.set-new-password-page button:not(.MuiIconButton-root) svg, -.password-confirmed-page button:not(.MuiIconButton-root) svg, -.forgot-password-page button svg, -.check-email-page button svg { - margin-right: 5px; -} - -.set-new-password-page - .MuiFormControl-root:has(#register-password-input) - + span.MuiTypography-root, -.password-confirmed-page - .MuiFormControl-root:has(#register-password-input) - + span.MuiTypography-root { - display: none; - visibility: hidden; -} - -/* /////////// */ -/* Check Email */ -/* /////////// */ -.check-email-page span.MuiTypography-root { - font-weight: 600; -} - /* AUTH */ .auth { height: 100vh; @@ -85,10 +8,8 @@ color: var(--env-var-color-3); } .auth button:not(.MuiIconButton-root), -.auth .field h3.MuiTypography-root, -.auth .field input, -.auth .field .input-error, .auth p, +.auth .field input, .auth span { font-size: var(--env-var-font-size-medium-plus); } @@ -106,13 +27,13 @@ cursor: pointer; transition: opacity 300ms ease-in; } -.auth p > span { +.auth p > span:not(.email-sent-to) { text-decoration: underline; text-underline-offset: 2px; cursor: pointer; transition: all 200ms; } -.auth p > span:hover { +.auth p > span:not(.email-sent-to):hover { color: var(--env-var-color-2); text-underline-offset: 4px; } @@ -132,15 +53,80 @@ width: 24px; height: 24px; } +.auth .field h3.MuiTypography-root, +.auth .field .input-error, .auth .check span.MuiTypography-root { font-size: var(--env-var-font-size-medium); } +.auth form + form { + margin: var(--env-var-spacing-1-plus) 0; +} +.auth .email-sent-to { + font-weight: 600; + cursor: default; +} -.auth > .MuiStack-root:nth-of-type(1) { +.auth > .MuiStack-root:nth-of-type(2) { height: var(--env-var-nav-bar-height); } .auth .background-pattern-svg { - top: 14%; + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -30%); + z-index: -1; + width: 100%; max-width: 800px; + height: 100%; + max-height: 800px; + + background-position: center; + background-size: cover; + background-repeat: no-repeat; +} + +.forgot-password-page h1, +.check-email-page h1, +.password-confirmed-page h1, +.set-new-password-page h1 { + font-size: 22px; +} + +@media (max-width: 800px) { + .auth h1 { + font-size: var(--env-var-font-size-large-plus); + } + .auth button:not(.MuiIconButton-root), + .auth .field input, + .auth p, + .auth span { + font-size: var(--env-var-font-size-medium); + } + .auth button:not(.MuiIconButton-root), + .auth .field .MuiInputBase-root:has(input) { + height: 36px; + } + .auth .check span.MuiTypography-root, + .auth .field .input-error, + .auth .field h3.MuiTypography-root { + font-size: var(--env-var-font-size-small-plus); + } + .auth .check span > span { + display: none; + } + .auth form + form { + margin: var(--env-var-spacing-1-plus) 0; + } + .auth .background-pattern-svg { + max-width: 750px; + max-height: 750px; + } + + .forgot-password-page h1, + .check-email-page h1, + .password-confirmed-page h1, + .set-new-password-page h1 { + font-size: 18px; + } } diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 95230bb72..44a30decc 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -9,6 +9,8 @@ import { Pagination, PaginationItem, Paper, + Typography, + Box, } from "@mui/material"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; @@ -105,41 +107,55 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { return ( <> - - - - - Monitor Name - Status - Date & Time - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; - - return ( - - {monitors[check.monitorId]?.name} - - - - - {new Date(check.createdAt).toLocaleString()} - - {check.statusCode} + {checks?.length === 0 && selectedMonitor === "0" ? ( + + No incidents recorded yet. + + ) : checks?.length === 0 ? ( + + + The monitor you have selected has no recorded incidents yet. + + + ) : ( + <> + +
+ + + Monitor Name + Status + Date & Time + Message - ); - })} - -
-
- {paginationComponent} + + + {checks.map((check) => { + const status = check.status === true ? "up" : "down"; + + return ( + + {monitors[check.monitorId]?.name} + + + + + {new Date(check.createdAt).toLocaleString()} + + {check.statusCode} + + ); + })} + + + + {paginationComponent} + + )} ); }; diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css index 1f176d470..286552d22 100644 --- a/Client/src/Pages/Incidents/index.css +++ b/Client/src/Pages/Incidents/index.css @@ -17,6 +17,12 @@ .incidents .MuiPagination-root { background-color: var(--env-var-color-8); } +.incidents > .MuiBox-root { + background-color: var(--env-var-color-8); + border: solid 1px var(--env-var-color-6); + border-radius: var(--env-var-radius-1); + padding: 60px; +} /* remove later */ .incidents .MuiSelect-select { diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 515f8ec0c..f9d53f7c0 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -78,7 +78,7 @@ const Incidents = () => { }; return ( - + {loading ? ( ) : ( diff --git a/Client/src/Pages/Integrations/index.jsx b/Client/src/Pages/Integrations/index.jsx index f7d693948..278754fea 100644 --- a/Client/src/Pages/Integrations/index.jsx +++ b/Client/src/Pages/Integrations/index.jsx @@ -108,7 +108,7 @@ const Integrations = () => { ]; return ( - + Integrations Connect BlueWave Uptime to your favorite service. diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css index a594348ab..ac9cd8a0d 100644 --- a/Client/src/Pages/Monitors/Configure/index.css +++ b/Client/src/Pages/Monitors/Configure/index.css @@ -75,7 +75,7 @@ .configure-monitor-form { display: flex; flex-direction: column; - gap: var(--env-var-spacing-4); + gap: var(--env-var-spacing-2); } body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root, diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.css b/Client/src/Pages/Monitors/CreateMonitor/index.css index 1eb542a44..3fa38f14a 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.css +++ b/Client/src/Pages/Monitors/CreateMonitor/index.css @@ -59,7 +59,7 @@ .create-monitor-form { display: flex; flex-direction: column; - gap: var(--env-var-spacing-4); + gap: var(--env-var-spacing-2); } .create-monitor .MuiStack-root .MuiButtonGroup-root button { diff --git a/Client/src/Pages/Monitors/Details/index.css b/Client/src/Pages/Monitors/Details/index.css index 359a002bc..127b7377a 100644 --- a/Client/src/Pages/Monitors/Details/index.css +++ b/Client/src/Pages/Monitors/Details/index.css @@ -3,12 +3,10 @@ color: var(--env-var-color-1); font-weight: 600; } -.monitor-details h2.MuiTypography-root, -.monitor-details h6.MuiTypography-root { +.monitor-details h2.MuiTypography-root { font-size: var(--env-var-font-size-large); } -.monitor-details h2.MuiTypography-root, -.monitor-details h4.MuiTypography-root { +.monitor-details h2.MuiTypography-root{ font-weight: 600; } .monitor-details h2.MuiTypography-root, @@ -35,15 +33,21 @@ } .monitor-details .stat-box { - flex: 1; + flex: 1 20%; min-width: 100px; border: solid 1px; border-radius: var(--env-var-radius-1); - padding: var(--env-var-spacing-1) 18px; + padding: var(--env-var-spacing-1) 16px; background-color: var(--env-var-color-8); } -.monitor-details .stat-box h6.MuiTypography-root { - margin-bottom: var(--env-var-spacing-1); +.monitor-details h6.MuiTypography-root { + font-size: var(--env-var-font-size-medium-plus); +} +.monitor-details h6.MuiTypography-root span.MuiTypography-root { + font-style: italic; + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-5); + opacity: 0.7; } .monitor-details button.MuiButtonBase-root, .monitor-details .stat-box { diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index 4168880d5..15cfd2c82 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -5,7 +5,6 @@ import { useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import axiosInstance from "../../../Utils/axiosConfig"; import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart"; -import { StatusLabel } from "../../../Components/Label"; import ButtonGroup from "@mui/material/ButtonGroup"; import Button from "../../../Components/Button"; import WestRoundedIcon from "@mui/icons-material/WestRounded"; @@ -18,20 +17,23 @@ import { formatDurationRounded, } from "../../../Utils/timeUtils"; import "./index.css"; +import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart"; const StatBox = ({ title, value }) => { return ( - + {title} - {value} + + {value} + ); }; StatBox.propTypes = { - title: PropTypes.string, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; @@ -114,12 +116,13 @@ const DetailsPage = () => { const { monitorId } = useParams(); const { authToken } = useSelector((state) => state.auth); const [dateRange, setDateRange] = useState("day"); + const [certificateExpiry, setCertificateExpiry] = useState("N/A"); const navigate = useNavigate(); const fetchMonitor = useCallback(async () => { try { const res = await axiosInstance.get( - `/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`, + `/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`, { headers: { Authorization: `Bearer ${authToken}`, @@ -137,71 +140,23 @@ const DetailsPage = () => { fetchMonitor(); }, [fetchMonitor]); + useEffect(() => { + const fetchCertificate = async () => { + const res = await axiosInstance.get( + `/monitors/certificate/${monitorId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); + setCertificateExpiry(res.data.data.certificateDate); + }; + fetchCertificate(); + }, [authToken, monitorId]); + const theme = useTheme(); - - /** - * Function to calculate uptime duration based on the most recent check. - * @param {Array} checks Array of check objects. - * @returns {number} Uptime duration in ms. - */ - - // TODO: This can be done more efficiently by iteratting backwards - // and breaking when the first down check is found, calculate current time - downtime - const calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const latestCheck = new Date(checks[checks.length - 1].createdAt); - let latestDownCheck = 0; - - // Checks are ordered oldest -> newest - // So we iterate backwards and find the first down check - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the first check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[0].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; - }; - - /** - * Helper function to get duration since last check - * @param {Array} checks Array of check objects. - * @returns {number} Timestamp of the most recent check. - */ - const getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted oldest -> newest, so last check is the most recent - return new Date() - new Date(checks[checks.length - 1].createdAt); - }; - - /** - * Helper function to count incidents (checks with status === false). - * @param {Array} checks Array of check objects. - * @returns {number} Number of incidents. - */ - const countIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); - }; - let loading = Object.keys(monitor).length === 0; - return ( {loading ? ( @@ -210,12 +165,12 @@ const DetailsPage = () => { <>