From b61a7ffc3c59cfef3ba36ae3e7a95e5c3ed670a4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 31 Jul 2025 15:37:40 -0700 Subject: [PATCH] components --- .../src/Components/Sidebar/collapseButton.jsx | 48 ++++ .../Sidebar/components/authFooter.jsx | 260 ++++++++++++++++++ .../Components/Sidebar/components/navItem.jsx | 89 ++++++ client/src/Components/Sidebar/index.jsx | 176 ++++++------ client/src/Components/Sidebar/logo.jsx | 28 +- client/src/Utils/Theme/globalTheme.js | 11 +- 6 files changed, 510 insertions(+), 102 deletions(-) create mode 100644 client/src/Components/Sidebar/collapseButton.jsx create mode 100644 client/src/Components/Sidebar/components/authFooter.jsx create mode 100644 client/src/Components/Sidebar/components/navItem.jsx diff --git a/client/src/Components/Sidebar/collapseButton.jsx b/client/src/Components/Sidebar/collapseButton.jsx new file mode 100644 index 000000000..551bcf25d --- /dev/null +++ b/client/src/Components/Sidebar/collapseButton.jsx @@ -0,0 +1,48 @@ +import IconButton from "@mui/material/IconButton"; +import ArrowRight from "../../assets/icons/right-arrow.svg?react"; +import ArrowLeft from "../../assets/icons/left-arrow.svg?react"; +import { useTheme } from "@mui/material/styles"; +import { useDispatch } from "react-redux"; +import { toggleSidebar } from "../../Features/UI/uiSlice"; + +const CollapseButton = ({ collapsed, setOpen }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + return ( + { + setOpen((prev) => + Object.fromEntries(Object.keys(prev).map((key) => [key, false])) + ); + dispatch(toggleSidebar()); + }} + > + {collapsed ? : } + + ); +}; + +export default CollapseButton; diff --git a/client/src/Components/Sidebar/components/authFooter.jsx b/client/src/Components/Sidebar/components/authFooter.jsx new file mode 100644 index 000000000..7ff2f36ef --- /dev/null +++ b/client/src/Components/Sidebar/components/authFooter.jsx @@ -0,0 +1,260 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Tooltip from "@mui/material/Tooltip"; +import IconButton from "@mui/material/IconButton"; +import Avatar from "../../Avatar"; +import ThemeSwitch from "../../ThemeSwitch"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Divider from "@mui/material/Divider"; +import DotsVertical from "../../../assets/icons/dots-vertical.svg?react"; +import LogoutSvg from "../../../assets/icons/logout.svg?react"; + +import { useTheme } from "@emotion/react"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { clearAuthState } from "../../../Features/Auth/authSlice"; +import { useDispatch } from "react-redux"; + +const AuthFooter = ({ collapsed, accountMenuItems }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + const [popup, setPopup] = useState(); + + const openPopup = (event, id) => { + setAnchorEl(event.currentTarget); + setPopup(id); + }; + + const closePopup = () => { + setAnchorEl(null); + }; + + const logout = async () => { + // Clear auth state + dispatch(clearAuthState()); + navigate("/login"); + }; + const renderAccountMenuItems = (user, items) => { + let filteredAccountMenuItems = [...items]; + + // If the user is in demo mode, remove the "Password" option + if (user.role?.includes("demo")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Password" + ); + } + + // If the user is NOT a superadmin, remove the "Team" option + if (user.role && !user.role.includes("superadmin")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Team" + ); + } + + return filteredAccountMenuItems.map((item) => ( + { + closePopup(); + navigate(item.path); + }} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; + return ( + + <> + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role?.includes("superadmin") + ? t("roles.superAdmin") + : authState.user?.role?.includes("admin") + ? t("roles.admin") + : authState.user?.role?.includes("user") + ? t("roles.teamMember") + : authState.user?.role?.includes("demo") + ? t("roles.demoUser") + : authState.user?.role} + + + + + + openPopup(event, "logout")} + > + + + + + + + + {collapsed && ( + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + )} + {/* TODO Do we need two dividers? */} + {collapsed && } + {/* */} + {renderAccountMenuItems(authState.user, accountMenuItems)} + + + {t("menu.logOut", "Log out")} + + + + ); +}; + +export default AuthFooter; diff --git a/client/src/Components/Sidebar/components/navItem.jsx b/client/src/Components/Sidebar/components/navItem.jsx new file mode 100644 index 000000000..f43b1bf1e --- /dev/null +++ b/client/src/Components/Sidebar/components/navItem.jsx @@ -0,0 +1,89 @@ +import Tooltip from "@mui/material/Tooltip"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router"; + +const NavItem = ({ item, collapsed, selected, onClick }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + + + + {item.icon} + + + + {item.name} + + + + + ); +}; + +export default NavItem; diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index cad7d3b7d..ac4a68c0f 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -1,21 +1,16 @@ -import IconButton from "@mui/material/IconButton"; import Stack from "@mui/material/Stack"; -import ArrowRight from "../../assets/icons/right-arrow.svg?react"; -import ArrowLeft from "../../assets/icons/left-arrow.svg?react"; -import List from "@mui/material/List"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Tooltip from "@mui/material/Tooltip"; -import Logo from "./logo"; -import ThemeSwitch from "../ThemeSwitch"; -import Avatar from "../Avatar"; +import List from "@mui/material/List"; +import Logo from "./logo"; +import CollapseButton from "./collapseButton"; +import Divider from "@mui/material/Divider"; +import NavItem from "./components/navItem"; +import AuthFooter from "./components/authFooter"; + import StarPrompt from "../StarPrompt"; import LockSvg from "../../assets/icons/lock.svg?react"; import UserSvg from "../../assets/icons/user.svg?react"; import TeamSvg from "../../assets/icons/user-two.svg?react"; -import LogoutSvg from "../../assets/icons/logout.svg?react"; import Support from "../../assets/icons/support.svg?react"; import Maintenance from "../../assets/icons/maintenance.svg?react"; import Monitors from "../../assets/icons/monitors.svg?react"; @@ -25,7 +20,6 @@ import PageSpeed from "../../assets/icons/page-speed.svg?react"; import Settings from "../../assets/icons/settings.svg?react"; import ArrowDown from "../../assets/icons/down-arrow.svg?react"; import ArrowUp from "../../assets/icons/up-arrow.svg?react"; -import DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import StatusPages from "../../assets/icons/status-pages.svg?react"; @@ -35,12 +29,18 @@ import Logs from "../../assets/icons/logs.svg?react"; // Utils import { useTheme } from "@mui/material/styles"; -import { useDispatch, useSelector } from "react-redux"; -import { toggleSidebar } from "../../Features/UI/uiSlice"; +import { useSelector } from "react-redux"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; +const URL_MAP = { + support: "https://discord.com/invite/NAb6H3UTjK", + discussions: "https://github.com/bluewave-labs/checkmate/discussions", + docs: "https://bluewavelabs.gitbook.io/checkmate", + changelog: "https://github.com/bluewave-labs/checkmate/releases", +}; + const getMenu = (t) => [ { name: t("menu.uptime"), path: "uptime", icon: }, { name: t("menu.pagespeed"), path: "pagespeed", icon: }, @@ -64,9 +64,25 @@ const getMenu = (t) => [ }, ]; +const getOtherMenuItems = (t) => [ + { name: t("menu.support"), path: "support", icon: }, + { + name: t("menu.discussions"), + path: "discussions", + icon: , + }, + { name: t("menu.docs"), path: "docs", icon: }, + { name: t("menu.changelog"), path: "changelog", icon: }, +]; + +const getAccountMenuItems = (t) => [ + { name: t("menu.profile"), path: "account/profile", icon: }, + { name: t("menu.password"), path: "account/password", icon: }, + { name: t("menu.team"), path: "account/team", icon: }, +]; + const Sidebar = () => { const theme = useTheme(); - const dispatch = useDispatch(); const { t } = useTranslation(); const navigate = useNavigate(); // Redux state @@ -76,100 +92,82 @@ const Sidebar = () => { const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false }); const menu = getMenu(t); - console.log(collapsed); + const otherMenuItems = getOtherMenuItems(t); + const accountMenuItems = getAccountMenuItems(t); + return ( - { - setOpen((prev) => - Object.fromEntries(Object.keys(prev).map((key) => [key, false])) - ); - dispatch(toggleSidebar()); - }} - > - {collapsed ? : } - - + + {menu.map((item) => { - return item.path ? ( - /* If item has a path */ - - navigate(`/${item.path}`)} - sx={{ - height: "37px", - gap: theme.spacing(4), - borderRadius: theme.shape.borderRadius, - px: theme.spacing(4), - pl: theme.spacing(5), - }} - > - {item.icon} - {!collapsed && {item.name}} - - - ) : null; + item={item} + collapsed={collapsed} + selected={selected} + onClick={() => navigate(`/${item.path}`)} + /> + ); })} + {!collapsed && } + + {otherMenuItems.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + + return ( + { + const url = URL_MAP[item.path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${item.path}`); + } + }} + /> + ); + })} + + + ); }; diff --git a/client/src/Components/Sidebar/logo.jsx b/client/src/Components/Sidebar/logo.jsx index 0a8a76f0c..0d7adf54f 100644 --- a/client/src/Components/Sidebar/logo.jsx +++ b/client/src/Components/Sidebar/logo.jsx @@ -1,10 +1,11 @@ import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router"; import { useTranslation } from "react-i18next"; -const Logo = () => { +const Logo = ({ collapsed }) => { const { t } = useTranslation(); const theme = useTheme(); const navigate = useNavigate(); @@ -35,20 +36,31 @@ const Logo = () => { sx={{ position: "relative", backgroundColor: theme.palette.accent.main, - color: theme.palette.accent.contrastText, borderRadius: theme.shape.borderRadius, userSelect: "none", }} > C - - {t("common.appName")} - + {" "} + + {t("common.appName")} + + ); diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index ec292142d..64ac46fd1 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -233,16 +233,16 @@ const baseTheme = (palette) => ({ }, MuiList: { styleOverrides: { - root: { + root: ({ theme }) => ({ padding: 0, - }, + }), }, }, MuiListItemButton: { styleOverrides: { - root: { - transition: "none", - }, + root: ({ theme }) => ({ + transition: "background-color .3s", + }), }, }, MuiListItemText: { @@ -279,6 +279,7 @@ const baseTheme = (palette) => ({ }), }, }, + MuiTableHead: { styleOverrides: { root: ({ theme }) => ({