diff --git a/Client/src/Components/ButtonSpinner/index.jsx b/Client/src/Components/ButtonSpinner/index.jsx index 2cfdaad5d..f584d1cac 100644 --- a/Client/src/Components/ButtonSpinner/index.jsx +++ b/Client/src/Components/ButtonSpinner/index.jsx @@ -18,25 +18,12 @@ const levelConfig = { variant: "contained", color: "error", }, - imagePrimary: { - color: "primary", - variant: "contained", - }, - imageSecondary: { - color: "secondary", - variant: "outlined", - }, - imageTertiary: { - color: "tertiary", - variant: "text", - }, }; /** - * ButtonSpinner component displays a button with loading loadingText capability. * @component * @param {Object} props - * @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The style level of the button. + * @param {'primary' | 'secondary' | 'tertiary' | 'error'} props.level - The style level of the button. * @param {string} props.label - The label text displayed on the button. * @param {React.ReactNode} [props.img] - Icon or image element to display within the button. * @param {boolean} [props.disabled=false] - Determines if the button is disabled. diff --git a/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx new file mode 100644 index 000000000..8343a9819 --- /dev/null +++ b/Client/src/Components/TabPanels/ProfileSettings/PasswordPanel.jsx @@ -0,0 +1,183 @@ +import TabPanel from "@mui/lab/TabPanel"; +import React, { useState } from "react"; +import AnnouncementsDualButtonWithIcon from "../../Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; +import { useTheme } from "@emotion/react"; +import { + Box, + Divider, + FormControl, + IconButton, + InputAdornment, + OutlinedInput, + Stack, + Typography, +} from "@mui/material"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import Visibility from "@mui/icons-material/Visibility"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; +import ButtonSpinner from "../../ButtonSpinner"; + +/** + * PasswordPanel component manages the form for editing password. + * + * @returns {JSX.Element} + */ + +const PasswordPanel = () => { + const theme = useTheme(); + //TODO - use redux loading state + //!! - currently all loading buttons are tied to the same state + const [isLoading, setIsLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + //TODO - implement save password function + const handleSavePassword = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + setIsOpen(false); + }, 2000); + }; + return ( + + + + } + subject="SSO login" + body="Since you logged in via SSO, you cannot reset or modify your password." + /> + + + + + Current Password + + + + + setShowPassword((show) => !show)} + sx={{ + width: "30px", + height: "30px", + "&:focus": { + outline: "none", + }, + }} + > + {!showPassword ? ( + + ) : ( + + )} + + + } + > + + + + + + Password + + + + + setShowPassword((show) => !show)} + sx={{ + width: "30px", + height: "30px", + "&:focus": { + outline: "none", + }, + }} + > + {!showPassword ? ( + + ) : ( + + )} + + + } + > + + + + + + Confirm new password + + + + } + body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol." + /> + + + + + + + + + + + ); +}; + +PasswordPanel.propTypes = { + // No props are being passed to this component, hence no specific PropTypes are defined. + }; + +export default PasswordPanel; diff --git a/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx new file mode 100644 index 000000000..024b1d499 --- /dev/null +++ b/Client/src/Components/TabPanels/ProfileSettings/ProfilePanel.jsx @@ -0,0 +1,294 @@ +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import TabPanel from "@mui/lab/TabPanel"; +import { + Avatar, + Box, + Divider, + Modal, + Stack, + TextField, + Typography, +} from "@mui/material"; +import ButtonSpinner from "../../ButtonSpinner"; +import Button from "../../Button"; + +/** + * ProfilePanel component displays a form for editing user profile information + * and allows for actions like updating profile picture, credentials, + * and deleting account. + * + * @returns {JSX.Element} + */ + +const ProfilePanel = () => { + const theme = useTheme(); + //TODO - use redux loading state + //!! - currently all loading buttons are tied to the same state + const [isLoading, setIsLoading] = useState(false); + //TODO - implement delete profile picture function + const handleDeletePicture = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }; + //TODO - implement update profile function + const handleUpdatePicture = () => {}; + const [isOpen, setIsOpen] = useState(false); + //TODO - implement delete account function + const handleDeleteAccount = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + setIsOpen(false); + }, 2000); + }; + //TODO - implement save profile function + const handleSaveProfile = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + setIsOpen(false); + }, 2000); + }; + + return ( + + + + + + First Name + + + {/* TODO - use existing textfield components */} + + + + + + Last Name + + + {/* TODO - use existing textfield components */} + + + + + + Email + + + After updating, you'll receive a confirmation email. + + + {/* TODO - use existing textfield components */} + + + + + + Your Photo + + + This photo will be displayed in your profile page. + + + + {/* TODO - Use Avatar component instead of @mui */} + + + {/* TODO - modal popup for update pfp? */} + + + + + + + + + + Delete account + + + Note that deleting your account will remove all data from our + system. This is permanent and non-recoverable. + + + setIsOpen(true)} + sx={{ + fontSize: "13px", + "&:focus": { + outline: "none", + }, + }} + /> + + + + + + + + + + + {/* TODO - Update ModalPopup Component with @mui for reusability */} + setIsOpen(false)} + disablePortal + > + + + Really delete this account? + + + If you delete your account, you will no longer be able to sign in, + and all of your data will be deleted. Deleting your account is + permanent and non-recoverable action. + + + setIsOpen(false)} + sx={{ fontSize: "13px" }} + /> + + + + + + ); +}; + +ProfilePanel.propTypes = { + // No props are being passed to this component, hence no specific PropTypes are defined. +}; + +export default ProfilePanel; diff --git a/Client/src/Components/TabPanels/ProfileSettings/TeamPanel.jsx b/Client/src/Components/TabPanels/ProfileSettings/TeamPanel.jsx new file mode 100644 index 000000000..326dfa2ef --- /dev/null +++ b/Client/src/Components/TabPanels/ProfileSettings/TeamPanel.jsx @@ -0,0 +1,569 @@ +import { useTheme } from "@emotion/react"; +import TabPanel from "@mui/lab/TabPanel"; +import { + Box, + Checkbox, + Container, + Divider, + MenuItem, + Modal, + Select, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import ButtonSpinner from "../../ButtonSpinner"; +import Button from "../../Button"; +import { useState } from "react"; + +/** + * TeamPanel component manages the organization and team members, + * providing functionalities like renaming the organization, managing team members, + * and inviting new members. + * + * @returns {JSX.Element} + */ + +const teamColumns = [ + { + id: "checkbox", + label: "", + sx: { minWidth: "20px", width: "40px" }, + }, + { + id: "name", + label: "NAME", + sx: { fontSize: "12px" }, + }, + { id: "email", label: "EMAIL", sx: { fontSize: "12px" } }, + { id: "role", label: "ROLE", sx: { fontSize: "12px" } }, +]; +//for testing, will be removed later +const teamConfig = [ + { + id: 0, + isChecked: false, + name: "John Connor", + email: "john@domain.com", + type: "admin", + role: "Administrator", + createdAt: "10/4/2022", + }, + { + id: 1, + isChecked: false, + name: "Adam McFadden", + email: "adam@domain.com", + type: "member", + role: "Member", + createdAt: "10/4/2022", + }, + { + id: 2, + isChecked: false, + name: "Cris Cross", + email: "cris@domain.com", + type: "member", + role: "Member", + createdAt: "10/4/2022", + }, + { + id: 3, + isChecked: false, + name: "Prince", + email: "prince@domain.com", + type: "member", + role: "Member", + createdAt: "10/4/2022", + }, +]; +const actionsConfig = [ + { + value: "bulk", + label: "Bulk actions", + }, +]; +const roleConfig = [ + { + value: "role", + label: "Change role to", + }, +]; + +const TeamPanel = () => { + const theme = useTheme(); + //TODO - use redux loading state + //!! - currently all loading buttons are tied to the same state + const [isLoading, setIsLoading] = useState(false); + const [orgStates, setOrgStates] = useState({ + name: "Bluewave Labs", + isLoading: false, + isOpen: false, + newName: "", + }); + const toggleOrgModal = (state) => { + setOrgStates((prev) => ({ + ...prev, + isOpen: state, + })); + }; + const toggleOrgLoading = (state) => { + setOrgStates((prev) => ({ + ...prev, + isLoading: state, + })); + }; + const handleRenameOrg = () => { + toggleOrgLoading(true); + + setTimeout(() => { + setOrgStates((prev) => ({ + ...prev, + name: prev.newName !== "" ? prev.newName : prev.name, + isLoading: false, + isOpen: false, + newName: "", + })); + }, 2000); + }; + + const [teamStates, setTeamStates] = useState({ + members: teamConfig, + filter: "", + }); + const handleCheckCell = (id) => { + const updatedTeamStates = [...teamStates.members]; + updatedTeamStates[id] = { + ...updatedTeamStates[id], + isChecked: !updatedTeamStates[id].isChecked, + }; + setTeamStates((prev) => ({ + ...prev, + members: updatedTeamStates, + })); + }; + const handleFilter = (filter) => { + setTeamStates((prev) => ({ + ...prev, + filter: filter, + })); + }; + //TODO - implement select action function + const handleSelectActionType = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }; + //TODO - implement select role function + const handleSelectRoleType = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }; + //TODO - implement save team function + const handleSaveTeam = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }; + const [toggleInviteModal, setToggleInviteModal] = useState(false); + const handleInviteMember = () => {}; + const handleMembersQuery = (type) => { + let count = 0; + teamStates.members.forEach((member) => { + type === "" ? count++ : member.type === type ? count++ : ""; + }); + return count; + }; + return ( + + + + + + Organization name + + + + toggleOrgModal(true)} + isLoading={orgStates.isLoading} + sx={{ fontSize: "13px" }} + /> + toggleOrgModal(true)} + /> + + + + + Team members + + + + + handleFilter("")} sx={{ cursor: "pointer" }}> + All + + {handleMembersQuery("")} + + + handleFilter("admin")} + sx={{ cursor: "pointer" }} + > + Administrator + + {handleMembersQuery("admin")} + + + handleFilter("member")} + sx={{ cursor: "pointer" }} + > + Member + + {handleMembersQuery("member")} + + + + setToggleInviteModal(true)} + /> + + + + + + + {actionsConfig.map((action) => ( + + {action.label} + + ))} + + + + + + {roleConfig.map((role) => ( + + {role.label} + + ))} + + + + + + + + {teamColumns.map((cell) => ( + + {cell.label} + + ))} + + + + {teamStates.members.map((cell) => + teamStates.filter === "" || + teamStates.filter === cell.type ? ( + + + handleCheckCell(cell.id)} + inputProps={{ "aria-label": "controlled" }} + /> + + + + + {cell.name} + + Created at {cell.createdAt} + + + {cell.email} + {cell.role} + + ) : ( + "" + ) + )} + + + + + + + + + + + + toggleOrgModal(false)} + disablePortal + > + + + Rename this organization? + + + setOrgStates((prev) => ({ + ...prev, + newName: event.target.value, + })) + } + > + + toggleOrgModal(false)} + sx={{ fontSize: "13px" }} + /> + + + + + setToggleInviteModal(false)} + disablePortal + > + + + Invite new team member + + + When you add a new team member, they will get access to all + monitors. + + + // setOrgStates((prev) => ({ + // ...prev, + // newName: event.target.value, + // })) + // } + > + + setToggleInviteModal(false)} + sx={{ fontSize: "13px" }} + /> + + + + + + ); +}; + +TeamPanel.propTypes = { + // No props are being passed to this component, hence no specific PropTypes are defined. +}; + +export default TeamPanel; diff --git a/Client/src/Pages/Demo/Demo.jsx b/Client/src/Pages/Demo/Demo.jsx index 583f8bda0..ef6747026 100644 --- a/Client/src/Pages/Demo/Demo.jsx +++ b/Client/src/Pages/Demo/Demo.jsx @@ -390,7 +390,7 @@ const Demo = () => { isLoading={isLoading} /> } @@ -398,7 +398,7 @@ const Demo = () => { isLoading={isLoading} /> } diff --git a/Client/src/Pages/Settings/index.css b/Client/src/Pages/Settings/index.css index e69de29bb..ebf065a7e 100644 --- a/Client/src/Pages/Settings/index.css +++ b/Client/src/Pages/Settings/index.css @@ -0,0 +1,133 @@ +.edit-profile-form, +.edit-password-form, +.delete-profile-form, +.edit-team-form { + width: inherit; +} +.edit-profile-form__wrapper, +.delete-profile-form__wrapper { + width: 90%; +} +.edit-profile-form__wrapper, +.delete-profile-form__wrapper, +.edit-password-form__wrapper, +.edit-team-form__wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; +} +.edit-profile-form__wrapper h1.MuiTypography-root, +.delete-profile-form__wrapper h1.MuiTypography-root, +.edit-password-form__wrapper h1.MuiTypography-root, +.edit-team-form__wrapper h1.MuiTypography-root { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-2); + font-weight: 600; +} +.edit-profile-form__wrapper p.MuiTypography-root, +.delete-profile-form__wrapper p.MuiTypography-root { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-2); + opacity: 0.6; +} +#modal-delete-account, +#modal-edit-org-name, +#modal-invite-member { + font-size: var(--env-var-font-size-large); + color: var(--env-var-color-2); + font-weight: 600; +} +.edit-profile-form__wrapper:not(:first-of-type), +.edit-password-form__wrapper:not(:first-of-type), +.edit-team-form__wrapper:not(:first-of-type) { + margin-top: 40px; +} +.edit-team-form__wrapper.compact { + margin-top: 30px; + color: var(--env-var-color-16); +} +.edit-profile-form__wrapper input, +.edit-password-form__wrapper input, +#edit-organization-name, +#input-team-member { + font-size: var(--env-var-font-size-medium); + padding: 10px; +} + +.edit-password-form__wrapper > .announcement-tile { + width: 100%; +} +.edit-password-form__wrapper + > .announcement-tile + .announcement-content-subject { + margin-bottom: 2px; +} +.edit-password-form__wrapper .announcement-tile { + margin: 0; +} +.edit-password-form__wrapper .announcement-tile .announcement-close { + margin-left: auto; +} +.MuiBox-root > .announcement-tile { + border-color: var(--env-var-color-14); + background-color: var(--lighter-env-var-color-14); +} +.MuiBox-root > .announcement-tile .announcement-content-body, +.MuiBox-root > .announcement-tile .announcement-close svg { + color: var(--env-var-color-15); + fill: var(--env-var-color-15) !important; +} + +#org-name-flex-container .Mui-disabled { + color: var(--env-var-color-2) !important; +} + +.edit-team-form__wrapper td { + padding: 10px 0; + padding-top: 20px; + vertical-align: text-top; +} +.edit-team-form__wrapper th { + padding: 14px 0; +} +.edit-team-form__wrapper th:first-of-type { + padding-left: 20px; +} +.edit-team-form__wrapper td { + font-size: (--env-var-font-size-medium); + color: var(--env-var-color-16); +} + +#select-actions, +#select-role { + padding: 5px 0; + padding-left: 15px; + padding-right: 50px; + line-height: 1.75; +} +.MuiInputBase-root:has(#select-actions) fieldset, +.MuiInputBase-root:has(#select-role) fieldset { + border-color: #a1a7b1; + margin: -1px; + border-width: 1px; +} +.MuiInputBase-root:has(#select-actions):hover fieldset, +.MuiInputBase-root:has(#select-role):hover fieldset { + border-color: var(--env-var-color-2); +} + +.members-query{ + display: inline-flex; + justify-content: center; + align-items: center; + margin-left: 6px; + padding: 2px; + font-size: 12px; + font-weight: 500; + color: var(--env-var-color-3); + background-color: #e0e9fd; + min-width: 22px; + min-height: 22px; + border-radius: 50%; +} \ No newline at end of file diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 639bbef54..c49b08a38 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -1,9 +1,81 @@ -import React from 'react' +import PropTypes from "prop-types"; +import { useState } from "react"; +import { + Box, + Tab, + useTheme, +} from "@mui/material"; +import TabContext from "@mui/lab/TabContext"; +import TabList from "@mui/lab/TabList"; +import "./index.css"; +import ProfilePanel from "../../Components/TabPanels/ProfileSettings/ProfilePanel"; +import PasswordPanel from "../../Components/TabPanels/ProfileSettings/PasswordPanel"; +import TeamPanel from "../../Components/TabPanels/ProfileSettings/TeamPanel"; + +/** + * Settings component renders a settings page with tabs for Profile, Password, and Team settings. + * + * @returns {JSX.Element} + */ + +const tabList = ["Profile", "Password", "Team"]; const Settings = () => { - return ( - Settings - ) -} + const theme = useTheme(); + //(tab) 0 - Profile + //(tab) 1 - Password + //(tab) 2 - Team + const [tab, setTab] = useState("0"); + const handleTabChange = (event, newTab) => { + setTab(newTab); + }; -export default Settings \ No newline at end of file + return ( + + + + + {tabList.map((label, index) => ( + + ))} + + + + + + + + ); +}; + +Settings.propTypes = { + // No props are being passed to this component, hence no specific PropTypes are defined. +}; + +export default Settings; diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js index 1f4e4de96..d2e7cf585 100644 --- a/Client/src/Utils/Theme.js +++ b/Client/src/Utils/Theme.js @@ -25,6 +25,7 @@ const otherColorsPurple = "#7f56d9"; const otherColorsWhite = "#fff"; const otherColorsGraishWhiteLight = "#f2f2f2"; const otherColorsStrongBlue = "#f2f2f2"; +const otherColorsSlateGray = "#667085"; // Global Font Family @@ -69,6 +70,7 @@ const theme = createTheme({ white: otherColorsWhite, graishWhiteLight: otherColorsGraishWhiteLight, strongBlue: otherColorsStrongBlue, + slateGray: otherColorsSlateGray, }, }, font :{ diff --git a/Client/src/index.css b/Client/src/index.css index 092992f09..3f7a64315 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -3,9 +3,9 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; + /* color-scheme: light dark; color: rgba(255, 255, 255, 0.87); - background-color: #242424; + background-color: #242424; */ font-synthesis: none; text-rendering: optimizeLegibility; @@ -28,6 +28,10 @@ --env-var-color-11: #5d6b98; --env-var-color-12: #182230; --env-var-color-13: #f9fafb; + --env-var-color-14: #fecf60; + --lighter-env-var-color-14: rgba(254, 207, 96, 0.1); + --env-var-color-15: #f79009; + --env-var-color-16: #667085; --env-var-radius-1: 4px; --env-var-radius-2: 8px;