266 lines
6.9 KiB
TypeScript
266 lines
6.9 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
Box,
|
|
Drawer,
|
|
List,
|
|
Divider,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
IconButton,
|
|
Tooltip,
|
|
useMediaQuery,
|
|
useTheme,
|
|
} from '@mui/material';
|
|
import TableViewIcon from '@mui/icons-material/TableView';
|
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
import MenuIcon from '@mui/icons-material/Menu';
|
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
import { ResourceConfig } from '../types/config';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
|
|
const drawerWidth = 240;
|
|
const collapsedWidth = 64;
|
|
|
|
interface AdminLayoutProps {
|
|
children: React.ReactNode;
|
|
onSelectResource: (resourceName: string | null) => void;
|
|
onLogout: () => void;
|
|
username?: string;
|
|
resources: ResourceConfig[];
|
|
}
|
|
|
|
export default function AdminLayout({
|
|
children,
|
|
onSelectResource,
|
|
resources,
|
|
}: AdminLayoutProps) {
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
|
const [mobileOpen, setMobileOpen] = React.useState(false);
|
|
|
|
const activeResourceName = location.pathname.split('/admin')[1] || null;
|
|
|
|
// AUTO-TOGGLE LOGIC (unchanged)
|
|
React.useEffect(() => {
|
|
if (isMobile) {
|
|
setIsCollapsed(false);
|
|
setMobileOpen(false);
|
|
} else {
|
|
if (location.pathname === '/admin' || location.pathname === '') {
|
|
setIsCollapsed(false);
|
|
} else {
|
|
setIsCollapsed(true);
|
|
}
|
|
}
|
|
}, [location.pathname, isMobile]);
|
|
|
|
const currentWidth = isMobile
|
|
? drawerWidth
|
|
: isCollapsed
|
|
? collapsedWidth
|
|
: drawerWidth;
|
|
|
|
const handleDrawerToggle = () => {
|
|
setMobileOpen((prev) => !prev);
|
|
};
|
|
|
|
const handleSidebarToggle = () => {
|
|
setIsCollapsed((prev) => !prev);
|
|
};
|
|
|
|
const drawerContent = (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
{!isMobile && (
|
|
<>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: isCollapsed ? 'center' : 'flex-end',
|
|
p: 1,
|
|
}}
|
|
>
|
|
<IconButton onClick={handleSidebarToggle}>
|
|
{isCollapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
|
</IconButton>
|
|
</Box>
|
|
<Divider />
|
|
</>
|
|
)}
|
|
|
|
{/* Mobile spacing (replaces Toolbar) */}
|
|
{isMobile && (
|
|
<Box sx={{ height: (theme) => theme.spacing(7) }} />
|
|
)}
|
|
|
|
<List>
|
|
<ListItem disablePadding>
|
|
<Tooltip
|
|
title={isCollapsed && !isMobile ? 'Dashboard' : ''}
|
|
placement="right"
|
|
>
|
|
<ListItemButton
|
|
selected={location.pathname === '/admin'}
|
|
onClick={() => navigate('/admin')}
|
|
sx={{
|
|
minHeight: 48,
|
|
justifyContent:
|
|
isCollapsed && !isMobile ? 'center' : 'initial',
|
|
px: 2.5,
|
|
}}
|
|
>
|
|
<ListItemIcon
|
|
sx={{
|
|
minWidth: 0,
|
|
mr: isCollapsed && !isMobile ? 0 : 3,
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<DashboardIcon
|
|
color={
|
|
location.pathname === '/admin'
|
|
? 'primary'
|
|
: 'inherit'
|
|
}
|
|
/>
|
|
</ListItemIcon>
|
|
{(!isCollapsed || isMobile) && (
|
|
<ListItemText primary="Dashboard" />
|
|
)}
|
|
</ListItemButton>
|
|
</Tooltip>
|
|
</ListItem>
|
|
</List>
|
|
|
|
<Divider />
|
|
|
|
<List sx={{ flexGrow: 1 }}>
|
|
{resources.map((res) => (
|
|
<ListItem key={res.name} disablePadding>
|
|
<Tooltip
|
|
title={
|
|
isCollapsed && !isMobile ? res.pluralLabel : ''
|
|
}
|
|
placement="right"
|
|
>
|
|
<ListItemButton
|
|
selected={activeResourceName === res.name}
|
|
onClick={() => onSelectResource(res.name)}
|
|
sx={{
|
|
minHeight: 48,
|
|
justifyContent:
|
|
isCollapsed && !isMobile
|
|
? 'center'
|
|
: 'initial',
|
|
px: 2.5,
|
|
}}
|
|
>
|
|
<ListItemIcon
|
|
sx={{
|
|
minWidth: 0,
|
|
mr: isCollapsed && !isMobile ? 0 : 3,
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<TableViewIcon
|
|
color={
|
|
activeResourceName === res.name
|
|
? 'primary'
|
|
: 'inherit'
|
|
}
|
|
/>
|
|
</ListItemIcon>
|
|
{(!isCollapsed || isMobile) && (
|
|
<ListItemText primary={res.pluralLabel} />
|
|
)}
|
|
</ListItemButton>
|
|
</Tooltip>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Box>
|
|
);
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex' }}>
|
|
{/* NAV */}
|
|
<Box
|
|
component="nav"
|
|
sx={{ width: { md: currentWidth }, flexShrink: { md: 0 } }}
|
|
>
|
|
{isMobile ? (
|
|
<Drawer
|
|
variant="temporary"
|
|
open={mobileOpen}
|
|
onClose={handleDrawerToggle}
|
|
ModalProps={{ keepMounted: true }}
|
|
sx={{
|
|
display: { xs: 'block', md: 'none' },
|
|
'& .MuiDrawer-paper': {
|
|
width: drawerWidth,
|
|
},
|
|
}}
|
|
>
|
|
{drawerContent}
|
|
</Drawer>
|
|
) : (
|
|
<Drawer
|
|
variant="permanent"
|
|
open
|
|
sx={{
|
|
display: { xs: 'none', md: 'block' },
|
|
width: currentWidth,
|
|
flexShrink: 0,
|
|
whiteSpace: 'nowrap',
|
|
[`& .MuiDrawer-paper`]: {
|
|
width: currentWidth,
|
|
overflowX: 'hidden',
|
|
transition: theme.transitions.create('width'),
|
|
},
|
|
}}
|
|
>
|
|
{drawerContent}
|
|
</Drawer>
|
|
)}
|
|
</Box>
|
|
|
|
{/* MAIN */}
|
|
<Box
|
|
component="main"
|
|
sx={{
|
|
flexGrow: 1,
|
|
p: { xs: 2, md: 3 },
|
|
width: {
|
|
xs: '100%',
|
|
md: `calc(100% - ${currentWidth}px)`,
|
|
},
|
|
}}
|
|
>
|
|
{/* Control row (replaces AppBar) */}
|
|
{isMobile && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
mb: 2,
|
|
height: (theme) => theme.spacing(7),
|
|
}}
|
|
>
|
|
<IconButton onClick={handleDrawerToggle}>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
</Box>
|
|
)}
|
|
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|