903 lines
35 KiB
JavaScript
903 lines
35 KiB
JavaScript
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 <ResetPassword />;
|
||
}
|
||
|
||
|
||
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 (
|
||
<div className="flex min-h-[100dvh] items-center justify-center">
|
||
<p>Loading...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// =====================
|
||
// Main Render / Layout
|
||
// =====================
|
||
return (
|
||
<ProfileCtx.Provider
|
||
value={{ financialProfile, setFinancialProfile,
|
||
scenario, setScenario,
|
||
user, setUser}}
|
||
>
|
||
<ChatCtx.Provider value={{ setChatSnapshot,
|
||
openSupport: () => {
|
||
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);
|
||
}
|
||
}}>
|
||
<div className="flex min-h-[100dvh] flex-col bg-gray-50 text-gray-800">
|
||
{/* Header */}
|
||
<header className="sticky top-0 z-40 flex items-center justify-between border-b bg-white/95 px-4 py-3 md:px-6 md:py-4 backdrop-blur supports-[backdrop-filter]:bg-white/70">
|
||
<h1 className="text-base md:text-lg font-semibold truncate pr-3">
|
||
AptivaAI - Career Guidance Platform
|
||
</h1>
|
||
|
||
{/* Mobile hamburger */}
|
||
<button
|
||
type="button"
|
||
aria-label="Open menu"
|
||
className="md:hidden inline-flex items-center justify-center rounded-md p-2 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||
onClick={() => setMobileNavOpen(v => !v)}
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-6 w-6">
|
||
<path d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
{showAuthedNav && (
|
||
<>
|
||
{/* Desktop NAV */}
|
||
<nav className="hidden md:flex md:space-x-6">
|
||
{/* 1) Planning */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/planning')}
|
||
>
|
||
Find Your Career
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||
<Link
|
||
to="/career-explorer"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Career Explorer
|
||
</Link>
|
||
<Link
|
||
to="/interest-inventory"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Interest Inventory
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2) Preparing */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/preparing')}
|
||
>
|
||
Preparing & UpSkilling for Your Career
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
<Link
|
||
to="/educational-programs"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Educational Programs
|
||
</Link>
|
||
<Link
|
||
to="/preparing?loan=1"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Education Repayment Calculator
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 3) Enhancing (Premium) */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/enhancing')}
|
||
>
|
||
Enhancing Your Career
|
||
{!canAccessPremium && (
|
||
<span className="text-xs ml-1 text-gray-600">
|
||
(Premium)
|
||
</span>
|
||
)}
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
<Link
|
||
to="/career-roadmap"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Roadmap & AI Career Coach
|
||
</Link>
|
||
<Link
|
||
to="/resume-optimizer"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Optimize Resume
|
||
</Link>
|
||
{/* etc. */}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 4) Retirement (Premium) */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/retirement')}
|
||
>
|
||
Retirement Planning (beta)
|
||
{!canAccessPremium && (
|
||
<span className="text-xs ml-1 text-gray-600">
|
||
(Premium)
|
||
</span>
|
||
)}
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
{/* Additional retirement menu items */}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 5) Profile */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold',
|
||
'min-w-0',
|
||
'max-w-[90px]',
|
||
'truncate'
|
||
)}
|
||
>
|
||
Profile
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||
<Link
|
||
to="/profile"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Account
|
||
</Link>
|
||
<Link
|
||
to="/financial-profile"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Financial Profile
|
||
</Link>
|
||
{canAccessPremium ? (
|
||
/* Premium users go straight to the wizard */
|
||
<Link
|
||
to="/profile/careers"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Career Profiles
|
||
</Link>
|
||
) : (
|
||
<span
|
||
className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed"
|
||
>
|
||
Career Profiles (Premium)
|
||
</span>
|
||
)}
|
||
|
||
{/* College Profiles (go straight to list) */}
|
||
{canAccessPremium ? (
|
||
<Link
|
||
to="/profile/college"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
College Profiles
|
||
</Link>
|
||
) : (
|
||
<span className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed">
|
||
College Profiles (Premium)
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
{/* LOGOUT + UPGRADE BUTTONS (desktop) */}
|
||
<div className="hidden md:flex items-center space-x-4 ml-4 relative z-10">
|
||
{showPremiumCTA && !canAccessPremium && (
|
||
<Button
|
||
className={cn(
|
||
'bg-green-500 hover:bg-green-600',
|
||
'max-w-fit text-center text-white',
|
||
'px-3 py-2',
|
||
'rounded',
|
||
'whitespace-nowrap',
|
||
'text-sm',
|
||
'font-semibold',
|
||
'shadow'
|
||
)}
|
||
style={{ minWidth: 0, width: 'auto' }}
|
||
onClick={() => navigate('/paywall')}
|
||
>
|
||
Upgrade to Premium
|
||
</Button>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => setSupportOpen(true)}
|
||
className="px-3 py-1 rounded hover:bg-gray-100"
|
||
>
|
||
Support
|
||
</button>
|
||
|
||
{/* LOGOUT BUTTON */}
|
||
<button
|
||
onClick={handleLogoutClick}
|
||
disabled={loggingOut}
|
||
className="text-red-600 hover:text-red-800 bg-transparent border-none disabled:opacity-60"
|
||
>
|
||
{loggingOut ? 'Signing out…' : 'Logout'}
|
||
</button>
|
||
|
||
{/* SHOW WARNING MODAL IF needed */}
|
||
{showLogoutWarning && (
|
||
<PromptModal
|
||
open={true}
|
||
title="End Onboarding?"
|
||
message="If you sign out now, your onboarding progress will be lost. Are you sure?"
|
||
confirmText="Sign Out"
|
||
cancelText="Nevermind"
|
||
onConfirm={confirmLogout}
|
||
onCancel={cancelLogout}
|
||
/>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</header>
|
||
|
||
{/* Mobile slide-down menu */}
|
||
{showAuthedNav && (
|
||
<div
|
||
className={cn(
|
||
"md:hidden border-b bg-white shadow-sm",
|
||
mobileNavOpen ? "block" : "hidden"
|
||
)}
|
||
>
|
||
<div className="px-4 py-3 space-y-2">
|
||
{/* Upgrade CTA (mobile) */}
|
||
{showPremiumCTA && !canAccessPremium && (
|
||
<Button
|
||
className={cn('bg-green-500 hover:bg-green-600 text-white w-full h-12')}
|
||
onClick={() => navigate('/paywall')}
|
||
>
|
||
Upgrade to Premium
|
||
</Button>
|
||
)}
|
||
|
||
{/* Primary sections (touch-friendly, no shadcn Button to avoid style overrides) */}
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleMobileSection('planning')}
|
||
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
|
||
>
|
||
<span className="text-base">Find Your Career</span>
|
||
<span className={cn("transition-transform", mobileSection==='planning' ? "rotate-180" : "")}>⌄</span>
|
||
</button>
|
||
{mobileSection === 'planning' && (
|
||
<div className="pl-3 space-y-1">
|
||
<Link
|
||
to="/planning"
|
||
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
|
||
>
|
||
Find Your Career — Overview
|
||
</Link>
|
||
<Link to="/career-explorer" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Career Explorer</Link>
|
||
<Link to="/interest-inventory" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Interest Inventory</Link>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleMobileSection('preparing')}
|
||
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
|
||
>
|
||
<span className="text-base">Preparing & UpSkilling for Your Career</span>
|
||
<span className={cn("transition-transform", mobileSection==='preparing' ? "rotate-180" : "")}>⌄</span>
|
||
</button>
|
||
{mobileSection === 'preparing' && (
|
||
<div className="pl-3 space-y-1">
|
||
<Link
|
||
to="/preparing"
|
||
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
|
||
>
|
||
Preparing — Overview
|
||
</Link>
|
||
<Link to="/educational-programs" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Educational Programs</Link>
|
||
<Link to="/preparing?loan=1" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Education Repayment Calculator</Link>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleMobileSection('enhancing')}
|
||
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
|
||
>
|
||
<span className="text-base">
|
||
Enhancing Your Career {!canAccessPremium && <span className="ml-1 text-xs text-gray-600">(Premium)</span>}
|
||
</span>
|
||
<span className={cn("transition-transform", mobileSection==='enhancing' ? "rotate-180" : "")}>⌄</span>
|
||
</button>
|
||
{mobileSection === 'enhancing' && (
|
||
<div className="pl-3 space-y-1">
|
||
<Link
|
||
to="/enhancing"
|
||
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
|
||
>
|
||
Enhancing — Overview {!canAccessPremium && <span className="ml-1 text-xs text-gray-500">(Premium)</span>}
|
||
</Link>
|
||
<Link to="/career-roadmap" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Roadmap & AI Career Coach</Link>
|
||
<Link to="/resume-optimizer" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Optimize Resume</Link>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleMobileSection('retirement')}
|
||
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
|
||
>
|
||
<span className="text-base">
|
||
Retirement Planning (beta) {!canAccessPremium && <span className="ml-1 text-xs text-gray-600">(Premium)</span>}
|
||
</span>
|
||
<span className={cn("transition-transform", mobileSection==='retirement' ? "rotate-180" : "")}>⌄</span>
|
||
</button>
|
||
{mobileSection === 'retirement' && (
|
||
<div className="pl-3 space-y-1">
|
||
<Link
|
||
to="/retirement"
|
||
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
|
||
>
|
||
Retirement — Overview {!canAccessPremium && <span className="ml-1 text-xs text-gray-500">(Premium)</span>}
|
||
</Link>
|
||
</div>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleMobileSection('profile')}
|
||
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
|
||
>
|
||
<span className="text-base">Profile</span>
|
||
<span className={cn("transition-transform", mobileSection==='profile' ? "rotate-180" : "")}>⌄</span>
|
||
</button>
|
||
{mobileSection === 'profile' && (
|
||
<div className="pl-3 space-y-1">
|
||
|
||
<Link to="/profile" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Account</Link>
|
||
<Link to="/financial-profile" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Financial Profile</Link>
|
||
{canAccessPremium ? (
|
||
<Link to="/profile/careers" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Career Profiles</Link>
|
||
) : (
|
||
<span className="block px-2 py-2 text-sm text-gray-400">Career Profiles (Premium)</span>
|
||
)}
|
||
{canAccessPremium ? (
|
||
<Link to="/profile/college" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">College Profiles</Link>
|
||
) : (
|
||
<span className="block px-2 py-2 text-sm text-gray-400">College Profiles (Premium)</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Support + Logout */}
|
||
<div className="flex items-center gap-3 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSupportOpen(true)}
|
||
className="px-3 py-1 rounded hover:bg-gray-100"
|
||
>
|
||
Support
|
||
</button>
|
||
<button
|
||
onClick={handleLogoutClick}
|
||
disabled={loggingOut}
|
||
className="flex-1 px-3 py-3 rounded border border-red-300 text-red-600 hover:bg-red-50 disabled:opacity-60"
|
||
>
|
||
{loggingOut ? 'Signing out…' : 'Logout'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* MAIN CONTENT */}
|
||
<main className="flex-1 p-4 md:p-6">
|
||
<Routes>
|
||
{/* Default */}
|
||
<Route
|
||
path="/"
|
||
element={<Navigate to={isAuthenticated ? AUTH_HOME : '/signin'} replace />}
|
||
/>
|
||
|
||
<Route path="/reset-password/:token" element={<ResetPasswordGate />} />
|
||
|
||
{/* Public (guest-only) routes */}
|
||
<Route
|
||
path="/signin"
|
||
element={
|
||
isAuthenticated ? (
|
||
<Navigate to={AUTH_HOME} replace />
|
||
) : (
|
||
<SignIn
|
||
setIsAuthenticated={setIsAuthenticated}
|
||
setUser={setUser}
|
||
setFinancialProfile={setFinancialProfile}
|
||
setScenario={setScenario}
|
||
/>
|
||
)
|
||
}
|
||
/>
|
||
|
||
<Route
|
||
path="/signup"
|
||
element={isAuthenticated ? <Navigate to={AUTH_HOME} replace /> : <SignUp setUser={setUser} />}
|
||
/>
|
||
|
||
<Route
|
||
path="/forgot-password"
|
||
element={isAuthenticated ? <Navigate to={AUTH_HOME} replace /> : <ForgotPassword />}
|
||
/>
|
||
|
||
|
||
<Route path="/paywall" element={<Paywall />} />
|
||
<Route path="/verify" element={<Verify />} />
|
||
|
||
{/* Authenticated routes */}
|
||
{isAuthenticated && (
|
||
<>
|
||
<Route path="/signin-landing" element={<VerificationGate><SignInLanding user={user} /></VerificationGate>} />
|
||
<Route path="/interest-inventory" element={<VerificationGate><InterestInventory /></VerificationGate>} />
|
||
<Route path="/profile" element={<VerificationGate><UserProfile /></VerificationGate>} />
|
||
<Route path="/planning" element={<VerificationGate><PlanningLanding /></VerificationGate>} />
|
||
<Route path="/career-explorer" element={<VerificationGate><CareerExplorer /></VerificationGate>} />
|
||
<Route path="/loan-repayment" element={<VerificationGate><LoanRepaymentPage /></VerificationGate>} />
|
||
<Route path="/educational-programs" element={<VerificationGate><EducationalProgramsPage /></VerificationGate>} />
|
||
<Route path="/preparing" element={<VerificationGate><PreparingLanding /></VerificationGate>} />
|
||
<Route path="/billing" element={<BillingResult />} />
|
||
|
||
{/* Premium-wrapped */}
|
||
<Route path="/enhancing" element={<VerificationGate><PremiumRoute user={user}><EnhancingLanding /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/retirement" element={<VerificationGate><PremiumRoute user={user}><RetirementLanding /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/career-roadmap/:careerId?" element={<VerificationGate><PremiumRoute user={user}><CareerRoadmap /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/profile/careers" element={<VerificationGate><CareerProfileList /></VerificationGate>} />
|
||
<Route path="/profile/careers/:id/edit" element={<VerificationGate><CareerProfileForm /></VerificationGate>} />
|
||
<Route path="/profile/college" element={<VerificationGate><CollegeProfileList /></VerificationGate>} />
|
||
<Route path="/profile/college/:careerId/:id?" element={<VerificationGate><CollegeProfileForm /></VerificationGate>} />
|
||
<Route path="/financial-profile" element={<VerificationGate><PremiumRoute user={user}><FinancialProfileForm /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/retirement-planner" element={<VerificationGate><PremiumRoute user={user}><RetirementPlanner /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/premium-onboarding" element={<VerificationGate><PremiumRoute user={user}><OnboardingContainer /></PremiumRoute></VerificationGate>} />
|
||
<Route path="/resume-optimizer" element={<VerificationGate><PremiumRoute user={user}><ResumeRewrite /></PremiumRoute></VerificationGate>} />
|
||
</>
|
||
)}
|
||
|
||
|
||
{/* 404 / Fallback */}
|
||
<Route
|
||
path="*"
|
||
element={<Navigate to={isAuthenticated ? AUTH_HOME : '/signin'} replace />}
|
||
/>
|
||
</Routes>
|
||
</main>
|
||
|
||
{/* Support modal mounted once at root so it centers correctly on desktop & mobile */}
|
||
<SupportModal
|
||
open={supportOpen}
|
||
onClose={() => setSupportOpen(false)}
|
||
/>
|
||
|
||
{isAuthenticated && (
|
||
<ChatDrawer
|
||
open={drawerOpen}
|
||
onOpenChange={setDrawerOpen}
|
||
|
||
pane={drawerPane}
|
||
setPane={setDrawerPane}
|
||
|
||
retireProps={retireProps}
|
||
|
||
pageContext={pageContext}
|
||
snapshot={chatSnapshot}
|
||
uiToolHandlers={uiToolHandlers}
|
||
canShowRetireBot={canShowRetireBot}
|
||
/>
|
||
)}
|
||
|
||
{/* Session Handler (Optional) */}
|
||
<SessionExpiredHandler />
|
||
</div>
|
||
</ChatCtx.Provider>
|
||
</ProfileCtx.Provider>
|
||
);
|
||
}
|
||
|
||
export default App;
|