sidebar
This commit is contained in:
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,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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user