Fixed tab errors
This commit is contained in:
parent
4f7b2f3e6a
commit
efef926e45
@ -23,52 +23,51 @@ import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
|
|||||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
type NavigationLinkType = {
|
type NavigationLinkType = {
|
||||||
name: string;
|
label: ReactElement<any> | string;
|
||||||
path: string;
|
path: string;
|
||||||
icon?: ReactElement<any>;
|
icon?: ReactElement<any>;
|
||||||
label?: ReactElement<any>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultNavItems: NavigationLinkType[] = [
|
const DefaultNavItems: NavigationLinkType[] = [
|
||||||
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||||
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
{ label: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
||||||
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
// { label: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
||||||
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
// { label: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
||||||
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
// { label: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
||||||
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
// { label: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ViewerNavItems: NavigationLinkType[] = [
|
const ViewerNavItems: NavigationLinkType[] = [
|
||||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const CandidateNavItems : NavigationLinkType[]= [
|
const CandidateNavItems : NavigationLinkType[]= [
|
||||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||||
{ name: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
|
{ label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
|
||||||
{ name: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
||||||
// { name: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
||||||
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
||||||
// { name: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' },
|
// { label: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' },
|
||||||
// { name: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
// { label: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
||||||
// { name: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
||||||
// { name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
||||||
// { name: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
|
// { label: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
|
||||||
// { name: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
|
// { label: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const EmployerNavItems: NavigationLinkType[] = [
|
const EmployerNavItems: NavigationLinkType[] = [
|
||||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||||
{ name: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
|
{ label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
|
||||||
{ name: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
|
{ label: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
|
||||||
{ name: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
|
{ label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
|
||||||
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||||
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
|
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
|
||||||
// { name: 'Search', icon: <SearchIcon />, path: '/employer/search' },
|
// { label: 'Search', icon: <SearchIcon />, path: '/employer/search' },
|
||||||
// { name: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
|
// { label: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
|
||||||
// { name: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
|
// { label: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
|
||||||
// { name: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
|
// { label: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
|
||||||
// { name: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
|
// { label: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
|
||||||
// { name: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
|
// { label: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Navigation links based on user type
|
// Navigation links based on user type
|
||||||
|
@ -170,7 +170,7 @@ interface HeaderProps {
|
|||||||
navigationLinks: NavigationLinkType[];
|
navigationLinks: NavigationLinkType[];
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
setSnack: SetSnackType,
|
setSnack: SetSnackType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||||
@ -190,13 +190,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
|
|
||||||
const name = (user?.firstName || user?.email || '');
|
const name = (user?.firstName || user?.email || '');
|
||||||
|
|
||||||
const navLinks : NavigationLinkType[] = [
|
const mainNavSections: NavigationLinkType[] = [
|
||||||
{name: "Home", path: "/", label: <BackstoryLogo/>},
|
{ path: '/', label: <BackstoryLogo /> },
|
||||||
...navigationLinks
|
...navigationLinks
|
||||||
];
|
];
|
||||||
|
|
||||||
// State for page navigation
|
// State for page navigation - only for main navigation
|
||||||
const [ currentTab, setCurrentTab ] = useState<string>("/");
|
const [currentTab, setCurrentTab] = useState<string | false>("/");
|
||||||
const [userMenuTab, setUserMenuTab] = useState<string>("");
|
const [userMenuTab, setUserMenuTab] = useState<string>("");
|
||||||
|
|
||||||
// State for mobile drawer
|
// State for mobile drawer
|
||||||
@ -206,6 +206,26 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||||
const userMenuOpen = Boolean(userMenuAnchor);
|
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
|
// User menu items
|
||||||
const userMenuItems = [
|
const userMenuItems = [
|
||||||
{
|
{
|
||||||
@ -244,15 +264,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = location.pathname.split('/');
|
const mainSection = getTailSection(location.pathname);
|
||||||
let tab = '/';
|
|
||||||
if (parts.length > 1) {
|
// Only update if the section is different from current tab
|
||||||
tab = `/${parts[1]}`;
|
if (mainSection !== currentTab) {
|
||||||
|
setCurrentTab(mainSection); // mainSection is either a string or false
|
||||||
}
|
}
|
||||||
if (tab !== currentTab) {
|
}, [location.pathname, currentTab]);
|
||||||
setCurrentTab(tab);
|
|
||||||
}
|
|
||||||
}, [location, currentTab]);
|
|
||||||
|
|
||||||
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setUserMenuAnchor(event.currentTarget);
|
setUserMenuAnchor(event.currentTarget);
|
||||||
@ -274,28 +292,34 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
setMobileOpen(!mobileOpen);
|
setMobileOpen(!mobileOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
|
||||||
|
if (newValue !== false) {
|
||||||
|
setCurrentTab(newValue);
|
||||||
|
navigate(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Render desktop navigation links
|
// Render desktop navigation links
|
||||||
const renderNavLinks = () => {
|
const renderNavLinks = () => {
|
||||||
return (
|
return (
|
||||||
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)}
|
<Tabs
|
||||||
|
value={currentTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
indicatorColor="secondary"
|
indicatorColor="secondary"
|
||||||
textColor="inherit"
|
textColor="inherit"
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
aria-label="Backstory navigation"
|
aria-label="Backstory navigation"
|
||||||
>
|
>
|
||||||
{navLinks.map((link) => (
|
{mainNavSections.map((section) => (
|
||||||
<Tab
|
<Tab
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: link.path === '/' ? "max-content" : "auto",
|
minWidth: section.path === '/' ? "max-content" : "auto",
|
||||||
}}
|
}}
|
||||||
key={link.name}
|
key={section.path}
|
||||||
value={link.path}
|
value={section.path}
|
||||||
label={link.label ? link.label : link.name}
|
label={section.label}
|
||||||
onClick={() => {
|
/>
|
||||||
navigate(link.path);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
@ -308,33 +332,21 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
<MobileMenuTabs
|
<MobileMenuTabs
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
onChange={(e, newValue) => setCurrentTab(newValue)}
|
onChange={handleTabChange}
|
||||||
>
|
>
|
||||||
{navLinks.map((link) => (
|
{mainNavSections.map((section) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={link.name}
|
key={section.path || '/'}
|
||||||
value={link.path}
|
value={section.path}
|
||||||
label={
|
label={
|
||||||
<MenuItemBox>
|
<MenuItemBox>
|
||||||
{link.path === '/' ? (
|
{section.label}
|
||||||
<Avatar
|
|
||||||
sx={{ width: 20, height: 20 }}
|
|
||||||
variant="rounded"
|
|
||||||
alt="Backstory logo"
|
|
||||||
src="/logo192.png"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
link.icon && link.icon
|
|
||||||
)}
|
|
||||||
<Typography variant="body2">
|
|
||||||
{link.path === '/' ? 'Backstory' : (link.label && typeof link.label === 'object' ? link.name : (link.label || link.name))}
|
|
||||||
</Typography>
|
|
||||||
</MenuItemBox>
|
</MenuItemBox>
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
handleDrawerToggle();
|
handleDrawerToggle();
|
||||||
setCurrentTab(link.path);
|
setCurrentTab(section.path);
|
||||||
navigate(link.path);
|
navigate(section.path);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -458,8 +470,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
>
|
>
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth="xl">
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
{/* Logo Section */}
|
|
||||||
|
|
||||||
{/* Navigation Links - Desktop */}
|
{/* Navigation Links - Desktop */}
|
||||||
<NavLinksContainer>
|
<NavLinksContainer>
|
||||||
{renderNavLinks()}
|
{renderNavLinks()}
|
||||||
@ -482,22 +492,22 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{sessionId && <CopyBubble
|
{sessionId && <CopyBubble
|
||||||
tooltip="Copy link"
|
tooltip="Copy link"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="copy link"
|
aria-label="copy link"
|
||||||
edge="end"
|
edge="end"
|
||||||
sx={{
|
sx={{
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
bgcolor: 'inherit',
|
bgcolor: 'inherit',
|
||||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||||
}}
|
}}
|
||||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||||
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
||||||
size="large"
|
size="large"
|
||||||
/>}
|
/>}
|
||||||
</UserActionsContainer>
|
</UserActionsContainer>
|
||||||
|
|
||||||
{/* Mobile Navigation Drawer */}
|
{/* Mobile Navigation Drawer */}
|
||||||
@ -507,18 +517,16 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={handleDrawerToggle}
|
onClose={handleDrawerToggle}
|
||||||
ModalProps={{
|
ModalProps={{
|
||||||
keepMounted: true, // Better open performance on mobile
|
keepMounted: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderDrawerContent()}
|
{renderDrawerContent()}
|
||||||
</MobileDrawer>
|
</MobileDrawer>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Container>
|
</Container>
|
||||||
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
||||||
</StyledAppBar>
|
</StyledAppBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { Header };
|
||||||
Header
|
|
||||||
};
|
|
@ -1,19 +1,66 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
|
// Assuming you're using React Router
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Local Storage Keys
|
// Storage Keys
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
|
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
|
||||||
SELECTED_JOB_ID: 'selectedJobId',
|
SELECTED_JOB_ID: 'selectedJobId',
|
||||||
SELECTED_EMPLOYER_ID: 'selectedEmployerId'
|
SELECTED_EMPLOYER_ID: 'selectedEmployerId',
|
||||||
|
LAST_ROUTE: 'lastVisitedRoute',
|
||||||
|
ROUTE_STATE: 'routeState',
|
||||||
|
ACTIVE_TAB: 'activeTab',
|
||||||
|
APPLIED_FILTERS: 'appliedFilters',
|
||||||
|
SIDEBAR_COLLAPSED: 'sidebarCollapsed'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Local Storage Utilities
|
// Route State Interface
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
interface RouteState {
|
||||||
|
lastRoute: string | null;
|
||||||
|
activeTab: string | null;
|
||||||
|
appliedFilters: Record<string, any>;
|
||||||
|
sidebarCollapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Enhanced App State Interface
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
selectedCandidate: Types.Candidate | null;
|
||||||
|
selectedJob: Types.Job | null;
|
||||||
|
selectedEmployer: Types.Employer | null;
|
||||||
|
routeState: RouteState;
|
||||||
|
isInitializing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppStateActions {
|
||||||
|
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
|
||||||
|
setSelectedJob: (job: Types.Job | null) => void;
|
||||||
|
setSelectedEmployer: (employer: Types.Employer | null) => void;
|
||||||
|
clearSelections: () => void;
|
||||||
|
|
||||||
|
// Route management
|
||||||
|
saveCurrentRoute: () => void;
|
||||||
|
restoreLastRoute: () => void;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
|
setFilters: (filters: Record<string, any>) => void;
|
||||||
|
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||||
|
clearRouteState: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppStateContextType = AppState & AppStateActions;
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Storage Utilities
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
function getStoredId(key: string): string | null {
|
function getStoredId(key: string): string | null {
|
||||||
@ -37,42 +84,62 @@ function setStoredId(key: string, id: string | null): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
function getStoredObject<T>(key: string, defaultValue: T): T {
|
||||||
// App State Interface
|
try {
|
||||||
// ============================
|
const stored = localStorage.getItem(key);
|
||||||
|
return stored ? JSON.parse(stored) : defaultValue;
|
||||||
export interface AppState {
|
} catch (error) {
|
||||||
selectedCandidate: Types.Candidate | null;
|
console.warn(`Failed to read ${key} from localStorage:`, error);
|
||||||
selectedJob: Types.Job | null;
|
return defaultValue;
|
||||||
selectedEmployer: Types.Employer | null;
|
}
|
||||||
isInitializing: boolean;
|
|
||||||
// Add more global state as needed:
|
|
||||||
// currentView: string;
|
|
||||||
// filters: Record<string, any>;
|
|
||||||
// searchQuery: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppStateActions {
|
function setStoredObject(key: string, value: any): void {
|
||||||
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
|
try {
|
||||||
setSelectedJob: (job: Types.Job | null) => void;
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
setSelectedEmployer: (employer: Types.Employer | null) => void;
|
} catch (error) {
|
||||||
clearSelections: () => void;
|
console.warn(`Failed to write ${key} to localStorage:`, error);
|
||||||
// Future actions can be added here
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppStateContextType = AppState & AppStateActions;
|
// ============================
|
||||||
|
// Route State Management
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
function getInitialRouteState(): RouteState {
|
||||||
|
return {
|
||||||
|
lastRoute: getStoredId(STORAGE_KEYS.LAST_ROUTE),
|
||||||
|
activeTab: getStoredId(STORAGE_KEYS.ACTIVE_TAB),
|
||||||
|
appliedFilters: getStoredObject(STORAGE_KEYS.APPLIED_FILTERS, {}),
|
||||||
|
sidebarCollapsed: getStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistRouteState(routeState: RouteState): void {
|
||||||
|
setStoredId(STORAGE_KEYS.LAST_ROUTE, routeState.lastRoute);
|
||||||
|
setStoredId(STORAGE_KEYS.ACTIVE_TAB, routeState.activeTab);
|
||||||
|
setStoredObject(STORAGE_KEYS.APPLIED_FILTERS, routeState.appliedFilters);
|
||||||
|
setStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, routeState.sidebarCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// App State Hook
|
// Enhanced App State Hook
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
export function useAppStateLogic(): AppStateContextType {
|
export function useAppStateLogic(): AppStateContextType {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
|
|
||||||
|
// Entity state
|
||||||
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
||||||
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
|
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
|
||||||
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
|
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
|
||||||
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// Route state
|
||||||
|
const [routeState, setRouteStateState] = useState<RouteState>(getInitialRouteState);
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Initialization Effect
|
// Initialization Effect
|
||||||
// ============================
|
// ============================
|
||||||
@ -82,7 +149,7 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
setIsInitializing(true);
|
setIsInitializing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get stored IDs
|
// Get stored entity IDs
|
||||||
const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID);
|
const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID);
|
||||||
const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID);
|
const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID);
|
||||||
const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID);
|
const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID);
|
||||||
@ -94,13 +161,11 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
promises.push(
|
promises.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Assuming apiClient.getCandidate exists
|
|
||||||
const candidate = await apiClient.getCandidate(candidateId);
|
const candidate = await apiClient.getCandidate(candidateId);
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
setSelectedCandidateState(candidate);
|
setSelectedCandidateState(candidate);
|
||||||
console.log('Restored candidate from storage:', candidate);
|
console.log('Restored candidate from storage:', candidate);
|
||||||
} else {
|
} else {
|
||||||
// Data not available, clear stored ID
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||||
console.log('Candidate not found, cleared from storage');
|
console.log('Candidate not found, cleared from storage');
|
||||||
}
|
}
|
||||||
@ -116,13 +181,11 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
promises.push(
|
promises.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Assuming apiClient.getJob exists
|
|
||||||
const job = await apiClient.getJob(jobId);
|
const job = await apiClient.getJob(jobId);
|
||||||
if (job) {
|
if (job) {
|
||||||
setSelectedJobState(job);
|
setSelectedJobState(job);
|
||||||
console.log('Restored job from storage:', job);
|
console.log('Restored job from storage:', job);
|
||||||
} else {
|
} else {
|
||||||
// Data not available, clear stored ID
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||||
console.log('Job not found, cleared from storage');
|
console.log('Job not found, cleared from storage');
|
||||||
}
|
}
|
||||||
@ -138,13 +201,11 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
promises.push(
|
promises.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Assuming apiClient.getEmployer exists
|
|
||||||
const employer = await apiClient.getEmployer(employerId);
|
const employer = await apiClient.getEmployer(employerId);
|
||||||
if (employer) {
|
if (employer) {
|
||||||
setSelectedEmployerState(employer);
|
setSelectedEmployerState(employer);
|
||||||
console.log('Restored employer from storage:', employer);
|
console.log('Restored employer from storage:', employer);
|
||||||
} else {
|
} else {
|
||||||
// Data not available, clear stored ID
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||||
console.log('Employer not found, cleared from storage');
|
console.log('Employer not found, cleared from storage');
|
||||||
}
|
}
|
||||||
@ -170,13 +231,29 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// State Setters with Persistence
|
// Auto-save current route
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't save certain routes (login, register, etc.)
|
||||||
|
const excludedRoutes = ['/login', '/register', '/verify-email', '/reset-password'];
|
||||||
|
const shouldSaveRoute = !excludedRoutes.some(route => location.pathname.startsWith(route));
|
||||||
|
|
||||||
|
if (shouldSaveRoute && !isInitializing) {
|
||||||
|
setRouteStateState(prev => {
|
||||||
|
const newState = { ...prev, lastRoute: location.pathname };
|
||||||
|
persistRouteState(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [location.pathname, isInitializing]);
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Entity State Setters with Persistence
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
|
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
|
||||||
setSelectedCandidateState(candidate);
|
setSelectedCandidateState(candidate);
|
||||||
|
|
||||||
// Persist ID to localStorage
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
|
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
|
||||||
|
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
@ -188,8 +265,6 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
|
|
||||||
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
||||||
setSelectedJobState(job);
|
setSelectedJobState(job);
|
||||||
|
|
||||||
// Persist ID to localStorage
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
|
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
|
||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
@ -201,8 +276,6 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
|
|
||||||
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
||||||
setSelectedEmployerState(employer);
|
setSelectedEmployerState(employer);
|
||||||
|
|
||||||
// Persist ID to localStorage
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
|
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
|
||||||
|
|
||||||
if (employer) {
|
if (employer) {
|
||||||
@ -217,7 +290,6 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
setSelectedJobState(null);
|
setSelectedJobState(null);
|
||||||
setSelectedEmployerState(null);
|
setSelectedEmployerState(null);
|
||||||
|
|
||||||
// Clear all from localStorage
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
||||||
@ -225,15 +297,83 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
console.log('Cleared all selections');
|
console.log('Cleared all selections');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Route State Actions
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
const saveCurrentRoute = useCallback(() => {
|
||||||
|
setRouteStateState(prev => {
|
||||||
|
const newState = { ...prev, lastRoute: location.pathname };
|
||||||
|
persistRouteState(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
const restoreLastRoute = useCallback(() => {
|
||||||
|
if (routeState.lastRoute && routeState.lastRoute !== location.pathname) {
|
||||||
|
navigate(routeState.lastRoute);
|
||||||
|
}
|
||||||
|
}, [routeState.lastRoute, location.pathname, navigate]);
|
||||||
|
|
||||||
|
const setActiveTab = useCallback((tab: string) => {
|
||||||
|
setRouteStateState(prev => {
|
||||||
|
const newState = { ...prev, activeTab: tab };
|
||||||
|
persistRouteState(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setFilters = useCallback((filters: Record<string, any>) => {
|
||||||
|
setRouteStateState(prev => {
|
||||||
|
const newState = { ...prev, appliedFilters: filters };
|
||||||
|
persistRouteState(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setSidebarCollapsed = useCallback((collapsed: boolean) => {
|
||||||
|
setRouteStateState(prev => {
|
||||||
|
const newState = { ...prev, sidebarCollapsed: collapsed };
|
||||||
|
persistRouteState(newState);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearRouteState = useCallback(() => {
|
||||||
|
const clearedState: RouteState = {
|
||||||
|
lastRoute: null,
|
||||||
|
activeTab: null,
|
||||||
|
appliedFilters: {},
|
||||||
|
sidebarCollapsed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
setRouteStateState(clearedState);
|
||||||
|
|
||||||
|
// Clear from localStorage
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.LAST_ROUTE);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.ACTIVE_TAB);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED);
|
||||||
|
|
||||||
|
console.log('Cleared all route state');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedCandidate,
|
selectedCandidate,
|
||||||
selectedJob,
|
selectedJob,
|
||||||
selectedEmployer,
|
selectedEmployer,
|
||||||
|
routeState,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
setSelectedCandidate,
|
setSelectedCandidate,
|
||||||
setSelectedJob,
|
setSelectedJob,
|
||||||
setSelectedEmployer,
|
setSelectedEmployer,
|
||||||
clearSelections
|
clearSelections,
|
||||||
|
saveCurrentRoute,
|
||||||
|
restoreLastRoute,
|
||||||
|
setActiveTab,
|
||||||
|
setFilters,
|
||||||
|
setSidebarCollapsed,
|
||||||
|
clearRouteState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,72 +402,45 @@ export function useAppState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Convenience Hooks for Specific State
|
// Convenience Hooks
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook specifically for candidate selection
|
|
||||||
* Useful when a component only cares about candidate state
|
|
||||||
*/
|
|
||||||
export function useSelectedCandidate() {
|
export function useSelectedCandidate() {
|
||||||
const { selectedCandidate, setSelectedCandidate } = useAppState();
|
const { selectedCandidate, setSelectedCandidate } = useAppState();
|
||||||
return { selectedCandidate, setSelectedCandidate };
|
return { selectedCandidate, setSelectedCandidate };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook specifically for job selection
|
|
||||||
*/
|
|
||||||
export function useSelectedJob() {
|
export function useSelectedJob() {
|
||||||
const { selectedJob, setSelectedJob } = useAppState();
|
const { selectedJob, setSelectedJob } = useAppState();
|
||||||
return { selectedJob, setSelectedJob };
|
return { selectedJob, setSelectedJob };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook specifically for employer selection
|
|
||||||
*/
|
|
||||||
export function useSelectedEmployer() {
|
export function useSelectedEmployer() {
|
||||||
const { selectedEmployer, setSelectedEmployer } = useAppState();
|
const { selectedEmployer, setSelectedEmployer } = useAppState();
|
||||||
return { selectedEmployer, setSelectedEmployer };
|
return { selectedEmployer, setSelectedEmployer };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function useRouteState() {
|
||||||
* Hook to check if the app is still initializing
|
const {
|
||||||
*/
|
routeState,
|
||||||
|
setActiveTab,
|
||||||
|
setFilters,
|
||||||
|
setSidebarCollapsed,
|
||||||
|
restoreLastRoute,
|
||||||
|
clearRouteState
|
||||||
|
} = useAppState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
routeState,
|
||||||
|
setActiveTab,
|
||||||
|
setFilters,
|
||||||
|
setSidebarCollapsed,
|
||||||
|
restoreLastRoute,
|
||||||
|
clearRouteState
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useAppInitializing() {
|
export function useAppInitializing() {
|
||||||
const { isInitializing } = useAppState();
|
const { isInitializing } = useAppState();
|
||||||
return isInitializing;
|
return isInitializing;
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
|
||||||
// Development Utilities
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug utility to log current app state (development only)
|
|
||||||
*/
|
|
||||||
export function useAppStateDebug() {
|
|
||||||
const appState = useAppState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.group('🔍 App State Debug');
|
|
||||||
console.log('Is Initializing:', appState.isInitializing);
|
|
||||||
console.log('Selected Candidate:', appState.selectedCandidate);
|
|
||||||
console.log('Selected Job:', appState.selectedJob);
|
|
||||||
console.log('Selected Employer:', appState.selectedEmployer);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer, appState.isInitializing]);
|
|
||||||
|
|
||||||
return appState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to manually clear all localStorage for this app (development/debugging)
|
|
||||||
*/
|
|
||||||
export function clearAppLocalStorage() {
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
|
|
||||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
|
|
||||||
console.log('Cleared all app localStorage');
|
|
||||||
}
|
}
|
@ -34,7 +34,6 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
console.log(candidates);
|
|
||||||
setCandidates(candidates);
|
setCandidates(candidates);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSnack("" + err);
|
setSnack("" + err);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user