import React, { useState, useEffect, useMemo } from 'react';
import {
Routes,
Route,
Navigate,
useNavigate,
useLocation,
Link,
} from 'react-router-dom';
import { Button } from './components/ui/button.js';
import { cn } from './utils/cn.js';
import PromptModal from './components/ui/PromptModal.js';
import PremiumRoute from './components/PremiumRoute.js';
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
import SignInLanding from './components/SignInLanding.js';
import SignIn from './components/SignIn.js';
import SignUp from './components/SignUp.js';
import PlanningLanding from './components/PlanningLanding.js';
import CareerExplorer from './components/CareerExplorer.js';
import PreparingLanding from './components/PreparingLanding.js';
import EducationalProgramsPage from './components/EducationalProgramsPage.js';
import EnhancingLanding from './components/EnhancingLanding.js';
import RetirementLanding from './components/RetirementLanding.js';
import InterestInventory from './components/InterestInventory.js';
import UserProfile from './components/UserProfile.js';
import FinancialProfileForm from './components/FinancialProfileForm.js';
import CareerProfileList from './components/CareerProfileList.js';
import CareerProfileForm from './components/CareerProfileForm.js';
import CollegeProfileList from './components/CollegeProfileList.js';
import CollegeProfileForm from './components/CollegeProfileForm.js';
import CareerRoadmap from './components/CareerRoadmap.js';
import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import { isOnboardingInProgress } from './utils/onboardingGuard.js';
import RetirementPlanner from './components/RetirementPlanner.js';
import ResumeRewrite from './components/ResumeRewrite.js';
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
import usePageContext from './utils/usePageContext.js';
import ChatDrawer from './components/ChatDrawer.js';
import ChatCtx from './contexts/ChatCtx.js';
import BillingResult from './components/BillingResult.js';
import SupportModal from './components/SupportModal.js';
import ForgotPassword from './components/ForgotPassword.js';
import ResetPassword from './components/ResetPassword.js';
import { clearToken } from './auth/authMemory.js';
import api from './auth/apiClient.js';
import * as safeLocal from './utils/safeLocal.js';
import VerificationGate from './components/VerificationGate.js';
import Verify from './components/Verify.js';
export const ProfileCtx = React.createContext();
function ResetPasswordGate() {
const location = useLocation();
useEffect(() => {
clearToken();
try { localStorage.removeItem('id'); } catch {}
}, [location.pathname]);
return ;
}
function App() {
const navigate = useNavigate();
const location = useLocation();
const { pageContext, snapshot: routeSnapshot } = usePageContext();
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerPane, setDrawerPane] = useState('support');
const [retireProps, setRetireProps] = useState(null);
const [supportOpen, setSupportOpen] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
// Mobile nav
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const [mobileSection, setMobileSection] = useState(null); // 'planning' | 'preparing' | 'enhancing' | 'retirement' | 'profile' | null
const toggleMobileSection = (key) => setMobileSection((k) => (k === key ? null : key));
const AUTH_HOME = '/signin-landing';
const prevPathRef = React.useRef(location.pathname);
useEffect(() => { prevPathRef.current = location.pathname; }, [location.pathname]);
// Close mobile nav on route change
useEffect(() => { setMobileNavOpen(false); }, [location.pathname]);
const IN_OB = (p) => p.startsWith('/premium-onboarding');
/* ------------------------------------------
ChatDrawer – route-aware tool handlers
------------------------------------------ */
const uiToolHandlers = useMemo(() => {
if (pageContext === "CareerExplorer") {
return {
// __tool:addCareerToComparison:{"socCode":"15-2051","careerName":"Data Scientist"}
addCareerToComparison: ({ socCode, careerName }) => {
console.log('[dispatch]', socCode, careerName);
window.dispatchEvent(
new CustomEvent("add-career", { detail: { socCode, careerName } })
);
},
// __tool:openCareerModal:{"socCode":"15-2051"}
openCareerModal: ({ socCode }) => {
window.dispatchEvent(
new CustomEvent("open-career", { detail: { socCode } })
);
}
};
}
return {}; // every other page exposes no UI tools
}, [pageContext]);
// route-change guard: only warn when LEAVING onboarding mid-flow
useEffect(() => {
const wasIn = IN_OB(prevPathRef.current);
const nowIn = IN_OB(location.pathname);
const leavingOnboarding = wasIn && !nowIn;
if (!leavingOnboarding) return;
// skip if not mid-flow or if final handoff set the suppress flag
if (!isOnboardingInProgress() || sessionStorage.getItem('suppressOnboardingGuard') === '1') return;
const ok = window.confirm(
"Onboarding is in progress. If you navigate away, your progress may not be saved. Continue?"
);
if (!ok) {
// bounce back to where the user was
navigate(prevPathRef.current, { replace: true });
} else {
// user chose to leave: clear client pointer and server draft immediately
try {
safeLocal.clearMany(['premiumOnboardingPointer', 'premiumOnboardingState']);
} catch {}
(async () => {
try { await api.delete('/api/premium/onboarding/draft'); } catch {}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
// browser close/refresh guard: only when currently on onboarding and mid-flow
useEffect(() => {
const onBeforeUnload = (e) => {
if (IN_OB(location.pathname)
&& isOnboardingInProgress()
&& sessionStorage.getItem('suppressOnboardingGuard') !== '1') {
e.preventDefault();
e.returnValue = "Onboarding is in progress. If you leave now, your progress may not be saved.";
}
};
window.addEventListener('beforeunload', onBeforeUnload);
return () => window.removeEventListener('beforeunload', onBeforeUnload);
}, [location.pathname]);
// Retirement bot is only relevant on these pages
const canShowRetireBot =
pageContext === 'RetirementPlanner' ||
pageContext === 'RetirementLanding';
// Auth states
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [chatSnapshot, setChatSnapshot] = useState(null);
// Loading state while verifying token
const [isLoading, setIsLoading] = useState(true);
// User states
const [financialProfile, setFinancialProfile] = useState(null);
const [scenario, setScenario] = useState(null);
// Logout warning modal
const [showLogoutWarning, setShowLogoutWarning] = useState(false);
// Check if user can access premium
const canAccessPremium = user?.is_premium || user?.is_pro_premium;
const isAuthScreen = React.useMemo(() => {
const p = location.pathname;
return (
p === '/signin' ||
p === '/signup' ||
p === '/forgot-password' ||
p.startsWith('/reset-password')
);
}, [location.pathname]);
const showAuthedNav = isAuthenticated && !isAuthScreen;
// List of premium paths for your CTA logic
const premiumPaths = [
'/career-roadmap',
'/paywall',
'/financial-profile',
'/retirement-planner',
'/premium-onboarding',
'/enhancing',
'/retirement',
'/resume-optimizer',
];
const showPremiumCTA = !premiumPaths.some(p =>
location.pathname.startsWith(p)
);
// ==============================
// 1) Single Rehydrate UseEffect
// ==============================
useEffect(() => {
let cancelled = false;
if (loggingOut) return;
// Skip auth probe on all public auth routes
if (
location.pathname.startsWith('/reset-password') ||
location.pathname === '/signin' ||
location.pathname === '/signup' ||
location.pathname === '/forgot-password'
) {
try { localStorage.removeItem('id'); } catch {}
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
return;
}
(async () => {
setIsLoading(true);
try {
// Fetch only the minimal fields App needs for nav/landing/support
const { data } = await api.get('/api/user-profile?fields=firstname,is_premium,is_pro_premium');
if (cancelled) return;
setUser(prev => ({
...(prev || {}),
firstname : data?.firstname || '',
is_premium : !!data?.is_premium,
is_pro_premium: !!data?.is_pro_premium,
}));
setIsAuthenticated(true);
} catch (err) {
if (cancelled) return;
clearToken();
setIsAuthenticated(false);
setUser(null);
// Only kick to /signin if you’re not already on a public page
const p = location.pathname;
const onPublic =
p === '/signin' ||
p === '/signup' ||
p === '/forgot-password' ||
p.startsWith('/reset-password') ||
p === '/paywall';
if (!onPublic) navigate('/signin?session=expired', { replace: true });
} finally {
if (!cancelled) setIsLoading(false);
}
})();
return () => { cancelled = true; };
// include isAuthScreen if you prefer, but this local check avoids a dep loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, navigate, loggingOut]);
// ==========================
// 2) Logout Handler + Modal
// ==========================
const handleLogoutClick = () => {
if (isOnboardingInProgress()) {
// Show a modal to confirm losing onboarding data
setShowLogoutWarning(true);
} else {
// No onboarding => just logout
confirmLogout();
}
};
const confirmLogout = async () => {
setLoggingOut(true);
// 1) Ask the server to clear the session cookie
try {
// If you created /logout (no /api prefix):
await api.post('/api/logout', {}); // axios client is withCredentials: true
// If your route is /api/signout instead, use:
// await api.post('/api/signout');
} catch (e) {
console.warn('Server logout failed (continuing client-side):', e?.message || e);
}
// 2) Clear client-side state/caches
clearToken(); // in-memory bearer (if any, for legacy flows)
safeLocal.clearMany([
'id',
'careerSuggestionsCache',
'lastSelectedCareerProfileId',
'selectedCareer',
'aiClickCount',
'aiClickDate',
'aiRecommendations',
'premiumOnboardingState',
'premiumOnboardingPointer',
'financialProfile',
'selectedScenario',
]);
// 3) Reset React state
setFinancialProfile(null);
setScenario(null);
setIsAuthenticated(false);
setUser(null);
setShowLogoutWarning(false);
// 4) Back to sign-in
navigate('/signin', { replace: true });
setLoggingOut(false);
};
const cancelLogout = () => {
setShowLogoutWarning(false);
};
// ====================================
// 3) If still verifying the token, show loading
// ====================================
if (isLoading) {
return (
);
}
// =====================
// Main Render / Layout
// =====================
return (
{
if (!isAuthenticated) return;
setDrawerPane('support'); setDrawerOpen(true);
},
openRetire : (props) => {
if (!isAuthenticated || !canShowRetireBot) return;
if (!canShowRetireBot) {
console.warn('Retirement bot disabled on this page');
return;
}
setRetireProps(props);
setDrawerPane('retire');
setDrawerOpen(true);
}
}}>
{/* Header */}
{/* Mobile slide-down menu */}
{showAuthedNav && (
{/* Upgrade CTA (mobile) */}
{showPremiumCTA && !canAccessPremium && (
)}
{/* Primary sections (touch-friendly, no shadcn Button to avoid style overrides) */}
{mobileSection === 'planning' && (
Find Your Career — Overview
Career Explorer
Interest Inventory
)}
{mobileSection === 'preparing' && (
Preparing — Overview
Educational Programs
Education Repayment Calculator
)}
{mobileSection === 'enhancing' && (
Enhancing — Overview {!canAccessPremium && (Premium)}
Roadmap & AI Career Coach
Optimize Resume
)}
{mobileSection === 'retirement' && (
Retirement — Overview {!canAccessPremium && (Premium)}
)}
{mobileSection === 'profile' && (
Account
Financial Profile
{canAccessPremium ? (
Career Profiles
) : (
Career Profiles (Premium)
)}
{canAccessPremium ? (
College Profiles
) : (
College Profiles (Premium)
)}
)}
{/* Support + Logout */}
)}
{/* MAIN CONTENT */}
{/* Default */}
}
/>
} />
{/* Public (guest-only) routes */}
) : (
)
}
/>
: }
/>
: }
/>
} />
} />
{/* Authenticated routes */}
{isAuthenticated && (
<>
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Premium-wrapped */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
>
)}
{/* 404 / Fallback */}
}
/>
{/* Support modal mounted once at root so it centers correctly on desktop & mobile */}
setSupportOpen(false)}
/>
{isAuthenticated && (
)}
{/* Session Handler (Optional) */}
);
}
export default App;