639 lines
18 KiB
TypeScript
639 lines
18 KiB
TypeScript
// components/layout/Header.tsx
|
|
import React, { useEffect, useState } from 'react';
|
|
import { NavigateFunction, useLocation } from 'react-router-dom';
|
|
import {
|
|
AppBar,
|
|
Toolbar,
|
|
Tooltip,
|
|
Typography,
|
|
Button,
|
|
IconButton,
|
|
Box,
|
|
Drawer,
|
|
Divider,
|
|
Avatar,
|
|
Tabs,
|
|
Tab,
|
|
Container,
|
|
Fade,
|
|
Popover,
|
|
Paper,
|
|
Menu,
|
|
MenuItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Collapse,
|
|
List,
|
|
ListItem,
|
|
ListItemButton,
|
|
SxProps,
|
|
} from '@mui/material';
|
|
import { styled, useTheme } from '@mui/material/styles';
|
|
import {
|
|
Menu as MenuIcon,
|
|
Dashboard,
|
|
Person,
|
|
Logout,
|
|
Settings,
|
|
ExpandMore,
|
|
ExpandLess,
|
|
KeyboardArrowDown,
|
|
} from '@mui/icons-material';
|
|
import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
|
|
import { getUserMenuItemsByGroup } from 'config/navigationConfig';
|
|
import { NavigationItem } from 'types/navigation';
|
|
import { Beta } from 'components/ui/Beta';
|
|
import { Candidate, Employer } from 'types/types';
|
|
import { SetSnackType } from 'components/Snack';
|
|
import { CopyBubble } from 'components/CopyBubble';
|
|
|
|
import 'components/layout/Header.css';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { useAppState } from 'hooks/GlobalContext';
|
|
|
|
// Styled components
|
|
const StyledAppBar = styled(AppBar, {
|
|
shouldForwardProp: (prop) => prop !== 'transparent',
|
|
})<{ transparent?: boolean }>(({ theme, transparent }) => ({
|
|
backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
|
|
boxShadow: transparent ? 'none' : '',
|
|
transition: 'background-color 0.3s ease',
|
|
borderRadius: 0,
|
|
padding: 0,
|
|
}));
|
|
|
|
const NavLinksContainer = styled(Box)(({ theme }) => ({
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
flex: 1,
|
|
[theme.breakpoints.down('md')]: {
|
|
display: 'none',
|
|
},
|
|
}));
|
|
|
|
const UserActionsContainer = styled(Box)(({ theme }) => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: theme.spacing(1),
|
|
}));
|
|
|
|
const UserButton = styled(Button)(({ theme }) => ({
|
|
color: theme.palette.primary.contrastText,
|
|
textTransform: 'none',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: theme.spacing(1),
|
|
padding: theme.spacing(0.5, 1.5),
|
|
borderRadius: theme.shape.borderRadius,
|
|
'&:hover': {
|
|
backgroundColor: theme.palette.action.hover,
|
|
},
|
|
}));
|
|
|
|
const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
|
'& .MuiDrawer-paper': {
|
|
width: 320,
|
|
backgroundColor: theme.palette.background.paper,
|
|
},
|
|
}));
|
|
|
|
const DropdownButton = styled(Button)(({ theme }) => ({
|
|
color: theme.palette.primary.contrastText,
|
|
textTransform: 'none',
|
|
minHeight: 48,
|
|
'&:hover': {
|
|
backgroundColor: theme.palette.action.hover,
|
|
},
|
|
}));
|
|
|
|
const UserMenuContainer = styled(Paper)(({ theme }) => ({
|
|
backgroundColor: theme.palette.background.paper,
|
|
borderRadius: theme.shape.borderRadius,
|
|
boxShadow: theme.shadows[8],
|
|
overflow: 'hidden',
|
|
minWidth: 200,
|
|
}));
|
|
|
|
interface HeaderProps {
|
|
transparent?: boolean;
|
|
className?: string;
|
|
navigate: NavigateFunction;
|
|
navigationItems: NavigationItem[];
|
|
currentPath: string;
|
|
sessionId?: string | null;
|
|
}
|
|
|
|
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|
const { user, logout } = useAuth();
|
|
const { setSnack } = useAppState();
|
|
const {
|
|
transparent = false,
|
|
className,
|
|
navigate,
|
|
navigationItems,
|
|
sessionId,
|
|
} = props;
|
|
const theme = useTheme();
|
|
const location = useLocation();
|
|
|
|
const name = (user?.firstName || user?.email || '');
|
|
|
|
// State for desktop dropdown menus
|
|
const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({});
|
|
|
|
// State for mobile drawer
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({});
|
|
|
|
// State for user menu
|
|
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
|
const userMenuOpen = Boolean(userMenuAnchor);
|
|
|
|
const isAdmin = user?.isAdmin || false;
|
|
|
|
// Get user menu items from navigation config
|
|
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
|
|
|
|
// Create user menu items array with proper actions
|
|
const createUserMenuItems = () => {
|
|
const items: Array<{
|
|
id: string;
|
|
label: string;
|
|
icon: React.ReactElement | null;
|
|
action: () => void;
|
|
group?: string;
|
|
}> = [];
|
|
|
|
// Add profile group items
|
|
userMenuGroups.profile.forEach(item => {
|
|
if (!item.divider) {
|
|
items.push({
|
|
id: item.id,
|
|
label: item.label as string,
|
|
icon: item.icon || null,
|
|
action: () => item.path && navigate(item.path),
|
|
group: 'profile'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add divider if we have items before system group
|
|
if (items.length > 0 && userMenuGroups.system.length > 0) {
|
|
items.push({
|
|
id: 'divider',
|
|
label: '',
|
|
icon: null,
|
|
action: () => { },
|
|
group: 'divider'
|
|
});
|
|
}
|
|
|
|
// Add account group items
|
|
userMenuGroups.account.forEach(item => {
|
|
if (!item.divider) {
|
|
items.push({
|
|
id: item.id,
|
|
label: item.label as string,
|
|
icon: item.icon || null,
|
|
action: () => item.path && navigate(item.path),
|
|
group: 'account'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add divider if we have items before system group
|
|
if (items.length > 0 && userMenuGroups.system.length > 0) {
|
|
items.push({
|
|
id: 'divider',
|
|
label: '',
|
|
icon: null,
|
|
action: () => { },
|
|
group: 'divider'
|
|
});
|
|
}
|
|
|
|
// Add admin group items
|
|
userMenuGroups.admin.forEach(item => {
|
|
if (!item.divider) {
|
|
items.push({
|
|
id: item.id,
|
|
label: item.label as string,
|
|
icon: item.icon || null,
|
|
action: () => item.path && navigate(item.path),
|
|
group: 'admin'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add divider if we have items before system group
|
|
if (items.length > 0 && userMenuGroups.admin.length > 0) {
|
|
items.push({
|
|
id: 'divider',
|
|
label: '',
|
|
icon: null,
|
|
action: () => { },
|
|
group: 'divider'
|
|
});
|
|
}
|
|
|
|
// Add system group items with special handling for logout
|
|
userMenuGroups.system.forEach(item => {
|
|
if (item.id === 'logout') {
|
|
items.push({
|
|
id: 'logout',
|
|
label: 'Logout',
|
|
icon: <Logout fontSize="small" />,
|
|
action: () => {
|
|
logout();
|
|
navigate('/');
|
|
},
|
|
group: 'system'
|
|
});
|
|
} else if (!item.divider) {
|
|
items.push({
|
|
id: item.id,
|
|
label: item.label as string,
|
|
icon: item.icon || null,
|
|
action: () => item.path && navigate(item.path),
|
|
group: 'system'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add other group items
|
|
userMenuGroups.other.forEach(item => {
|
|
if (!item.divider) {
|
|
items.push({
|
|
id: item.id,
|
|
label: item.label as string,
|
|
icon: item.icon || null,
|
|
action: () => item.path && navigate(item.path),
|
|
group: 'other'
|
|
});
|
|
}
|
|
});
|
|
|
|
return items;
|
|
};
|
|
|
|
const userMenuItems = createUserMenuItems();
|
|
|
|
// Helper function to check if current path matches navigation item
|
|
const isCurrentPath = (item: NavigationItem): boolean => {
|
|
if (!item.path) return false;
|
|
if (item.exact) {
|
|
return location.pathname === item.path;
|
|
}
|
|
return location.pathname.startsWith(item.path);
|
|
};
|
|
|
|
// Helper function to check if any child is current path
|
|
const hasActiveChild = (item: NavigationItem): boolean => {
|
|
if (!item.children) return false;
|
|
return item.children.some(child => isCurrentPath(child) || hasActiveChild(child));
|
|
};
|
|
|
|
// Desktop dropdown handlers
|
|
const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string) => {
|
|
setDropdownAnchors(prev => ({ ...prev, [itemId]: event.currentTarget }));
|
|
};
|
|
|
|
const handleDropdownClose = (itemId: string) => {
|
|
setDropdownAnchors(prev => ({ ...prev, [itemId]: null }));
|
|
};
|
|
|
|
// Mobile accordion handlers
|
|
const handleMobileToggle = (itemId: string) => {
|
|
setMobileExpanded(prev => ({ ...prev, [itemId]: !prev[itemId] }));
|
|
};
|
|
|
|
const handleDrawerToggle = () => {
|
|
setMobileOpen(!mobileOpen);
|
|
};
|
|
|
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
setUserMenuAnchor(event.currentTarget);
|
|
};
|
|
|
|
const handleUserMenuClose = () => {
|
|
setUserMenuAnchor(null);
|
|
};
|
|
|
|
const handleUserMenuAction = (item: { id: string; label: string; icon: React.ReactElement | null; action: () => void; group?: string }) => {
|
|
if (item.group !== 'divider') {
|
|
item.action();
|
|
handleUserMenuClose();
|
|
}
|
|
};
|
|
|
|
// Navigation handlers
|
|
const handleNavigate = (path: string) => {
|
|
navigate(path);
|
|
setMobileOpen(false);
|
|
// Close all dropdowns
|
|
setDropdownAnchors({});
|
|
};
|
|
|
|
// Render desktop navigation with dropdowns
|
|
const renderDesktopNavigation = () => {
|
|
return (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', justifyContent: 'space-between' }}>
|
|
{navigationItems.map((item, index) => {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
|
|
|
if (hasChildren) {
|
|
return (
|
|
<Box key={item.id} sx={{
|
|
mr: (index === 0 || index === navigationItems.length - 1) ? "auto" : "unset",
|
|
}}>
|
|
<DropdownButton
|
|
onClick={(e) => handleDropdownOpen(e, item.id)}
|
|
endIcon={<KeyboardArrowDown />}
|
|
sx={{
|
|
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
|
color: isActive ? 'secondary.main' : 'primary.contrastText',
|
|
}}
|
|
>
|
|
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
|
|
{item.label}
|
|
</DropdownButton>
|
|
<Menu
|
|
anchorEl={dropdownAnchors[item.id]}
|
|
open={Boolean(dropdownAnchors[item.id])}
|
|
onClose={() => handleDropdownClose(item.id)}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
TransitionComponent={Fade}
|
|
>
|
|
{item.children?.map(child => (
|
|
<MenuItem
|
|
key={child.id}
|
|
onClick={() => child.path && handleNavigate(child.path)}
|
|
selected={isCurrentPath(child)}
|
|
disabled={!child.path}
|
|
>
|
|
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
|
|
<ListItemText>{child.label}</ListItemText>
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
</Box>
|
|
);
|
|
} else {
|
|
return (
|
|
<DropdownButton
|
|
key={item.id}
|
|
onClick={() => item.path && handleNavigate(item.path)}
|
|
sx={{
|
|
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
|
color: isActive ? 'secondary.main' : 'primary.contrastText',
|
|
mr: (index === 0 || index === navigationItems.length - 1) ? "auto" : "unset",
|
|
}}
|
|
>
|
|
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
|
|
{item.label}
|
|
</DropdownButton>
|
|
);
|
|
}
|
|
})}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
// Render mobile accordion navigation
|
|
const renderMobileNavigation = () => {
|
|
const renderNavigationItem = (item: NavigationItem, depth: number = 0) => {
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
|
const isExpanded = mobileExpanded[item.id];
|
|
|
|
return (
|
|
<Box key={item.id}>
|
|
<ListItem disablePadding sx={{ pl: depth * 2 }}>
|
|
<ListItemButton
|
|
onClick={() => {
|
|
if (hasChildren) {
|
|
handleMobileToggle(item.id);
|
|
} else if (item.path) {
|
|
handleNavigate(item.path);
|
|
}
|
|
}}
|
|
selected={isActive}
|
|
sx={{
|
|
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
|
'&.Mui-selected': {
|
|
backgroundColor: 'primary.main',
|
|
color: 'primary.contrastText',
|
|
'& .MuiListItemIcon-root': {
|
|
color: 'primary.contrastText',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
{item.icon && (
|
|
<ListItemIcon sx={{ minWidth: 36 }}>
|
|
{item.icon}
|
|
</ListItemIcon>
|
|
)}
|
|
<ListItemText
|
|
primary={item.label}
|
|
sx={{
|
|
'& .MuiTypography-root': {
|
|
fontSize: depth > 0 ? '0.875rem' : '1rem',
|
|
fontWeight: depth === 0 ? 500 : 400,
|
|
}
|
|
}}
|
|
/>
|
|
{hasChildren && (
|
|
<IconButton size="small">
|
|
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
|
</IconButton>
|
|
)}
|
|
</ListItemButton>
|
|
</ListItem>
|
|
{hasChildren && (
|
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
|
<List disablePadding>
|
|
{item.children?.map(child => renderNavigationItem(child, depth + 1))}
|
|
</List>
|
|
</Collapse>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<List sx={{ pt: 0 }}>
|
|
{navigationItems.map((item) => renderNavigationItem(item))}
|
|
<Divider sx={{ my: 1 }} />
|
|
{!user && (
|
|
<ListItem disablePadding>
|
|
<ListItemButton onClick={() => handleNavigate('/login')}>
|
|
<ListItemText primary="Login" />
|
|
</ListItemButton>
|
|
</ListItem>
|
|
)}
|
|
</List>
|
|
);
|
|
};
|
|
|
|
// Render user menu content
|
|
const renderUserMenu = () => {
|
|
return (
|
|
<UserMenuContainer>
|
|
<List dense>
|
|
{userMenuItems.map((item, index) => (
|
|
item.group === 'divider' ? (
|
|
<Divider key={`divider-${index}`} />
|
|
) : (
|
|
<ListItem key={item.id} disablePadding>
|
|
<ListItemButton onClick={() => handleUserMenuAction(item)}>
|
|
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
|
|
<ListItemText primary={item.label} />
|
|
</ListItemButton>
|
|
</ListItem>
|
|
)
|
|
))}
|
|
</List>
|
|
</UserMenuContainer>
|
|
);
|
|
};
|
|
|
|
// Render user account section
|
|
const renderUserSection = () => {
|
|
if (!user) {
|
|
return (
|
|
<Button
|
|
color="info"
|
|
variant="contained"
|
|
onClick={() => navigate("/login")}
|
|
sx={{
|
|
display: { xs: 'none', sm: 'block' },
|
|
color: theme.palette.primary.contrastText,
|
|
}}
|
|
>
|
|
Login
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<UserButton
|
|
onClick={handleUserMenuOpen}
|
|
aria-controls={userMenuOpen ? 'user-menu' : undefined}
|
|
aria-haspopup="true"
|
|
aria-expanded={userMenuOpen ? 'true' : undefined}
|
|
>
|
|
<Avatar sx={{
|
|
width: 32,
|
|
height: 32,
|
|
bgcolor: theme.palette.secondary.main,
|
|
}}>
|
|
{name.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
|
{name}
|
|
</Box>
|
|
<ExpandMore fontSize="small" />
|
|
</UserButton>
|
|
|
|
<Popover
|
|
id="user-menu"
|
|
open={userMenuOpen}
|
|
anchorEl={userMenuAnchor}
|
|
onClose={handleUserMenuClose}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'right',
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
TransitionComponent={Fade}
|
|
>
|
|
{renderUserMenu()}
|
|
</Popover>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<StyledAppBar
|
|
position="fixed"
|
|
transparent={transparent}
|
|
className={className}
|
|
sx={{ overflow: "hidden" }}
|
|
>
|
|
<Container maxWidth="xl">
|
|
<Toolbar disableGutters>
|
|
{/* Navigation Links - Desktop */}
|
|
<NavLinksContainer>
|
|
{renderDesktopNavigation()}
|
|
</NavLinksContainer>
|
|
|
|
{/* User Actions Section */}
|
|
<UserActionsContainer>
|
|
{renderUserSection()}
|
|
|
|
{/* Mobile Menu Button */}
|
|
<Tooltip title="Open Menu">
|
|
<IconButton
|
|
color="inherit"
|
|
aria-label="open drawer"
|
|
edge="end"
|
|
onClick={handleDrawerToggle}
|
|
sx={{ display: { md: 'none' } }}
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
|
|
{sessionId && (
|
|
<CopyBubble
|
|
tooltip="Copy link"
|
|
color="inherit"
|
|
aria-label="copy link"
|
|
edge="end"
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
opacity: 1,
|
|
bgcolor: 'inherit',
|
|
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
|
}}
|
|
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
|
onClick={() => {
|
|
navigate(`${window.location.pathname}?id=${sessionId}`);
|
|
setSnack("Link copied!");
|
|
}}
|
|
size="large"
|
|
/>
|
|
)}
|
|
</UserActionsContainer>
|
|
|
|
{/* Mobile Navigation Drawer */}
|
|
<MobileDrawer
|
|
variant="temporary"
|
|
anchor="right"
|
|
open={mobileOpen}
|
|
onClose={handleDrawerToggle}
|
|
ModalProps={{
|
|
keepMounted: true,
|
|
}}
|
|
>
|
|
{renderMobileNavigation()}
|
|
</MobileDrawer>
|
|
</Toolbar>
|
|
</Container>
|
|
<Beta
|
|
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
|
|
onClick={() => { navigate('/docs/beta'); }}
|
|
/>
|
|
</StyledAppBar>
|
|
);
|
|
};
|
|
|
|
export { Header }; |