2025-06-04 11:11:42 -07:00

532 lines
14 KiB
TypeScript

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,
} from '@mui/material';
import { styled, useTheme } from '@mui/material/styles';
import {
Menu as MenuIcon,
Dashboard,
Person,
Logout,
Settings,
ExpandMore,
} from '@mui/icons-material';
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types';
import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import 'components/layout/Header.css';
import { useAuth } from 'hooks/AuthContext';
// 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: 280,
backgroundColor: theme.palette.background.paper,
},
}));
const MobileMenuTabs = styled(Tabs)(({ theme }) => ({
'& .MuiTabs-flexContainer': {
flexDirection: 'column',
},
'& .MuiTab-root': {
minHeight: 48,
textTransform: 'uppercase',
justifyContent: 'flex-start',
alignItems: 'center',
padding: theme.spacing(1, 2),
gap: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
'& .MuiTypography-root': {
color: theme.palette.primary.contrastText,
},
'& .MuiSvgIcon-root': {
color: theme.palette.primary.contrastText,
},
},
},
'& .MuiTabs-indicator': {
display: 'none',
},
}));
const UserMenuContainer = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[8],
overflow: 'hidden',
minWidth: 200,
}));
const UserMenuTabs = styled(Tabs)(({ theme }) => ({
'& .MuiTabs-flexContainer': {
flexDirection: 'column',
},
'& .MuiTab-root': {
minHeight: 48,
textTransform: 'uppercase',
justifyContent: 'flex-start',
alignItems: 'center',
padding: theme.spacing(1, 2),
gap: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
},
'& .MuiTabs-indicator': {
display: 'none',
},
}));
const MenuItemBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
width: '100%',
'& .MuiSvgIcon-root': {
color: theme.palette.primary.main,
},
}));
const MenuDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(0.5, 0),
backgroundColor: theme.palette.divider,
}));
interface HeaderProps {
transparent?: boolean;
className?: string;
navigate: NavigateFunction;
navigationLinks: NavigationLinkType[];
currentPath: string;
sessionId?: string | null;
setSnack: SetSnackType;
}
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth();
const candidate: Candidate | null = (user && user.userType === "candidate") ? user as Candidate : null;
const employer: Employer | null = (user && user.userType === "employer") ? user as Employer : null;
const {
transparent = false,
className,
navigate,
navigationLinks,
sessionId,
setSnack,
} = props;
const theme = useTheme();
const location = useLocation();
const name = (user?.firstName || user?.email || '');
const mainNavSections: NavigationLinkType[] = [
{ path: '/', label: <BackstoryLogo /> },
...navigationLinks
];
// State for page navigation - only for main navigation
const [currentTab, setCurrentTab] = useState<string | false>("/");
const [userMenuTab, setUserMenuTab] = useState<string>("");
// State for mobile drawer
const [mobileOpen, setMobileOpen] = useState(false);
// State for user menu
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor);
// Helper function to determine which action page we're in
const getTailSection = (pathname: string): string | false => {
// Handle home page
if (pathname === '/') return '/';
// Split path and check against main sections
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) return '/';
const lastSegment = `/${segments[segments.length - 1]}`;
// Check if this matches any of our main navigation sections
const matchingSection = mainNavSections.find(section =>
section.path === lastSegment ||
(section.path !== '/' && pathname.endsWith(section.path))
);
return matchingSection ? matchingSection.path : false; // Return false for routes that shouldn't show in main nav
};
// User menu items
const userMenuItems = [
{
id: 'profile',
label: 'Profile',
icon: <Person fontSize="small" />,
action: () => navigate(`/${user?.userType}/profile`)
},
{
id: 'dashboard',
label: 'Dashboard',
icon: <Dashboard fontSize="small" />,
action: () => navigate(`/${user?.userType}/dashboard`)
},
{
id: 'settings',
label: 'Settings',
icon: <Settings fontSize="small" />,
action: () => navigate(`/${user?.userType}/settings`)
},
{
id: 'divider',
label: '',
icon: null,
action: () => { }
},
{
id: 'logout',
label: 'Logout',
icon: <Logout fontSize="small" />,
action: () => {
logout();
navigate('/');
}
}
];
useEffect(() => {
const mainSection = getTailSection(location.pathname);
// Only update if the section is different from current tab
if (mainSection !== currentTab) {
setCurrentTab(mainSection); // mainSection is either a string or false
}
}, [location.pathname, currentTab]);
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchor(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchor(null);
setUserMenuTab("");
};
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
if (item.id !== 'divider') {
item.action();
handleUserMenuClose();
}
};
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
if (newValue !== false) {
setCurrentTab(newValue);
navigate(newValue);
}
};
// Render desktop navigation links
const renderNavLinks = () => {
return (
<Tabs
value={currentTab}
onChange={handleTabChange}
indicatorColor="secondary"
textColor="inherit"
variant="fullWidth"
allowScrollButtonsMobile
aria-label="Backstory navigation"
>
{mainNavSections.map((section) => (
<Tab
sx={{
minWidth: section.path === '/' ? "max-content" : "auto",
}}
key={section.path}
value={section.path}
label={section.label}
/>
))}
</Tabs>
);
};
// Render mobile drawer content
const renderDrawerContent = () => {
return (
<>
<MobileMenuTabs
orientation="vertical"
value={currentTab}
onChange={handleTabChange}
>
{mainNavSections.map((section) => (
<Tab
key={section.path || '/'}
value={section.path}
label={
<MenuItemBox>
{section.label}
</MenuItemBox>
}
onClick={(e) => {
handleDrawerToggle();
setCurrentTab(section.path);
navigate(section.path);
}}
/>
))}
</MobileMenuTabs>
<MenuDivider />
{!user && (
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="contained"
color="secondary"
fullWidth
onClick={() => { handleDrawerToggle(); navigate("/login"); }}
>
Login
</Button>
</Box>
)}
</>
);
};
// Render user menu content
const renderUserMenu = () => {
return (
<UserMenuContainer>
<UserMenuTabs
orientation="vertical"
value={userMenuTab}
onChange={(e, newValue) => setUserMenuTab(newValue)}
>
{userMenuItems.map((item, index) => (
item.id === 'divider' ? (
<MenuDivider key={`divider-${index}`} />
) : (
<Tab
key={item.id}
value={item.id}
label={
<MenuItemBox>
{item.icon}
<Typography variant="body2">{item.label}</Typography>
</MenuItemBox>
}
onClick={() => handleUserMenuAction(item)}
/>
)
))}
</UserMenuTabs>
</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>
{renderNavLinks()}
</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,
}}
>
{renderDrawerContent()}
</MobileDrawer>
</Toolbar>
</Container>
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
</StyledAppBar>
);
};
export { Header };