nav
This commit is contained in:
54
src/App.jsx
54
src/App.jsx
@@ -5,7 +5,47 @@ import { useAuth } from './auth/useAuth';
|
|||||||
import LoginPage from './components/LoginPage';
|
import LoginPage from './components/LoginPage';
|
||||||
import AppShell from './components/AppShell';
|
import AppShell from './components/AppShell';
|
||||||
import SectionPage from './pages/SectionPage';
|
import SectionPage from './pages/SectionPage';
|
||||||
import { NAV_ITEMS } from './nav';
|
import SubPage from './pages/SubPage';
|
||||||
|
import { NAV_ITEMS, SUB_NAV } from './nav';
|
||||||
|
|
||||||
|
function sectionRoutes(path) {
|
||||||
|
const subNav = SUB_NAV[path];
|
||||||
|
const routePath = path === '/' ? undefined : path.slice(1);
|
||||||
|
|
||||||
|
if (subNav) {
|
||||||
|
return (
|
||||||
|
<Route key={path} path={routePath} element={<SectionPage sectionPath={path} />}>
|
||||||
|
{subNav.map(({ segment, label }) =>
|
||||||
|
segment ? (
|
||||||
|
<Route
|
||||||
|
key={segment}
|
||||||
|
path={segment}
|
||||||
|
element={<SubPage title={label} />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Route key="index" index element={<SubPage title={label} />} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = NAV_ITEMS.find((n) => n.path === path)?.label;
|
||||||
|
|
||||||
|
if (path === '/') {
|
||||||
|
return (
|
||||||
|
<Route key={path} index element={<SectionPage sectionPath={path}><SubPage title={label} /></SectionPage>} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={routePath}
|
||||||
|
element={<SectionPage sectionPath={path}><SubPage title={label} /></SectionPage>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
@@ -34,17 +74,7 @@ export default function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<AppShell />}>
|
<Route path="/" element={<AppShell />}>
|
||||||
{NAV_ITEMS.map(({ path, label }) =>
|
{NAV_ITEMS.map((item) => sectionRoutes(item.path))}
|
||||||
path === '/' ? (
|
|
||||||
<Route key={path} index element={<SectionPage title={label} />} />
|
|
||||||
) : (
|
|
||||||
<Route
|
|
||||||
key={path}
|
|
||||||
path={path.slice(1)}
|
|
||||||
element={<SectionPage title={label} />}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { NavLink, Outlet } from 'react-router-dom';
|
|||||||
import UserMenu from './UserMenu';
|
import UserMenu from './UserMenu';
|
||||||
import { NAV_ITEMS } from '../nav';
|
import { NAV_ITEMS } from '../nav';
|
||||||
|
|
||||||
const SIDEBAR_WIDTH = 240;
|
const SIDEBAR_WIDTH = 144;
|
||||||
|
|
||||||
export default function AppShell() {
|
export default function AppShell() {
|
||||||
return (
|
return (
|
||||||
@@ -31,8 +31,9 @@ export default function AppShell() {
|
|||||||
end={path === '/'}
|
end={path === '/'}
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
px: 2,
|
px: 1.5,
|
||||||
py: 1.25,
|
py: 1.25,
|
||||||
|
borderLeft: '3px solid transparent',
|
||||||
color: theme.nav.item,
|
color: theme.nav.item,
|
||||||
'& .MuiListItemIcon-root': { color: 'inherit' },
|
'& .MuiListItemIcon-root': { color: 'inherit' },
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -43,12 +44,11 @@ export default function AppShell() {
|
|||||||
bgcolor: theme.nav.itemActive,
|
bgcolor: theme.nav.itemActive,
|
||||||
color: theme.nav.itemActiveText,
|
color: theme.nav.itemActiveText,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
borderLeft: `3px solid ${theme.nav.itemActiveAccent}`,
|
borderLeftColor: theme.nav.itemActiveAccent,
|
||||||
pl: '13px',
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||||
<Icon />
|
<Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={label} />
|
<ListItemText primary={label} />
|
||||||
|
|||||||
@@ -1,13 +1,68 @@
|
|||||||
import Box from '@mui/material/Box';
|
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 Typography from '@mui/material/Typography';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
const SUB_NAV_WIDTH = 80;
|
||||||
|
|
||||||
|
export default function PageLayout({ title, subNav, basePath, children }) {
|
||||||
|
const hasSubNav = subNav?.length > 0;
|
||||||
|
|
||||||
export default function PageLayout({ title, children }) {
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', gap: 2, minHeight: 0 }}>
|
||||||
|
<Box
|
||||||
|
component={hasSubNav ? 'nav' : 'div'}
|
||||||
|
aria-label={hasSubNav ? 'Unternavigation' : undefined}
|
||||||
|
sx={{
|
||||||
|
width: SUB_NAV_WIDTH,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasSubNav &&
|
||||||
|
subNav.map(({ segment, label, icon: Icon }) => {
|
||||||
|
const to = segment ? `${basePath}/${segment}` : basePath;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={segment || 'index'} title={label} placement="right">
|
||||||
|
<IconButton
|
||||||
|
component={NavLink}
|
||||||
|
to={to}
|
||||||
|
end={!segment}
|
||||||
|
aria-label={label}
|
||||||
|
disableRipple
|
||||||
|
sx={(theme) => ({
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
'&.active': {
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon sx={{ fontSize: 40 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 2 }}>
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function UserMenu() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ px: 2, py: 1.5, alignSelf: 'flex-start' }}>
|
<Box sx={{ px: 1.5, py: 1.5, alignSelf: 'flex-start' }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||||
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
|
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
|
||||||
|
|||||||
24
src/nav.js
24
src/nav.js
@@ -4,6 +4,13 @@ import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
|||||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||||
import PeopleIcon from '@mui/icons-material/People';
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
import ViewListIcon from '@mui/icons-material/ViewList';
|
||||||
|
import CategoryIcon from '@mui/icons-material/Category';
|
||||||
|
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
||||||
|
import ListAltIcon from '@mui/icons-material/ListAlt';
|
||||||
|
import ArchiveIcon from '@mui/icons-material/Archive';
|
||||||
|
import GroupsIcon from '@mui/icons-material/Groups';
|
||||||
|
import ContactPageIcon from '@mui/icons-material/ContactPage';
|
||||||
|
|
||||||
export const NAV_ITEMS = [
|
export const NAV_ITEMS = [
|
||||||
{ path: '/', label: 'Home', icon: HomeIcon },
|
{ path: '/', label: 'Home', icon: HomeIcon },
|
||||||
@@ -13,3 +20,20 @@ export const NAV_ITEMS = [
|
|||||||
{ path: '/kunde', label: 'Kunde', icon: PeopleIcon },
|
{ path: '/kunde', label: 'Kunde', icon: PeopleIcon },
|
||||||
{ path: '/infos', label: 'Infos', icon: InfoIcon },
|
{ path: '/infos', label: 'Infos', icon: InfoIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const SUB_NAV = {
|
||||||
|
'/artikel': [
|
||||||
|
{ segment: '', label: 'Übersicht', icon: ViewListIcon },
|
||||||
|
{ segment: 'kategorien', label: 'Kategorien', icon: CategoryIcon },
|
||||||
|
{ segment: 'lager', label: 'Lager', icon: WarehouseIcon },
|
||||||
|
],
|
||||||
|
'/auftrag': [
|
||||||
|
{ segment: '', label: 'Aktive Aufträge', icon: ListAltIcon },
|
||||||
|
{ segment: 'archiv', label: 'Archiv', icon: ArchiveIcon },
|
||||||
|
],
|
||||||
|
'/kunde': [
|
||||||
|
{ segment: '', label: 'Kundenliste', icon: PeopleIcon },
|
||||||
|
{ segment: 'gruppen', label: 'Gruppen', icon: GroupsIcon },
|
||||||
|
{ segment: 'kontakte', label: 'Kontakte', icon: ContactPageIcon },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import Typography from '@mui/material/Typography';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import PageLayout from '../components/PageLayout';
|
import PageLayout from '../components/PageLayout';
|
||||||
|
import { NAV_ITEMS, SUB_NAV } from '../nav';
|
||||||
|
|
||||||
|
export default function SectionPage({ sectionPath, children }) {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const title = NAV_ITEMS.find((item) => item.path === sectionPath)?.label ?? '';
|
||||||
|
const subNav = SUB_NAV[sectionPath];
|
||||||
|
const activeSub = subNav?.find((item) => {
|
||||||
|
const subPath = item.segment ? `${sectionPath}/${item.segment}` : sectionPath;
|
||||||
|
return pathname === subPath || pathname === `${subPath}/`;
|
||||||
|
});
|
||||||
|
const pageTitle = activeSub && subNav ? activeSub.label : title;
|
||||||
|
|
||||||
export default function SectionPage({ title }) {
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={title}>
|
<PageLayout title={pageTitle} subNav={subNav} basePath={sectionPath}>
|
||||||
<Typography color="text.secondary">Inhalt folgt.</Typography>
|
{children ?? <Outlet />}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/pages/SubPage.jsx
Normal file
9
src/pages/SubPage.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default function SubPage({ title, description }) {
|
||||||
|
return (
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{description ?? `${title} — Inhalt folgt.`}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user