Compare commits
3 Commits
d9c96e85f7
...
3de26c0fff
| Author | SHA1 | Date | |
|---|---|---|---|
| 3de26c0fff | |||
| 79b5a767f3 | |||
| fdcd565431 |
@@ -11,8 +11,8 @@ import { LoginRoute } from "./routes/auth/LoginRoute";
|
|||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
import { HomeRoute } from "./routes/HomeRoute";
|
import { HomeRoute } from "./routes/HomeRoute";
|
||||||
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
|
||||||
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
||||||
|
import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function BaseAuthenticatedPage(): React.ReactElement {
|
|
||||||
return <p>todo authenticated</p>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
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(false);
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
125
matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { mdiMessageTextFast } from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
|
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
||||||
|
import MuiAppBar from "@mui/material/AppBar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import { RouterLink } from "../RouterLink";
|
||||||
|
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 {
|
||||||
|
menuOpen: boolean;
|
||||||
|
onToggleMenu: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardHeader({
|
||||||
|
menuOpen,
|
||||||
|
onToggleMenu,
|
||||||
|
}: DashboardHeaderProps) {
|
||||||
|
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={200}
|
||||||
|
>
|
||||||
|
<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: 3 }}>{getMenuIcon(menuOpen)}</Box>
|
||||||
|
<RouterLink to="/">
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<LogoContainer>
|
||||||
|
<Icon path={mdiMessageTextFast} size="2em" />
|
||||||
|
</LogoContainer>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "700",
|
||||||
|
ml: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
MatrixGW
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</RouterLink>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={1}
|
||||||
|
sx={{ marginLeft: "auto" }}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
205
matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { mdiBug, mdiForum, mdiKeyVariant, mdiLinkLock } from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
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 { useTheme } from "@mui/material/styles";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import * as React from "react";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem";
|
||||||
|
import DashboardSidebarPageItem from "./DashboardSidebarPageItem";
|
||||||
|
import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants";
|
||||||
|
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 isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
|
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
const [isFullyExpanded, setIsFullyExpanded] = 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]);
|
||||||
|
|
||||||
|
const mini = !disableCollapsibleSidebar && !expanded;
|
||||||
|
|
||||||
|
const handleSetSidebarExpanded = React.useCallback(
|
||||||
|
(newExpanded: boolean) => () => {
|
||||||
|
setExpanded(newExpanded);
|
||||||
|
},
|
||||||
|
[setExpanded]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePageItemClick = React.useCallback(() => {
|
||||||
|
if (!isOverSmViewport) {
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
title="Messages"
|
||||||
|
icon={<Icon path={mdiForum} size={"1.5em"} />}
|
||||||
|
href="/"
|
||||||
|
/>
|
||||||
|
<DashboardSidebarDividerItem />
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
title="Matrix link"
|
||||||
|
icon={<Icon path={mdiLinkLock} size={"1.5em"} />}
|
||||||
|
href="/matrixlink"
|
||||||
|
/>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
title="API tokens"
|
||||||
|
icon={<Icon path={mdiKeyVariant} size={"1.5em"} />}
|
||||||
|
href="/tokens"
|
||||||
|
/>
|
||||||
|
<DashboardSidebarPageItem
|
||||||
|
title="WS Debug"
|
||||||
|
icon={<Icon path={mdiBug} size={"1.5em"} />}
|
||||||
|
href="/wsdebug"
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
[mini, hasDrawerTransitions, isFullyExpanded]
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
hasDrawerTransitions,
|
||||||
|
};
|
||||||
|
}, [handlePageItemClick, mini, isFullyExpanded, 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,10 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const DashboardSidebarContext = React.createContext<{
|
||||||
|
onPageItemClick: () => void;
|
||||||
|
mini: boolean;
|
||||||
|
fullyExpanded: boolean;
|
||||||
|
hasDrawerTransitions: boolean;
|
||||||
|
} | null>(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,142 @@
|
|||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
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 Typography from "@mui/material/Typography";
|
||||||
|
import type {} from "@mui/material/themeCssVarsAugmentation";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Link, matchPath, useLocation } from "react-router";
|
||||||
|
import DashboardSidebarContext from "./DashboardSidebarContext";
|
||||||
|
import { MINI_DRAWER_WIDTH } from "./constants";
|
||||||
|
|
||||||
|
export interface DashboardSidebarPageItemProps {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
action?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardSidebarPageItem({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
href,
|
||||||
|
action,
|
||||||
|
disabled = false,
|
||||||
|
}: DashboardSidebarPageItemProps) {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const sidebarContext = React.useContext(DashboardSidebarContext);
|
||||||
|
if (!sidebarContext) {
|
||||||
|
throw new Error("Sidebar context was used without a provider.");
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
onPageItemClick,
|
||||||
|
mini = false,
|
||||||
|
fullyExpanded = true,
|
||||||
|
} = sidebarContext;
|
||||||
|
|
||||||
|
const hasExternalHref = href
|
||||||
|
? href.startsWith("http://") || href.startsWith("https://")
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const LinkComponent = hasExternalHref ? "a" : Link;
|
||||||
|
|
||||||
|
const selected = !!matchPath(href, pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ListItem disablePadding style={{ padding: "5px" }}>
|
||||||
|
<ListItemButton
|
||||||
|
selected={selected}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{
|
||||||
|
height: mini ? 50 : "auto",
|
||||||
|
}}
|
||||||
|
{...{
|
||||||
|
LinkComponent,
|
||||||
|
...(hasExternalHref
|
||||||
|
? {
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
to: href,
|
||||||
|
onClick: onPageItemClick,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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}
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</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