From 7ef10b4fcd8ac6af1f29b98c3e8b2de3b3d7269f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 17:53:00 +0000 Subject: [PATCH 01/13] initial commit --- client/src/App.tsx | 2 +- .../Components/v1/Layouts/AppLayout/index.jsx | 31 -------- .../v2/design-elements/OfflineBanner.tsx | 76 +++++++++++++++++++ .../Components/v2/design-elements/index.tsx | 1 + client/src/Components/v2/layout/AppLayout.tsx | 39 ++++++++++ .../src/Components/v2/layout/RootLayout.tsx | 0 client/src/Components/v2/sidebar/index.tsx | 0 client/src/Utils/ApiClient.ts | 15 ++++ client/src/locales/en.json | 6 ++ 9 files changed, 138 insertions(+), 32 deletions(-) delete mode 100644 client/src/Components/v1/Layouts/AppLayout/index.jsx create mode 100644 client/src/Components/v2/design-elements/OfflineBanner.tsx create mode 100644 client/src/Components/v2/layout/AppLayout.tsx create mode 100644 client/src/Components/v2/layout/RootLayout.tsx create mode 100644 client/src/Components/v2/sidebar/index.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index ef3625cad..0a6b364f8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,7 +8,7 @@ import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; import { logger } from "./Utils/Logger"; // Import the logger import { Routes } from "./Routes"; -import AppLayout from "@/Components/v1/Layouts/AppLayout"; +import AppLayout from "@/Components/v2/layout/AppLayout"; import type { RootState } from "@/Types/state"; function App() { diff --git a/client/src/Components/v1/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx deleted file mode 100644 index 0dbfb1e7a..000000000 --- a/client/src/Components/v1/Layouts/AppLayout/index.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Box from "@mui/material/Box"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import BackgroundSVG from "../../../../assets/Images/background.svg"; -import { useSelector } from "react-redux"; - -const AppLayout = ({ children }) => { - const theme = useTheme(); - const ui = useSelector((state) => state.ui); - return ( - - {children} - - ); -}; - -AppLayout.propTypes = { - children: PropTypes.node, -}; - -export default AppLayout; diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx new file mode 100644 index 000000000..fa5690c12 --- /dev/null +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -0,0 +1,76 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { WifiOff } from "lucide-react"; + +interface OfflineBannerProps { + visible: boolean; + onRetry?: () => void; + isRetrying?: boolean; +} + +export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (!visible) { + return null; + } + + return ( + + + + + {t("components.offlineBanner.serverUnreachable")} + + {onRetry && ( + + )} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index b7b1d484e..9a4b97a44 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -19,3 +19,4 @@ export * from "./Gauge"; export * from "./Tabs"; export * from "./SplitBox"; export * from "./TextLink"; +export * from "./OfflineBanner"; diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx new file mode 100644 index 000000000..1310863f9 --- /dev/null +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -0,0 +1,39 @@ +import Box from "@mui/material/Box"; +import { ThemeProvider, useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import BackgroundSVG from "@/assets/Images/background.svg"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; +import { OfflineBanner } from "@/Components/v2/design-elements"; + +interface AppLayoutProps { + children: React.ReactNode; +} + +const AppLayout = ({ children }: AppLayoutProps) => { + const theme = useTheme(); + const mode = useSelector((state: RootState) => state.ui.mode); + const v2theme = mode === "dark" ? darkTheme : lightTheme; + + return ( + + + + {children} + + + ); +}; + +export default AppLayout; diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index c39839558..4b2f4971f 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -15,6 +15,15 @@ type StoreType = { let storeInstance: StoreType | null = null; let interceptorsInitialized = false; +type ServerUnreachableCallback = (unreachable: boolean) => void; +let serverUnreachableCallback: ServerUnreachableCallback | null = null; + +export const setServerUnreachableCallback = ( + callback: ServerUnreachableCallback +): void => { + serverUnreachableCallback = callback; +}; + export const initApiClient = (store: StoreType): void => { storeInstance = store; @@ -41,6 +50,12 @@ export const initApiClient = (store: StoreType): void => { const onSuccess = (response: AxiosResponse) => response; const onError = (error: AxiosError) => { + // Handle network errors (server unreachable) + if (error.code === "ERR_NETWORK") { + serverUnreachableCallback?.(true); + return Promise.reject(error); + } + if (error.response?.status === 401) { if (window.location.pathname !== "/login") { window.location.href = "/login"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbd9ea7b7..096fabd9d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -325,6 +325,12 @@ "week": "Showing statistics for past 7 days.", "month": "Showing statistics for past 30 days." } + }, + "offlineBanner": { + "serverUnreachable": "Unable to reach server", + "retry": "Retry", + "retrying": "Retrying...", + "reconnected": "Connection restored" } }, "configure": "Configure", From d1d96bd2baddfb0eb0cde646e2ec77b765091b43 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 19:21:57 +0000 Subject: [PATCH 02/13] sidebar --- .../v1/Layouts/HomeLayout/index.jsx | 1 - client/src/Components/v2/layout/AppLayout.tsx | 32 ++--- .../src/Components/v2/layout/RootLayout.tsx | 46 ++++++++ client/src/Components/v2/sidebar/Logo.tsx | 60 ++++++++++ client/src/Components/v2/sidebar/Menu.tsx | 41 +++++++ client/src/Components/v2/sidebar/NavItem.tsx | 110 ++++++++++++++++++ client/src/Components/v2/sidebar/index.tsx | 69 +++++++++++ client/src/Hooks/useSidebar.js | 29 ----- client/src/Hooks/useSidebar.ts | 25 ++++ client/src/Routes/index.jsx | 4 +- 10 files changed, 369 insertions(+), 48 deletions(-) create mode 100644 client/src/Components/v2/sidebar/Logo.tsx create mode 100644 client/src/Components/v2/sidebar/Menu.tsx create mode 100644 client/src/Components/v2/sidebar/NavItem.tsx delete mode 100644 client/src/Hooks/useSidebar.js create mode 100644 client/src/Hooks/useSidebar.ts diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index b078fe048..adadf638f 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -14,7 +14,6 @@ const HomeLayout = () => { flexDirection="row" > - {/* Spacer for fixed sidebar */} { const v2theme = mode === "dark" ? darkTheme : lightTheme; return ( - - + + - {children} - - + + {children} + ); }; diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx index e69de29bb..b0c0effe1 100644 --- a/client/src/Components/v2/layout/RootLayout.tsx +++ b/client/src/Components/v2/layout/RootLayout.tsx @@ -0,0 +1,46 @@ +import { Sidebar } from "@/Components/v2/sidebar"; +import { Outlet } from "react-router"; +import Stack from "@mui/material/Stack"; + +// TEMP +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; +import { ThemeProvider, useTheme } from "@mui/material"; + +const RootLayout = () => { + const mode = useSelector((state: RootState) => state.ui.mode); + const v2theme = mode === "dark" ? darkTheme : lightTheme; + const theme = useTheme(); + return ( + + + + + + + + + + + ); +}; + +export default RootLayout; diff --git a/client/src/Components/v2/sidebar/Logo.tsx b/client/src/Components/v2/sidebar/Logo.tsx new file mode 100644 index 000000000..131ac03bc --- /dev/null +++ b/client/src/Components/v2/sidebar/Logo.tsx @@ -0,0 +1,60 @@ +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 { toggleSidebar } from "@/Features/UI/uiSlice.js"; +import { useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import useSidebar from "@/Hooks/useSidebar"; + +export const Logo = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useDispatch(); + const { collapsed } = useSidebar(); + return ( + { + dispatch(toggleSidebar()); + }} + sx={{ cursor: "pointer" }} + > + + C + + + {" "} + + {t("common.appName")} + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx new file mode 100644 index 000000000..ad58f4455 --- /dev/null +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -0,0 +1,41 @@ +import { Icon } from "@/Components/v2/design-elements"; + +import { + Globe, + Gauge, + Link, + Bell, + FileText, + AlertTriangle, + Wifi, + Wrench, + Database, + Settings, +} from "lucide-react"; + +export const getMenu = (t: Function) => { + return [ + { name: t("menu.uptime"), path: "uptime", icon: }, + { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + { + name: t("menu.infrastructure"), + path: "infrastructure", + icon: , + }, + { + name: t("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"), + icon: , + path: "settings", + }, + ]; +}; diff --git a/client/src/Components/v2/sidebar/NavItem.tsx b/client/src/Components/v2/sidebar/NavItem.tsx new file mode 100644 index 000000000..756b10d60 --- /dev/null +++ b/client/src/Components/v2/sidebar/NavItem.tsx @@ -0,0 +1,110 @@ +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 "@mui/material/styles"; +import useSidebar from "@/Hooks/useSidebar"; + +export interface NavData { + name: string; + icon: React.ReactElement; +} + +export const NavItem = ({ + item, + selected, + onClick, +}: { + item: NavData; + selected: boolean; + onClick: (event: React.MouseEvent) => void; +}) => { + const { collapsed } = useSidebar(); + const theme = useTheme(); + const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary; + + const buttonBgColor = selected ? theme.palette.action.selected : "transparent"; + const buttonBgHoverColor = selected + ? theme.palette.action.selected + : theme.palette.action.hover; + const fontWeight = selected ? 600 : 400; + return ( + + + + {item.icon} + + + + {item.name} + + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index e69de29bb..10725c6a0 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import List from "@mui/material/List"; +import { useSidebar } from "@/Hooks/useSidebar.js"; +import { Logo } from "@/Components/v2/sidebar/Logo"; +import { getMenu } from "@/Components/v2/sidebar/Menu"; +import { NavItem } from "@/Components/v2/sidebar/NavItem"; + +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { useTheme, useMediaQuery } from "@mui/material"; +import { useDispatch } from "react-redux"; +import { toggleSidebar } from "@/Features/UI/uiSlice"; + +export const Sidebar = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const { width, transition } = useSidebar(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const menu = getMenu(t); + + return ( + t.zIndex.drawer : "auto", + }} + > + + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + { + navigate(`/${item.path}`); + if (isSmall) { + dispatch(toggleSidebar()); + } + }} + /> + ); + })} + + + ); +}; diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js deleted file mode 100644 index 97724b417..000000000 --- a/client/src/Hooks/useSidebar.js +++ /dev/null @@ -1,29 +0,0 @@ -import { useSelector } from "react-redux"; - -// CSS variable names for sidebar widths -const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)"; -const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)"; - -// Transition timing for sidebar width changes -const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; - -/** - * Hook to get sidebar state and computed width - * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout - * - * @returns {Object} Sidebar state and styles - * @returns {boolean} collapsed - Whether the sidebar is collapsed - * @returns {string} width - CSS width value based on collapsed state - * @returns {string} transition - CSS transition for width changes - */ -export const useSidebar = () => { - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); - - return { - collapsed, - width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, - transition: SIDEBAR_TRANSITION, - }; -}; - -export default useSidebar; diff --git a/client/src/Hooks/useSidebar.ts b/client/src/Hooks/useSidebar.ts new file mode 100644 index 000000000..1cf705dd1 --- /dev/null +++ b/client/src/Hooks/useSidebar.ts @@ -0,0 +1,25 @@ +import type { RootState } from "@/Types/state"; +import { useSelector } from "react-redux"; + +// CSS variable names for sidebar widths +const SIDEBAR_WIDTH_VAR = 250; +const SIDEBAR_COLLAPSED_WIDTH_VAR = 64; + +// Transition timing for sidebar width changes +const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; + +export const useSidebar = () => { + const collapsed = useSelector( + (state: RootState) => state.ui.sidebar?.collapsed ?? false + ); + + return { + collapsed, + collapsedWidth: SIDEBAR_COLLAPSED_WIDTH_VAR, + expandedWidth: SIDEBAR_WIDTH_VAR, + width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, + transition: SIDEBAR_TRANSITION, + }; +}; + +export default useSidebar; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 07dc1793c..0a66b0cf3 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -4,7 +4,7 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; import { useSelector } from "react-redux"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; -import HomeLayout from "@/Components/v1/Layouts/HomeLayout"; +import RootLayout from "@/Components/v2/layout/RootLayout"; import NotFound from "@/Pages/NotFound"; // Auth @@ -67,7 +67,7 @@ const Routes = () => { path="/" element={ - + } > From 1db5f4b70f946e0f69ad0a631667cb5d0be46aa1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 21:54:31 +0000 Subject: [PATCH 03/13] sidebar --- .../src/Components/v2/layout/RootLayout.tsx | 7 ++++++- client/src/Components/v2/sidebar/Logo.tsx | 8 ++++---- client/src/Components/v2/sidebar/index.tsx | 19 ++++++++++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx index b0c0effe1..0b6defb91 100644 --- a/client/src/Components/v2/layout/RootLayout.tsx +++ b/client/src/Components/v2/layout/RootLayout.tsx @@ -1,8 +1,9 @@ import { Sidebar } from "@/Components/v2/sidebar"; import { Outlet } from "react-router"; import Stack from "@mui/material/Stack"; +import { useMediaQuery } from "@mui/material"; +import { useSidebar } from "@/Hooks/useSidebar"; -// TEMP import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; @@ -12,6 +13,9 @@ const RootLayout = () => { const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "dark" ? darkTheme : lightTheme; const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const { collapsedWidth } = useSidebar(); + return ( @@ -28,6 +32,7 @@ const RootLayout = () => { : "rgba(0, 0, 0, 0.01)", display: "flex", alignItems: "center", + paddingLeft: isSmall ? `${collapsedWidth + 12}px` : 12, }} > { +export const Logo = (props: StackProps) => { const { t } = useTranslation(); const theme = useTheme(); const dispatch = useDispatch(); const { collapsed } = useSidebar(); return ( { dispatch(toggleSidebar()); }} sx={{ cursor: "pointer" }} + {...props} > { const dispatch = useDispatch(); const navigate = useNavigate(); + const location = useLocation(); const { t } = useTranslation(); const { width, transition } = useSidebar(); const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); + useEffect(() => { + dispatch(setCollapsed({ collapsed: isSmall })); + }, [isSmall, dispatch]); + return ( { height: "100%", }} > - + {menu.map((item) => { const selected = location.pathname.startsWith(`/${item.path}`); return ( @@ -57,7 +66,7 @@ export const Sidebar = () => { onClick={() => { navigate(`/${item.path}`); if (isSmall) { - dispatch(toggleSidebar()); + dispatch(setCollapsed({ collapsed: true })); } }} /> From 0f6c273dad4cae9d594edbf8d777bbdbdcde02d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:17:13 +0000 Subject: [PATCH 04/13] bottom menu --- client/src/Components/v2/sidebar/Menu.tsx | 19 ++++++++ client/src/Components/v2/sidebar/index.tsx | 50 ++++++++++++++++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx index ad58f4455..8292ea490 100644 --- a/client/src/Components/v2/sidebar/Menu.tsx +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -11,6 +11,12 @@ import { Wrench, Database, Settings, + HelpCircle, + MessageCircle, + Code, + User, + Lock, + Users, } from "lucide-react"; export const getMenu = (t: Function) => { @@ -39,3 +45,16 @@ export const getMenu = (t: Function) => { }, ]; }; + +export const getBottomMenu = (t: Function) => { + return [ + { 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: }, + ]; +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index 568703791..e278092eb 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -1,9 +1,10 @@ import { useEffect } from "react"; import Stack from "@mui/material/Stack"; import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; import { useSidebar } from "@/Hooks/useSidebar.js"; import { Logo } from "@/Components/v2/sidebar/Logo"; -import { getMenu } from "@/Components/v2/sidebar/Menu"; +import { getMenu, getBottomMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; import { useNavigate, useLocation } from "react-router"; @@ -12,6 +13,13 @@ import { useTheme, useMediaQuery } from "@mui/material"; import { useDispatch } from "react-redux"; import { setCollapsed } from "@/Features/UI/uiSlice"; +const URL_MAP: Record = { + 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", +}; + export const Sidebar = () => { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -21,11 +29,24 @@ export const Sidebar = () => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); + const bottomMenu = getBottomMenu(t); useEffect(() => { dispatch(setCollapsed({ collapsed: isSmall })); }, [isSmall, dispatch]); + const handleNavClick = (path: string) => { + const url = URL_MAP[path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${path}`); + } + if (isSmall) { + dispatch(setCollapsed({ collapsed: true })); + } + }; + return ( { disablePadding sx={{ px: theme.spacing(6), - height: "100%", + flex: 1, }} > { key={item.path} item={item} selected={selected} - onClick={() => { - navigate(`/${item.path}`); - if (isSmall) { - dispatch(setCollapsed({ collapsed: true })); - } - }} + onClick={() => handleNavClick(item.path)} /> ); })} + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + ); }; From a289a197a11b8a760ec43da8dd6eb3db70f9fbd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:24:35 +0000 Subject: [PATCH 05/13] starprompt --- .../src/Components/v2/sidebar/StarPrompt.tsx | 115 ++++++++++++++++++ client/src/Components/v2/sidebar/index.tsx | 2 + 2 files changed, 117 insertions(+) create mode 100644 client/src/Components/v2/sidebar/StarPrompt.tsx diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx new file mode 100644 index 000000000..cacc442cc --- /dev/null +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -0,0 +1,115 @@ +import { Typography, IconButton, Stack, Box } from "@mui/material"; +import { Icon } from "@/Components/v2/design-elements"; +import { X } from "lucide-react"; + +import { useTheme } from "@mui/material"; +import { useSelector, useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { setStarPromptOpen } from "@/Features/UI/uiSlice.js"; +import type { RootState } from "@/Types/state.js"; +import useSidebar from "@/Hooks/useSidebar.js"; + +export const StarPrompt = ({ + repoUrl = "https://github.com/bluewave-labs/checkmate", +}: { + repoUrl?: string; +}) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const isOpen = useSelector((state: RootState) => state.ui?.starPromptOpen ?? true); + const mode = useSelector((state: RootState) => state.ui.mode); + const { collapsed } = useSidebar(); + const handleClose = () => { + dispatch(setStarPromptOpen(false)); + }; + + const handleStarClick = () => { + window.open(repoUrl, "_blank"); + }; + + if (collapsed) return null; + if (!isOpen) return null; + + return ( + + + + {t("starPromptTitle")} + + + + + + + + {t("starPromptDescription")} + + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index e278092eb..e3056c9f4 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -6,6 +6,7 @@ import { useSidebar } from "@/Hooks/useSidebar.js"; import { Logo } from "@/Components/v2/sidebar/Logo"; import { getMenu, getBottomMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; +import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt"; import { useNavigate, useLocation } from "react-router"; import { useTranslation } from "react-i18next"; @@ -89,6 +90,7 @@ export const Sidebar = () => { ); })} + Date: Tue, 10 Feb 2026 22:28:47 +0000 Subject: [PATCH 06/13] styling --- client/src/Components/v2/sidebar/StarPrompt.tsx | 12 +----------- client/src/locales/en.json | 7 ++++++- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx index cacc442cc..665f0a440 100644 --- a/client/src/Components/v2/sidebar/StarPrompt.tsx +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -52,13 +52,7 @@ export const StarPrompt = ({ > {t("starPromptTitle")} @@ -68,7 +62,6 @@ export const StarPrompt = ({ sx={{ color: theme.palette.text.primary, padding: 0, - marginTop: theme.spacing(-5), "&:hover": { backgroundColor: "transparent", opacity: 0.8, @@ -83,10 +76,7 @@ export const StarPrompt = ({ Date: Tue, 10 Feb 2026 22:47:39 +0000 Subject: [PATCH 07/13] 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", From a390ee5f21343d308fbfb48ebbecde5ae8a67d32 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:54:23 +0000 Subject: [PATCH 08/13] auth footer --- .../Components/v2/design-elements/Avatar.tsx | 3 +-- .../src/Components/v2/sidebar/Authfooter.tsx | 11 -------- client/src/Components/v2/sidebar/index.tsx | 25 ++++++------------- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx index 1adec4741..74c06f8f3 100644 --- a/client/src/Components/v2/design-elements/Avatar.tsx +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -8,7 +8,7 @@ interface AvatarProps { src?: string; small?: boolean; sx?: object; - onClick?: Function; + onClick?: (event: React.MouseEvent) => void; } export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { @@ -17,7 +17,6 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { 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(() => { diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index cc84dc0e2..3194cf44a 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -6,7 +6,6 @@ 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"; @@ -97,7 +96,6 @@ const AuthFooter = ({ return ( @@ -240,7 +233,6 @@ const AuthFooter = ({ fontSize: 12, whiteSpace: "nowrap", overflow: "visible", - // wordBreak: "break-word", }} > {authState.user?.role} @@ -248,9 +240,6 @@ const AuthFooter = ({ )} - {/* TODO Do we need two dividers? */} - {collapsed && } - {/* */} {renderAccountMenuItems(authState.user, accountMenuItems)} { const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation(); - const { width, transition } = useSidebar(); + const { width, transition, collapsed } = useSidebar(); const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); @@ -110,23 +111,11 @@ export const Sidebar = () => { })} - - {accountMenu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - + + ); }; From 4c5c2d2d5e74ad275270a77d4a081cf412523a09 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:12:33 +0000 Subject: [PATCH 09/13] auth footer --- .../Components/v2/design-elements/Icon.tsx | 1 + .../src/Components/v2/sidebar/Authfooter.tsx | 27 +++++++++++-------- client/src/Components/v2/sidebar/index.tsx | 2 +- client/src/locales/en.json | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx index b32cd84b2..22f939750 100644 --- a/client/src/Components/v2/design-elements/Icon.tsx +++ b/client/src/Components/v2/design-elements/Icon.tsx @@ -4,6 +4,7 @@ interface IconProps { icon: LucideIcon; size?: number; strokeWidth?: number; + stroke?: string; } const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 3194cf44a..d63663a01 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -15,7 +15,6 @@ 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[]) => { @@ -45,7 +44,7 @@ const getRoleDisplayText = (user: any, t: Function) => { return user.role; }; -const AuthFooter = ({ +export const AuthFooter = ({ collapsed, accountMenuItems, }: { @@ -86,6 +85,9 @@ const AuthFooter = ({ gap: theme.spacing(2), borderRadius: theme.shape.borderRadius, pl: theme.spacing(4), + "& svg": { + stroke: theme.palette.text.secondary, + }, }} > {item.icon} @@ -140,6 +142,7 @@ const AuthFooter = ({ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", + color: theme.palette.text.primary, }} > {authState.user?.firstName} {authState.user?.lastName} @@ -148,7 +151,10 @@ const AuthFooter = ({ textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap" - sx={{ textTransform: "capitalize", opacity: 0.8 }} + sx={{ + textTransform: "capitalize", + color: theme.palette.text.secondary, + }} > {getRoleDisplayText(authState.user, t)} @@ -162,6 +168,9 @@ const AuthFooter = ({ ml: "50px", "&:focus": { outline: "none" }, alignSelf: "center", + "& svg": { + stroke: theme.palette.text.secondary, + }, }} onClick={(event) => openPopup(event)} > @@ -244,9 +253,12 @@ const AuthFooter = ({ ); }; - -AuthFooter.propTypes = { - collapsed: PropTypes.bool, - accountMenuItems: PropTypes.array, -}; - -export default AuthFooter; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index deb2730e2..9bd36a853 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -7,7 +7,7 @@ import { Logo } from "@/Components/v2/sidebar/Logo"; import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt"; -import AuthFooter from "@/Components/v2/sidebar/Authfooter"; +import { AuthFooter } from "@/Components/v2/sidebar/Authfooter"; import { useNavigate, useLocation } from "react-router"; import { useTranslation } from "react-i18next"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index dd747b5e4..314ba7e50 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -359,7 +359,8 @@ "starPrompt": { "title": "Star Checkmate", "description": "See the latest releases and help grow the community on GitHub" - } + }, + "authFooter": {} }, "starPrompt": { "title": "Star Checkmate", From 71f3c904722da8f82c326fd32300661d81fc2c89 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:16:12 +0000 Subject: [PATCH 10/13] strings --- client/src/Components/v2/sidebar/Authfooter.tsx | 15 +++++++++------ client/src/locales/en.json | 11 ++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index d63663a01..2bebb49b2 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -36,10 +36,13 @@ const getFilteredAccountMenuItems = (user: any, items: any[]) => { 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"); + if (user.role.includes("superadmin")) + return t("components.sidebar.authFooter.roles.superAdmin"); + if (user.role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); + if (user.role.includes("user")) + return t("components.sidebar.authFooter.roles.teamMember"); + if (user.role.includes("demo")) + return t("components.sidebar.authFooter.roles.demoUser"); return user.role; }; @@ -160,7 +163,7 @@ export const AuthFooter = ({ - {t("menu.logOut", "Log out")} + {t("components.sidebar.authFooter.logOut")} diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 314ba7e50..b49ebaa43 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -360,7 +360,16 @@ "title": "Star Checkmate", "description": "See the latest releases and help grow the community on GitHub" }, - "authFooter": {} + "authFooter": { + "navControls": "Controls", + "logOut": "Log out", + "roles": { + "superAdmin": "Super admin", + "admin": "Admin", + "teamMember": "Team member", + "demoUser": "Demo user" + } + } }, "starPrompt": { "title": "Star Checkmate", From 442fc2ce22d37f1c77561f16bedb5ed17912ce9e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:24:51 +0000 Subject: [PATCH 11/13] account tab fix --- .../src/Components/v2/sidebar/Authfooter.tsx | 315 ++++++------------ client/src/Pages/Account/index.tsx | 7 +- 2 files changed, 117 insertions(+), 205 deletions(-) diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 2bebb49b2..4b369cb67 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -1,103 +1,75 @@ 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 { MoreVertical, LogOut } from "lucide-react"; - import { useTheme } from "@mui/material"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useNavigate } from "react-router"; +import { MoreVertical, LogOut } from "lucide-react"; +import { Avatar, Icon } from "@/Components/v2/design-elements"; import { clearAuthState } from "@/Features/Auth/authSlice.js"; -import { useDispatch } from "react-redux"; 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("components.sidebar.authFooter.roles.superAdmin"); - if (user.role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); - if (user.role.includes("user")) - return t("components.sidebar.authFooter.roles.teamMember"); - if (user.role.includes("demo")) - return t("components.sidebar.authFooter.roles.demoUser"); - - return user.role; -}; - -export const AuthFooter = ({ - collapsed, - accountMenuItems, -}: { +interface AuthFooterProps { collapsed: boolean; - accountMenuItems: any[]; -}) => { + accountMenuItems: Array<{ + name: string; + path: string; + icon: React.ReactNode; + }>; +} + +export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => { const { t } = useTranslation(); const theme = useTheme(); - const authState = useSelector((state: RootState) => state.auth); + const user = useSelector((state: RootState) => state.auth.user); const navigate = useNavigate(); const dispatch = useDispatch(); - const [anchorEl, setAnchorEl] = useState(null); - const openPopup = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); + const menuOpen = Boolean(anchorEl); + const handleMenuOpen = (e: React.MouseEvent) => + setAnchorEl(e.currentTarget); + const handleMenuClose = () => setAnchorEl(null); + + const handleNavigate = (path: string) => { + handleMenuClose(); + navigate(path); }; - const closePopup = () => { - setAnchorEl(null); - }; - - const logout = async () => { + const handleLogout = () => { 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), - "& svg": { - stroke: theme.palette.text.secondary, - }, - }} - > - {item.icon} - {item.name} - - )); + const getRoleText = () => { + const role = user?.role ?? ""; + if (role.includes("superadmin")) + return t("components.sidebar.authFooter.roles.superAdmin"); + if (role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); + if (role.includes("user")) return t("components.sidebar.authFooter.roles.teamMember"); + if (role.includes("demo")) return t("components.sidebar.authFooter.roles.demoUser"); + return role; }; + + const filteredMenuItems = accountMenuItems.filter((item) => { + if (!user) return false; + if (item.name === "Password" && user.role?.includes("demo")) return false; + if (item.name === "Team" && !user.role?.includes("superadmin")) return false; + return true; + }); + + const menuItemSx = { + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + "& svg": { stroke: theme.palette.text.secondary }, + }; + return ( { - openPopup(e); - }} - sx={{ - cursor: collapsed ? "pointer" : "default", - }} + small + onClick={collapsed ? handleMenuOpen : undefined} + sx={{ cursor: collapsed ? "pointer" : "default" }} /> - + {!collapsed && ( - - {authState.user?.firstName} {authState.user?.lastName} - - - {getRoleDisplayText(authState.user, t)} - - - + + {user?.firstName} {user?.lastName} + + + {getRoleText()} + + openPopup(event)} > - - + + )} + {collapsed && ( - - + - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - + {user?.firstName} {user?.lastName} + + + {getRoleText()} + + )} - {renderAccountMenuItems(authState.user, accountMenuItems)} + {filteredMenuItems.map((item) => ( + handleNavigate(item.path)} + sx={menuItemSx} + > + {item.icon} + {item.name} + + ))} { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(TAB_MAP[open]); + // Sync activeTab when open prop changes (e.g., navigating from sidebar) + useEffect(() => { + setActiveTab(TAB_MAP[open]); + }, [open]); + return ( Date: Tue, 10 Feb 2026 23:32:58 +0000 Subject: [PATCH 12/13] cleanup --- client/src/Components/v1/ArrowLeft/index.jsx | 36 --- client/src/Components/v1/Avatar/index.css | 0 client/src/Components/v1/Avatar/index.jsx | 73 ----- client/src/Components/v1/Label/index.css | 6 - client/src/Components/v1/Label/index.jsx | 172 ----------- .../v1/Layouts/HomeLayout/index.css | 29 -- .../v1/Layouts/HomeLayout/index.jsx | 31 -- .../v1/Sidebar/components/authFooter.jsx | 276 ------------------ .../v1/Sidebar/components/collapseButton.jsx | 55 ---- .../Components/v1/Sidebar/components/logo.jsx | 67 ----- .../v1/Sidebar/components/navItem.jsx | 96 ------ client/src/Components/v1/Sidebar/index.jsx | 152 ---------- client/src/Components/v1/StarPrompt/index.jsx | 110 ------- .../src/Components/v2/sidebar/Authfooter.tsx | 2 +- client/src/locales/en.json | 2 +- 15 files changed, 2 insertions(+), 1105 deletions(-) delete mode 100644 client/src/Components/v1/ArrowLeft/index.jsx delete mode 100644 client/src/Components/v1/Avatar/index.css delete mode 100644 client/src/Components/v1/Avatar/index.jsx delete mode 100644 client/src/Components/v1/Label/index.css delete mode 100644 client/src/Components/v1/Label/index.jsx delete mode 100644 client/src/Components/v1/Layouts/HomeLayout/index.css delete mode 100644 client/src/Components/v1/Layouts/HomeLayout/index.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/authFooter.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/collapseButton.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/logo.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/navItem.jsx delete mode 100644 client/src/Components/v1/Sidebar/index.jsx delete mode 100644 client/src/Components/v1/StarPrompt/index.jsx diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx deleted file mode 100644 index e18a7f2d5..000000000 --- a/client/src/Components/v1/ArrowLeft/index.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import Icon from "../Icon"; -import PropTypes from "prop-types"; - -const ArrowLeft = ({ type, color = "#667085", ...props }) => { - if (type === "double") { - return ( - - ); - } else if (type === "long") { - return ( - - ); - } else { - return ( - - ); - } -}; - -ArrowLeft.propTypes = { - color: PropTypes.string, - type: PropTypes.oneOf(["double", "long", "default"]), -}; -export default ArrowLeft; diff --git a/client/src/Components/v1/Avatar/index.css b/client/src/Components/v1/Avatar/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx deleted file mode 100644 index d089080ff..000000000 --- a/client/src/Components/v1/Avatar/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Avatar as MuiAvatar } from "@mui/material"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -/** - * @component - * @param {Object} props - * @param {string} props.src - Path to image for avatar - * @param {boolean} props.small - Specifies if avatar should be large - * @param {Object} [props.sx] - Additional styles to apply to the button. - * @returns {JSX.Element} - * @example - * // Render a red label - * - */ - -const Avatar = ({ src, small, sx, onClick = () => {} }) => { - 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; - - 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) || ""} - - ); -}; - -Avatar.propTypes = { - src: PropTypes.string, - small: PropTypes.bool, - sx: PropTypes.object, - onClick: PropTypes.func, -}; - -export default Avatar; diff --git a/client/src/Components/v1/Label/index.css b/client/src/Components/v1/Label/index.css deleted file mode 100644 index d3fe53da5..000000000 --- a/client/src/Components/v1/Label/index.css +++ /dev/null @@ -1,6 +0,0 @@ -.label { - display: inline-flex; - justify-content: center; - align-items: center; - line-height: normal; -} diff --git a/client/src/Components/v1/Label/index.jsx b/client/src/Components/v1/Label/index.jsx deleted file mode 100644 index 2ed9b93bf..000000000 --- a/client/src/Components/v1/Label/index.jsx +++ /dev/null @@ -1,172 +0,0 @@ -import PropTypes from "prop-types"; -import { Box } from "@mui/material"; -import { useTheme } from "@mui/material"; -import "./index.css"; - -/** - * @typedef {Object} Styles - * @param {string} [color] - The text color - * @param {string} [backgroundColor] - The background color - * @param {string} [borderColor] - The border color - */ - -/** - * @component - * @param {Object} props - * @param {string} props.label - The label of the label - * @param {Styles} props.styles - CSS Styles passed from parent component - * @param {React.ReactNode} children - Children passed from parent component - * @returns {JSX.Element} - */ - -const BaseLabel = ({ label, styles, children }) => { - const theme = useTheme(); - // Grab the default borderRadius from the theme to match button style - const { borderRadius } = theme.shape; - // Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though. - const padding = theme.spacing(3, 5); - - return ( - - {children} - {label} - - ); -}; - -BaseLabel.propTypes = { - label: PropTypes.string.isRequired, - styles: PropTypes.shape({ - color: PropTypes.string, - backgroundColor: PropTypes.string, - }), - children: PropTypes.node, -}; - -// Produces a lighter color based on a hex color and a percent -// lightenColor("#067647", 20) will produce a color 20% lighter than #067647 -const lightenColor = (color, percent) => { - let r = parseInt(color.substring(1, 3), 16); - let g = parseInt(color.substring(3, 5), 16); - let b = parseInt(color.substring(5, 7), 16); - - const amt = Math.round((255 * percent) / 100); - - r = r + amt <= 255 ? r + amt : 255; - g = g + amt <= 255 ? g + amt : 255; - b = b + amt <= 255 ? b + amt : 255; - - r = r.toString(16).padStart(2, "0"); - g = g.toString(16).padStart(2, "0"); - b = b.toString(16).padStart(2, "0"); - - return `#${r}${g}${b}`; -}; - -/** - * @component - * @param {Object} props - * @param {string} props.label - The label of the label - * @param {string} props.color - The color of the label, specified in #RRGGBB format - * @returns {JSX.Element} - * @example - * // Render a red label - * - */ - -const ColoredLabel = ({ label, color }) => { - const theme = useTheme(); - // If an invalid color is passed, default to the labelGray color - if (typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) { - color = theme.palette.primary.lowContrast; - } - - // Calculate lighter shades for border and bg - const borderColor = lightenColor(color, 20); - const bgColor = lightenColor(color, 75); - - return ( - - ); -}; - -ColoredLabel.propTypes = { - label: PropTypes.string.isRequired, - color: PropTypes.string.isRequired, -}; - -/** - * @component - * @param {Object} props - * @param {'up' | 'down' | 'paused' | 'pending' | 'cannot resolve' | 'published' | 'unpublished'} props.status - The status for the label - * @param {string} props.text - The text of the label - * @returns {JSX.Element} - * @example - * // Render an active label - * - */ - -const statusToTheme = { - up: "success", - down: "error", - paused: "warning", - pending: "warning", - "cannot resolve": "error", - published: "success", - unpublished: "error", -}; - -const StatusLabel = ({ status, text, customStyles }) => { - const theme = useTheme(); - - const themeColor = statusToTheme[status]; - - return ( - - - - ); -}; - -StatusLabel.propTypes = { - status: PropTypes.oneOf([ - "up", - "down", - "paused", - "pending", - "cannot resolve", - "published", - "unpublished", - ]), - text: PropTypes.string, - customStyles: PropTypes.object, -}; - -export { BaseLabel, ColoredLabel, StatusLabel }; diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css deleted file mode 100644 index 61808aa57..000000000 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ /dev/null @@ -1,29 +0,0 @@ -.home-layout { - position: relative; - min-height: 100vh; - margin: 0 auto; - padding: 0; - overflow-x: hidden; - width: 100%; -} - -/* TODO go for this approach for responsiveness. The aside needs to be taken care of */ -/* @media (max-width: 1000px) { - .home-layout { - flex-direction: column !important; - } -} */ - -.home-layout > .home-content-wrapper { - min-height: calc(100vh - var(--env-var-spacing-2) * 2); - flex: 1; -} - -.home-content-wrapper { - padding: var(--env-var-spacing-2); - max-width: 1400px; - margin: 0 auto; - flex: 1; - min-width: 0; - overflow-x: hidden; -} diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx deleted file mode 100644 index adadf638f..000000000 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Sidebar from "../../Sidebar/index.jsx"; -import { Outlet } from "react-router"; -import { Box, Stack } from "@mui/material"; -import { useSidebar } from "@/Hooks/useSidebar.js"; - -import "./index.css"; - -const HomeLayout = () => { - const { width, transition } = useSidebar(); - - return ( - - - - - - - - ); -}; - -export default HomeLayout; diff --git a/client/src/Components/v1/Sidebar/components/authFooter.jsx b/client/src/Components/v1/Sidebar/components/authFooter.jsx deleted file mode 100644 index 16b60d9f2..000000000 --- a/client/src/Components/v1/Sidebar/components/authFooter.jsx +++ /dev/null @@ -1,276 +0,0 @@ -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/index.jsx"; -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Divider from "@mui/material/Divider"; -import Icon from "../../Icon"; - -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.js"; -import { useDispatch } from "react-redux"; -import PropTypes from "prop-types"; - -const getFilteredAccountMenuItems = (user, items) => { - 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, t) => { - 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 }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const navigate = useNavigate(); - const dispatch = useDispatch(); - - const [anchorEl, setAnchorEl] = useState(null); - - const openPopup = (event) => { - setAnchorEl(event.currentTarget); - }; - - const closePopup = () => { - setAnchorEl(null); - }; - - const logout = async () => { - dispatch(clearAuthState()); - navigate("/login"); - }; - const renderAccountMenuItems = (user, items) => { - 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), - color: theme.palette.primary.contrastTextTertiary, - "& svg": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - {item.icon} - {item.name} - - )); - }; - return ( - - collapsed && 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/v1/Sidebar/components/collapseButton.jsx b/client/src/Components/v1/Sidebar/components/collapseButton.jsx deleted file mode 100644 index e3a9c91f5..000000000 --- a/client/src/Components/v1/Sidebar/components/collapseButton.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import IconButton from "@mui/material/IconButton"; -import ArrowRight from "../../ArrowRight/index.jsx"; -import ArrowLeft from "../../ArrowLeft/index.jsx"; -import { useTheme } from "@mui/material/styles"; -import { useDispatch } from "react-redux"; -import { toggleSidebar } from "../../../../Features/UI/uiSlice.js"; -import PropTypes from "prop-types"; - -const CollapseButton = ({ collapsed }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const arrowIcon = collapsed ? ( - - ) : ( - - ); - return ( - { - dispatch(toggleSidebar()); - }} - > - {arrowIcon} - - ); -}; - -CollapseButton.propTypes = { - collapsed: PropTypes.bool.isRequired, -}; -export default CollapseButton; diff --git a/client/src/Components/v1/Sidebar/components/logo.jsx b/client/src/Components/v1/Sidebar/components/logo.jsx deleted file mode 100644 index a0c0ff84e..000000000 --- a/client/src/Components/v1/Sidebar/components/logo.jsx +++ /dev/null @@ -1,67 +0,0 @@ -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"; -import PropTypes from "prop-types"; - -const Logo = ({ collapsed }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const navigate = useNavigate(); - - return ( - navigate("/")} - sx={{ cursor: "pointer" }} - > - - C - - - {" "} - - {t("common.appName")} - - - - ); -}; - -Logo.propTypes = { - collapsed: PropTypes.bool, -}; - -export default Logo; diff --git a/client/src/Components/v1/Sidebar/components/navItem.jsx b/client/src/Components/v1/Sidebar/components/navItem.jsx deleted file mode 100644 index 170d39bee..000000000 --- a/client/src/Components/v1/Sidebar/components/navItem.jsx +++ /dev/null @@ -1,96 +0,0 @@ -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 PropTypes from "prop-types"; - -const NavItem = ({ item, collapsed, selected, onClick }) => { - const theme = useTheme(); - const iconStroke = selected - ? theme.palette.primary.contrastText - : theme.palette.primary.contrastTextTertiary; - - const buttonBgColor = selected ? theme.palette.secondary.main : "transparent"; - const buttonBgHoverColor = selected - ? theme.palette.secondary.main - : theme.palette.tertiary.main; - const fontWeight = selected ? 600 : 400; - return ( - - - - {item.icon} - - - - {item.name} - - - - - ); -}; - -NavItem.propTypes = { - item: PropTypes.object, - collapsed: PropTypes.bool, - selected: PropTypes.bool, - onClick: PropTypes.func, -}; -export default NavItem; diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx deleted file mode 100644 index 763a4123c..000000000 --- a/client/src/Components/v1/Sidebar/index.jsx +++ /dev/null @@ -1,152 +0,0 @@ -import Stack from "@mui/material/Stack"; - -import List from "@mui/material/List"; -import Logo from "./components/logo.jsx"; -import CollapseButton from "./components/collapseButton.jsx"; -import Divider from "@mui/material/Divider"; -import NavItem from "./components/navItem.jsx"; -import AuthFooter from "./components/authFooter.jsx"; - -import StarPrompt from "../StarPrompt/index.jsx"; -import Icon from "../Icon"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router"; -import { useSidebar } from "@/Hooks/useSidebar.js"; - -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: }, - { name: t("menu.infrastructure"), path: "infrastructure", icon: }, - { - name: t("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"), - icon: , - path: "settings", - }, -]; - -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 { t } = useTranslation(); - const navigate = useNavigate(); - const { collapsed, width, transition } = useSidebar(); - - const menu = getMenu(t); - const otherMenuItems = getOtherMenuItems(t); - const accountMenuItems = getAccountMenuItems(t); - - return ( - - - - - {menu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - 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}`); - } - }} - /> - ); - })} - - - - - ); -}; - -export default Sidebar; diff --git a/client/src/Components/v1/StarPrompt/index.jsx b/client/src/Components/v1/StarPrompt/index.jsx deleted file mode 100644 index f1dd5e136..000000000 --- a/client/src/Components/v1/StarPrompt/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from "react"; -import { Typography, IconButton, Stack, Box } from "@mui/material"; -import Icon from "../Icon"; -import { useTheme } from "@emotion/react"; -import { useSelector, useDispatch } from "react-redux"; -import { useTranslation } from "react-i18next"; -import { setStarPromptOpen } from "../../../Features/UI/uiSlice.js"; - -const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true); - const mode = useSelector((state) => state.ui.mode); - - const handleClose = () => { - dispatch(setStarPromptOpen(false)); - }; - - const handleStarClick = () => { - window.open(repoUrl, "_blank"); - }; - - if (!isOpen) return null; - - return ( - - - - {t("starPromptTitle")} - - - - - - - - {t("starPromptDescription")} - - - - - ); -}; - -export default StarPrompt; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 4b369cb67..62ea5610b 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -51,7 +51,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => if (role.includes("superadmin")) return t("components.sidebar.authFooter.roles.superAdmin"); if (role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); - if (role.includes("user")) return t("components.sidebar.authFooter.roles.teamMember"); + if (role.includes("user")) return t("components.sidebar.authFooter.roles.user"); if (role.includes("demo")) return t("components.sidebar.authFooter.roles.demoUser"); return role; }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b49ebaa43..86b220998 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -366,7 +366,7 @@ "roles": { "superAdmin": "Super admin", "admin": "Admin", - "teamMember": "Team member", + "user": "User", "demoUser": "Demo user" } } From 10bfdc9da13de9ebd2c406638f972f001aeefe3c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:53:06 +0000 Subject: [PATCH 13/13] hook up offline banner --- .../v2/design-elements/OfflineBanner.tsx | 49 ++---- client/src/Components/v2/layout/AppLayout.tsx | 32 +++- client/src/Pages/ServerUnreachable.jsx | 166 ------------------ client/src/Routes/index.jsx | 7 - client/src/Utils/ApiClient.ts | 6 +- client/src/locales/en.json | 10 +- 6 files changed, 59 insertions(+), 211 deletions(-) delete mode 100644 client/src/Pages/ServerUnreachable.jsx diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx index fa5690c12..062b1ca0d 100644 --- a/client/src/Components/v2/design-elements/OfflineBanner.tsx +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -1,30 +1,39 @@ import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; import { WifiOff } from "lucide-react"; +import { useState, useEffect } from "react"; interface OfflineBannerProps { visible: boolean; - onRetry?: () => void; - isRetrying?: boolean; } -export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => { +export const OfflineBanner = ({ visible }: OfflineBannerProps) => { const theme = useTheme(); const { t } = useTranslation(); + const [shouldRender, setShouldRender] = useState(visible); + const [isAnimating, setIsAnimating] = useState(false); - if (!visible) { - return null; - } + useEffect(() => { + if (visible) { + setShouldRender(true); + requestAnimationFrame(() => setIsAnimating(true)); + } else { + setIsAnimating(false); + const timer = setTimeout(() => setShouldRender(false), 1000); + return () => clearTimeout(timer); + } + }, [visible]); + + if (!shouldRender) return null; return ( {t("components.offlineBanner.serverUnreachable")} - {onRetry && ( - - )} ); diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx index 3ae2729e3..7c9c3c903 100644 --- a/client/src/Components/v2/layout/AppLayout.tsx +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -1,10 +1,12 @@ import Box from "@mui/material/Box"; +import { useState, useEffect, useRef } from "react"; import { ThemeProvider, useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import BackgroundSVG from "@/assets/Images/background.svg"; import type { RootState } from "@/Types/state"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; import { OfflineBanner } from "@/Components/v2/design-elements"; +import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; interface AppLayoutProps { children: React.ReactNode; @@ -15,6 +17,34 @@ const AppLayout = ({ children }: AppLayoutProps) => { const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "dark" ? darkTheme : lightTheme; + const [serverUnreachable, setServerUnreachable] = useState(false); + const retryIntervalRef = useRef(null); + + useEffect(() => { + setServerUnreachableCallback(setServerUnreachable); + }, []); + + useEffect(() => { + if (serverUnreachable) { + retryIntervalRef.current = window.setInterval(async () => { + try { + await get("/health", { timeout: 5000 }); + } catch { + // NO_OP + } + }, 5000); + } else if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + retryIntervalRef.current = null; + } + + return () => { + if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + } + }; + }, [serverUnreachable]); + return ( { }} > - + {children} diff --git a/client/src/Pages/ServerUnreachable.jsx b/client/src/Pages/ServerUnreachable.jsx deleted file mode 100644 index 5051ee95f..000000000 --- a/client/src/Pages/ServerUnreachable.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from "react"; -import { Box, Typography, Button, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { get } from "@/Utils/ApiClient"; -import Alert from "@/Components/v1/Alert/index.jsx"; -import { createToast } from "@/Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -import Background from "@/assets/Images/background-grid.svg?react"; -import Logo from "@/assets/icons/checkmate-icon.svg?react"; -import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; -import LanguageSelector from "@/Components/LanguageSelector.jsx"; - -const ServerUnreachable = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - // State for tracking connection check status - const [isCheckingConnection, setIsCheckingConnection] = useState(false); - - const handleRetry = React.useCallback(async () => { - setIsCheckingConnection(true); - try { - // Try to connect to the backend with a simple API call - // We'll use any lightweight endpoint that doesn't require authentication - await get("/health", { timeout: 5000 }); - - // If successful, show toast and navigate to login page - createToast({ - body: t("errorPages.serverUnreachable.toasts.reconnected"), - }); - navigate("/login"); - } catch (error) { - // If still unreachable, stay on this page and show toast - createToast({ - body: t("errorPages.serverUnreachable.toasts.stillUnreachable"), - }); - } finally { - setIsCheckingConnection(false); - } - }, [navigate, t]); - - return ( - - - - - - {/* Header with logo */} - - - - {t("common.appName")} - - - - - - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - - - - - - - {t("errorPages.serverUnreachable.description")} - - - - - - - - - ); -}; - -export default ServerUnreachable; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0a66b0cf3..7d9163c43 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -25,9 +25,6 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/"; import Infrastructure from "@/Pages/Infrastructure/Monitors"; import InfrastructureDetails from "@/Pages/Infrastructure/Details/index"; -// Server Status -import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; - // Checks import Checks from "../Pages/Checks/index"; @@ -432,10 +429,6 @@ const Routes = () => { } /> - } - /> { } ); - const onSuccess = (response: AxiosResponse) => response; + const onSuccess = (response: AxiosResponse) => { + // Server is reachable, hide offline banner if shown + serverUnreachableCallback?.(false); + return response; + }; const onError = (error: AxiosError) => { // Handle network errors (server unreachable) if (error.code === "ERR_NETWORK") { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 86b220998..d2a066c11 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -582,7 +582,7 @@ "notFoundButton": "Go to the main dashboard", "notifications": { "fallback": { - "actionButton": "Let's create your first notification channel!", + "actionButton": "Create notification channel!", "checks": [ "Alert teams about downtime or performance issues", "Let engineers know when incidents happen", @@ -1019,7 +1019,7 @@ } }, "fallback": { - "actionButton": "Let's create your first infrastructure monitor!", + "actionButton": "Create a monitor!", "checks": [ "Track the performance of your servers", "Identify bottlenecks and optimize usage", @@ -1087,7 +1087,7 @@ }, "maintenanceWindow": { "fallback": { - "actionButton": "Let's create your first maintenance window!", + "actionButton": "Create a maintenance window!", "checks": [ "Mark your maintenance periods", "Eliminate any misunderstandings", @@ -1228,7 +1228,7 @@ } }, "fallback": { - "actionButton": "Let's create your first PageSpeed monitor!", + "actionButton": "Create a monitor!", "checks": [ "Report on the user experience of a page", "Help analyze webpage speed", @@ -1247,7 +1247,7 @@ "Build trust with transparent service monitoring", "Reduce support requests during incidents" ], - "actionButton": "Let's create your first status page!" + "actionButton": "Create a status page!" }, "monitorsList": { "chartTypeHeatmap": "Heatmap",