Files
dashfront/src/components/AppShell.jsx
2026-10-04 02:13:50 +02:00

265 lines
7.9 KiB
JavaScript

import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ButtonBase from '@mui/material/ButtonBase';
import Typography from '@mui/material/Typography';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import UserMenu from './UserMenu';
import HelpBadge from './HelpBadge';
import { NAV_ITEMS, SUB_NAV, resolveActiveNav, resolveActiveSub } from '../nav';
import { artikelQueryPath } from './article/artikelQuery';
import { defaultCategoryPath } from './category/categoryRoute';
import { navigationAutomat } from '../automats';
import { ChatFab, ChatDrawer } from './chat';
const SIDEBAR_WIDTH = 144;
const SUB_NAV_WIDTH = 88;
class AppSidebar extends PureComponent {
constructor(props) {
super(props);
this.state = {
lastDeliveryPath: navigationAutomat.state.lastDeliveryPath,
lastArticleSku: navigationAutomat.state.lastArticleSku,
};
}
componentDidMount() {
this.unsub = navigationAutomat.subscribe(this, (state) => ({
lastDeliveryPath: state.lastDeliveryPath,
lastArticleSku: state.lastArticleSku,
}));
}
componentWillUnmount() {
this.unsub?.();
}
render() {
const { activeNav, navigate } = this.props;
const { lastDeliveryPath, lastArticleSku } = this.state;
return (
<Box
component="nav"
sx={(theme) => ({
width: SIDEBAR_WIDTH,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.nav.backgroundColor || '#1a3533',
background: theme.nav.background,
position: 'sticky',
top: 0,
alignSelf: 'flex-start',
height: '100vh',
})}
>
<List disablePadding sx={{ flexGrow: 1 }}>
{NAV_ITEMS.map(({ path, label, help, icon: Icon }) => {
const active = path === activeNav;
return (
<ListItemButton
key={path}
type="button"
selected={active}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (path === '/artikel') {
if (lastArticleSku) {
navigate(`/artikel/${lastArticleSku}`);
} else {
navigate(artikelQueryPath());
}
} else if (path === '/lieferung') {
if (lastDeliveryPath) {
navigate(lastDeliveryPath);
} else {
navigate(path);
}
} else {
navigate(path);
}
}}
sx={(theme) => ({
borderRadius: 0,
px: 1.5,
py: 1.25,
borderLeft: '3px solid transparent',
color: theme.nav.item,
'& .MuiListItemIcon-root': { color: 'inherit' },
'&:hover': {
bgcolor: theme.nav.itemHover,
color: theme.nav.itemHoverText,
},
'&.Mui-selected': {
bgcolor: theme.nav.itemActive,
color: theme.nav.itemActiveText,
fontWeight: 600,
borderLeftColor: theme.nav.itemActiveAccent,
'&:hover': {
bgcolor: theme.nav.itemActive,
color: theme.nav.itemActiveText,
},
},
})}
>
<ListItemIcon sx={{ minWidth: 36 }}>
<HelpBadge title={label} help={help} placement="right">
<Icon />
</HelpBadge>
</ListItemIcon>
<ListItemText primary={label} />
</ListItemButton>
);
})}
</List>
<UserMenu />
</Box>
);
}
}
class AppSubNav extends PureComponent {
render() {
const { activeNav, activeSub, navigate } = this.props;
const subNav = activeNav ? SUB_NAV[activeNav] : null;
if (!subNav?.length) {
return (
<Box
sx={{
width: SUB_NAV_WIDTH,
flexShrink: 0,
}}
/>
);
}
return (
<Box
component="nav"
aria-label="Unternavigation"
sx={{
width: SUB_NAV_WIDTH,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1.5,
position: 'sticky',
top: 0,
alignSelf: 'flex-start',
py: 4,
}}
>
{subNav.map(({ segment, label, title, help, icon: Icon }) => {
const active = segment === activeSub;
return (
<ButtonBase
key={segment || 'index'}
type="button"
aria-label={label}
aria-current={active ? 'page' : undefined}
disableRipple
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
if (!segment && activeNav === '/artikel') {
navigate(artikelQueryPath());
return;
}
if (segment === 'kategorien') {
navigate(defaultCategoryPath());
return;
}
navigate(segment ? `${activeNav}/${segment}` : activeNav);
}}
sx={(theme) => ({
width: SUB_NAV_WIDTH,
py: 1,
px: 0.5,
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
color: active
? theme.palette.primary.main
: theme.palette.text.secondary,
bgcolor: active ? theme.palette.action.selected : 'transparent',
'&:hover': {
bgcolor: theme.palette.action.hover,
color: theme.palette.primary.main,
},
})}
>
<HelpBadge title={title || label} help={help} placement="right">
<Icon sx={{ fontSize: 32 }} />
</HelpBadge>
{label ? (
<Typography
variant="caption"
noWrap
sx={{
fontSize: '0.68rem',
fontWeight: 500,
lineHeight: 1.2,
textAlign: 'center',
maxWidth: '100%',
}}
>
{label}
</Typography>
) : null}
</ButtonBase>
);
})}
</Box>
);
}
}
export default function AppShell() {
const navigate = useNavigate();
const { pathname } = useLocation();
const activeNav = resolveActiveNav(pathname);
const activeSub = activeNav ? resolveActiveSub(pathname, activeNav) : null;
return (
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
<AppSidebar activeNav={activeNav} navigate={navigate} />
<Box
component="main"
sx={(theme) => ({
flexGrow: 1,
display: 'flex',
gap: 2,
py: 4,
px: 3,
bgcolor: theme.nav.mainBackground,
minWidth: 0,
})}
>
<AppSubNav
activeNav={activeNav}
activeSub={activeSub}
navigate={navigate}
/>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Outlet />
</Box>
</Box>
<ChatFab />
<ChatDrawer />
</Box>
);
}