proposal-system/web/src/components/Sidebar.tsx

190 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useLocation, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import {
Drawer,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Toolbar,
Box,
Typography,
Tooltip,
useMediaQuery,
useTheme,
} from '@mui/material';
import DashboardIcon from '@mui/icons-material/Dashboard';
import DescriptionIcon from '@mui/icons-material/Description';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import AssignmentIcon from '@mui/icons-material/Assignment';
import PeopleIcon from '@mui/icons-material/People';
import { selectSidebarOpen, setSidebarOpen } from '../app/slices/uiSlice';
import { selectUser } from '../app/slices/authSlice';
import type { RootState } from '../app/store';
import { DRAWER_WIDTH } from '../constants';
const COLLAPSED_WIDTH = 72;
const dispatcherNav = [
{ label: 'Dashboard', path: '/', icon: <DashboardIcon fontSize="small" /> },
{ label: 'My Proposals', path: '/proposals', icon: <DescriptionIcon fontSize="small" /> },
{ label: 'New Proposal', path: '/proposals/new', icon: <AddCircleIcon fontSize="small" /> },
];
const adminNav = [
{ label: 'Admin Queue', path: '/admin', icon: <AdminPanelSettingsIcon fontSize="small" /> },
{ label: 'All Proposals', path: '/admin/proposals', icon: <AssignmentIcon fontSize="small" /> },
];
const sysadminNav = [
{ label: 'Users', path: '/admin/users', icon: <PeopleIcon fontSize="small" /> },
];
const ALL_NAV_PATHS = new Set(
[...dispatcherNav, ...adminNav, ...sysadminNav].map(n => n.path)
);
function isNavActive(pathname: string, itemPath: string): boolean {
if (pathname === itemPath) return true;
if (ALL_NAV_PATHS.has(pathname)) return false;
if (itemPath === '/' || itemPath === '/admin') return false;
return pathname.startsWith(itemPath + '/');
}
export default function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const theme = useTheme();
const open = useSelector((state: RootState) => selectSidebarOpen(state));
const user = useSelector((state: RootState) => selectUser(state));
const isMobile = useMediaQuery(theme.breakpoints.down('md')); // <900px
const isCollapsed = useMediaQuery(theme.breakpoints.between('md', 'lg')); // 900–1200px
const isAdmin = user?.role === 'Admin' || user?.role === 'SysAdmin';
const isSysAdmin = user?.role === 'SysAdmin';
const navItems = dispatcherNav;
const drawerVariant = isMobile ? 'temporary' : 'persistent';
const effectiveWidth = isCollapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH;
const handleNavClick = (path: string) => {
navigate(path);
if (isMobile) {
dispatch(setSidebarOpen(false));
}
};
const handleDrawerClose = () => {
dispatch(setSidebarOpen(false));
};
const renderNavItem = (item: { label: string; path: string; icon: React.ReactNode }) => {
const button = (
<ListItemButton
key={item.path}
selected={isNavActive(location.pathname, item.path)}
onClick={() => handleNavClick(item.path)}
sx={{
py: 1,
justifyContent: isCollapsed ? 'center' : 'flex-start',
minHeight: 40,
}}
>
<ListItemIcon
sx={{
minWidth: isCollapsed ? 0 : 40,
justifyContent: 'center',
}}
>
{item.icon}
</ListItemIcon>
{!isCollapsed && (
<ListItemText primary={item.label} primaryTypographyProps={{ fontSize: '13px' }} />
)}
</ListItemButton>
);
if (isCollapsed) {
return (
<Tooltip key={item.path} title={item.label} placement="right" arrow>
{button}
</Tooltip>
);
}
return button;
};
return (
<Drawer
variant={drawerVariant}
open={open}
onClose={handleDrawerClose}
sx={{
width: open ? effectiveWidth : 0,
flexShrink: 0,
transition: 'width 250ms ease',
'& .MuiDrawer-paper': {
width: effectiveWidth,
boxSizing: 'border-box',
overflowX: 'hidden',
},
}}
>
<Toolbar sx={{ minHeight: 64 }} />
<Box sx={{ overflow: 'auto', flexGrow: 1, pt: 1 }}>
{isAdmin && (
<>
{!isCollapsed && (
<Typography
sx={{
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#94A3B8',
px: 2.5,
mb: 0.5,
}}
>
Admin
</Typography>
)}
<List disablePadding>
{adminNav.map((item) => renderNavItem(item))}
{isSysAdmin && sysadminNav.map((item) => renderNavItem(item))}
</List>
</>
)}
{!isCollapsed && (
<Typography
sx={{
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#94A3B8',
px: 2.5,
mt: isAdmin ? 2 : 0,
mb: 0.5,
}}
>
Proposals
</Typography>
)}
<List disablePadding>
{navItems.map((item) => renderNavItem(item))}
</List>
</Box>
{user && !isCollapsed && (
<Box sx={{ p: 2, mt: 'auto', borderTop: '1px solid #DDE3E8' }}>
<Box sx={{ fontSize: '13px', fontWeight: 500, color: '#1F2933' }}>{user.displayName}</Box>
<Box sx={{ fontSize: '11px', color: '#8A949E' }}>{user.role}</Box>
</Box>
)}
</Drawer>
);
}