431 lines
12 KiB
TypeScript
431 lines
12 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { NavigateFunction, useLocation } from 'react-router-dom';
|
|
import {
|
|
AppBar,
|
|
Toolbar,
|
|
Tooltip,
|
|
Typography,
|
|
Button,
|
|
IconButton,
|
|
Box,
|
|
Menu,
|
|
MenuItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Drawer,
|
|
Divider,
|
|
Avatar,
|
|
Tabs,
|
|
Tab,
|
|
Container,
|
|
Fade,
|
|
} 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/Beta';
|
|
import { useUser } from 'hooks/useUser';
|
|
import { Candidate, Employer, Viewer } from 'types/types';
|
|
import { SetSnackType } from 'components/Snack';
|
|
import { CopyBubble } from 'components/CopyBubble';
|
|
|
|
import 'components/layout/Header.css';
|
|
import { useSecureAuth } from 'hooks/useSecureAuth';
|
|
|
|
// 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,
|
|
},
|
|
}));
|
|
|
|
interface HeaderProps {
|
|
transparent?: boolean;
|
|
className?: string;
|
|
navigate: NavigateFunction;
|
|
navigationLinks: NavigationLinkType[];
|
|
showLogin?: boolean;
|
|
currentPath: string;
|
|
sessionId?: string | null;
|
|
setSnack: SetSnackType,
|
|
}
|
|
|
|
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|
const { user, logout } = useSecureAuth();
|
|
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 viewer: Viewer | null = (user && user.userType === "viewer") ? user as Viewer : null;
|
|
const {
|
|
transparent = false,
|
|
className,
|
|
navigate,
|
|
navigationLinks,
|
|
showLogin,
|
|
sessionId,
|
|
setSnack,
|
|
} = props;
|
|
const theme = useTheme();
|
|
const location = useLocation();
|
|
|
|
const name = (user?.firstName || user?.email || '');
|
|
|
|
const BackstoryLogo = () => {
|
|
return <Typography
|
|
variant="h6"
|
|
className="BackstoryLogo"
|
|
noWrap
|
|
sx={{
|
|
cursor: "pointer",
|
|
fontWeight: 700,
|
|
letterSpacing: '.2rem',
|
|
color: theme.palette.primary.contrastText,
|
|
textDecoration: 'none',
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 1,
|
|
textTransform: "uppercase",
|
|
}}
|
|
>
|
|
<Avatar sx={{ width: 24, height: 24 }}
|
|
variant="rounded"
|
|
alt="Backstory logo"
|
|
src="/logo192.png" />
|
|
Backstory
|
|
</Typography>
|
|
};
|
|
|
|
const navLinks : NavigationLinkType[] = [
|
|
{name: "Home", path: "/", label: <BackstoryLogo/>},
|
|
...navigationLinks
|
|
];
|
|
// State for page navigation
|
|
const [ currentTab, setCurrentTab ] = 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);
|
|
|
|
useEffect(() => {
|
|
const parts = location.pathname.split('/');
|
|
let tab = '/';
|
|
if (parts.length > 1) {
|
|
tab = `/${parts[1]}`;
|
|
}
|
|
if (tab !== currentTab) {
|
|
setCurrentTab(tab);
|
|
}
|
|
}, [location, currentTab]);
|
|
|
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
setUserMenuAnchor(event.currentTarget);
|
|
};
|
|
|
|
const handleUserMenuClose = () => {
|
|
setUserMenuAnchor(null);
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
handleUserMenuClose();
|
|
logout();
|
|
navigate('/');
|
|
};
|
|
|
|
const handleDrawerToggle = () => {
|
|
setMobileOpen(!mobileOpen);
|
|
};
|
|
|
|
// Render desktop navigation links
|
|
const renderNavLinks = () => {
|
|
return (
|
|
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)}
|
|
indicatorColor="secondary"
|
|
textColor="inherit"
|
|
variant="fullWidth"
|
|
allowScrollButtonsMobile
|
|
aria-label="Backstory navigation"
|
|
>
|
|
{navLinks.map((link) => (
|
|
<Tab
|
|
sx={{
|
|
minWidth: link.path === '/' ? "max-content" : "auto",
|
|
}}
|
|
key={link.name}
|
|
value={link.path}
|
|
label={link.label ? link.label : link.name}
|
|
onClick={() => {
|
|
navigate(link.path);
|
|
}}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
);
|
|
};
|
|
|
|
// Render mobile drawer content
|
|
const renderDrawerContent = () => {
|
|
return (
|
|
<>
|
|
<Tabs
|
|
orientation="vertical"
|
|
value={currentTab} >
|
|
{navLinks.map((link) => (
|
|
<Tab
|
|
key={link.name}
|
|
value={link.path}
|
|
label={
|
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
{link.icon && <Box sx={{ mr: 1 }}>{link.icon}</Box>}
|
|
{link.name}
|
|
</Box>
|
|
}
|
|
onClick={(e) => { handleDrawerToggle() ; setCurrentTab(link.path); navigate(link.path);} }
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
<Divider />
|
|
{!user && (showLogin === undefined || showLogin !== false) && (
|
|
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<Button
|
|
variant="contained"
|
|
color="secondary"
|
|
fullWidth
|
|
onClick={() => { navigate("/login"); }}
|
|
>
|
|
Login
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// Render user account section
|
|
const renderUserSection = () => {
|
|
if (showLogin !== undefined && showLogin === false) {
|
|
return <></>;
|
|
}
|
|
|
|
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>
|
|
|
|
<Menu
|
|
id="user-menu"
|
|
anchorEl={userMenuAnchor}
|
|
open={userMenuOpen}
|
|
onClose={handleUserMenuClose}
|
|
slots={{
|
|
transition: Fade,
|
|
}}
|
|
slotProps={{
|
|
list: {
|
|
'aria-labelledby': 'user-button',
|
|
sx: {
|
|
display: 'flex',
|
|
flexDirection: 'column', // Adjusted for menu items
|
|
alignItems: 'center',
|
|
gap: '1rem',
|
|
textTransform: 'uppercase', // All caps as requested
|
|
},
|
|
},
|
|
paper: {
|
|
sx: {
|
|
minWidth: 200, // Optional: ensures reasonable menu width
|
|
},
|
|
},
|
|
}}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'right',
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right',
|
|
}}
|
|
sx={{
|
|
"& .MuiList-root": { gap: 0 },
|
|
"& .MuiMenuItem-root": { gap: 1, display: "flex", flexDirection: "row", width: "100%" },
|
|
"& .MuiSvgIcon-root": { color: "#D4A017" }
|
|
}}
|
|
>
|
|
<MenuItem onClick={() => { handleUserMenuClose(); navigate(`/${user.userType}/profile`) }}>
|
|
<Person fontSize="small" />
|
|
<Box>Profile</Box>
|
|
</MenuItem>
|
|
<MenuItem onClick={() => { handleUserMenuClose(); navigate(`/${user.userType}/dashboard`) }}>
|
|
<Dashboard fontSize="small" />
|
|
<Box>Dashboard</Box>
|
|
</MenuItem>
|
|
<MenuItem onClick={() => { handleUserMenuClose(); navigate(`/${user.userType}settings`) }}>
|
|
<Settings fontSize="small" />
|
|
<Box>Settings</Box>
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={handleLogout}>
|
|
<Logout fontSize="small" />
|
|
<Box>Logout</Box>
|
|
</MenuItem>
|
|
</Menu>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<StyledAppBar
|
|
position="fixed"
|
|
transparent={transparent}
|
|
className={className}
|
|
sx={{ overflow: "hidden" }}
|
|
>
|
|
<Container maxWidth="xl">
|
|
<Toolbar disableGutters>
|
|
{/* Logo Section */}
|
|
|
|
{/* 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, // Better open performance on mobile
|
|
}}
|
|
>
|
|
{renderDrawerContent()}
|
|
</MobileDrawer>
|
|
</Toolbar>
|
|
</Container>
|
|
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
|
</StyledAppBar>
|
|
);
|
|
};
|
|
|
|
export {
|
|
Header
|
|
}; |