From efef926e45315f0f5e646f60df8cba0e0a520643 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Wed, 4 Jun 2025 11:11:42 -0700 Subject: [PATCH] Fixed tab errors --- .../src/components/layout/BackstoryLayout.tsx | 65 ++-- frontend/src/components/layout/Header.tsx | 146 ++++----- frontend/src/hooks/GlobalContext.tsx | 289 ++++++++++++------ frontend/src/pages/FindCandidatePage.tsx | 1 - 4 files changed, 310 insertions(+), 191 deletions(-) diff --git a/frontend/src/components/layout/BackstoryLayout.tsx b/frontend/src/components/layout/BackstoryLayout.tsx index c3dfb4d..c3873f9 100644 --- a/frontend/src/components/layout/BackstoryLayout.tsx +++ b/frontend/src/components/layout/BackstoryLayout.tsx @@ -23,52 +23,51 @@ import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext'; import { useSelectedCandidate } from 'hooks/GlobalContext'; type NavigationLinkType = { - name: string; + label: ReactElement | string; path: string; - icon?: ReactElement; - label?: ReactElement; + icon?: ReactElement; }; const DefaultNavItems: NavigationLinkType[] = [ - { name: 'Find a Candidate', path: '/find-a-candidate', icon: }, - { name: 'Docs', path: '/docs', icon: }, - // { name: 'How It Works', path: '/how-it-works', icon: }, - // { name: 'For Candidates', path: '/for-candidates', icon: }, - // { name: 'For Employers', path: '/for-employers', icon: }, - // { name: 'Pricing', path: '/pricing', icon: }, + { label: 'Find a Candidate', path: '/find-a-candidate', icon: }, + { label: 'Docs', path: '/docs', icon: }, + // { label: 'How It Works', path: '/how-it-works', icon: }, + // { label: 'For Candidates', path: '/for-candidates', icon: }, + // { label: 'For Employers', path: '/for-employers', icon: }, + // { label: 'Pricing', path: '/pricing', icon: }, ]; const ViewerNavItems: NavigationLinkType[] = [ - { name: 'Chat', path: '/chat', icon: }, + { label: 'Chat', path: '/chat', icon: }, ]; const CandidateNavItems : NavigationLinkType[]= [ - { name: 'Chat', path: '/chat', icon: }, - { name: 'Job Analysis', path: '/candidate/job-analysis', icon: }, - { name: 'Resume Builder', path: '/candidate/resume-builder', icon: }, - // { name: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: }, - // { name: 'Dashboard', icon: , path: '/candidate/dashboard' }, - // { name: 'Profile', icon: , path: '/candidate/profile' }, - // { name: 'Backstory', icon: , path: '/candidate/backstory' }, - // { name: 'Resumes', icon: , path: '/candidate/resumes' }, - // { name: 'Q&A Setup', icon: , path: '/candidate/qa-setup' }, - // { name: 'Analytics', icon: , path: '/candidate/analytics' }, - // { name: 'Settings', icon: , path: '/candidate/settings' }, + { label: 'Chat', path: '/chat', icon: }, + { label: 'Job Analysis', path: '/candidate/job-analysis', icon: }, + { label: 'Resume Builder', path: '/candidate/resume-builder', icon: }, + // { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: }, + // { label: 'Dashboard', icon: , path: '/candidate/dashboard' }, + // { label: 'Profile', icon: , path: '/candidate/profile' }, + // { label: 'Backstory', icon: , path: '/candidate/backstory' }, + // { label: 'Resumes', icon: , path: '/candidate/resumes' }, + // { label: 'Q&A Setup', icon: , path: '/candidate/qa-setup' }, + // { label: 'Analytics', icon: , path: '/candidate/analytics' }, + // { label: 'Settings', icon: , path: '/candidate/settings' }, ]; const EmployerNavItems: NavigationLinkType[] = [ - { name: 'Chat', path: '/chat', icon: }, - { name: 'Job Analysis', path: '/employer/job-analysis', icon: }, - { name: 'Resume Builder', path: '/employer/resume-builder', icon: }, - { name: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: }, - { name: 'Find a Candidate', path: '/find-a-candidate', icon: }, - // { name: 'Dashboard', icon: , path: '/employer/dashboard' }, - // { name: 'Search', icon: , path: '/employer/search' }, - // { name: 'Saved', icon: , path: '/employer/saved' }, - // { name: 'Jobs', icon: , path: '/employer/jobs' }, - // { name: 'Company', icon: , path: '/employer/company' }, - // { name: 'Analytics', icon: , path: '/employer/analytics' }, - // { name: 'Settings', icon: , path: '/employer/settings' }, + { label: 'Chat', path: '/chat', icon: }, + { label: 'Job Analysis', path: '/employer/job-analysis', icon: }, + { label: 'Resume Builder', path: '/employer/resume-builder', icon: }, + { label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: }, + { label: 'Find a Candidate', path: '/find-a-candidate', icon: }, + // { label: 'Dashboard', icon: , path: '/employer/dashboard' }, + // { label: 'Search', icon: , path: '/employer/search' }, + // { label: 'Saved', icon: , path: '/employer/saved' }, + // { label: 'Jobs', icon: , path: '/employer/jobs' }, + // { label: 'Company', icon: , path: '/employer/company' }, + // { label: 'Analytics', icon: , path: '/employer/analytics' }, + // { label: 'Settings', icon: , path: '/employer/settings' }, ]; // Navigation links based on user type diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index a4cff48..528e592 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -170,7 +170,7 @@ interface HeaderProps { navigationLinks: NavigationLinkType[]; currentPath: string; sessionId?: string | null; - setSnack: SetSnackType, + setSnack: SetSnackType; } const Header: React.FC = (props: HeaderProps) => { @@ -190,13 +190,13 @@ const Header: React.FC = (props: HeaderProps) => { const name = (user?.firstName || user?.email || ''); - const navLinks : NavigationLinkType[] = [ - {name: "Home", path: "/", label: }, + const mainNavSections: NavigationLinkType[] = [ + { path: '/', label: }, ...navigationLinks ]; - // State for page navigation - const [ currentTab, setCurrentTab ] = useState("/"); + // State for page navigation - only for main navigation + const [currentTab, setCurrentTab] = useState("/"); const [userMenuTab, setUserMenuTab] = useState(""); // State for mobile drawer @@ -206,6 +206,26 @@ const Header: React.FC = (props: HeaderProps) => { const [userMenuAnchor, setUserMenuAnchor] = useState(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 = [ { @@ -244,15 +264,13 @@ const Header: React.FC = (props: HeaderProps) => { ]; useEffect(() => { - const parts = location.pathname.split('/'); - let tab = '/'; - if (parts.length > 1) { - tab = `/${parts[1]}`; + 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 } - if (tab !== currentTab) { - setCurrentTab(tab); - } - }, [location, currentTab]); + }, [location.pathname, currentTab]); const handleUserMenuOpen = (event: React.MouseEvent) => { setUserMenuAnchor(event.currentTarget); @@ -274,28 +292,34 @@ const Header: React.FC = (props: HeaderProps) => { setMobileOpen(!mobileOpen); }; + const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => { + if (newValue !== false) { + setCurrentTab(newValue); + navigate(newValue); + } + }; + // Render desktop navigation links const renderNavLinks = () => { return ( - setCurrentTab(newValue)} + - {navLinks.map((link) => ( - { - navigate(link.path); - }} - /> + > + {mainNavSections.map((section) => ( + ))} ); @@ -308,33 +332,21 @@ const Header: React.FC = (props: HeaderProps) => { setCurrentTab(newValue)} + onChange={handleTabChange} > - {navLinks.map((link) => ( + {mainNavSections.map((section) => ( - {link.path === '/' ? ( - - ) : ( - link.icon && link.icon - )} - - {link.path === '/' ? 'Backstory' : (link.label && typeof link.label === 'object' ? link.name : (link.label || link.name))} - + {section.label} } onClick={(e) => { handleDrawerToggle(); - setCurrentTab(link.path); - navigate(link.path); + setCurrentTab(section.path); + navigate(section.path); }} /> ))} @@ -458,8 +470,6 @@ const Header: React.FC = (props: HeaderProps) => { > - {/* Logo Section */} - {/* Navigation Links - Desktop */} {renderNavLinks()} @@ -482,22 +492,22 @@ const Header: React.FC = (props: HeaderProps) => { - {sessionId && { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }} - size="large" - />} + {sessionId && { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }} + size="large" + />} {/* Mobile Navigation Drawer */} @@ -507,18 +517,16 @@ const Header: React.FC = (props: HeaderProps) => { open={mobileOpen} onClose={handleDrawerToggle} ModalProps={{ - keepMounted: true, // Better open performance on mobile + keepMounted: true, }} > {renderDrawerContent()} - { navigate('/docs/beta'); }} /> + { navigate('/docs/beta'); }} /> ); }; -export { - Header -}; \ No newline at end of file +export { Header }; \ No newline at end of file diff --git a/frontend/src/hooks/GlobalContext.tsx b/frontend/src/hooks/GlobalContext.tsx index bf4bc42..2151442 100644 --- a/frontend/src/hooks/GlobalContext.tsx +++ b/frontend/src/hooks/GlobalContext.tsx @@ -1,19 +1,66 @@ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; import * as Types from 'types/types'; +// Assuming you're using React Router +import { useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from 'hooks/AuthContext'; // ============================ -// Local Storage Keys +// Storage Keys // ============================ const STORAGE_KEYS = { SELECTED_CANDIDATE_ID: 'selectedCandidateId', 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; // ============================ -// Local Storage Utilities +// Route State Interface +// ============================ + +interface RouteState { + lastRoute: string | null; + activeTab: string | null; + appliedFilters: Record; + 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) => void; + setSidebarCollapsed: (collapsed: boolean) => void; + clearRouteState: () => void; +} + +export type AppStateContextType = AppState & AppStateActions; + +// ============================ +// Storage Utilities // ============================ function getStoredId(key: string): string | null { @@ -37,42 +84,62 @@ function setStoredId(key: string, id: string | null): void { } } -// ============================ -// App State Interface -// ============================ - -export interface AppState { - selectedCandidate: Types.Candidate | null; - selectedJob: Types.Job | null; - selectedEmployer: Types.Employer | null; - isInitializing: boolean; - // Add more global state as needed: - // currentView: string; - // filters: Record; - // searchQuery: string; +function getStoredObject(key: string, defaultValue: T): T { + try { + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : defaultValue; + } catch (error) { + console.warn(`Failed to read ${key} from localStorage:`, error); + return defaultValue; + } } -export interface AppStateActions { - setSelectedCandidate: (candidate: Types.Candidate | null) => void; - setSelectedJob: (job: Types.Job | null) => void; - setSelectedEmployer: (employer: Types.Employer | null) => void; - clearSelections: () => void; - // Future actions can be added here +function setStoredObject(key: string, value: any): void { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.warn(`Failed to write ${key} to localStorage:`, error); + } } -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 { + const location = useLocation(); + const navigate = useNavigate(); const { apiClient } = useAuth(); + + // Entity state const [selectedCandidate, setSelectedCandidateState] = useState(null); const [selectedJob, setSelectedJobState] = useState(null); const [selectedEmployer, setSelectedEmployerState] = useState(null); const [isInitializing, setIsInitializing] = useState(true); + // Route state + const [routeState, setRouteStateState] = useState(getInitialRouteState); + // ============================ // Initialization Effect // ============================ @@ -82,7 +149,7 @@ export function useAppStateLogic(): AppStateContextType { setIsInitializing(true); try { - // Get stored IDs + // Get stored entity IDs const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID); const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID); const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID); @@ -94,13 +161,11 @@ export function useAppStateLogic(): AppStateContextType { promises.push( (async () => { try { - // Assuming apiClient.getCandidate exists const candidate = await apiClient.getCandidate(candidateId); if (candidate) { setSelectedCandidateState(candidate); console.log('Restored candidate from storage:', candidate); } else { - // Data not available, clear stored ID setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null); console.log('Candidate not found, cleared from storage'); } @@ -116,13 +181,11 @@ export function useAppStateLogic(): AppStateContextType { promises.push( (async () => { try { - // Assuming apiClient.getJob exists const job = await apiClient.getJob(jobId); if (job) { setSelectedJobState(job); console.log('Restored job from storage:', job); } else { - // Data not available, clear stored ID setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null); console.log('Job not found, cleared from storage'); } @@ -138,13 +201,11 @@ export function useAppStateLogic(): AppStateContextType { promises.push( (async () => { try { - // Assuming apiClient.getEmployer exists const employer = await apiClient.getEmployer(employerId); if (employer) { setSelectedEmployerState(employer); console.log('Restored employer from storage:', employer); } else { - // Data not available, clear stored ID setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null); 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) => { setSelectedCandidateState(candidate); - - // Persist ID to localStorage setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null); if (candidate) { @@ -188,8 +265,6 @@ export function useAppStateLogic(): AppStateContextType { const setSelectedJob = useCallback((job: Types.Job | null) => { setSelectedJobState(job); - - // Persist ID to localStorage setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null); if (job) { @@ -201,8 +276,6 @@ export function useAppStateLogic(): AppStateContextType { const setSelectedEmployer = useCallback((employer: Types.Employer | null) => { setSelectedEmployerState(employer); - - // Persist ID to localStorage setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null); if (employer) { @@ -217,7 +290,6 @@ export function useAppStateLogic(): AppStateContextType { setSelectedJobState(null); setSelectedEmployerState(null); - // Clear all from localStorage setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null); setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null); setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null); @@ -225,15 +297,83 @@ export function useAppStateLogic(): AppStateContextType { 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) => { + 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 { selectedCandidate, selectedJob, selectedEmployer, + routeState, isInitializing, setSelectedCandidate, setSelectedJob, 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() { const { selectedCandidate, setSelectedCandidate } = useAppState(); return { selectedCandidate, setSelectedCandidate }; } -/** - * Hook specifically for job selection - */ export function useSelectedJob() { const { selectedJob, setSelectedJob } = useAppState(); return { selectedJob, setSelectedJob }; } -/** - * Hook specifically for employer selection - */ export function useSelectedEmployer() { const { selectedEmployer, setSelectedEmployer } = useAppState(); return { selectedEmployer, setSelectedEmployer }; } -/** - * Hook to check if the app is still initializing - */ +export function useRouteState() { + const { + routeState, + setActiveTab, + setFilters, + setSidebarCollapsed, + restoreLastRoute, + clearRouteState + } = useAppState(); + + return { + routeState, + setActiveTab, + setFilters, + setSidebarCollapsed, + restoreLastRoute, + clearRouteState + }; +} + export function useAppInitializing() { const { isInitializing } = useAppState(); 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'); } \ No newline at end of file diff --git a/frontend/src/pages/FindCandidatePage.tsx b/frontend/src/pages/FindCandidatePage.tsx index 93f554d..d56b472 100644 --- a/frontend/src/pages/FindCandidatePage.tsx +++ b/frontend/src/pages/FindCandidatePage.tsx @@ -34,7 +34,6 @@ const CandidateListingPage = (props: BackstoryPageProps) => { } return result; }); - console.log(candidates); setCandidates(candidates); } catch (err) { setSnack("" + err);