Compare commits

...

2 Commits

Author SHA1 Message Date
7a166fe920 Menu re-work almost done 2025-06-11 10:36:35 -07:00
4689aa66c6 not working 2025-06-11 10:23:55 -07:00
4 changed files with 278 additions and 117 deletions

View File

@ -79,7 +79,7 @@ interface BackstoryLayoutProps {
}
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
const { page, chatRef } = props;
const { page, chatRef, } = props;
const { setSnack } = useAppState();
const navigate = useNavigate();
const location = useLocation();
@ -91,18 +91,12 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
setNavigationItems(getMainNavigationItems(userType, user?.isAdmin ? true : false));
}, [user]);
useEffect(() => {
console.log({ guest, user });
}, [guest, user]);
// Generate dynamic routes from navigation config
const generateRoutes = () => {
if (!guest && !user) return null;
const userType = user?.userType || null;
const isAdmin = user?.isAdmin ? true : false;
// Get all routes from navigation config
const routes = getAllRoutes(userType, isAdmin);
return routes.map((route, index) => {
@ -132,7 +126,6 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
flexDirection: "column"
}}>
<Header
setSnack={setSnack}
currentPath={page}
navigate={navigate}
navigationItems={navigationItems}

View File

@ -1,5 +1,5 @@
// components/layout/Header.tsx
import React, { JSX, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { NavigateFunction, useLocation } from 'react-router-dom';
import {
AppBar,
@ -40,7 +40,7 @@ import {
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';
@ -121,11 +121,11 @@ interface HeaderProps {
navigationItems: NavigationItem[];
currentPath: string;
sessionId?: string | null;
setSnack: SetSnackType;
}
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth();
const { setSnack } = useAppState();
const {
transparent = false,
className,
@ -133,8 +133,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
navigationItems,
sessionId,
} = props;
const { setSnack } = useAppState();
const theme = useTheme();
const location = useLocation();
@ -151,52 +149,97 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor);
// 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`)
},
{
// Get user menu items from navigation config
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null);
// 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 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: () => { }
},
{
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'
});
}
}
];
});
if (user?.isAdmin) {
const divider = userMenuItems.findIndex(p => p.id === 'divider');
if (divider !== -1) {
userMenuItems.splice(divider, 0, ...[
{ id: 'divider', label: '', icon: null, action: () => { } },
{ id: 'generate', label: 'Generate Candidate', icon: <FaceRetouchingNaturalIcon fontSize="small" />, action: () => { navigate('/admin/generate-candidate'); } }
]);
}
// 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 => {
@ -239,8 +282,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
setUserMenuAnchor(null);
};
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
if (item.id !== 'divider') {
const handleUserMenuAction = (item: { id: string; label: string; icon: React.ReactElement | null; action: () => void; group?: string }) => {
if (item.group !== 'divider') {
item.action();
handleUserMenuClose();
}
@ -257,23 +300,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Render desktop navigation with dropdowns
const renderDesktopNavigation = () => {
return (
<Box sx={{ display: 'flex', width: "100%", alignItems: 'center', justifyContent: "space-between", "& > :last-of-type": { marginRight: "auto"} }}>
{navigationItems.map((item, index) => {
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{navigationItems.map((item) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item);
// Default is centered for all menu items
let sx : SxProps = { justifycontent: "center"};
// First item ("Backstory") is left aligned
if (index === 0) {
sx = { marginRight: "auto" };
}
// If there is an Admin menu, it is on the far right
if (item.userTypes?.includes('admin')) {
sx = { marginLeft: "auto"};
}
if (hasChildren) {
return (
<Box key={item.id} sx={sx}>
<Box key={item.id}>
<DropdownButton
onClick={(e) => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />}
@ -293,16 +327,15 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
TransitionComponent={Fade}
>
{item.children?.map((child) => (
{item.children?.map(child => (
<MenuItem
key={child.id}
onClick={() => child.path && handleNavigate(child.path)}
selected={isCurrentPath(child)}
disabled={!child.path}
sx={{ alignContent: 'center', margin: "0 !important" }}
>
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
<ListItemText sx={{ p: 0, margin: "0 !important", "& > *": { m: 0 } }}>{child.label}</ListItemText>
<ListItemText>{child.label}</ListItemText>
</MenuItem>
))}
</Menu>
@ -316,7 +349,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? 'secondary.main' : 'primary.contrastText',
...sx
}}
>
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
@ -383,7 +415,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
{hasChildren && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List disablePadding>
{item.children?.map((child) => renderNavigationItem(child, depth + 1))}
{item.children?.map(child => renderNavigationItem(child, depth + 1))}
</List>
</Collapse>
)}
@ -412,13 +444,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<UserMenuContainer>
<List dense>
{userMenuItems.map((item, index) => (
item.id === 'divider' ? (
item.group === 'divider' ? (
<Divider key={`divider-${index}`} />
) : (
<ListItem key={item.id} disablePadding>
<ListItemButton onClick={() => handleUserMenuAction(item)} sx={{ alignContent: "center" }}>
<ListItemButton onClick={() => handleUserMenuAction(item)}>
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemText primary={item.label} sx={{ padding: 0 }} />
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
)

View File

@ -1,5 +1,3 @@
// config/navigationConfig.tsx
import React from 'react';
import {
Chat as ChatIcon,
@ -8,18 +6,19 @@ import {
BarChart as BarChartIcon,
Settings as SettingsIcon,
Work as WorkIcon,
Info as InfoIcon,
Person as PersonIcon,
Business as BusinessIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
Search as SearchIcon,
Bookmark as BookmarkIcon,
History as HistoryIcon,
QuestionAnswer as QuestionAnswerIcon,
AttachMoney as AttachMoneyIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
BubbleChart,
} from '@mui/icons-material';
import SchoolIcon from '@mui/icons-material/School';
import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { HomePage } from 'pages/HomePage';
import { CandidateChatPage } from 'pages/CandidateChatPage';
@ -27,20 +26,25 @@ import { DocsPage } from 'pages/DocsPage';
import { CreateProfilePage } from 'pages/candidate/ProfileWizard';
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { BetaPage } from 'pages/BetaPage';
import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from 'pages/GenerateCandidate';
import { Settings } from 'pages/candidate/Settings';
import { LoginPage } from 'pages/LoginPage';
import { CandidateDashboard } from 'pages/candidate/Dashboard';
import { EmailVerificationPage } from 'components/EmailVerificationComponents';
import { Box, Typography } from '@mui/material';
import { CandidateDashboard } from 'pages/candidate/Dashboard';
import { NavigationConfig, NavigationItem } from 'types/navigation';
import { CandidateProfile } from 'pages/candidate/Profile';
import { DocumentManager } from 'components/DocumentManager';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { HowItWorks } from 'pages/HowItWorks';
import SchoolIcon from '@mui/icons-material/School';
import { CandidateProfile } from 'pages/candidate/Profile';
import { Settings } from 'pages/candidate/Settings';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { DocumentManager } from 'components/DocumentManager';
// Beta page components for placeholder routes
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>);
const QASetupPage = () => (<BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>);
const SearchPage = () => (<BetaPage><Typography variant="h4">Search</Typography></BetaPage>);
const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></BetaPage>);
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>);
@ -61,7 +65,9 @@ export const navigationConfig: NavigationConfig = {
{ id: 'candidate-docs', label: 'Documents', icon: <BubbleChart />, path: '/candidate/documents', component: <Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}><VectorVisualizer /><DocumentManager /></Box>, userTypes: ['candidate'] },
{ id: 'candidate-qa-setup', label: 'Q&A Setup', icon: <QuizIcon />, path: '/candidate/qa-setup', component: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-analytics', label: 'Analytics', icon: <AnalyticsIcon />, path: '/candidate/analytics', component: <BetaPage><Box>Candidate analytics page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-job-analysis', label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['candidate'], },
{ id: 'candidate-job-analysis', label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['candidate'], showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'profile',},
{ id: 'candidate-resumes', label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes', component: <BetaPage><Box>Candidate resumes page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-settings', label: 'Settings', path: '/candidate/settings', icon: <SettingsIcon />, component: <Settings />, userTypes: ['candidate'], },
],
@ -78,22 +84,99 @@ export const navigationConfig: NavigationConfig = {
{ id: 'employer-settings', label: 'Settings', path: '/employer/settings', icon: <SettingsIcon />, component: <SettingsPage />, userTypes: ['employer'], },
],
},
// { id: 'find-candidate', label: 'Find a Candidate', path: '/find-a-candidate', icon: <PersonSearchIcon />, component: <CandidateListingPage />, userTypes: ['guest', 'candidate', 'employer'], },
{
id: 'admin-menu',
label: 'Admin',
icon: <PersonIcon />,
userTypes: ['admin'],
id: 'global-tools',
label: 'Tools',
icon: <SettingsIcon />,
userTypes: ['candidate', 'employer'],
showInNavigation: true,
children: [
{ id: 'generate-candidate', label: 'Generate Candidate', path: '/admin/generate-candidate', icon: <FaceRetouchingNaturalIcon />, component: <GenerateCandidate />, userTypes: ['admin'] },
{ id: 'docs', label: 'Docs', path: '/docs/*', icon: <LibraryBooksIcon />, component: <DocsPage />, userTypes: ['admin'], },
{
id: 'knowledge-explorer',
label: 'Knowledge Explorer',
path: '/knowledge-explorer',
icon: <WorkIcon />,
component: <VectorVisualizerPage />,
userTypes: ['candidate', 'employer'],
showInNavigation: true,
},
{
id: 'job-analysis',
label: 'Job Analysis',
path: '/job-analysis',
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ['candidate', 'employer'],
showInNavigation: true,
},
{
id: 'generate-candidate',
label: 'Generate Candidate',
path: '/generate-candidate',
icon: <PersonIcon />,
component: <GenerateCandidate />,
userTypes: ['candidate', 'employer'],
showInNavigation: true,
},
],
},
// User menu only items (not shown in main navigation)
{
id: 'user-profile',
label: 'Profile',
path: '/profile',
icon: <PersonIcon />,
component: <AnalyticsPage />, // Replace with actual profile page
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'profile',
},
{
id: 'account-settings',
label: 'Account Settings',
path: '/account/settings',
icon: <SettingsIcon />,
component: <SettingsPage />,
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'account',
},
{
id: 'billing',
label: 'Billing',
path: '/billing',
icon: <AttachMoneyIcon />,
component: <AnalyticsPage />, // Replace with actual billing page
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'account',
},
{
id: 'user-menu-divider',
label: '',
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
divider: true,
},
{
id: 'logout',
label: 'Logout',
icon: <PersonIcon />, // This will be handled specially in Header
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'system',
},
// Auth routes (special handling)
{
id: 'auth',
label: 'Auth',
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
children: [
{
id: 'register',
@ -101,6 +184,7 @@ export const navigationConfig: NavigationConfig = {
path: '/register',
component: <BetaPage><CreateProfilePage /></BetaPage>,
userTypes: ['guest'],
showInNavigation: false,
},
{
id: 'login',
@ -108,6 +192,7 @@ export const navigationConfig: NavigationConfig = {
path: '/login/*',
component: <LoginPage />,
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
{
id: 'verify-email',
@ -115,13 +200,15 @@ export const navigationConfig: NavigationConfig = {
path: '/login/verify-email',
component: <EmailVerificationPage />,
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
{
id: 'logout',
id: 'logout-page',
label: 'Logout',
path: '/logout',
component: <LogoutPage />,
userTypes: ['candidate', 'employer'],
showInNavigation: false,
},
],
},
@ -132,6 +219,7 @@ export const navigationConfig: NavigationConfig = {
path: '*',
component: <BetaPage />,
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
],
};
@ -139,9 +227,11 @@ export const navigationConfig: NavigationConfig = {
// Utility functions for working with navigation config
export const getNavigationItemsForUser = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => {
const currentUserType = userType || 'guest';
const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items
.filter(item => !item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes('admin') && isAdmin))
.filter(item => item.showInNavigation !== false) // Default to true if not specified
.map(item => ({
...item,
children: item.children ? filterItems(item.children) : undefined,
@ -180,6 +270,50 @@ export const getMainNavigationItems = (userType: 'guest' | 'candidate' | 'employ
.filter(item =>
item.id !== 'auth' &&
item.id !== 'catch-all' &&
item.showInNavigation !== false &&
(item.path || (item.children && item.children.length > 0))
);
};
export const getUserMenuItems = (userType: 'candidate' | 'employer' | 'guest' | null): NavigationItem[] => {
if (!userType) return [];
const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => {
const menuItems: NavigationItem[] = [];
items.forEach(item => {
if (!item.userTypes || item.userTypes.includes(userType)) {
if (item.showInUserMenu) {
menuItems.push(item);
}
if (item.children) {
menuItems.push(...extractUserMenuItems(item.children));
}
}
});
return menuItems;
};
return extractUserMenuItems(navigationConfig.items);
};
export const getUserMenuItemsByGroup = (userType: 'candidate' | 'employer' | 'guest' | null): { [key: string]: NavigationItem[] } => {
const menuItems = getUserMenuItems(userType);
const grouped: { [key: string]: NavigationItem[] } = {
profile: [],
account: [],
system: [],
other: []
};
menuItems.forEach(item => {
const group = item.userMenuGroup || 'other';
if (!grouped[group]) {
grouped[group] = [];
}
grouped[group].push(item);
});
return grouped;
};

View File

@ -1,4 +1,3 @@
// types/navigation.ts
import { ReactElement } from 'react';
export interface NavigationItem {
@ -11,6 +10,9 @@ export interface NavigationItem {
userTypes?: ('candidate' | 'employer' | 'guest' | 'admin')[];
exact?: boolean;
divider?: boolean;
showInNavigation?: boolean; // Controls if item appears in main navigation
showInUserMenu?: boolean; // Controls if item appears in user menu
userMenuGroup?: 'profile' | 'account' | 'system'; // Groups items in user menu
}
export interface NavigationConfig {