diff --git a/.dockerignore b/.dockerignore index e6b9c067d..fdfa00585 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ Client/node_modules/ Server/node_modules/ -*.env \ No newline at end of file +Server/*.env \ No newline at end of file diff --git a/Client/package-lock.json b/Client/package-lock.json index dfc221fc4..8e84d7a57 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,12 +11,12 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.2.0", - "@mui/lab": "6.0.0-beta.19", - "@mui/material": "6.2.0", + "@mui/icons-material": "6.2.1", + "@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", @@ -1071,15 +1071,15 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.66", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.66.tgz", - "integrity": "sha512-1SzcNbtIms0o/Dx+599B6QbvR5qUMBUjwc2Gs47h1HsF7RcEFXxqaq7zrWkIWbvGctIIPx0j330oGx/SkF+UmA==", + "version": "5.0.0-beta.67", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.67.tgz", + "integrity": "sha512-pwjibXqBrSw1Z1fZEIhWJnMJHSvh3BLz0pZcrX/x01B/BVf6QUbFlGVV/k8KCsZ9sPzln1l8bBJ+ynIIE9LtjA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@floating-ui/react-dom": "^2.1.1", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.2.0", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "@popperjs/core": "^2.11.8", "clsx": "^2.1.1", "prop-types": "^15.8.1" @@ -1103,9 +1103,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.0.tgz", - "integrity": "sha512-Nn5PSkUqbDrvezpiiiYZiAbX4SFEiy3CbikUL6pWOXEUsq+L1j50OOyyUIHpaX2Hr+5V5UxTh+fPeC4nsGNhdw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz", + "integrity": "sha512-U/8vS1+1XiHBnnRRESSG1gvr6JDHdPjrpnW6KEebkAQWBn6wrpbSF/XSZ8/vJIRXH5NyDmMHi4Ro5Q70//JKhA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1113,9 +1113,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.0.tgz", - "integrity": "sha512-WR1EEhGOSvxAsoTSzWZBlrWFjul8wziDrII4rC3PvMBHhBYBqEc2n/0aamfFbwkH5EiYb96aqc6kYY6tB310Sw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.1.tgz", + "integrity": "sha512-bP0XtW+t5KFL+wjfQp2UctN/8CuWqF1qaxbYuCAsJhL+AzproM8gGOh2n8sNBcrjbVckzDNqaXqxdpn+OmoWug==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1128,7 +1128,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.2.0", + "@mui/material": "^6.2.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1139,16 +1139,16 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.19.tgz", - "integrity": "sha512-t7iub8kjpLdA5uDGwGnNRjtGc1vYEUnDwSROjKrnYqjOlCQhBajFa8uoQtaST6jE/VEk6cxpDMnN5MalC6YpCg==", + "version": "6.0.0-beta.20", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.20.tgz", + "integrity": "sha512-Sg3aTrSSsfXcSHwKhnZbXmz7kUZzvXww3vO4p8U/DbMVSGJxll1GRpKNQkFLpZy2y7ZBTgKaZTbPYw4Q6UKrsA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/base": "5.0.0-beta.66", - "@mui/system": "^6.2.0", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.2.0", + "@mui/base": "5.0.0-beta.67", + "@mui/system": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "clsx": "^2.1.1", "prop-types": "^15.8.1" }, @@ -1162,8 +1162,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.2.0", - "@mui/material-pigment-css": "^6.2.0", + "@mui/material": "^6.2.1", + "@mui/material-pigment-css": "^6.2.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1184,18 +1184,18 @@ } }, "node_modules/@mui/material": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.0.tgz", - "integrity": "sha512-7FXXUPIyYzP02a7GvqwJ7ocmdP+FkvLvmy/uxG1TDmTlsr8nEClQp75uxiVznJqAY/jJy4d+Rj/fNWNxwidrYQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", + "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.2.0", - "@mui/system": "^6.2.0", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.2.0", + "@mui/core-downloads-tracker": "^6.2.1", + "@mui/system": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", + "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", @@ -1212,7 +1212,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.2.0", + "@mui/material-pigment-css": "^6.2.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1239,13 +1239,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.0.tgz", - "integrity": "sha512-lYd2MrVddhentF1d/cMXKnwlDjr/shbO3A2eGq22PCYUoZaqtAGZMc0U86KnJ/Sh5YzNYePqTOaaowAN8Qea8A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz", + "integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.2.0", + "@mui/utils": "^6.2.1", "prop-types": "^15.8.1" }, "engines": { @@ -1266,9 +1266,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.0.tgz", - "integrity": "sha512-rV4YCu6kcCjMnHFXU/tQcL6XfYVfFVR8n3ZVNGnk2rpXnt/ctOPJsF+eUQuhkHciueLVKpI06+umr1FxWWhVmQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz", + "integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1300,16 +1300,16 @@ } }, "node_modules/@mui/system": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.0.tgz", - "integrity": "sha512-DCeqev9Cd4f4pm3O1lqSGW/DIHHBG6ZpqMX9iIAvN4asYv+pPWv2/lKov9kWk5XThhxFnGSv93SRNE1kNRRg5Q==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz", + "integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.2.0", - "@mui/styled-engine": "^6.2.0", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.2.0", + "@mui/private-theming": "^6.2.1", + "@mui/styled-engine": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1340,9 +1340,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "version": "7.2.20", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz", + "integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1354,13 +1354,13 @@ } }, "node_modules/@mui/utils": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.0.tgz", - "integrity": "sha512-77CaFJi+OIi2SjbPwCis8z5DXvE0dfx9hBz5FguZHt1VYFlWEPCWTHcMsQCahSErnfik5ebLsYK8+D+nsjGVfw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz", + "integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", + "@mui/types": "^7.2.20", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1448,9 +1448,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", @@ -1485,9 +1485,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", @@ -2372,9 +2372,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.16", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz", - "integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==", + "version": "18.3.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz", + "integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2392,11 +2392,11 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", - "dependencies": { + "peerDependencies": { "@types/react": "*" } }, diff --git a/Client/package.json b/Client/package.json index b1a8409cf..1e22e76d3 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,12 +14,12 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.2.0", - "@mui/lab": "6.0.0-beta.19", - "@mui/material": "6.2.0", + "@mui/icons-material": "6.2.1", + "@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/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx index 3760c04b8..39e8a3f42 100644 --- a/Client/src/Components/Avatar/index.jsx +++ b/Client/src/Components/Avatar/index.jsx @@ -2,28 +2,7 @@ import { Avatar as MuiAvatar } from "@mui/material"; 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; -}; - +import { useTheme } from "@emotion/react"; /** * @component * @param {Object} props @@ -38,6 +17,7 @@ const stringToColor = (string) => { const Avatar = ({ src, small, sx }) => { const { user } = useSelector((state) => state.auth); + const theme = useTheme(); const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; const border = small ? 1 : 3; @@ -57,7 +37,7 @@ const Avatar = ({ src, small, sx }) => { fontSize: small ? "16px" : "22px", color: "white", fontWeight: 400, - backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`), + backgroundColor: theme.palette.primary.main, // Same BG color as checkmate BG in sidebar display: "inline-flex", "&::before": { content: `""`, @@ -74,7 +54,7 @@ const Avatar = ({ src, small, sx }) => { }} > {user.firstName?.charAt(0)} - {user.lastName?.charAt(0)} + {user.lastName?.charAt(0) || ''} ); }; diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index e519a079f..9f9a5b9da 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(); @@ -228,6 +228,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => { ); })} + {displayData.length === 0 && ( + + + {emptyMessage} + + + )} @@ -317,6 +327,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 feed25f24..d6d25fbc2 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -28,7 +28,6 @@ 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 Support from "../../assets/icons/support.svg?react"; -import Dashboard from "../../assets/icons/dashboard.svg?react"; import Account from "../../assets/icons/user-edit.svg?react"; import Maintenance from "../../assets/icons/maintenance.svg?react"; import Monitors from "../../assets/icons/monitors.svg?react"; @@ -178,13 +177,7 @@ function Sidebar() { direction="row" alignItems="center" gap={theme.spacing(4)} - onClick={() => - window.open( - "https://github.com/bluewave-labs/bluewave-uptime", - "_blank", - "noreferrer" - ) - } + onClick={() => navigate("/")} sx={{ cursor: "pointer" }} > + * ) + * + * @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) }, - }} - /> - - - - - 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 new file mode 100644 index 000000000..08ae51c4f --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx @@ -0,0 +1,131 @@ +import { useRef, useEffect } from "react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import PropTypes from "prop-types"; +import { useNavigate } from "react-router"; + +/** + * Renders the email step of the login process which includes an email 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 EmailStep = ({ form, errors, onSubmit, onChange }) => { + const theme = useTheme(); + const inputRef = useRef(null); + const navigate = useNavigate(); + + 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 email address + + + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email ? true : false} + helperText={errors.email} + ref={inputRef} + /> + + + + + + + Forgot password? + + + Reset password + + + + + ); +}; + +EmailStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default EmailStep; diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx new file mode 100644 index 000000000..34df4959d --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx @@ -0,0 +1,159 @@ +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"; +import { useSelector } from "react-redux"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import PropTypes from "prop-types"; + +/** + * Renders the password 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 PasswordStep = ({ 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 + + + + + ); +}; + +PasswordStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, +}; + +export default PasswordStep \ No newline at end of file diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx new file mode 100644 index 000000000..c1687a0ec --- /dev/null +++ b/Client/src/Pages/Auth/Login/Login.jsx @@ -0,0 +1,213 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { credentials } from "../../../Validation/validation"; +import { login } from "../../../Features/Auth/authSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../main"; +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"; + +const DEMO = import.meta.env.VITE_APP_DEMO; + +/** + * Displays the login page. + */ + +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 === 0) { + 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(1); + } + } else if (step === 1) { + 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 ? ( + + ) : ( + step === 1 && ( + setStep(0)} + /> + ) + )} + + + ); +}; + +export default 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 = () => {