dev1/src/App.js

622 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Dashboard from './components/Dashboard.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 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';
export const ProfileCtx = React.createContext();
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);
/* ------------------------------------------
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]);
// 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;
// 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.includes(location.pathname);
// Helper to see if user is midpremium-onboarding
function isOnboardingInProgress() {
try {
const stored = JSON.parse(localStorage.getItem('premiumOnboardingState') || '{}');
// If step < 4 (example), user is in progress
return stored.step && stored.step < 4;
} catch (e) {
return false;
}
}
// ==============================
// 1) Single Rehydrate UseEffect
// ==============================
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
// No token => not authenticated
setIsLoading(false);
return;
}
// If we have a token, validate it by fetching user
fetch('/api/user-profile', {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => {
if (!res.ok) throw new Error('Token invalid on server side');
return res.json();
})
.then((profile) => {
// Successfully got user profile => user is authenticated
setUser(profile);
setIsAuthenticated(true);
})
.catch((err) => {
console.error(err);
// Invalid token => remove it, force sign in
localStorage.removeItem('token');
navigate('/signin?session=expired');
})
.finally(() => {
// Either success or fail, we're done loading
setIsLoading(false);
});
}, [navigate]);
// ==========================
// 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 = () => {
localStorage.removeItem('token');
localStorage.removeItem('id');
localStorage.removeItem('careerSuggestionsCache');
localStorage.removeItem('lastSelectedCareerProfileId');
localStorage.removeItem('selectedCareer');
localStorage.removeItem('aiClickCount');
localStorage.removeItem('aiClickDate');
localStorage.removeItem('aiRecommendations');
localStorage.removeItem('premiumOnboardingState'); // ← NEW
localStorage.removeItem('financialProfile'); // ← if you cache it
setFinancialProfile(null); // ← reset any React-context copy
setScenario(null);
setIsAuthenticated(false);
setUser(null);
setShowLogoutWarning(false);
// Reset auth
setIsAuthenticated(false);
setUser(null);
setShowLogoutWarning(false);
navigate('/signin');
};
const cancelLogout = () => {
setShowLogoutWarning(false);
};
// ====================================
// 3) If still verifying the token, show loading
// ====================================
if (isLoading) {
return (
<div className="flex h-screen 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: () => { setDrawerPane('support'); setDrawerOpen(true); },
openRetire : (props) => {
setRetireProps(props);
setDrawerPane('retire');
setDrawerOpen(true);
if (pageContext === 'RetirementPlanner' || pageContext === 'RetirementLanding') {
setRetireProps(props);
setDrawerPane('retire');
setDrawerOpen(true);
} else {
console.warn('Retirement bot disabled on this page');
}
}}}>
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
{/* Header */}
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
<h1 className="text-lg font-semibold">
AptivaAI - Career Guidance Platform
</h1>
{isAuthenticated && (
<>
{/* NAV MENU */}
<nav className="flex 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>
</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 */}
<div className="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>
)}
{/* LOGOUT BUTTON */}
<button
className="text-red-600 hover:text-red-800 bg-transparent border-none"
onClick={handleLogoutClick}
>
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>
{/* MAIN CONTENT */}
<main className="flex-1 p-6">
<Routes>
{/* Default to /signin */}
<Route path="/" element={<Navigate to="/signin" />} />
{/* Public routes */}
<Route
path="/signin"
element={
<SignIn
setIsAuthenticated={setIsAuthenticated}
setUser={setUser}
setFinancialProfile={setFinancialProfile}
setScenario={setScenario}
/>
}
/>
<Route
path="/signup"
element={<SignUp setUser={setUser} />}
/>
<Route path="/paywall" element={<Paywall />} />
{/* Authenticated routes */}
{isAuthenticated && (
<>
<Route path="/signin-landing" element={<SignInLanding user={user} />}/>
<Route path="/interest-inventory" element={<InterestInventory />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/planning" element={<PlanningLanding />} />
<Route path="/career-explorer" element={<CareerExplorer />} />
<Route path="/loan-repayment" element={<LoanRepaymentPage />}/>
<Route path="/educational-programs" element={<EducationalProgramsPage />} />
<Route path="/preparing" element={<PreparingLanding />} />
<Route path="/billing" element={<BillingResult />} />
{/* Premium-only routes */}
<Route
path="/enhancing"
element={
<PremiumRoute user={user}>
<EnhancingLanding />
</PremiumRoute>
}
/>
<Route
path="/retirement"
element={
<PremiumRoute user={user}>
<RetirementLanding />
</PremiumRoute>
}
/>
<Route
path="/career-roadmap/:careerId?"
element={
<PremiumRoute user={user}>
<CareerRoadmap />
</PremiumRoute>
}
/>
<Route path="/profile/careers" element={<CareerProfileList />} />
<Route path="/profile/careers/:id/edit" element={<CareerProfileForm />} />
<Route path="/profile/college/" element={<CollegeProfileList />} />
<Route path="/profile/college/:careerId/:id?" element={<CollegeProfileForm />} />
<Route
path="/financial-profile"
element={
<PremiumRoute user={user}>
<FinancialProfileForm />
</PremiumRoute>
}
/>
<Route
path="/retirement-planner"
element={
<PremiumRoute user={user}>
<RetirementPlanner />
</PremiumRoute>
}
/>
<Route
path="/premium-onboarding"
element={
<PremiumRoute user={user}>
<OnboardingContainer />
</PremiumRoute>
}
/>
<Route
path="/resume-optimizer"
element={
<PremiumRoute user={user}>
<ResumeRewrite />
</PremiumRoute>
}
/>
</>
)}
{/* 404 / Fallback */}
<Route path="*" element={<Navigate to="/signin" />} />
</Routes>
</main>
<ChatDrawer
open={drawerOpen}
onOpenChange={setDrawerOpen}
pane={drawerPane}
setPane={setDrawerPane}
retireProps={retireProps}
pageContext={pageContext}
snapshot={chatSnapshot}
uiToolHandlers={uiToolHandlers}
/>
{/* Session Handler (Optional) */}
<SessionExpiredHandler />
</div>
</ChatCtx.Provider>
</ProfileCtx.Provider>
);
}
export default App;