From 0d98353625baa7a711689ddc8ed830b1472ba70d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:47:39 +0000 Subject: [PATCH] auth footer --- .../Components/v2/design-elements/Avatar.tsx | 48 +++ .../Components/v2/design-elements/index.tsx | 1 + .../src/Components/v2/sidebar/Authfooter.tsx | 279 ++++++++++++++++++ client/src/Components/v2/sidebar/Menu.tsx | 88 +++++- .../src/Components/v2/sidebar/StarPrompt.tsx | 4 +- client/src/Components/v2/sidebar/index.tsx | 20 +- client/src/locales/en.json | 34 ++- 7 files changed, 454 insertions(+), 20 deletions(-) create mode 100644 client/src/Components/v2/design-elements/Avatar.tsx create mode 100644 client/src/Components/v2/sidebar/Authfooter.tsx diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx new file mode 100644 index 000000000..1adec4741 --- /dev/null +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -0,0 +1,48 @@ +import { Avatar as MuiAvatar } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { useTheme } from "@mui/material"; +import type { RootState } from "@/Types/state"; + +interface AvatarProps { + src?: string; + small?: boolean; + sx?: object; + onClick?: Function; +} + +export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { + const { user } = useSelector((state: RootState) => state.auth); + const theme = useTheme(); + if (!user) return null; + + const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; + const border = small ? 1 : 3; + + const [image, setImage] = useState(); + useEffect(() => { + if (user.avatarImage) { + setImage(`data:image/png;base64,${user.avatarImage}`); + } + }, [user?.avatarImage]); + + return ( + + {user.firstName?.charAt(0)} + {user.lastName?.charAt(0) || ""} + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 9a4b97a44..ed6a885f6 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -20,3 +20,4 @@ export * from "./Tabs"; export * from "./SplitBox"; export * from "./TextLink"; export * from "./OfflineBanner"; +export * from "./Avatar"; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx new file mode 100644 index 000000000..cc84dc0e2 --- /dev/null +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -0,0 +1,279 @@ +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, Icon } from "@/Components/v2/design-elements"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Divider from "@mui/material/Divider"; +import { MoreVertical, LogOut } from "lucide-react"; + +import { useTheme } from "@mui/material"; +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.js"; +import { useDispatch } from "react-redux"; +import PropTypes from "prop-types"; +import type { RootState } from "@/Types/state.js"; + +const getFilteredAccountMenuItems = (user: any, items: any[]) => { + if (!user) return []; + + let filtered = [...items]; + + if (user.role?.includes("demo")) { + filtered = filtered.filter((item) => item.name !== "Password"); + } + + if (!user.role?.includes("superadmin")) { + filtered = filtered.filter((item) => item.name !== "Team"); + } + + return filtered; +}; + +const getRoleDisplayText = (user: any, t: Function) => { + if (!user?.role) return ""; + + if (user.role.includes("superadmin")) return t("roles.superAdmin"); + if (user.role.includes("admin")) return t("roles.admin"); + if (user.role.includes("user")) return t("roles.teamMember"); + if (user.role.includes("demo")) return t("roles.demoUser"); + + return user.role; +}; + +const AuthFooter = ({ + collapsed, + accountMenuItems, +}: { + collapsed: boolean; + accountMenuItems: any[]; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const authState = useSelector((state: RootState) => state.auth); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + + const openPopup = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const closePopup = () => { + setAnchorEl(null); + }; + + const logout = async () => { + dispatch(clearAuthState()); + navigate("/login"); + }; + const renderAccountMenuItems = (user: any, items: any[]) => { + const filteredItems = getFilteredAccountMenuItems(user, items); + + return filteredItems.map((item) => ( + { + closePopup(); + navigate(item.path); + }} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; + return ( + + { + openPopup(e); + }} + sx={{ + cursor: collapsed ? "pointer" : "default", + }} + /> + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {getRoleDisplayText(authState.user, t)} + + + + openPopup(event)} + > + + + + + + {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")} + + + + ); +}; + +AuthFooter.propTypes = { + collapsed: PropTypes.bool, + accountMenuItems: PropTypes.array, +}; + +export default AuthFooter; diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx index 8292ea490..918a82858 100644 --- a/client/src/Components/v2/sidebar/Menu.tsx +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -21,25 +21,53 @@ import { export const getMenu = (t: Function) => { return [ - { name: t("menu.uptime"), path: "uptime", icon: }, - { name: t("menu.pagespeed"), path: "pagespeed", icon: }, { - name: t("menu.infrastructure"), + name: t("components.sidebar.menu.uptime"), + path: "uptime", + icon: , + }, + { + name: t("components.sidebar.menu.pagespeed"), + path: "pagespeed", + icon: , + }, + { + name: t("components.sidebar.menu.infrastructure"), path: "infrastructure", icon: , }, { - name: t("menu.notifications"), + name: t("components.sidebar.menu.notifications"), path: "notifications", icon: , }, - { name: t("menu.checks"), path: "checks", icon: }, - { name: t("menu.incidents"), path: "incidents", icon: }, - { name: t("menu.statusPages"), path: "status", icon: }, - { name: t("menu.maintenance"), path: "maintenance", icon: }, - { name: t("menu.logs"), path: "logs", icon: }, { - name: t("menu.settings"), + name: t("components.sidebar.menu.checks"), + path: "checks", + icon: , + }, + { + name: t("components.sidebar.menu.incidents"), + path: "incidents", + icon: , + }, + { + name: t("components.sidebar.menu.statusPages"), + path: "status", + icon: , + }, + { + name: t("components.sidebar.menu.maintenance"), + path: "maintenance", + icon: , + }, + { + name: t("components.sidebar.menu.logs"), + path: "logs", + icon: , + }, + { + name: t("components.sidebar.menu.settings"), icon: , path: "settings", }, @@ -48,13 +76,45 @@ export const getMenu = (t: Function) => { export const getBottomMenu = (t: Function) => { return [ - { name: t("menu.support"), path: "support", icon: }, { - name: t("menu.discussions"), + name: t("components.sidebar.bottomMenu.support"), + path: "support", + icon: , + }, + { + name: t("components.sidebar.bottomMenu.discussions"), path: "discussions", icon: , }, - { name: t("menu.docs"), path: "docs", icon: }, - { name: t("menu.changelog"), path: "changelog", icon: }, + { + name: t("components.sidebar.bottomMenu.docs"), + path: "docs", + icon: , + }, + { + name: t("components.sidebar.bottomMenu.changelog"), + path: "changelog", + icon: , + }, + ]; +}; + +export const getAccountMenu = (t: Function) => { + return [ + { + name: t("components.sidebar.accountMenu.profile"), + path: "account/profile", + icon: , + }, + { + name: t("components.sidebar.accountMenu.password"), + path: "account/password", + icon: , + }, + { + name: t("components.sidebar.accountMenu.team"), + path: "account/team", + icon: , + }, ]; }; diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx index 665f0a440..5319f6bcc 100644 --- a/client/src/Components/v2/sidebar/StarPrompt.tsx +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -54,7 +54,7 @@ export const StarPrompt = ({ variant="subtitle2" mt={theme.spacing(3)} > - {t("starPromptTitle")} + {t("components.sidebar.starPrompt.title")} - {t("starPromptDescription")} + {t("components.sidebar.starPrompt.description")} { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); const bottomMenu = getBottomMenu(t); + const accountMenu = getAccountMenu(t); useEffect(() => { dispatch(setCollapsed({ collapsed: isSmall })); @@ -109,6 +110,23 @@ export const Sidebar = () => { })} + + {accountMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index a9090e5bc..dd747b5e4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -333,10 +333,38 @@ "reconnected": "Connection restored" }, "sidebar": { - "menu": {}, - "bottomMenu": {} + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastructure", + "notifications": "Notifications", + "checks": "Checks", + "incidents": "Incidents", + "statusPages": "Status pages", + "maintenance": "Maintenance", + "logs": "Logs", + "settings": "Settings" + }, + "bottomMenu": { + "support": "Support", + "discussions": "Discussions", + "docs": "Docs", + "changelog": "Changelog" + }, + "accountMenu": { + "profile": "Profile", + "password": "Password", + "team": "Team" + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "See the latest releases and help grow the community on GitHub" + } }, - "starPrompt": {} + "starPrompt": { + "title": "Star Checkmate", + "description": "See the latest releases and help grow the community on GitHub" + } }, "configure": "Configure", "confirmPassword": "Confirm password",