This commit is contained in:
Alex Holliday committed 2026-02-10 19:21:57 +00:00
1 parent 7ef10b4fcd
commit d1d96bd2ba
10 files changed
+369 -48

No files matched your search

@@ -14,7 +14,6 @@ const HomeLayout = () => {
flexDirection="row"
>
<Sidebar />
{/* Spacer for fixed sidebar */}
<Box
sx={{
width,
+16 -16
View File
@@ -16,23 +16,23 @@ const AppLayout = ({ children }: AppLayoutProps) => {
const v2theme = mode === "dark" ? darkTheme : lightTheme;
return (
<ThemeProvider theme={v2theme}>
<Box
sx={{
minHeight: "100vh",
// @ts-expect-error custom palette property
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
<Box
sx={{
minHeight: "100vh",
// @ts-expect-error custom palette property
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
<ThemeProvider theme={v2theme}>
<OfflineBanner visible={false} />
{children}
</Box>
</ThemeProvider>
</ThemeProvider>
{children}
</Box>
);
};
@@ -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 (
<Stack flexDirection="row">
<ThemeProvider theme={v2theme}>
<Sidebar />
</ThemeProvider>
<Stack
flex={1}
padding={6}
overflow={"hidden"}
sx={{
backgroundColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.01)"
: "rgba(0, 0, 0, 0.01)",
display: "flex",
alignItems: "center",
}}
>
<Stack
maxWidth={1280}
width="100%"
paddingY={theme.spacing(6)}
flex={1}
>
<Outlet />
</Stack>
</Stack>
</Stack>
);
};
export default RootLayout;
+60
View File
@@ -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 (
<Stack
pb={theme.spacing(6)}
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => {
dispatch(toggleSidebar());
}}
sx={{ cursor: "pointer" }}
>
<Typography
minWidth={39}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.primary.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.primary.contrastText}
fontSize={18}
>
C
</Typography>
<Box
overflow={"hidden"}
sx={{
transition: "opacity 900ms ease, width 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
width: collapsed ? 0 : "100%",
}}
>
{" "}
<Typography
lineHeight={1}
mt={theme.spacing(2)}
variant="h2"
fontWeight={500}
>
{t("common.appName")}
</Typography>
</Box>
</Stack>
);
};
+41
View File
@@ -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: <Icon icon={Globe} /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon icon={Gauge} /> },
{
name: t("menu.infrastructure"),
path: "infrastructure",
icon: <Icon icon={Link} />,
},
{
name: t("menu.notifications"),
path: "notifications",
icon: <Icon icon={Bell} />,
},
{ name: t("menu.checks"), path: "checks", icon: <Icon icon={FileText} /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Icon icon={AlertTriangle} /> },
{ name: t("menu.statusPages"), path: "status", icon: <Icon icon={Wifi} /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon icon={Wrench} /> },
{ name: t("menu.logs"), path: "logs", icon: <Icon icon={Database} /> },
{
name: t("menu.settings"),
icon: <Icon icon={Settings} />,
path: "settings",
},
];
};
@@ -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 (
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
sx={{
backgroundColor: buttonBgColor,
backgroundImage: "none",
border: 1,
borderColor: "transparent",
"&:hover": {
backgroundColor: buttonBgHoverColor,
backgroundImage: "none",
},
height: 32,
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
onClick={onClick}
>
<ListItemIcon
sx={{
minWidth: 0,
"& svg": {
height: 16,
width: 16,
opacity: 0.81,
transition: "stroke 0.2s ease",
},
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: iconStroke,
},
".MuiListItemButton-root:hover &": {
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: theme.palette.primary.main,
},
},
}}
>
{item.icon}
</ListItemIcon>
<Box
sx={{
overflow: "hidden",
transition: "opacity 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
}}
>
<Typography
variant="body1"
sx={{
fontWeight: fontWeight,
opacity: 0.9,
}}
>
{item.name}
</Typography>
</Box>
</ListItemButton>
</Tooltip>
);
};
@@ -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 (
<Stack
component="aside"
position={{ xs: "fixed", md: "sticky" }}
top={0}
left={0}
minHeight={"100vh"}
maxHeight={"100vh"}
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
borderRight={`1px solid ${theme.palette.divider}`}
width={width}
sx={{
transition: transition,
zIndex: isSmall ? (t) => t.zIndex.drawer : "auto",
}}
>
<List
component="nav"
disablePadding
sx={{
px: theme.spacing(6),
height: "100%",
}}
>
<Logo />
{menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
selected={selected}
onClick={() => {
navigate(`/${item.path}`);
if (isSmall) {
dispatch(toggleSidebar());
}
}}
/>
);
})}
</List>
</Stack>
);
};
-29
View File
@@ -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;
+25
View File
@@ -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;
+2 -2
View File
@@ -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={
<ProtectedRoute>
<HomeLayout />
<RootLayout />
</ProtectedRoute>
}
>