Add base authenticated route
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
export function BaseAuthenticatedPage(): React.ReactElement {
|
||||
return <p>todo authenticated</p>;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { mdiMessageTextFast } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import * as React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import DashboardHeader from "./DashboardHeader";
|
||||
import DashboardSidebar from "./DashboardSidebar";
|
||||
|
||||
export default function BaseAuthenticatedPage(): React.ReactElement {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] =
|
||||
React.useState(true);
|
||||
const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] =
|
||||
React.useState(false);
|
||||
|
||||
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const isNavigationExpanded = isOverMdViewport
|
||||
? isDesktopNavigationExpanded
|
||||
: isMobileNavigationExpanded;
|
||||
|
||||
const setIsNavigationExpanded = React.useCallback(
|
||||
(newExpanded: boolean) => {
|
||||
if (isOverMdViewport) {
|
||||
setIsDesktopNavigationExpanded(newExpanded);
|
||||
} else {
|
||||
setIsMobileNavigationExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[
|
||||
isOverMdViewport,
|
||||
setIsDesktopNavigationExpanded,
|
||||
setIsMobileNavigationExpanded,
|
||||
]
|
||||
);
|
||||
|
||||
const handleToggleHeaderMenu = React.useCallback(
|
||||
(isExpanded: boolean) => {
|
||||
setIsNavigationExpanded(isExpanded);
|
||||
},
|
||||
[setIsNavigationExpanded]
|
||||
);
|
||||
|
||||
const layoutRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={layoutRef}
|
||||
sx={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<DashboardHeader
|
||||
logo={<Icon path={mdiMessageTextFast} size="2em" color={"white"} />}
|
||||
title=""
|
||||
menuOpen={isNavigationExpanded}
|
||||
onToggleMenu={handleToggleHeaderMenu}
|
||||
/>
|
||||
<DashboardSidebar
|
||||
expanded={isNavigationExpanded}
|
||||
setExpanded={setIsNavigationExpanded}
|
||||
container={layoutRef?.current ?? undefined}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Toolbar sx={{ displayPrint: "none" }} />
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
overflow: "auto",
|
||||
padding: "50px",
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
130
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
130
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import * as React from "react";
|
||||
import { styled, useTheme } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import MuiAppBar from "@mui/material/AppBar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Link } from "react-router";
|
||||
import ThemeSwitcher from "./ThemeSwitcher";
|
||||
|
||||
const AppBar = styled(MuiAppBar)(({ theme }) => ({
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: (theme.vars ?? theme).palette.divider,
|
||||
boxShadow: "none",
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
}));
|
||||
|
||||
const LogoContainer = styled("div")({
|
||||
position: "relative",
|
||||
height: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& img": {
|
||||
maxHeight: 40,
|
||||
},
|
||||
});
|
||||
|
||||
export interface DashboardHeaderProps {
|
||||
logo?: React.ReactNode;
|
||||
title?: string;
|
||||
menuOpen: boolean;
|
||||
onToggleMenu: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function DashboardHeader({
|
||||
logo,
|
||||
title,
|
||||
menuOpen,
|
||||
onToggleMenu,
|
||||
}: DashboardHeaderProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleMenuOpen = React.useCallback(() => {
|
||||
onToggleMenu(!menuOpen);
|
||||
}, [menuOpen, onToggleMenu]);
|
||||
|
||||
const getMenuIcon = React.useCallback(
|
||||
(isExpanded: boolean) => {
|
||||
const expandMenuActionText = "Expand";
|
||||
const collapseMenuActionText = "Collapse";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={`${
|
||||
isExpanded ? collapseMenuActionText : expandMenuActionText
|
||||
} menu`}
|
||||
enterDelay={1000}
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`${
|
||||
isExpanded ? collapseMenuActionText : expandMenuActionText
|
||||
} navigation menu`}
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
{isExpanded ? <MenuOpenIcon /> : <MenuIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
[handleMenuOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppBar color="inherit" position="absolute" sx={{ displayPrint: "none" }}>
|
||||
<Toolbar sx={{ backgroundColor: "inherit", mx: { xs: -0.75, sm: -1 } }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Box sx={{ mr: 1 }}>{getMenuIcon(menuOpen)}</Box>
|
||||
<Link to="/" style={{ textDecoration: "none" }}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{logo ? <LogoContainer>{logo}</LogoContainer> : null}
|
||||
{title ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: (theme.vars ?? theme).palette.primary.main,
|
||||
fontWeight: "700",
|
||||
ml: 1,
|
||||
whiteSpace: "nowrap",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{ marginLeft: "auto" }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<ThemeSwitcher />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
281
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
281
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import * as React from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import Box from "@mui/material/Box";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import BarChartIcon from "@mui/icons-material/BarChart";
|
||||
import DescriptionIcon from "@mui/icons-material/Description";
|
||||
import LayersIcon from "@mui/icons-material/Layers";
|
||||
import { matchPath, useLocation } from "react-router";
|
||||
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||
import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants";
|
||||
import DashboardSidebarPageItem from "./DashboardSidebarPageItem";
|
||||
import DashboardSidebarHeaderItem from "./DashboardSidebarHeaderItem";
|
||||
import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem";
|
||||
import {
|
||||
getDrawerSxTransitionMixin,
|
||||
getDrawerWidthTransitionMixin,
|
||||
} from "./mixins";
|
||||
|
||||
export interface DashboardSidebarProps {
|
||||
expanded?: boolean;
|
||||
setExpanded: (expanded: boolean) => void;
|
||||
disableCollapsibleSidebar?: boolean;
|
||||
container?: Element;
|
||||
}
|
||||
|
||||
export default function DashboardSidebar({
|
||||
expanded = true,
|
||||
setExpanded,
|
||||
disableCollapsibleSidebar = false,
|
||||
container,
|
||||
}: DashboardSidebarProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [expandedItemIds, setExpandedItemIds] = React.useState<string[]>([]);
|
||||
|
||||
const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const [isFullyExpanded, setIsFullyExpanded] = React.useState(expanded);
|
||||
const [isFullyCollapsed, setIsFullyCollapsed] = React.useState(!expanded);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
const drawerWidthTransitionTimeout = setTimeout(() => {
|
||||
setIsFullyExpanded(true);
|
||||
}, theme.transitions.duration.enteringScreen);
|
||||
|
||||
return () => clearTimeout(drawerWidthTransitionTimeout);
|
||||
}
|
||||
|
||||
setIsFullyExpanded(false);
|
||||
|
||||
return () => {};
|
||||
}, [expanded, theme.transitions.duration.enteringScreen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!expanded) {
|
||||
const drawerWidthTransitionTimeout = setTimeout(() => {
|
||||
setIsFullyCollapsed(true);
|
||||
}, theme.transitions.duration.leavingScreen);
|
||||
|
||||
return () => clearTimeout(drawerWidthTransitionTimeout);
|
||||
}
|
||||
|
||||
setIsFullyCollapsed(false);
|
||||
|
||||
return () => {};
|
||||
}, [expanded, theme.transitions.duration.leavingScreen]);
|
||||
|
||||
const mini = !disableCollapsibleSidebar && !expanded;
|
||||
|
||||
const handleSetSidebarExpanded = React.useCallback(
|
||||
(newExpanded: boolean) => () => {
|
||||
setExpanded(newExpanded);
|
||||
},
|
||||
[setExpanded]
|
||||
);
|
||||
|
||||
const handlePageItemClick = React.useCallback(
|
||||
(itemId: string, hasNestedNavigation: boolean) => {
|
||||
if (hasNestedNavigation && !mini) {
|
||||
setExpandedItemIds((previousValue) =>
|
||||
previousValue.includes(itemId)
|
||||
? previousValue.filter(
|
||||
(previousValueItemId) => previousValueItemId !== itemId
|
||||
)
|
||||
: [...previousValue, itemId]
|
||||
);
|
||||
} else if (!isOverSmViewport && !hasNestedNavigation) {
|
||||
setExpanded(false);
|
||||
}
|
||||
},
|
||||
[mini, setExpanded, isOverSmViewport]
|
||||
);
|
||||
|
||||
const hasDrawerTransitions =
|
||||
isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport);
|
||||
|
||||
const getDrawerContent = React.useCallback(
|
||||
(viewport: "phone" | "tablet" | "desktop") => (
|
||||
<React.Fragment>
|
||||
<Toolbar />
|
||||
<Box
|
||||
component="nav"
|
||||
aria-label={`${viewport.charAt(0).toUpperCase()}${viewport.slice(1)}`}
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
overflow: "auto",
|
||||
scrollbarGutter: mini ? "stable" : "auto",
|
||||
overflowX: "hidden",
|
||||
pt: !mini ? 0 : 2,
|
||||
...(hasDrawerTransitions
|
||||
? getDrawerSxTransitionMixin(isFullyExpanded, "padding")
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<List
|
||||
dense
|
||||
sx={{
|
||||
padding: mini ? 0 : 0.5,
|
||||
mb: 4,
|
||||
width: mini ? MINI_DRAWER_WIDTH : "auto",
|
||||
}}
|
||||
>
|
||||
<DashboardSidebarHeaderItem>Main items</DashboardSidebarHeaderItem>
|
||||
<DashboardSidebarPageItem
|
||||
id="employees"
|
||||
title="Employees"
|
||||
icon={<PersonIcon />}
|
||||
href="/employees"
|
||||
selected={
|
||||
!!matchPath("/employees/*", pathname) || pathname === "/"
|
||||
}
|
||||
/>
|
||||
<DashboardSidebarDividerItem />
|
||||
<DashboardSidebarHeaderItem>
|
||||
Example items
|
||||
</DashboardSidebarHeaderItem>
|
||||
<DashboardSidebarPageItem
|
||||
id="reports"
|
||||
title="Reports"
|
||||
icon={<BarChartIcon />}
|
||||
href="/reports"
|
||||
selected={!!matchPath("/reports", pathname)}
|
||||
defaultExpanded={!!matchPath("/reports", pathname)}
|
||||
expanded={expandedItemIds.includes("reports")}
|
||||
nestedNavigation={
|
||||
<List
|
||||
dense
|
||||
sx={{
|
||||
padding: 0,
|
||||
my: 1,
|
||||
pl: mini ? 0 : 1,
|
||||
minWidth: 240,
|
||||
}}
|
||||
>
|
||||
<DashboardSidebarPageItem
|
||||
id="sales"
|
||||
title="Sales"
|
||||
icon={<DescriptionIcon />}
|
||||
href="/reports/sales"
|
||||
selected={!!matchPath("/reports/sales", pathname)}
|
||||
/>
|
||||
<DashboardSidebarPageItem
|
||||
id="traffic"
|
||||
title="Traffic"
|
||||
icon={<DescriptionIcon />}
|
||||
href="/reports/traffic"
|
||||
selected={!!matchPath("/reports/traffic", pathname)}
|
||||
/>
|
||||
</List>
|
||||
}
|
||||
/>
|
||||
<DashboardSidebarPageItem
|
||||
id="integrations"
|
||||
title="Integrations"
|
||||
icon={<LayersIcon />}
|
||||
href="/integrations"
|
||||
selected={!!matchPath("/integrations", pathname)}
|
||||
/>
|
||||
</List>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
),
|
||||
[mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname]
|
||||
);
|
||||
|
||||
const getDrawerSharedSx = React.useCallback(
|
||||
(isTemporary: boolean) => {
|
||||
const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH;
|
||||
|
||||
return {
|
||||
displayPrint: "none",
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
...getDrawerWidthTransitionMixin(expanded),
|
||||
...(isTemporary ? { position: "absolute" } : {}),
|
||||
[`& .MuiDrawer-paper`]: {
|
||||
position: "absolute",
|
||||
width: drawerWidth,
|
||||
boxSizing: "border-box",
|
||||
backgroundImage: "none",
|
||||
...getDrawerWidthTransitionMixin(expanded),
|
||||
},
|
||||
};
|
||||
},
|
||||
[expanded, mini]
|
||||
);
|
||||
|
||||
const sidebarContextValue = React.useMemo(() => {
|
||||
return {
|
||||
onPageItemClick: handlePageItemClick,
|
||||
mini,
|
||||
fullyExpanded: isFullyExpanded,
|
||||
fullyCollapsed: isFullyCollapsed,
|
||||
hasDrawerTransitions,
|
||||
};
|
||||
}, [
|
||||
handlePageItemClick,
|
||||
mini,
|
||||
isFullyExpanded,
|
||||
isFullyCollapsed,
|
||||
hasDrawerTransitions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DashboardSidebarContext.Provider value={sidebarContextValue}>
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={expanded}
|
||||
onClose={handleSetSidebarExpanded(false)}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: {
|
||||
xs: "block",
|
||||
sm: disableCollapsibleSidebar ? "block" : "none",
|
||||
md: "none",
|
||||
},
|
||||
...getDrawerSharedSx(true),
|
||||
}}
|
||||
>
|
||||
{getDrawerContent("phone")}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: {
|
||||
xs: "none",
|
||||
sm: disableCollapsibleSidebar ? "none" : "block",
|
||||
md: "none",
|
||||
},
|
||||
...getDrawerSharedSx(false),
|
||||
}}
|
||||
>
|
||||
{getDrawerContent("tablet")}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: "none", md: "block" },
|
||||
...getDrawerSharedSx(false),
|
||||
}}
|
||||
>
|
||||
{getDrawerContent("desktop")}
|
||||
</Drawer>
|
||||
</DashboardSidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from "react";
|
||||
|
||||
const DashboardSidebarContext = React.createContext(null);
|
||||
|
||||
export default DashboardSidebarContext;
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "react";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||
|
||||
export default function DashboardSidebarDividerItem() {
|
||||
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||
if (!sidebarContext) {
|
||||
throw new Error("Sidebar context was used without a provider.");
|
||||
}
|
||||
const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Divider
|
||||
sx={{
|
||||
borderBottomWidth: 1,
|
||||
my: 1,
|
||||
mx: -0.5,
|
||||
...(hasDrawerTransitions
|
||||
? getDrawerSxTransitionMixin(fullyExpanded, "margin")
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import ListSubheader from "@mui/material/ListSubheader";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||
import { DRAWER_WIDTH } from "./constants";
|
||||
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||
|
||||
export interface DashboardSidebarHeaderItemProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardSidebarHeaderItem({
|
||||
children,
|
||||
}: DashboardSidebarHeaderItemProps) {
|
||||
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||
if (!sidebarContext) {
|
||||
throw new Error("Sidebar context was used without a provider.");
|
||||
}
|
||||
const {
|
||||
mini = false,
|
||||
fullyExpanded = true,
|
||||
hasDrawerTransitions,
|
||||
} = sidebarContext;
|
||||
|
||||
return (
|
||||
<ListSubheader
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
height: mini ? 0 : 36,
|
||||
...(hasDrawerTransitions
|
||||
? getDrawerSxTransitionMixin(fullyExpanded, "height")
|
||||
: {}),
|
||||
px: 1.5,
|
||||
py: 0,
|
||||
minWidth: DRAWER_WIDTH,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ListSubheader>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import * as React from "react";
|
||||
import { type Theme, type SxProps } from "@mui/material/styles";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Grow from "@mui/material/Grow";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { Link } from "react-router";
|
||||
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||
import { MINI_DRAWER_WIDTH } from "./constants";
|
||||
|
||||
export interface DashboardSidebarPageItemProps {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
href: string;
|
||||
action?: React.ReactNode;
|
||||
defaultExpanded?: boolean;
|
||||
expanded?: boolean;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
nestedNavigation?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardSidebarPageItem({
|
||||
id,
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
action,
|
||||
defaultExpanded = false,
|
||||
expanded = defaultExpanded,
|
||||
selected = false,
|
||||
disabled = false,
|
||||
nestedNavigation,
|
||||
}: DashboardSidebarPageItemProps) {
|
||||
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||
if (!sidebarContext) {
|
||||
throw new Error("Sidebar context was used without a provider.");
|
||||
}
|
||||
const {
|
||||
onPageItemClick,
|
||||
mini = false,
|
||||
fullyExpanded = true,
|
||||
fullyCollapsed = false,
|
||||
} = sidebarContext;
|
||||
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (onPageItemClick) {
|
||||
onPageItemClick(id, !!nestedNavigation);
|
||||
}
|
||||
}, [onPageItemClick, id, nestedNavigation]);
|
||||
|
||||
let nestedNavigationCollapseSx: SxProps<Theme> = { display: "none" };
|
||||
if (mini && fullyCollapsed) {
|
||||
nestedNavigationCollapseSx = {
|
||||
fontSize: 18,
|
||||
position: "absolute",
|
||||
top: "41.5%",
|
||||
right: "2px",
|
||||
transform: "translateY(-50%) rotate(-90deg)",
|
||||
};
|
||||
} else if (!mini && fullyExpanded) {
|
||||
nestedNavigationCollapseSx = {
|
||||
ml: 0.5,
|
||||
fontSize: 20,
|
||||
transform: `rotate(${expanded ? 0 : -90}deg)`,
|
||||
transition: (theme: Theme) =>
|
||||
theme.transitions.create("transform", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: 100,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const hasExternalHref = href
|
||||
? href.startsWith("http://") || href.startsWith("https://")
|
||||
: false;
|
||||
|
||||
const LinkComponent = hasExternalHref ? "a" : Link;
|
||||
|
||||
const miniNestedNavigationSidebarContextValue = React.useMemo(() => {
|
||||
return {
|
||||
onPageItemClick: onPageItemClick ?? (() => {}),
|
||||
mini: false,
|
||||
fullyExpanded: true,
|
||||
fullyCollapsed: false,
|
||||
hasDrawerTransitions: false,
|
||||
};
|
||||
}, [onPageItemClick]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListItem
|
||||
disablePadding
|
||||
{...(nestedNavigation && mini
|
||||
? {
|
||||
onMouseEnter: () => {
|
||||
setIsHovered(true);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setIsHovered(false);
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
sx={{
|
||||
display: "block",
|
||||
py: 0,
|
||||
px: 1,
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
height: mini ? 50 : "auto",
|
||||
}}
|
||||
{...(nestedNavigation && !mini
|
||||
? {
|
||||
onClick: handleClick,
|
||||
}
|
||||
: {})}
|
||||
{...(!nestedNavigation
|
||||
? {
|
||||
LinkComponent,
|
||||
...(hasExternalHref
|
||||
? {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
}
|
||||
: {}),
|
||||
to: href,
|
||||
onClick: handleClick,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{icon || mini ? (
|
||||
<Box
|
||||
sx={
|
||||
mini
|
||||
? {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "calc(50% - 6px)",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: mini ? "center" : "auto",
|
||||
}}
|
||||
>
|
||||
{icon ?? null}
|
||||
{!icon && mini ? (
|
||||
<Avatar
|
||||
sx={{
|
||||
fontSize: 10,
|
||||
height: 16,
|
||||
width: 16,
|
||||
}}
|
||||
>
|
||||
{title
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map((titleWord) => titleWord.charAt(0).toUpperCase())}
|
||||
</Avatar>
|
||||
) : null}
|
||||
</ListItemIcon>
|
||||
{mini ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: -18,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: MINI_DRAWER_WIDTH - 28,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
) : null}
|
||||
{!mini ? (
|
||||
<ListItemText
|
||||
primary={title}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{action && !mini && fullyExpanded ? action : null}
|
||||
{nestedNavigation ? (
|
||||
<ExpandMoreIcon sx={nestedNavigationCollapseSx} />
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
{nestedNavigation && mini ? (
|
||||
<Grow in={isHovered}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
left: MINI_DRAWER_WIDTH - 2,
|
||||
pl: "6px",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{
|
||||
pt: 0.2,
|
||||
pb: 0.2,
|
||||
transform: "translateY(-50px)",
|
||||
}}
|
||||
>
|
||||
<DashboardSidebarContext.Provider
|
||||
value={miniNestedNavigationSidebarContextValue}
|
||||
>
|
||||
{nestedNavigation}
|
||||
</DashboardSidebarContext.Provider>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Grow>
|
||||
) : null}
|
||||
</ListItem>
|
||||
{nestedNavigation && !mini ? (
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
{nestedNavigation}
|
||||
</Collapse>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "react";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||
import { getDrawerSxTransitionMixin } from "./mixins";
|
||||
|
||||
export default function DashboardSidebarDividerItem() {
|
||||
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||
if (!sidebarContext) {
|
||||
throw new Error("Sidebar context was used without a provider.");
|
||||
}
|
||||
const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Divider
|
||||
sx={{
|
||||
borderBottomWidth: 1,
|
||||
my: 1,
|
||||
mx: -0.5,
|
||||
...(hasDrawerTransitions
|
||||
? getDrawerSxTransitionMixin(fullyExpanded, "margin")
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
59
matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx
Normal file
59
matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { useTheme, useColorScheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const theme = useTheme();
|
||||
|
||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||
const preferredMode = prefersDarkMode ? "dark" : "light";
|
||||
|
||||
const { mode, setMode } = useColorScheme();
|
||||
|
||||
const paletteMode = !mode || mode === "system" ? preferredMode : mode;
|
||||
|
||||
const toggleMode = React.useCallback(() => {
|
||||
setMode(paletteMode === "dark" ? "light" : "dark");
|
||||
}, [setMode, paletteMode]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={`${paletteMode === "dark" ? "Light" : "Dark"} mode`}
|
||||
enterDelay={1000}
|
||||
>
|
||||
<div>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`Switch to ${
|
||||
paletteMode === "dark" ? "light" : "dark"
|
||||
} mode`}
|
||||
onClick={toggleMode}
|
||||
>
|
||||
<React.Fragment>
|
||||
<LightModeIcon
|
||||
sx={{
|
||||
display: "inline",
|
||||
[theme.getColorSchemeSelector("dark")]: {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<DarkModeIcon
|
||||
sx={{
|
||||
display: "none",
|
||||
[theme.getColorSchemeSelector("dark")]: {
|
||||
display: "inline",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
2
matrixgw_frontend/src/widgets/dashboard/constants.ts
Normal file
2
matrixgw_frontend/src/widgets/dashboard/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DRAWER_WIDTH = 240; // px
|
||||
export const MINI_DRAWER_WIDTH = 90; // px
|
||||
23
matrixgw_frontend/src/widgets/dashboard/mixins.ts
Normal file
23
matrixgw_frontend/src/widgets/dashboard/mixins.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type Theme } from "@mui/material/styles";
|
||||
|
||||
export function getDrawerSxTransitionMixin(
|
||||
isExpanded: boolean,
|
||||
property: string
|
||||
) {
|
||||
return {
|
||||
transition: (theme: Theme) =>
|
||||
theme.transitions.create(property, {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: isExpanded
|
||||
? theme.transitions.duration.enteringScreen
|
||||
: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getDrawerWidthTransitionMixin(isExpanded: boolean) {
|
||||
return {
|
||||
...getDrawerSxTransitionMixin(isExpanded, "width"),
|
||||
overflowX: "hidden",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user