article and relayout
This commit is contained in:
@@ -3,26 +3,19 @@ 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 { NavLink, Outlet } from 'react-router-dom';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import UserMenu from './UserMenu';
|
||||
import { NAV_ITEMS } from '../nav';
|
||||
import { artikelQueryPath } from '../pages/artikel/artikelQuery';
|
||||
|
||||
const SIDEBAR_WIDTH = 144;
|
||||
|
||||
const FOCUSABLE =
|
||||
'a, button, input, textarea, select, [role="button"], [role="menuitem"], [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
||||
|
||||
function preserveFocusOnBackground(e) {
|
||||
if (e.target.closest(FOCUSABLE)) return;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export default function AppShell() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseDown={preserveFocusOnBackground}
|
||||
sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
|
||||
<Box
|
||||
component="nav"
|
||||
sx={(theme) => ({
|
||||
@@ -34,37 +27,48 @@ export default function AppShell() {
|
||||
})}
|
||||
>
|
||||
<List disablePadding sx={{ flexGrow: 1 }}>
|
||||
{NAV_ITEMS.map(({ path, label, icon: Icon }) => (
|
||||
<ListItemButton
|
||||
key={path}
|
||||
component={NavLink}
|
||||
to={path}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
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,
|
||||
},
|
||||
'&.active': {
|
||||
bgcolor: theme.nav.itemActive,
|
||||
color: theme.nav.itemActiveText,
|
||||
fontWeight: 600,
|
||||
borderLeftColor: theme.nav.itemActiveAccent,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={label} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
{NAV_ITEMS.map(({ path, label, icon: Icon }) => {
|
||||
const active = pathname === path || pathname.startsWith(`${path}/`);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
key={path}
|
||||
type="button"
|
||||
selected={active}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
navigate(path === '/artikel' ? artikelQueryPath() : 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 }}>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={label} />
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
|
||||
<UserMenu />
|
||||
|
||||
@@ -2,12 +2,27 @@ import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { artikelQueryPath } from '../pages/artikel/artikelQuery';
|
||||
|
||||
const SUB_NAV_WIDTH = 80;
|
||||
|
||||
function isSubNavActive(pathname, basePath, segment, knownSegments) {
|
||||
if (!segment) {
|
||||
if (pathname === basePath || pathname === `${basePath}/`) return true;
|
||||
// /artikel/:sku detail — highlight overview, not kategorien/lager
|
||||
const rest = pathname.slice(basePath.length).replace(/^\//, '');
|
||||
return Boolean(rest) && !rest.includes('/') && !knownSegments.has(rest);
|
||||
}
|
||||
const subPath = `${basePath}/${segment}`;
|
||||
return pathname === subPath || pathname === `${subPath}/`;
|
||||
}
|
||||
|
||||
export default function PageLayout({ title, subNav, basePath, children }) {
|
||||
const hasSubNav = subNav?.length > 0;
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const knownSegments = new Set((subNav ?? []).map((item) => item.segment).filter(Boolean));
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, minHeight: 0 }}>
|
||||
@@ -25,31 +40,33 @@ export default function PageLayout({ title, subNav, basePath, children }) {
|
||||
>
|
||||
{hasSubNav &&
|
||||
subNav.map(({ segment, label, icon: Icon }) => {
|
||||
const to = segment ? `${basePath}/${segment}` : basePath;
|
||||
const active = isSubNavActive(pathname, basePath, segment, knownSegments);
|
||||
|
||||
return (
|
||||
<Tooltip key={segment || 'index'} title={label} placement="right">
|
||||
<IconButton
|
||||
component={NavLink}
|
||||
to={to}
|
||||
end={!segment}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
disableRipple
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
if (!segment && basePath === '/artikel') {
|
||||
navigate(artikelQueryPath());
|
||||
return;
|
||||
}
|
||||
navigate(segment ? `${basePath}/${segment}` : basePath);
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
color: theme.palette.text.secondary,
|
||||
color: active ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': {
|
||||
bgcolor: theme.palette.action.hover,
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
'&.active': {
|
||||
bgcolor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Icon sx={{ fontSize: 40 }} />
|
||||
|
||||
@@ -10,7 +10,15 @@ export default function SectionLayout({ sectionPath }) {
|
||||
const subPath = item.segment ? `${sectionPath}/${item.segment}` : sectionPath;
|
||||
return pathname === subPath || pathname === `${subPath}/`;
|
||||
});
|
||||
const pageTitle = activeSub && subNav ? activeSub.label : title;
|
||||
|
||||
const knownSegments = new Set((subNav ?? []).map((item) => item.segment).filter(Boolean));
|
||||
const rest = pathname.slice(sectionPath.length).replace(/^\//, '').split('/')[0];
|
||||
const detailSku =
|
||||
sectionPath === '/artikel' && rest && !knownSegments.has(rest)
|
||||
? decodeURIComponent(rest)
|
||||
: null;
|
||||
|
||||
const pageTitle = detailSku ?? (activeSub && subNav ? activeSub.label : title);
|
||||
|
||||
return (
|
||||
<PageLayout title={pageTitle} subNav={subNav} basePath={sectionPath}>
|
||||
|
||||
Reference in New Issue
Block a user