diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..fbe457426 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,14 @@ +language: "en-CA" +tone_instructions: "Tale as old as time (ooh-ooh) Tune as old as song Bittersweet and strange Finding you can change Learning you were wrong Certain as the sun Rising in the east Tale as old as time Song as old as rhyme Beauty and the beast" +early_access: false +reviews: + profile: "assertive" + request_changes_workflow: false + high_level_summary: false + poem: false + review_status: true + auto_review: + enabled: true + drafts: false +chat: + auto_reply: false diff --git a/Client/src/App.jsx b/Client/src/App.jsx index abd2d37b7..f1c24049a 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -20,6 +20,8 @@ import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed"; import ProtectedRoute from "./Components/ProtectedRoute"; import Details from "./Pages/Details"; import Maintenance from "./Pages/Maintenance"; +import "react-toastify/dist/ReactToastify.css"; +import { ToastContainer } from "react-toastify"; function App() { return ( @@ -92,6 +94,7 @@ function App() { element={} /> + ); } diff --git a/Client/src/Components/Alert/index.css b/Client/src/Components/Alert/index.css index 9f0c397e3..c65bfa38c 100644 --- a/Client/src/Components/Alert/index.css +++ b/Client/src/Components/Alert/index.css @@ -1,9 +1,7 @@ .alert { margin: 0; width: fit-content; - padding: var(--env-var-spacing-1-plus); +} +.alert, .alert button{ font-size: var(--env-var-font-size-medium); -} -.alert .MuiStack-root { - max-width: 85%; -} +} \ No newline at end of file diff --git a/Client/src/Components/Alert/index.jsx b/Client/src/Components/Alert/index.jsx index bf2531bba..3fa0d82ab 100644 --- a/Client/src/Components/Alert/index.jsx +++ b/Client/src/Components/Alert/index.jsx @@ -1,10 +1,12 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; +import { Box, IconButton, Stack } from "@mui/material"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; +import CloseIcon from "@mui/icons-material/Close"; import "./index.css"; +import Button from "../Button"; /** * Icons mapping for different alert variants. @@ -22,10 +24,13 @@ const icons = { * @param {'info' | 'error' | 'warning'} props.variant - The type of alert. * @param {string} [props.title] - The title of the alert. * @param {string} [props.body] - The body text of the alert. + * @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification. + * @param {boolean} [props.hasIcon] - Whether to display an icon in the alert. + * @param {function} props.onClick - Toast dismiss function. * @returns {JSX.Element} */ -const Alert = ({ variant, title, body }) => { +const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { const theme = useTheme(); const { bg, border, color } = theme.alert[variant]; const icon = icons[variant]; @@ -34,19 +39,67 @@ const Alert = ({ variant, title, body }) => { - {icon && {icon}} + {hasIcon && {icon}} {title && {title}} {body && {body}} + {hasIcon && isToast && ( + + )} + {isToast && ( + + + + )} ); }; @@ -55,6 +108,16 @@ Alert.propTypes = { variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, title: PropTypes.string, body: PropTypes.string, + isToast: PropTypes.bool, + hasIcon: PropTypes.bool, + onClick: function (props, propName, componentName) { + if (props.isToast && !props[propName]) { + return new Error( + `Prop '${propName}' is required when 'isToast' is true in '${componentName}'.` + ); + } + return null; + }, }; export default Alert; diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx new file mode 100644 index 000000000..a80c7bfb6 --- /dev/null +++ b/Client/src/Components/BasicTable/index.jsx @@ -0,0 +1,146 @@ +import { + TableContainer, + Paper, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + TablePagination, +} from "@mui/material"; +import React, { useState } from "react"; +import PropTypes from "prop-types"; + +/** + * BasicTable Component + * Renders a table with optional pagination. + * + * @component + * @param {Object} props - Component props. + * @param {Array} props.headers - Array of header titles for the table. + * @param {Array} props.rowItems - Array of row data objects for the table. + * @param {boolean} props.paginated - Flag to enable pagination. + * + * @example + * + * Headers is an array of objets that have a unique ID and a name + * Rows is an array of objects that have a unique ID and some data to display + * Row data can be any valid React node + * Rows are split into groups according to the number of headers, so if you have 3 headers then + * Rows will go under the index % 3 column. + * ie: Row 0 -> col 0 % 3 = 0 + * ie: Row 1 -> col 1 % 3 = 1 + * ie: Row 2 -> col 2 % 3 = 2 + * ie: Row 3 -> col 3 % 3 = 0 + * + * Thus items are put in their correct columns + * + * + * const headers = [ + * { id: 1, name: "ID" }, + * { id: 2, name: "Name" }, + * { id: 3, name: "Age" }, + * ]; + * const rows = [ + * { id: 1, data: 999 }, + * { id: 2, data:
Alex Holliday
}, + * { id: 3, data:
5
}, + * // More rows... + * ]; + * + */ + +const BasicTable = ({ headers, rowItems, paginated }) => { + // Add headers to props validation + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); // Reset to first page after changing rows per page + }; + + const numOfCols = headers.length; + + const groupedRowItems = rowItems.reduce((acc, item, idx) => { + const rowIndex = Math.floor((item.id - 1) / numOfCols); + if (!acc[rowIndex]) acc[rowIndex] = { items: [], idx }; + acc[rowIndex].items.push({ item }); + return acc; + }, {}); + + const paginatedRowsKeys = Object.keys(groupedRowItems).slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ); + + return ( + <> + + + + + {headers.map((header) => { + return {header.name}; + })} + + + + {paginated === false && + Object.keys(groupedRowItems).map((key) => { + return ( + + {groupedRowItems[key].items.map(({ item }) => { + return {item.data}; + })} + + ); + })} + {paginated === true && + paginatedRowsKeys.map((key) => ( + + {groupedRowItems[key].items.map(({ item }) => ( + {item.data} + ))} + + ))} + +
+
+ {paginated === true && ( + + )} + + ); +}; + +BasicTable.propTypes = { + headers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + name: PropTypes.string.isRequired, + }) + ).isRequired, + rowItems: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + data: PropTypes.node, + }) + ).isRequired, + paginated: PropTypes.bool, +}; + +export default BasicTable; diff --git a/Client/src/Components/Inputs/Field/index.css b/Client/src/Components/Inputs/Field/index.css new file mode 100644 index 000000000..3a8904b27 --- /dev/null +++ b/Client/src/Components/Inputs/Field/index.css @@ -0,0 +1,89 @@ +.field { + min-width: 250px; +} +.field h3.MuiTypography-root, +.field h5.MuiTypography-root, +.field input, +.field textarea, +.field .input-error { + font-size: var(--env-var-font-size-medium); +} +.field h3.MuiTypography-root, +.field h5.MuiTypography-root, +.field input, +.field textarea { + color: var(--env-var-color-5); +} +.field h3.MuiTypography-root { + font-weight: 500; +} +.field h3.MuiTypography-root span.field-required { + color: var(--env-var-color-19); + margin-left: 2px; +} +.field h3.MuiTypography-root span.field-optional { + opacity: 0.6; + margin-left: 4px; +} +.field h5.MuiTypography-root { + position: relative; + opacity: 0.8; + padding-right: var(--env-var-spacing-1-minus); +} +.field .MuiInputBase-root:has(input) { + height: 34px; +} +.field .MuiInputBase-root:has(.MuiInputAdornment-root) { + padding-right: var(--env-var-spacing-1-minus); +} +.field .MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) { + padding-right: 0; +} +.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root { + padding: 0 var(--env-var-spacing-1-minus); + font-size: var(--env-var-font-size-medium); + border-radius: 0; + height: 34px; +} +.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root svg { + width: 20px; + height: 20px; + margin-right: 5px; +} +.field .MuiInputBase-root .MuiInputAdornment-root .MuiButtonBase-root:hover { + border-color: var(--env-var-color-29); +} +.field input { + height: 100%; + padding: 0 var(--env-var-spacing-1-minus); +} +.field .MuiInputBase-root:has(textarea) { + padding: var(--env-var-spacing-1-minus); +} +.field .MuiOutlinedInput-root fieldset { + border-color: var(--env-var-color-29); + border-radius: var(--env-var-radius-1); +} +.field:not(:has(.input-error)) + .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) + fieldset { + border-color: var(--env-var-color-29); +} +.field .input-error { + color: var(--env-var-color-24); + opacity: 0.8; +} +.field:has(.input-error) .MuiOutlinedInput-root fieldset { + border-color: var(--env-var-color-19); +} + +.register-page .field .MuiOutlinedInput-root fieldset, +.register-page .field input, +.login-page .field .MuiOutlinedInput-root fieldset, +.login-page .field input, +.forgot-password-page .field .MuiOutlinedInput-root fieldset, +.forgot-password-page .field input, +.set-new-password-page .field .MuiOutlinedInput-root fieldset, +.set-new-password-page .field input { + border-radius: var(--env-var-radius-2); +} diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx new file mode 100644 index 000000000..06594450a --- /dev/null +++ b/Client/src/Components/Inputs/Field/index.jsx @@ -0,0 +1,158 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { + IconButton, + InputAdornment, + Stack, + TextField, + Typography, +} from "@mui/material"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import Visibility from "@mui/icons-material/Visibility"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import Button from "../../Button"; +import "./index.css"; +import { useState } from "react"; + +/** + * @param {Object} props + * @param {string} [props.type] - Type of input field (e.g., 'text', 'password'). + * @param {string} props.id - ID of the input field. + * @param {string} [props.label] - Label for the input field. + * @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk. + * @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text. + * @param {boolean} [props.hasCopy] - Indicates if the field supports copying. + * @param {string} [props.autoComplete] - Autocomplete value for the input field. + * @param {string} [props.placeholder] - Placeholder text for the input field. + * @param {string} props.value - Value of the input field. + * @param {function} props.onChange - Function called on input change. + * @param {string} [props.error] - Error message to display for the input field. + * @param {boolean} [props.disabled] - Indicates if the input field is disabled. + */ + +const Field = ({ + type = "text", + id, + label, + isRequired, + isOptional, + hasCopy, + autoComplete, + placeholder, + value, + onChange, + error, + disabled, +}) => { + const theme = useTheme(); + + // TODO - are we using this feature anywhere ? + const [copy, setCopy] = useState(false); + const handleCopy = () => { + setCopy(true); + setTimeout(() => setCopy(false), 1000); + }; + + const [isVisible, setVisible] = useState(false); + + return ( + + {label && ( + + {label} + {isRequired ? * : ""} + {isOptional ? (optional) : ""} + + )} + + https:// + + ), + endAdornment: + type === "password" ? ( + + setVisible((show) => !show)} + tabIndex={-1} + sx={{ + color: theme.palette.section.borderColor, + padding: `calc(${theme.gap.xs} / 2)`, + "&:focus": { + outline: "none", + }, + "& .MuiTouchRipple-root": { + pointerEvents: "none", + display: "none", + }, + }} + > + {!isVisible ? : } + + + ) : ( + hasCopy && ( + + - - -
- -
-
{header}
-
- - -
-
- - ); -}; - -AnnouncementUpdateSubscription.propTypes = { - title: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - cancel: PropTypes.string.isRequired, - positive: PropTypes.string.isRequired, - header: PropTypes.string.isRequired, - button: PropTypes.string.isRequired, -}; - -export default AnnouncementUpdateSubscription; diff --git a/Client/src/Components/Toast/AnnouncementUpdateSubscription/announcementUpdateSubscription.css b/Client/src/Components/Toast/AnnouncementUpdateSubscription/announcementUpdateSubscription.css deleted file mode 100644 index 6064a86cd..000000000 --- a/Client/src/Components/Toast/AnnouncementUpdateSubscription/announcementUpdateSubscription.css +++ /dev/null @@ -1,86 +0,0 @@ -.update-subscription { - margin: 20px; -} - -.update-subscription, -.update-subscription-dialog { - max-width: 770px; - border-radius: 4px; - border: 1px solid #eaecf0; -} - -.update-subscription-dialog { - padding: 15px 30px; -} - -.subscription-dialog-title { - font-size: 1rem; - font-weight: 700; - color: var(--env-var-color-1); -} - -.v-spacing-small { - height: 10px; -} - -.v-spacing-medium { - height: 15px; -} - -.subscription-dialog-text { - font-size: 13px; - color: var(--env-var-color-2); -} - -.subscription-dialog-controls { - display: flex; -} - -.controls-negative { - padding: 12px 16px; - margin-right: 12px; - border-radius: var(--env-var-radius-1); - border: 1px solid var(--env-var-color-6); - background-color: var(--env-var-color-8); - font-size: var(--env-var-font-size-medium); -} - -.controls-positive, -.subscribe-button { - padding: 12px 16px; - margin-right: 12px; - border-radius: var(--env-var-radius-1); - border: 1px solid var(--env-var-color-7); - background-color: var(--env-var-color-3); - color: var(--env-var-color-8); - font-size: var(--env-var-font-size-medium); -} - -.update-subscription-title { - color: var(--env-var-color-5); - padding: 0 20px; - font-weight: 700; - font-size: 13px; - margin-bottom: 10px; -} - -.subscription-email { - display: flex; - padding: 0 20px; -} - -.subscription-email-field { - width: 290px; - height: 24px; - border-radius: var(--env-var-radius-1); - border: 1px solid var(--env-var-color-4); - box-shadow: 0px 1px 2px 0px rgba(101, 182, 28, 0.05); - padding: 5px 15px; - margin-right: 20px; -} - -.subscribe-button { - width: 100px; - height: 34px; - padding: 2px 15px; -} diff --git a/Client/src/Components/Toast/AnnouncementsDualButton/AnnouncementsDualButton.jsx b/Client/src/Components/Toast/AnnouncementsDualButton/AnnouncementsDualButton.jsx deleted file mode 100644 index 2abbadb1e..000000000 --- a/Client/src/Components/Toast/AnnouncementsDualButton/AnnouncementsDualButton.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import "./announcementsDualButton.css"; -import React from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; - -/** - * @component - * @param {Object} props - * @param {string} props.subject - The announcement subject text - * @param {string} props.body - The announcement body text content - * @param {string} props.esc - The text for the escape button - * @param {string} props.primary - The text for the primary button - * @returns {JSX.Element} - Renders the announcement dual button component - */ - -const AnnouncementsDualButton = ({ subject, body, esc, primary }) => { - const theme = useTheme(); - - const fontLookup = { - default: theme.font.default.font, - }; - - const fontFamily = fontLookup["default"]; - - return ( -
-
-
{subject}
-
-
{body}
-
-
{esc}
-
{primary}
-
-
-
-
- -
-
- ); -}; - -AnnouncementsDualButton.propTypes = { - subject: PropTypes.string.isRequired, - body: PropTypes.string.isRequired, - esc: PropTypes.string.isRequired, - primary: PropTypes.string.isRequired, -}; - -export default AnnouncementsDualButton; diff --git a/Client/src/Components/Toast/AnnouncementsDualButton/announcementsDualButton.css b/Client/src/Components/Toast/AnnouncementsDualButton/announcementsDualButton.css deleted file mode 100644 index aff02ab46..000000000 --- a/Client/src/Components/Toast/AnnouncementsDualButton/announcementsDualButton.css +++ /dev/null @@ -1,25 +0,0 @@ -.announcement-without-tile { - margin: 20px; - max-width: 343px; - width: fit-content; - display: flex; - border: 1px solid var(--env-var-color-4); - padding: 16px; - border-radius: var(--env-var-radius-1); -} - -.h-spacing { - width: 20px; -} - -.announcement-without-content-subject { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); - font-weight: 700; - margin-bottom: 10px; -} - -.announcement-without-content-body { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); -} diff --git a/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx b/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx deleted file mode 100644 index bd2675f4c..000000000 --- a/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import "./announcementsDualButtonWithIcon.css"; -import CloseIcon from "@mui/icons-material/Close"; -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; - -/** - * @component - * @param {Object} props - * @param {React.ReactNode} props.icon - Optional React node for an announcement icon - * @param {string} props.subject - The announcement subject text - * @param {string} props.body - The announcement body text content - * @param {string} props.esc - The text for the escape button (usually "Close") - * @param {string} props.primary - The text for the primary button - * @param {function} props.closeToast - Function to close the toast notification - * @returns {JSX.Element} - Renders the announcement dual button with icon component - */ - -const AnnouncementsDualButtonWithIcon = ({ - icon, - subject, - body, - esc, - primary, - closeToast, -}) => { - const theme = useTheme(); - - const fontLookup = { - default: theme.font.default.font, - }; - - const fontFamily = fontLookup["default"]; - - return ( -
- {icon &&
{icon}
} -
- {subject && ( -
{subject}
- )} - {body &&
{body}
} - {(esc || primary) && ( -
- {esc && ( -
- {esc} -
- )} - {primary && ( -
{primary}
- )} -
- )} -
-
- -
-
- ); -}; - -AnnouncementsDualButtonWithIcon.propTypes = { - icon: PropTypes.object, - subject: PropTypes.string, - body: PropTypes.string, - esc: PropTypes.string, - primary: PropTypes.string, - closeToast: PropTypes.func.isRequired, -}; - -export default AnnouncementsDualButtonWithIcon; diff --git a/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css b/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css deleted file mode 100644 index 02f132374..000000000 --- a/Client/src/Components/Toast/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css +++ /dev/null @@ -1,58 +0,0 @@ -.announcement-tile { - margin: 20px; - width: fit-content; - display: flex; - border: 1px solid var(--env-var-color-4); - padding: 16px; - border-radius: var(--env-var-radius-1); -} - -.info-icon-frame { - display: flex; - width: var(--env-var-default-2); - height: var(--env-var-default-2); - border: 1px solid var(--env-var-color-6); - border-radius: var(--env-var-radius-2); - justify-content: center; - align-items: center; -} - -.announcement-content { - padding: 0 16px; -} - -.announcement-content-subject { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); - font-weight: 700; - margin-bottom: 10px; -} - -.announcement-content-body { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); - font-weight: 300; -} - -.announcement-content-controllers { - display: flex; - font-size: var(--env-var-font-size-medium); - margin-top: 15px; -} - -.controllers-button-esc { - font-size: var(--env-var-font-size-medium); - font-weight: 600; - margin-right: 12px; -} - -.controllers-button-primary { - font-size: var(--env-var-font-size-medium); - font-weight: 600; - color: var(--env-var-color-3); -} - -.announcement-close { - width: var(--env-var-default-1); - height: var(--env-var-default-1); -} diff --git a/Client/src/Components/Toast/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx b/Client/src/Components/Toast/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx deleted file mode 100644 index 20a179052..000000000 --- a/Client/src/Components/Toast/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import "./announcementsMessageBar.css"; -import React from "react"; -import CloseIcon from "@mui/icons-material/Close"; -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; - -/** - * @component - * @param {Object} props - * @param {string} props.message - The announcement message text content (required) - * @returns {JSX.Element} - Renders the announcement message bar component - */ -const AnnouncementsMessageBar = ({ message }) => { - const theme = useTheme(); - - const fontLookup = { - default: theme.font.default.font, - }; - - const fontFamily = fontLookup["default"]; - - return ( -
-
{message}
-
- -
-
- ); -}; - -AnnouncementsMessageBar.propTypes = { - message: PropTypes.string.isRequired, -}; - -export default AnnouncementsMessageBar; diff --git a/Client/src/Components/Toast/AnnouncementsMessageBar/announcementsMessageBar.css b/Client/src/Components/Toast/AnnouncementsMessageBar/announcementsMessageBar.css deleted file mode 100644 index 7a6e30b0a..000000000 --- a/Client/src/Components/Toast/AnnouncementsMessageBar/announcementsMessageBar.css +++ /dev/null @@ -1,8 +0,0 @@ -.announcement-messagebar-body { - margin: 20px; - font-size: var(--env-var-font-size-medium); - padding: 0 16px 0 0; - justify-content: center; - align-items: center; - margin: auto; -} diff --git a/Client/src/Components/Toast/index.css b/Client/src/Components/Toast/index.css deleted file mode 100644 index c778d22f5..000000000 --- a/Client/src/Components/Toast/index.css +++ /dev/null @@ -1,3 +0,0 @@ -.Toastify__toast-container { - width: auto; -} \ No newline at end of file diff --git a/Client/src/Components/Toast/index.jsx b/Client/src/Components/Toast/index.jsx deleted file mode 100644 index 41367ec11..000000000 --- a/Client/src/Components/Toast/index.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from "prop-types"; -import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; -import AnnouncementsDualButtonWithIcon from "./AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; -import "react-toastify/dist/ReactToastify.css"; -import { toast, ToastContainer } from "react-toastify"; -import "./index.css"; - -/** - * ToastComponent displays a notification that is triggered by error messages. - * - * @component - * @param {Object} props - The component props - * @param {string} props.subject - The subject or title of the toast message - * @param {string} props.body - The body content of the toast message - * @param {string} props.esc - The text for the dismiss action button - * @param {string} props.primary - The text for the primary action button - * @returns {JSX.Element} JSX element representing the ToastComponent - */ - -const ToastComponent = ({ subject, body, esc, primary }) => { - const displayMsg = () => { - toast( - ({ closeToast, toastProps }) => ( - } - subject={subject} - body={body} - esc={esc} - primary={primary} - closeToast={closeToast} - /> - ), - { closeButton: false } - ); - }; - - return ( -
- - -
- ); -}; - -ToastComponent.propTypes = { - subject: PropTypes.string.isRequired, - body: PropTypes.string.isRequired, - esc: PropTypes.string.isRequired, - primary: PropTypes.string.isRequired, -}; - -export default ToastComponent; diff --git a/Client/src/Features/Monitors/monitorsSlice.js b/Client/src/Features/Monitors/monitorsSlice.js index d5c257021..8086f1eb4 100644 --- a/Client/src/Features/Monitors/monitorsSlice.js +++ b/Client/src/Features/Monitors/monitorsSlice.js @@ -50,11 +50,14 @@ export const getMonitorsByUserId = createAsyncThunk( async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await axiosInstance.get("/monitors/user/" + user._id, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const res = await axiosInstance.get( + `/monitors/user/${user._id}?limit=25`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); return res.data; } catch (error) { if (error.response && error.response.data) { diff --git a/Client/src/Pages/Account/index.css b/Client/src/Pages/Account/index.css index acf71988b..608e08b94 100644 --- a/Client/src/Pages/Account/index.css +++ b/Client/src/Pages/Account/index.css @@ -139,7 +139,7 @@ } } -.account .MuiStack-root:has(p.MuiTypography-root.input-error) { +.account .MuiStack-root:has(span.MuiTypography-root.input-error) { position: relative; } .account .text-field-icon { @@ -150,11 +150,9 @@ height: 18px !important; opacity: 0.8; } -.account p.MuiTypography-root.input-error { - color: var(--env-var-color-24); - opacity: 0.8; +.account span.MuiTypography-root.input-error { position: absolute; - top: calc(100% + 5px); + top: 100%; } .account .email-text-field, .account .password-text-field { diff --git a/Client/src/Pages/CreateNewMonitor/index.css b/Client/src/Pages/CreateNewMonitor/index.css index 4702127f5..8ea9b2529 100644 --- a/Client/src/Pages/CreateNewMonitor/index.css +++ b/Client/src/Pages/CreateNewMonitor/index.css @@ -55,6 +55,13 @@ outline: none; border: none; } +.create-monitor-form .MuiStack-root:has(span.MuiTypography-root.input-error) { + position: relative; +} +.create-monitor-form span.MuiTypography-root.input-error { + position: absolute; + top: 100%; +} .monitors-gaps-medium { height: var(--env-var-spacing-2); diff --git a/Client/src/Pages/CreateNewMonitor/index.jsx b/Client/src/Pages/CreateNewMonitor/index.jsx index 805aa6426..cb10a49e1 100644 --- a/Client/src/Pages/CreateNewMonitor/index.jsx +++ b/Client/src/Pages/CreateNewMonitor/index.jsx @@ -1,6 +1,5 @@ import "./index.css"; import ConfigBox from "../../Components/ConfigBox"; -import FlexibileTextField from "../../Components/TextFields/Flexibile/FlexibileTextField"; import React, { useState } from "react"; import RadioButton from "../../Components/RadioButton"; import CustomizableCheckBox from "../../Components/Checkbox/CustomizableCheckbox"; @@ -12,6 +11,7 @@ import { createMonitor } from "../../Features/Monitors/monitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import WestRoundedIcon from "@mui/icons-material/WestRounded"; +import Field from "../../Components/Inputs/Field"; const CreateNewMonitor = () => { const MS_PER_MINUTE = 60000; @@ -86,6 +86,7 @@ const CreateNewMonitor = () => { //obj to submit let monitor = { ...generalSettings, + url: "https://" + generalSettings.url, ...checks, }; @@ -162,25 +163,22 @@ const CreateNewMonitor = () => { } rightLayout={ - handleChange(event, "url", setGeneralSettings) } error={errors["url"]} /> - - Friendly name (optional) - - } + diff --git a/Client/src/Pages/Demo/Demo.jsx b/Client/src/Pages/Demo/Demo.jsx index 42e30b6d2..9d687f2e8 100644 --- a/Client/src/Pages/Demo/Demo.jsx +++ b/Client/src/Pages/Demo/Demo.jsx @@ -37,9 +37,11 @@ import { useSelector } from "react-redux"; import { useDispatch } from "react-redux"; import { getMonitors } from "../../Features/Monitors/monitorsSlice"; import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice"; -import ImageField from "../../Components/TextFields/Image"; +import ImageField from "../../Components/Inputs/Image"; import ProgressUpload from "../../Components/ProgressBars"; import Alert from "../../Components/Alert"; +import { createToast } from "../../Utils/toastUtils"; +import Field from "../../Components/Inputs/Field"; const cols = [ { @@ -201,6 +203,9 @@ const Demo = () => { setIsLoading(false); }, 4000); }; + + //fields + const [visibility, setVisibility] = useState(false); return (
@@ -421,14 +426,23 @@ const Demo = () => { - + console.log("changed")} + /> - } label="image.jpg" size="2 MB" /> + } + label="image.jpg" + size="2 MB" + progress={50} + onClick={() => console.log("click")} + /> - + { body="Since you logged in via SSO, you cannot reset or modify your password." /> - + - + + + + + + + + +
diff --git a/Client/src/Pages/ForgotPassword/index.jsx b/Client/src/Pages/ForgotPassword/index.jsx index 5012b9d67..839e6a882 100644 --- a/Client/src/Pages/ForgotPassword/index.jsx +++ b/Client/src/Pages/ForgotPassword/index.jsx @@ -2,13 +2,13 @@ import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPatt import "./index.css"; import React from "react"; import Logomark from "../../assets/Images/key-password.png"; -import EmailTextField from "../../Components/TextFields/Email/EmailTextField"; import Button from "../../Components/Button"; import LeftArrow from "../../assets/Images/arrow-left.png"; import { useState, useEffect } from "react"; import { recoveryValidation } from "../../Validation/validation"; import axiosInstance from "../../Utils/axiosConfig"; import { useNavigate } from "react-router-dom"; +import Field from "../../Components/Inputs/Field"; const ForgotPassword = () => { const navigate = useNavigate(); @@ -76,12 +76,14 @@ const ForgotPassword = () => {
-
+
-
-
diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx index a8f30623f..e00061071 100644 --- a/Client/src/Pages/Monitors/index.jsx +++ b/Client/src/Pages/Monitors/index.jsx @@ -6,7 +6,7 @@ import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice"; import { useNavigate } from "react-router-dom"; import Button from "../../Components/Button"; import ServerStatus from "../../Components/Charts/Servers/ServerStatus"; -import SearchTextField from "../../Components/TextFields/Search/SearchTextField"; +import SearchTextField from "../../Components/Inputs/Search/SearchTextField"; import MonitorTable from "../../Components/MonitorTable"; import { useTheme } from "@emotion/react"; diff --git a/Client/src/Pages/PlayGround/PlayGround.jsx b/Client/src/Pages/PlayGround/PlayGround.jsx index bde0b6761..70916508c 100644 --- a/Client/src/Pages/PlayGround/PlayGround.jsx +++ b/Client/src/Pages/PlayGround/PlayGround.jsx @@ -1,19 +1,8 @@ import React from "react"; -import EmailTextField from "../../Components/TextFields/Email/EmailTextField"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; -import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; -import WebsiteTextField from "../../Components/TextFields/Website/WebsiteTextField"; -import DescriptionTextField from "../../Components/TextFields/Description/DescriptionTextField"; -import AnnouncementsDualButton from "../../Components/Toast/AnnouncementsDualButton/AnnouncementsDualButton"; -import AnnouncementsDualButtonWithIcon from "../../Components/Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; -import AnnouncementsMessageBar from "../../Components/Toast/AnnouncementsMessageBar/AnnouncementsMessageBar"; -import AnnouncementUpdateSubscription from "../../Components/Toast/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription"; import PlayGroundCharts from "./PlayGround-Charts"; import PlayGroundPopupModals from "./PlayGround-Popup-Modals"; import PlayGroundTooltips from "./PlayGround-Tooltips"; -import Toast from "../../Components/Toast"; +import Field from "../../Components/Inputs/Field"; // This Component is just for the development and test // purposes and just to see what my components look like while development @@ -22,99 +11,71 @@ function PlayGround() { return (
{/* Default state of Email Text Fields */} - - {/* Email Text Field when validation error occures */} - } - helperText="This is an error message" - /> - {/* Email Text Field while mouse hover */} - } - helperText="This is a hint text to help user." - /> +
+ + {/* Email Text Field when validation error occures */} + +

- {/* Now, illustration of the Website text fields */} - - - + {/* Now, illustration of the Website text fields */} +
+ + +

{/* Now, illustration of the Description text fields */} - - - - -
- } - subject="We’ve just released a new feature" - body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid - pariatur, ipsum dolor." - esc="Dismiss" - primary="View changes" - /> - - } - subject="There was a problem with that action" - body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid - pariatur, ipsum dolor." - esc="Dismiss" - primary="Learn more" - /> - - } - subject="Successfully updated profile" - body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid - pariatur, ipsum dolor." - esc="Dismiss" - primary="Learn more" - /> - - - - - - +
+ + +


diff --git a/Client/src/Pages/Register/index.css b/Client/src/Pages/Register/index.css index 8d4b99f1b..948f0afd0 100644 --- a/Client/src/Pages/Register/index.css +++ b/Client/src/Pages/Register/index.css @@ -1,5 +1,4 @@ .register-page { - width: var(--env-var-width-1); display: flex; justify-content: center; min-height: var(--env-var-height-1); @@ -40,16 +39,6 @@ font-weight: 600; } -#register-firstname-input, -#register-lastname-input, -#register-email-input, -#register-password-input { - font-size: var(--env-var-font-size-medium); - width: 294px; - height: 11px; - padding: 12px 14px; -} - .register-bottom-spacing { height: 50px; } diff --git a/Client/src/Pages/Register/index.jsx b/Client/src/Pages/Register/index.jsx index 617e04edd..12332f27e 100644 --- a/Client/src/Pages/Register/index.jsx +++ b/Client/src/Pages/Register/index.jsx @@ -4,9 +4,6 @@ import { useNavigate } from "react-router-dom"; import "./index.css"; import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern"; import Logomark from "../../assets/Images/Logomark.png"; -import EmailTextField from "../../Components/TextFields/Email/EmailTextField"; -import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField"; -import StringTextField from "../../Components/TextFields/Text/TextField"; import Check from "../../Components/Check/Check"; import Button from "../../Components/Button"; import Google from "../../assets/Images/Google.png"; @@ -14,6 +11,8 @@ import { registerValidation } from "../../Validation/validation"; import axiosInstance from "../../Utils/axiosConfig"; import { useDispatch } from "react-redux"; import { register } from "../../Features/Auth/authSlice"; +import { createToast } from "../../Utils/toastUtils"; +import Field from "../../Components/Inputs/Field"; const Register = () => { const dispatch = useDispatch(); @@ -91,20 +90,34 @@ const Register = () => { } } catch (error) { if (error.name === "ValidationError") { - // TODO Handle validation errors - console.log(error); - alert(error); + // validation errors + createToast({ + variant: "info", + body: + error && error.details && error.details.length > 0 + ? error.details[0].message + : "Error validating data.", + hasIcon: false, + }); } else if (error.response) { - // TODO handle dispatch errors - alert(error.response.msg); + // dispatch errors + createToast({ + variant: "info", + body: error.response.msg, + hasIcon: false, + }); } else { - // TODO handle unknown errors - console.log(error); - alert("Unknown error"); + // unknown errors + createToast({ + variant: "info", + body: "Unknown error.", + hasIcon: false, + }); } } }; + return (
@@ -116,47 +129,65 @@ const Register = () => { alt="Logomark" />
-
Create an account
+
+ Create Uptime Manager admin account +
-
-
-
- +
+ {/* TODO - hook up to form state and run checks */} +
diff --git a/Client/src/Pages/SetNewPassword/index.jsx b/Client/src/Pages/SetNewPassword/index.jsx index d0b4242ee..c06787b17 100644 --- a/Client/src/Pages/SetNewPassword/index.jsx +++ b/Client/src/Pages/SetNewPassword/index.jsx @@ -1,7 +1,6 @@ import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern"; import "./index.css"; import LockIcon from "../../assets/Images/lock-icon.png"; -import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField"; import Check from "../../Components/Check/Check"; import Button from "../../Components/Button"; import LeftArrow from "../../assets/Images/arrow-left.png"; @@ -10,6 +9,7 @@ import { useState, useEffect } from "react"; import { newPasswordValidation } from "../../Validation/validation"; import axiosInstance from "../../Utils/axiosConfig"; import { useNavigate } from "react-router-dom"; +import Field from "../../Components/Inputs/Field"; const SetNewPassword = () => { const navigate = useNavigate(); @@ -83,22 +83,24 @@ const SetNewPassword = () => {
-
-
diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js index 60273cd95..f3ed11fc3 100644 --- a/Client/src/Utils/Theme.js +++ b/Client/src/Utils/Theme.js @@ -91,6 +91,7 @@ const theme = createTheme({ pY: "40px", }, gap: { + xs: "4px", small: "8px", medium: "12px", ml: "16px", @@ -101,7 +102,7 @@ const theme = createTheme({ info: { color: secondaryMain, bg: otherColorsWhite, - border: "#d0d5dd", + border: sectionBorder, }, error: { color: "#d92d20", @@ -109,9 +110,9 @@ const theme = createTheme({ border: "#f04438", }, warning: { - color: "#f79009", - bg: "#fffaef", - border: "#fecf60" + color: "#DC6803", + bg: "#fffcf5", + border: "#fec84b", }, }, }); diff --git a/Client/src/Utils/toastUtils.jsx b/Client/src/Utils/toastUtils.jsx new file mode 100644 index 000000000..73c21c260 --- /dev/null +++ b/Client/src/Utils/toastUtils.jsx @@ -0,0 +1,44 @@ +import PropTypes from "prop-types"; +import { toast } from "react-toastify"; +import Alert from "../Components/Alert"; + +/** + * @param {object} props + * @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error"). + * @param {string} props.title - The title of the alert. + * @param {string} props.body - The body/content of the alert. + * @param {boolean} props.hasIcon - Whether the alert should include an icon. + * @param {object} [props.config] - Additional configuration props for the toast. + */ + +export const createToast = ({ variant, title, body, hasIcon, config = {} }) => { + const toastConfig = { + position: "bottom-right", + autoClose: 3000, + hideProgressBar: true, + closeButton: false, + ...config, + }; + + toast( + ({ closeToast }) => ( + + ), + toastConfig + ); +}; + +createToast.propTypes = { + variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, + title: PropTypes.string, + body: PropTypes.string, + hasIcon: PropTypes.bool, + config: PropTypes.object, +}; diff --git a/Client/src/index.css b/Client/src/index.css index 4af5f4e84..674acc13e 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -59,6 +59,7 @@ --env-var-spacing-1: 12px; --env-var-spacing-1-plus: 16px; + --env-var-spacing-1-minus: 10px; --env-var-spacing-2: 24px; --env-var-spacing-3: 32px; --env-var-spacing-4: 40px; @@ -124,6 +125,18 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +.Toastify__toast-container { + width: auto; +} +.Toastify [class^="Toastify__toast"] { + padding: 0; + margin: 0; +} +.Toastify__toast{ + min-height: 0; + border-radius: 4px; +} + @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 184158e55..979a0b87e 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -60,6 +60,21 @@ const registerController = async (req, res, next) => { return; } + // Check if an admin user exists, if so, error + try { + const admin = await req.db.checkAdmin(req, res); + console.log(admin); + if (admin === true) { + throw new Error(errorMessages.AUTH_ADMIN_EXISTS); + } + } catch (error) { + console.log("WEEEEEEE", error.message); + error.service = SERVICE_NAME; + error.status = 403; + next(error); + return; + } + // Create a new user try { const newUser = await req.db.insertUser(req, res); diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 3cb1711c7..52433c8d7 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -87,12 +87,6 @@ const getMonitorsByUserId = async (req, res, next) => { const userId = req.params.userId; const monitors = await req.db.getMonitorsByUserId(req, res); - if (monitors && monitors.length === 0) { - const err = new Error(errorMessages.MONITOR_GET_BY_USER_ID); - err.status = 404; - throw err; - } - return res.json({ success: true, msg: successMessages.MONITOR_GET_BY_USER_ID(userId), diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 36741711c..9b5a485f8 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -293,19 +293,19 @@ const getMonitorById = async (req, res) => { */ const getMonitorsByUserId = async (req, res) => { try { - const limit = req.body.limit; + const limit = req.query.limit; const monitors = await Monitor.find({ userId: req.params.userId }); // Map each monitor to include its associated checks const monitorsWithChecks = await Promise.all( monitors.map(async (monitor) => { - - if(limit) { + if (limit) { // Checks are order oldest -> newest - const checks = await Check.find({ monitorId: monitor._id }).sort({ - createdAt: 1, - }).limit(limit);; + const checks = await Check.find({ monitorId: monitor._id }) + .sort({ + createdAt: 1, + }) + .limit(limit); return { ...monitor.toObject(), checks }; - } else { // Checks are order oldest -> newest const checks = await Check.find({ monitorId: monitor._id }).sort({ diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index d19aedad9..ae7a5852e 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -5,7 +5,7 @@ const Monitor = require("../models/Monitor"); router.get("/", monitorController.getAllMonitors); router.get("/:monitorId", monitorController.getMonitorById); -router.get("/user/:userId?limit", monitorController.getMonitorsByUserId); +router.get("/user/:userId", monitorController.getMonitorsByUserId); router.post("/", monitorController.createMonitor); router.post( diff --git a/Server/utils/messages.js b/Server/utils/messages.js index aded84d59..787adc8f6 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -4,6 +4,7 @@ const errorMessages = { UNKNOWN_ERROR: "An unknown error occurred", // Auth Controller UNAUTHORIZED: "Unauthorized access", + AUTH_ADMIN_EXISTS: "Admin already exists", //Error handling middleware UNKNOWN_SERVICE: "Unknown service",