State save for EducationalProgramsPage to keep it from wiping the career on refresh. And added FinancialAidWizard.

This commit is contained in:
Josh 2025-06-02 16:06:57 +00:00
parent 81a28f42f8
commit 0c8cd4a969
11 changed files with 862 additions and 474 deletions

View File

@ -31,8 +31,8 @@ import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js'; import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import MultiScenarioView from './components/MultiScenarioView.js'; import MultiScenarioView from './components/MultiScenarioView.js';
// 1) Import your ResumeRewrite component // Import your ResumeRewrite component
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed import ResumeRewrite from './components/ResumeRewrite.js'; // adjust path if needed
function App() { function App() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,37 +42,42 @@ function App() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Define premium paths (including /enhancing and /retirement) // Premium paths (including /enhancing, /retirement)
const premiumPaths = [ const premiumPaths = [
'/career-roadmap', '/career-roadmap',
'/paywall', '/paywall',
'/financial-profile', '/financial-profile',
'/multi-scenario', '/multi-scenario',
'/premium-onboarding', '/premium-onboarding',
'/enhancing', '/enhancing',
'/retirement', '/retirement',
'/resume-optimizer', '/resume-optimizer',
]; ];
const showPremiumCTA = !premiumPaths.includes(location.pathname); const showPremiumCTA = !premiumPaths.includes(location.pathname);
// We'll define "canAccessPremium" for user // We'll define "canAccessPremium" for user
const canAccessPremium = user?.is_premium || user?.is_pro_premium; const canAccessPremium = user?.is_premium || user?.is_pro_premium;
// Rehydrate user if there's a token // ====================
// Single Rehydrate UseEffect
// ====================
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
// No token? -> not authenticated
if (!token) { if (!token) {
setIsLoading(false); setIsLoading(false);
return; return;
} }
// Validate token/fetch user profile // Token exists, let's check with the server
fetch('https://dev1.aptivaai.com/api/user-profile', { fetch('https://dev1.aptivaai.com/api/user-profile', {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
.then((res) => { .then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error('Token invalid/expired'); // For example, 401 => invalid or expired token
throw new Error('Token invalid on server side');
} }
return res.json(); return res.json();
}) })
@ -82,14 +87,19 @@ function App() {
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
// Server says token invalid -> remove from localStorage
localStorage.removeItem('token'); localStorage.removeItem('token');
// Force user to sign in again
navigate('/signin?session=expired');
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
}); });
}, []); }, [navigate]);
// Logout // ====================
// Logout Handler
// ====================
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('careerSuggestionsCache'); localStorage.removeItem('careerSuggestionsCache');
@ -98,6 +108,7 @@ function App() {
navigate('/signin'); navigate('/signin');
}; };
// If we're still verifying the token, show a loading indicator
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex h-screen items-center justify-center"> <div className="flex h-screen items-center justify-center">
@ -106,6 +117,9 @@ function App() {
); );
} }
// ====================
// Main App Render
// ====================
return ( return (
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800"> <div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
{/* Header */} {/* Header */}
@ -122,15 +136,15 @@ function App() {
<div className="relative group"> <div className="relative group">
<Button <Button
style={{ color: '#1f2937' }} style={{ color: '#1f2937' }}
className={` className={cn(
bg-white 'bg-white',
border border-gray-300 'border border-gray-300',
hover:bg-gray-100 'hover:bg-gray-100',
hover:text-blue-700 'hover:text-blue-700',
whitespace-nowrap 'whitespace-nowrap',
text-xs sm:text-sm md:text-base 'text-xs sm:text-sm md:text-base',
font-semibold 'font-semibold'
`} )}
onClick={() => navigate('/planning')} onClick={() => navigate('/planning')}
> >
Find Your Career Find Your Career
@ -155,21 +169,20 @@ function App() {
<div className="relative group"> <div className="relative group">
<Button <Button
style={{ color: '#1f2937' }} style={{ color: '#1f2937' }}
className={` className={cn(
bg-white 'bg-white',
border border-gray-300 'border border-gray-300',
hover:bg-gray-100 'hover:bg-gray-100',
hover:text-blue-700 'hover:text-blue-700',
whitespace-nowrap 'whitespace-nowrap',
text-xs sm:text-sm md:text-base 'text-xs sm:text-sm md:text-base',
font-semibold 'font-semibold'
`} )}
onClick={() => navigate('/preparing')} onClick={() => navigate('/preparing')}
> >
Preparing for Your Career Preparing for Your Career
</Button> </Button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> <div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
{/* Only Educational Programs as submenu */}
<Link <Link
to="/educational-programs" to="/educational-programs"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700" className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
@ -180,157 +193,138 @@ function App() {
</div> </div>
{/* 3) Enhancing Your Career (Premium) */} {/* 3) Enhancing Your Career (Premium) */}
<div className="relative group"> <div className="relative group">
<Button <Button
style={{ color: '#1f2937' }} style={{ color: '#1f2937' }}
className={` className={cn(
bg-white 'bg-white',
border border-gray-300 'border border-gray-300',
hover:bg-gray-100 'hover:bg-gray-100',
hover:text-blue-700 'hover:text-blue-700',
whitespace-nowrap 'whitespace-nowrap',
text-xs sm:text-sm md:text-base 'text-xs sm:text-sm md:text-base',
font-semibold 'font-semibold'
`} )}
onClick={() => navigate('/enhancing')} 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"
> >
Enhancing Your Career Career Roadmap
{!canAccessPremium && ( </Link>
<span className="text-xs ml-1 text-gray-600">(Premium)</span> <Link
)} to="/resume-optimizer"
</Button> className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
{/* SUBMENU */} Optimize Resume
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> </Link>
<Link <Link
to="/career-roadmap" to="/networking"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700" className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
> >
Career Roadmap Networking
</Link> </Link>
<Link
{/* Optimize Resume */} to="/interview-help"
<Link className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
to="/resume-optimizer" >
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700" Interview Help
> </Link>
Optimize Resume <Link
</Link> to="/job-search"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
{/* Networking (placeholder) */} >
<Link Job Search
to="/networking" </Link>
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Networking
</Link>
{/* Interview Help (placeholder) */}
<Link
to="/interview-help"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Interview Help
</Link>
{/* Job Search (placeholder) */}
<Link
to="/job-search"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Job Search
</Link>
</div>
</div> </div>
</div>
{/* 4) Retirement Planning (Premium) */} {/* 4) Retirement Planning (Premium) */}
<div className="relative group"> <div className="relative group">
<Button <Button
style={{ color: '#1f2937' }} style={{ color: '#1f2937' }}
className={` className={cn(
bg-white 'bg-white',
border border-gray-300 'border border-gray-300',
hover:bg-gray-100 'hover:bg-gray-100',
hover:text-blue-700 'hover:text-blue-700',
whitespace-nowrap 'whitespace-nowrap',
text-xs sm:text-sm md:text-base 'text-xs sm:text-sm md:text-base',
font-semibold 'font-semibold'
`} )}
onClick={() => navigate('/retirement')} onClick={() => navigate('/retirement')}
> >
Retirement Planning Retirement Planning
{!canAccessPremium && ( {!canAccessPremium && (
<span className="text-xs ml-1 text-gray-600">(Premium)</span> <span className="text-xs ml-1 text-gray-600">
(Premium)
</span>
)} )}
</Button> </Button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> <div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
{/* Example retirement submenu item */} {/* Add more retirement submenu items if needed */}
{/* <Link
to="/retirement/financial-tools"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Financial Tools
</Link> */}
{/* Add more retirement submenu items here if needed */}
</div> </div>
</div> </div>
{/* 5) Profile */} {/* 5) Profile */}
<div className="relative group"> <div className="relative group">
<Button <Button
style={{ color: '#1f2937' }} style={{ color: '#1f2937' }}
className={` className={cn(
bg-white 'bg-white',
border border-gray-300 'border border-gray-300',
hover:bg-gray-100 'hover:bg-gray-100',
hover:text-blue-700 'hover:text-blue-700',
whitespace-nowrap 'whitespace-nowrap',
text-xs sm:text-sm md:text-base 'text-xs sm:text-sm md:text-base',
font-semibold 'font-semibold',
min-w-0 'min-w-0',
max-w-[90px] 'max-w-[90px]',
truncate '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"
> >
Profile Account
</Button> </Link>
<Link
{/* DROPDOWN MENU FOR PROFILE */} to="/financial-profile"
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50"> className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
{/* Account (Links to UserProfile.js) */} >
<Link Financial Profile
to="/profile" </Link>
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Account
</Link>
{/* Financial Profile (Links to FinancialProfileForm.js) */}
<Link
to="/financial-profile"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Financial Profile
</Link>
</div>
</div> </div>
</div>
</nav> </nav>
{/* LOGOUT + UPGRADE BUTTONS */} {/* LOGOUT + UPGRADE BUTTONS */}
<div className="flex items-center space-x-4 ml-4 relative z-10"> <div className="flex items-center space-x-4 ml-4 relative z-10">
{showPremiumCTA && !canAccessPremium && ( {showPremiumCTA && !canAccessPremium && (
<Button <Button
className=" className={cn(
bg-green-500 hover:bg-green-600 'bg-green-500 hover:bg-green-600',
max-w-fit text-center text-white 'max-w-fit text-center text-white',
px-3 py-2 'px-3 py-2',
rounded 'rounded',
whitespace-nowrap 'whitespace-nowrap',
text-sm 'text-sm',
font-semibold 'font-semibold',
shadow 'shadow'
" )}
style={{ minWidth: 0, width: 'auto' }} style={{ minWidth: 0, width: 'auto' }}
onClick={() => navigate('/paywall')} onClick={() => navigate('/paywall')}
> >
@ -348,7 +342,6 @@ function App() {
)} )}
</header> </header>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 p-6"> <main className="flex-1 p-6">
<Routes> <Routes>
@ -359,12 +352,13 @@ function App() {
<Route <Route
path="/signin" path="/signin"
element={ element={
<SignIn setIsAuthenticated={setIsAuthenticated} setUser={setUser} /> <SignIn
setIsAuthenticated={setIsAuthenticated}
setUser={setUser}
/>
} }
/> />
<Route path="/signup" element={<SignUp />} /> <Route path="/signup" element={<SignUp />} />
{/* Paywall (public) */}
<Route path="/paywall" element={<Paywall />} /> <Route path="/paywall" element={<Paywall />} />
{/* Authenticated routes */} {/* Authenticated routes */}
@ -376,13 +370,13 @@ function App() {
<Route path="/profile" element={<UserProfile />} /> <Route path="/profile" element={<UserProfile />} />
<Route path="/planning" element={<PlanningLanding />} /> <Route path="/planning" element={<PlanningLanding />} />
<Route path="/career-explorer" element={<CareerExplorer />} /> <Route path="/career-explorer" element={<CareerExplorer />} />
<Route path="/educational-programs" element={<EducationalProgramsPage />} /> <Route
path="/educational-programs"
element={<EducationalProgramsPage />}
/>
<Route path="/preparing" element={<PreparingLanding />} /> <Route path="/preparing" element={<PreparingLanding />} />
{/* {/* Premium-only routes */}
1) EnhancingLanding is premium-only
2) RetirementLanding is premium-only
*/}
<Route <Route
path="/enhancing" path="/enhancing"
element={ element={
@ -399,8 +393,6 @@ function App() {
</PremiumRoute> </PremiumRoute>
} }
/> />
{/* Other Premium-only routes */}
<Route <Route
path="/career-roadmap" path="/career-roadmap"
element={ element={
@ -433,8 +425,6 @@ function App() {
</PremiumRoute> </PremiumRoute>
} }
/> />
{/* Resume Optimizer route */}
<Route <Route
path="/resume-optimizer" path="/resume-optimizer"
element={ element={
@ -451,7 +441,7 @@ function App() {
</Routes> </Routes>
</main> </main>
{/* Session Handler */} {/* Session Handler (Optional) */}
<SessionExpiredHandler /> <SessionExpiredHandler />
</div> </div>
); );

View File

@ -748,8 +748,8 @@ function CareerExplorer() {
/> />
)} )}
<div className="flex justify-between items-center mb-4"> <div className="mb-4">
<h2 className="text-xl font-semibold"> <h2 className="text-2xl font-semibold mb-2">
Explore Careers - use these tools to find your best fit Explore Careers - use these tools to find your best fit
</h2> </h2>
<CareerSearch <CareerSearch
@ -868,44 +868,52 @@ function CareerExplorer() {
)} )}
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<select <select
className="border px-3 py-1 rounded" className="border px-3 py-1 rounded"
value={selectedJobZone} value={selectedJobZone}
onChange={(e) => setSelectedJobZone(e.target.value)} onChange={(e) => setSelectedJobZone(e.target.value)}
> >
<option value="">All Preparation Levels</option> <option value="">All Preparation Levels</option>
{Object.entries(jobZoneLabels).map(([zone, label]) => ( {Object.entries(jobZoneLabels).map(([zone, label]) => (
<option key={zone} value={zone}> <option key={zone} value={zone}>
{label} {label}
</option> </option>
))} ))}
</select> </select>
<select
className="border px-3 py-1 rounded"
value={selectedFit}
onChange={(e) => setSelectedFit(e.target.value)}
>
<option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
<Button
onClick={() => {
if (!userProfile?.interest_inventory_answers) {
alert('Please complete the Interest Inventory to get suggestions.');
return;
}
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
}}
className="bg-green-600 text-white px-3 py-1 text-xs sm:text-sm"
>
Reload Career Suggestions
</Button>
{/* Legend container with less internal gap, plus a left margin */}
<div className="flex items-center gap-1 ml-4">
<span className="warning-icon"></span>
<span>= Limited Data for this career path</span>
</div>
</div>
<select
className="border px-3 py-1 rounded"
value={selectedFit}
onChange={(e) => setSelectedFit(e.target.value)}
>
<option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
<Button
onClick={() => {
if (!userProfile?.interest_inventory_answers) {
alert('No interest inventory answers. Complete the interest inventory first!');
return;
}
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
}}
className="bg-green-600 text-white px-3 py-1 text-xs sm:text-sm"
>
Reload Career Suggestions
</Button>
</div>
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */} {/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
<CareerSuggestions <CareerSuggestions

View File

@ -153,84 +153,90 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</div> </div>
{/* Economic Projections */} {/* Economic Projections */}
<div className="md:w-1/2 overflow-x-auto"> <div className="md:w-1/2 overflow-x-auto">
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3> <h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
<table className="w-full text-left border border-gray-300 rounded"> <table className="w-full text-left border border-gray-300 rounded">
<thead className="bg-gray-100"> <thead className="bg-gray-100">
<tr> <tr>
<th className="px-3 py-2 border-b"></th> <th className="px-3 py-2 border-b"></th>
{careerDetails.economicProjections.state && ( {careerDetails.economicProjections.state && (
<th className="px-3 py-2 border-b"> <th className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.area} {careerDetails.economicProjections.state.area}
</th> </th>
)} )}
{careerDetails.economicProjections.national && ( {careerDetails.economicProjections.national && (
<th className="px-3 py-2 border-b">National</th> <th className="px-3 py-2 border-b">National</th>
)} )}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className="px-3 py-2 border-b font-semibold"> <td className="px-3 py-2 border-b font-semibold">Current Jobs</td>
Current Jobs {careerDetails.economicProjections.state && (
</td> <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state && ( {careerDetails.economicProjections.state.base.toLocaleString()}
<td className="px-3 py-2 border-b"> </td>
{careerDetails.economicProjections.state.base.toLocaleString()} )}
</td> {careerDetails.economicProjections.national && (
)} <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.national && ( {careerDetails.economicProjections.national.base.toLocaleString()}
<td className="px-3 py-2 border-b"> </td>
{careerDetails.economicProjections.national.base.toLocaleString()} )}
</td> </tr>
)} <tr>
</tr> <td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
<tr> {careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b font-semibold"> <td className="px-3 py-2 border-b">
Jobs in 10 yrs {careerDetails.economicProjections.state.projection.toLocaleString()}
</td> </td>
{careerDetails.economicProjections.state && ( )}
<td className="px-3 py-2 border-b"> {careerDetails.economicProjections.national && (
{careerDetails.economicProjections.state.projection.toLocaleString()} <td className="px-3 py-2 border-b">
</td> {careerDetails.economicProjections.national.projection.toLocaleString()}
)} </td>
{careerDetails.economicProjections.national && ( )}
<td className="px-3 py-2 border-b"> </tr>
{careerDetails.economicProjections.national.projection.toLocaleString()} <tr>
</td> <td className="px-3 py-2 border-b font-semibold">Growth %</td>
)} {careerDetails.economicProjections.state && (
</tr> <td className="px-3 py-2 border-b">
<tr> {careerDetails.economicProjections.state.percentChange}%
<td className="px-3 py-2 border-b font-semibold">Growth %</td> </td>
{careerDetails.economicProjections.state && ( )}
<td className="px-3 py-2 border-b"> {careerDetails.economicProjections.national && (
{careerDetails.economicProjections.state.percentChange}% <td className="px-3 py-2 border-b">
</td> {careerDetails.economicProjections.national.percentChange}%
)} </td>
{careerDetails.economicProjections.national && ( )}
<td className="px-3 py-2 border-b"> </tr>
{careerDetails.economicProjections.national.percentChange}% <tr>
</td> <td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
)} {careerDetails.economicProjections.state && (
</tr> <td className="px-3 py-2 border-b">
<tr> {careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
<td className="px-3 py-2 border-b font-semibold"> </td>
Annual Openings )}
</td> {careerDetails.economicProjections.national && (
{careerDetails.economicProjections.state && ( <td className="px-3 py-2 border-b">
<td className="px-3 py-2 border-b"> {careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()} </td>
</td> )}
)} </tr>
{careerDetails.economicProjections.national && ( </tbody>
<td className="px-3 py-2 border-b"> </table>
{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
</td> {/* Conditional disclaimer when AI risk is Moderate or High */}
)} {(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
</tr> <p className="text-sm text-red-600 mt-2">
</tbody> Note: These 10-year projections may change if AI-driven
</table> tools significantly affect {careerDetails.title} tasks.
</div> With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk,
its possible that some tasks or responsibilities could be automated
over time.
</p>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -64,8 +64,8 @@ const CareerSearch = ({ onCareerSelected }) => {
return ( return (
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
<h2>Search for Career (select from suggestions)</h2> <h4>Search for Career (select from suggestions)</h4>
<h3>We have an extensive database with thousands of recognized job titles. If you dont see your exact title, please choose the closest matchthis helps us provide the most accurate guidance.</h3> <h5>We have an extensive database with thousands of recognized job titles. If you dont see your exact title, please choose the closest matchthis helps us provide the most accurate guidance.</h5>
<input <input
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}

View File

@ -84,10 +84,14 @@ function EducationalProgramsPage() {
const [maxDistance, setMaxDistance] = useState(100); const [maxDistance, setMaxDistance] = useState(100);
const [inStateOnly, setInStateOnly] = useState(false); const [inStateOnly, setInStateOnly] = useState(false);
const [careerTitle, setCareerTitle] = useState(location.state?.careerTitle || ''); const [careerTitle, setCareerTitle] = useState(location.state?.careerTitle || '');
const [selectedCareer, setSelectedCareer] = useState(location.state?.foundObj || '');
const [showSearch, setShowSearch] = useState(true);
// If user picks a new career from CareerSearch // If user picks a new career from CareerSearch
const handleCareerSelected = (foundObj) => { const handleCareerSelected = (foundObj) => {
setCareerTitle(foundObj.title || ''); setCareerTitle(foundObj.title || '');
setSelectedCareer(foundObj);
localStorage.setItem('selectedCareer', JSON.stringify(foundObj));
let rawCips = Array.isArray(foundObj.cip_code) ? foundObj.cip_code : [foundObj.cip_code]; let rawCips = Array.isArray(foundObj.cip_code) ? foundObj.cip_code : [foundObj.cip_code];
const cleanedCips = rawCips.map((code) => { const cleanedCips = rawCips.map((code) => {
@ -96,15 +100,23 @@ function EducationalProgramsPage() {
}); });
setCipCodes(cleanedCips); setCipCodes(cleanedCips);
setsocCode(foundObj.soc_code); setsocCode(foundObj.soc_code);
setShowSearch(false);
}; };
function handleChangeCareer() {
// Optionally remove from localStorage if the user is truly 'unselecting' it
localStorage.removeItem('selectedCareer');
setSelectedCareer(null);
setShowSearch(true);
}
// Fixed handleSelectSchool (removed extra brace) // Fixed handleSelectSchool (removed extra brace)
const handleSelectSchool = (school) => { const handleSelectSchool = (school) => {
const proceed = window.confirm( const proceed = window.confirm(
'Youre about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?' 'Youre about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
); );
if (proceed) { if (proceed) {
navigate('/milestone-tracker', { state: { selectedSchool: school } }); navigate('/career-roadmap', { state: { selectedSchool: school } });
} }
}; };
@ -147,50 +159,7 @@ function EducationalProgramsPage() {
loadKsaData(); loadKsaData();
}, []); }, []);
async function fetchAiKsaFallback(socCode, careerTitle) {
// Optionally show a “loading” indicator
setLoadingKsa(true);
setKsaError(null);
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No auth token found; cannot fetch AI-based KSAs.');
}
// Call the new endpoint in server3.js
const resp = await fetch(
`/api/premium/ksa/${socCode}?careerTitle=${encodeURIComponent(careerTitle)}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
if (!resp.ok) {
throw new Error(`AI KSA endpoint returned status ${resp.status}`);
}
const json = await resp.json();
// Expect shape: { source: 'chatgpt' | 'db' | 'local', data: { knowledge, skills, abilities } }
// The arrays from server may already be in the “IM/LV” format
// so we can combine them into one array for display:
const finalKsa = [...json.data.knowledge, ...json.data.skills, ...json.data.abilities];
finalKsa.forEach(item => {
item.onetSocCode = socCode;
});
const combined = combineIMandLV(finalKsa);
setKsaForCareer(combined);
} catch (err) {
console.error('Error fetching AI-based KSAs:', err);
setKsaError('Could not load AI-based KSAs. Please try again later.');
setKsaForCareer([]);
} finally {
setLoadingKsa(false);
}
}
// Filter: only IM >=3, then combine IM+LV // Filter: only IM >=3, then combine IM+LV
useEffect(() => { useEffect(() => {
@ -201,12 +170,8 @@ useEffect(() => {
} }
if (!allKsaData.length) { if (!allKsaData.length) {
// We haven't loaded local data yet (or it failed to load). return;
// We can either wait, or directly try fallback now. }
// For example:
fetchAiKsaFallback(socCode, careerTitle);
return;
}
// Otherwise, we have local data loaded: // Otherwise, we have local data loaded:
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode); let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
@ -230,28 +195,46 @@ useEffect(() => {
// Load user profile // Load user profile
useEffect(() => { useEffect(() => {
async function loadUserProfile() { async function loadUserProfile() {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
console.warn('No token found, cannot load user-profile.'); console.warn('No token found, cannot load user-profile.');
return; return;
}
const res = await fetch('/api/user-profile', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
throw new Error('Failed to fetch user profile');
}
const data = await res.json();
setUserZip(data.zipcode || '');
setUserState(data.state || '');
} catch (err) {
console.error('Error loading user profile:', err);
} }
const res = await fetch('/api/user-profile', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Failed to fetch user profile');
const data = await res.json();
setUserZip(data.zipcode || '');
setUserState(data.state || '');
} catch (err) {
console.error('Error loading user profile:', err);
} }
loadUserProfile();
}, []); // Then handle localStorage:
const stored = localStorage.getItem('selectedCareer');
if (stored) {
const parsed = JSON.parse(stored);
setSelectedCareer(parsed);
setCareerTitle(parsed.title || '');
// Re-set CIP code logic (like in handleCareerSelected)
let rawCips = Array.isArray(parsed.cip_code)
? parsed.cip_code
: [parsed.cip_code];
const cleanedCips = rawCips.map((code) => code.toString().replace('.', '').slice(0, 4));
setCipCodes(cleanedCips);
setsocCode(parsed.soc_code);
setShowSearch(false);
}
}
loadUserProfile();
}, []);
// Fetch schools once CIP codes are set // Fetch schools once CIP codes are set
useEffect(() => { useEffect(() => {
@ -519,100 +502,190 @@ useEffect(() => {
); );
} }
async function fetchAiKsaFallback(socCode, careerTitle) {
// Optionally show a “loading” indicator
setLoadingKsa(true);
setKsaError(null);
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No auth token found; cannot fetch AI-based KSAs.');
}
// Call the new endpoint in server3.js
const resp = await fetch(
`/api/premium/ksa/${socCode}?careerTitle=${encodeURIComponent(careerTitle)}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
if (!resp.ok) {
throw new Error(`AI KSA endpoint returned status ${resp.status}`);
}
const json = await resp.json();
// Expect shape: { source: 'chatgpt' | 'db' | 'local', data: { knowledge, skills, abilities } }
// The arrays from server may already be in the “IM/LV” format
// so we can combine them into one array for display:
const finalKsa = [...json.data.knowledge, ...json.data.skills, ...json.data.abilities];
finalKsa.forEach(item => {
item.onetSocCode = socCode;
});
const combined = combineIMandLV(finalKsa);
setKsaForCareer(combined);
} catch (err) {
console.error('Error fetching AI-based KSAs:', err);
setKsaError('Could not load AI-based KSAs. Please try again later.');
setKsaForCareer([]);
} finally {
setLoadingKsa(false);
}
}
return ( return (
<div className="p-4"> <div className="p-4">
{/* KSA Section */} {/* 1. If user is allowed to search for a career again, show CareerSearch */}
{renderKsaSection()} {showSearch && (
<div className="mb-4">
<h2 className="text-xl font-semibold mb-2">Find a Career</h2>
<CareerSearch onCareerSelected={handleCareerSelected} />
</div>
)}
{/* School List */} {/* 2. If the user has already selected a career and we're not showing search */}
<h2 className="text-xl font-semibold mb-4"> {selectedCareer && !showSearch && (
Schools for: {careerTitle || 'Unknown Career'} <div className="mb-4">
</h2> <p className="text-gray-700">
<strong>Currently selected:</strong> {selectedCareer.title}
</p>
<button
onClick={handleChangeCareer}
className="mt-2 rounded bg-green-400 px-3 py-1 hover:bg-green-300"
>
Change Career
</button>
</div>
)}
{/* Filter Bar */} {/* If were loading or errored out, handle that up front */}
<div className="mb-4 flex flex-wrap items-center space-x-4"> {loading && (
<label className="text-sm text-gray-600"> <div>Loading skills and educational programs...</div>
Sort: )}
<select {error && (
className="ml-2 rounded border px-2 py-1 text-sm" <div className="text-red-600">
value={sortBy} Error: {error}
onChange={(e) => setSortBy(e.target.value)} </div>
> )}
<option value="tuition">Tuition</option>
<option value="distance">Distance</option>
</select>
</label>
<label className="text-sm text-gray-600"> {/* 3. Display CIP-based data only if we have CIP codes (means we have a known career) */}
Tuition (max): {cipCodes.length > 0 ? (
<input <>
type="number" {/* KSA section */}
className="ml-2 w-20 rounded border px-2 py-1 text-sm" {renderKsaSection()}
value={maxTuition}
onChange={(e) => setMaxTuition(Number(e.target.value))}
/>
</label>
<label className="text-sm text-gray-600"> {/* School List */}
Distance (max): <h2 className="text-xl font-semibold mb-4">
<input Schools for: {careerTitle || 'Unknown Career'}
type="number" </h2>
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxDistance}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</label>
{userState && ( {/* Filter Bar */}
<label className="inline-flex items-center space-x-2 text-sm text-gray-600"> <div className="mb-4 flex flex-wrap items-center space-x-4">
<input <label className="text-sm text-gray-600">
type="checkbox" Sort:
checked={inStateOnly} <select
onChange={(e) => setInStateOnly(e.target.checked)} className="ml-2 rounded border px-2 py-1 text-sm"
/> value={sortBy}
<span>In-State Only</span> onChange={(e) => setSortBy(e.target.value)}
>
<option value="tuition">Tuition</option>
<option value="distance">Distance</option>
</select>
</label> </label>
)}
<label className="text-sm text-gray-600">
Tuition (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxTuition}
onChange={(e) => setMaxTuition(Number(e.target.value))}
/>
</label>
<label className="text-sm text-gray-600">
Distance (max):
<input
type="number"
className="ml-2 w-20 rounded border px-2 py-1 text-sm"
value={maxDistance}
onChange={(e) => setMaxDistance(Number(e.target.value))}
/>
</label>
{userState && (
<label className="inline-flex items-center space-x-2 text-sm text-gray-600">
<input
type="checkbox"
checked={inStateOnly}
onChange={(e) => setInStateOnly(e.target.checked)}
/>
<span>In-State Only</span>
</label>
)}
</div>
{/* Display the sorted/filtered schools */}
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
{filteredAndSortedSchools.map((school, idx) => {
const displayWebsite = ensureHttp(school['Website']);
return (
<div key={idx} className="rounded border p-3 text-sm">
<strong>
{school['Website'] ? (
<a
href={displayWebsite}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{school['INSTNM'] || 'Unnamed School'}
</a>
) : (
school['INSTNM'] || 'Unnamed School'
)}
</strong>
<p>Degree Type: {school['CREDDESC'] || 'N/A'}</p>
<p>In-State Tuition: ${school['In_state cost'] || 'N/A'}</p>
<p>Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}</p>
<p>Distance: {school.distance !== null ? `${school.distance} mi` : 'N/A'}</p>
<button
onClick={() => handleSelectSchool(school)}
className="mt-3 rounded bg-green-600 px-3 py-1 text-white hover:bg-blue-700"
>
Select School
</button>
</div>
);
})}
</div>
</>
) : (
/* 4. If no CIP codes, user hasn't picked a valid career or there's no data */
<div className="mt-4">
<p className="text-gray-600">
You have not selected a career (or no CIP codes found) so no programs can be shown.
</p>
</div> </div>
)}
</div>
);
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
{filteredAndSortedSchools.map((school, idx) => {
// 1) Ensure the website has a protocol:
const displayWebsite = ensureHttp(school['Website']);
return (
<div key={idx} className="rounded border p-3 text-sm">
<strong>
{school['Website'] ? (
<a
href={displayWebsite}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{school['INSTNM'] || 'Unnamed School'}
</a>
) : (
school['INSTNM'] || 'Unnamed School'
)}
</strong>
<p>Degree Type: {school['CREDDESC'] || 'N/A'}</p>
<p>In-State Tuition: ${school['In_state cost'] || 'N/A'}</p>
<p>Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}</p>
<p>Distance: {school.distance !== null ? `${school.distance} mi` : 'N/A'}</p>
<button
onClick={() => handleSelectSchool(school)}
className="mt-3 rounded bg-green-600 px-3 py-1 text-white hover:bg-blue-700"
>
Select School
</button>
</div>
);
})}
</div>
</div>
);
} }
export default EducationalProgramsPage; export default EducationalProgramsPage;

View File

@ -0,0 +1,210 @@
import React, { useState } from 'react';
import { Button } from './ui/button.js'; // adjust path as needed
function FinancialAidWizard({ onAidEstimated, onClose }) {
const [step, setStep] = useState(1);
// Basic user inputs
const [isDependent, setIsDependent] = useState(true);
const [incomeRange, setIncomeRange] = useState('');
const [isVeteran, setIsVeteran] = useState(false);
// A list of custom scholarships the user can add
const [scholarships, setScholarships] = useState([]);
// Pell estimate logic (simple bracket)
function getPellEstimate() {
switch (incomeRange) {
case '<30k': return 6000;
case '30-50k': return 3500;
case '50-80k': return 1500;
default: return 0;
}
}
// Optional GI Bill or other known assistance
function getVeteranEstimate() {
return isVeteran ? 2000 : 0;
}
// Sum up user-entered scholarships
function getUserScholarshipsTotal() {
return scholarships.reduce((sum, s) => sum + (parseFloat(s.amount) || 0), 0);
}
// Combine everything
function calculateAid() {
const pell = getPellEstimate();
const vet = getVeteranEstimate();
const userScholarships = getUserScholarshipsTotal();
return pell + vet + userScholarships;
}
// Step navigation
const handleNext = () => setStep(step + 1);
const handleBack = () => setStep(step - 1);
const handleFinish = () => {
onAidEstimated(Math.round(calculateAid()));
onClose();
};
// Add / remove scholarships
function addScholarship() {
setScholarships([...scholarships, { name: '', amount: '' }]);
}
function updateScholarship(index, field, value) {
const updated = [...scholarships];
updated[index][field] = value;
setScholarships(updated);
}
function removeScholarship(index) {
setScholarships(scholarships.filter((_, i) => i !== index));
}
return (
<div className="p-4 bg-white rounded shadow-md">
{step === 1 && (
<div>
<h2 className="text-xl font-bold mb-2">Basic Info</h2>
<div className="space-y-2">
<p>Are you considered a dependent or independent student?</p>
<label>
<input
type="radio"
checked={isDependent}
onChange={() => setIsDependent(true)}
/>
<span className="ml-1">Dependent</span>
</label>
<label className="ml-4">
<input
type="radio"
checked={!isDependent}
onChange={() => setIsDependent(false)}
/>
<span className="ml-1">Independent</span>
</label>
</div>
<div className="mt-4 space-y-1">
<label className="block font-medium">Approx. Income Range</label>
<select
value={incomeRange}
onChange={(e) => setIncomeRange(e.target.value)}
className="border p-2 w-full rounded"
>
<option value="">-- Select Range --</option>
<option value="<30k">{"< $30k"}</option>
<option value="30-50k">$30k - $50k</option>
<option value="50-80k">$50k - $80k</option>
<option value=">80k">{"> $80k"}</option>
</select>
</div>
<div className="mt-4 flex justify-end">
<Button onClick={handleNext}>Next </Button>
</div>
</div>
)}
{step === 2 && (
<div>
<h2 className="text-xl font-bold mb-2">Military / Veteran?</h2>
<div className="flex items-center space-x-2 mb-4">
<input
type="checkbox"
checked={isVeteran}
onChange={(e) => setIsVeteran(e.target.checked)}
/>
<label>Im a veteran or have GI Bill benefits</label>
</div>
<h2 className="text-xl font-bold mb-2">Add Scholarships / Grants</h2>
<p className="text-sm mb-2">
Have a known scholarship, state grant, or other funding?
</p>
<div className="space-y-2 mb-2">
{scholarships.map((sch, i) => (
<div key={i} className="flex space-x-2 items-center">
<input
type="text"
placeholder="Scholarship Name"
value={sch.name}
onChange={(e) => updateScholarship(i, 'name', e.target.value)}
className="border p-2 rounded w-1/2"
/>
<input
type="number"
placeholder="Amount"
value={sch.amount}
onChange={(e) => updateScholarship(i, 'amount', e.target.value)}
className="border p-2 rounded w-1/3"
/>
{/* Remove scholarship button */}
<Button
className="bg-red-600 hover:bg-red-700"
onClick={() => removeScholarship(i)}
>
Remove
</Button>
</div>
))}
</div>
<Button
className="bg-green-500 text-gray-800 hover:bg-green-300"
onClick={addScholarship}
>
+ Add Another
</Button>
<div className="mt-4 flex justify-between">
<Button
className="bg-blue-400 text-gray-800 hover:bg-blue-300"
onClick={handleBack}
>
Back
</Button>
<Button onClick={handleNext}>Next </Button>
</div>
</div>
)}
{step === 3 && (
<div>
<h2 className="text-xl font-bold mb-2">Your Estimated Financial Aid</h2>
<div className="mb-4 p-3 border rounded bg-gray-50">
<span className="font-semibold">Approx. Total Aid:</span>{" "}
{`$${calculateAid().toLocaleString()}`}
</div>
<div className="mb-4 text-sm space-y-2">
<p className="font-semibold">Important Disclaimers:</p>
<ul className="list-disc ml-4">
<li>This is an approximationfinal amounts require FAFSA and official award letters.</li>
<li>Scholarship amounts can vary by school, state, or specific qualifications.</li>
<li>If you have special circumstances (assets, unique tax situations), your actual aid may differ.</li>
</ul>
</div>
<div className="flex justify-between">
<Button
className="bg-blue-400 text-gray-800 hover:bg-blue-300"
onClick={handleBack}
>
Back
</Button>
<Button className="bg-green-600 hover:bg-green-700" onClick={handleFinish}>
Use This Estimate
</Button>
</div>
</div>
)}
</div>
);
}
export default FinancialAidWizard;

View File

@ -1,6 +1,6 @@
// CollegeOnboarding.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import authFetch from '../../utils/authFetch.js'; import Modal from '../../components/ui/modal.js';
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId }) { function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId }) {
// CIP / iPEDS local states (purely for CIP data and suggestions) // CIP / iPEDS local states (purely for CIP data and suggestions)
@ -10,13 +10,16 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
const [programSuggestions, setProgramSuggestions] = useState([]); const [programSuggestions, setProgramSuggestions] = useState([]);
const [availableProgramTypes, setAvailableProgramTypes] = useState([]); const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
// ---- DESCTRUCTURE PARENT DATA FOR ALL FIELDS EXCEPT TUITION/PROGRAM_LENGTH ---- // Show/hide the financial aid wizard
const [showAidWizard, setShowAidWizard] = useState(false);
// Destructure parent data
const { const {
college_enrollment_status = '', college_enrollment_status = '',
selected_school = '', selected_school = '',
selected_program = '', selected_program = '',
program_type = '', program_type = '',
academic_calendar = 'semester', academic_calendar = 'semester', // <-- ACADEMIC CALENDAR
annual_financial_aid = '', annual_financial_aid = '',
is_online = false, is_online = false,
existing_college_debt = '', existing_college_debt = '',
@ -34,7 +37,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
tuition_paid = '', tuition_paid = '',
} = data; } = data;
// ---- 1. LOCAL STATES for auto/manual logic on TWO fields ---- // Local states for auto/manual logic on tuition & program length
const [manualTuition, setManualTuition] = useState(''); const [manualTuition, setManualTuition] = useState('');
const [autoTuition, setAutoTuition] = useState(0); const [autoTuition, setAutoTuition] = useState(0);
@ -67,7 +70,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
setManualProgramLength(e.target.value); setManualProgramLength(e.target.value);
}; };
// CIP fetch // Fetch CIP data (example)
useEffect(() => { useEffect(() => {
async function fetchCipData() { async function fetchCipData() {
try { try {
@ -85,7 +88,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
fetchCipData(); fetchCipData();
}, []); }, []);
// iPEDS fetch // Fetch iPEDS data (example)
useEffect(() => { useEffect(() => {
async function fetchIpedsData() { async function fetchIpedsData() {
try { try {
@ -104,7 +107,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
fetchIpedsData(); fetchIpedsData();
}, []); }, []);
// handleSchoolChange // Handle school name input
const handleSchoolChange = (e) => { const handleSchoolChange = (e) => {
const value = e.target.value; const value = e.target.value;
setData(prev => ({ setData(prev => ({
@ -167,7 +170,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
setAutoProgramLength('0.00'); setAutoProgramLength('0.00');
}; };
// once we have school+program, load possible program types // once we have school + program, load possible program types
useEffect(() => { useEffect(() => {
if (!selected_program || !selected_school || !schoolData.length) return; if (!selected_program || !selected_school || !schoolData.length) return;
const possibleTypes = schoolData const possibleTypes = schoolData
@ -184,8 +187,11 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
if (!icTuitionData.length) return; if (!icTuitionData.length) return;
if (!selected_school || !program_type || !credit_hours_per_year) return; if (!selected_school || !program_type || !credit_hours_per_year) return;
const found = schoolData.find(s => s.INSTNM.toLowerCase() === selected_school.toLowerCase()); const found = schoolData.find(
s => s.INSTNM.toLowerCase() === selected_school.toLowerCase()
);
if (!found) return; if (!found) return;
const unitId = found.UNITID; const unitId = found.UNITID;
if (!unitId) return; if (!unitId) return;
@ -283,6 +289,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
nextStep(); nextStep();
}; };
// displayedTuition / displayedProgramLength
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition); const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength); const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
@ -293,6 +300,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
{(college_enrollment_status === 'currently_enrolled' || {(college_enrollment_status === 'currently_enrolled' ||
college_enrollment_status === 'prospective_student') ? ( college_enrollment_status === 'prospective_student') ? (
<div className="space-y-4"> <div className="space-y-4">
{/* In District, In State, Online, etc. */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <input
type="checkbox" type="checkbox"
@ -337,6 +345,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
<label className="font-medium">Defer Loan Payments until Graduation?</label> <label className="font-medium">Defer Loan Payments until Graduation?</label>
</div> </div>
{/* School / Program */}
<div className="space-y-1"> <div className="space-y-1">
<label className="block font-medium">School Name*</label> <label className="block font-medium">School Name*</label>
<input <input
@ -394,6 +403,23 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
</select> </select>
</div> </div>
{/* Academic Calendar (just re-added) */}
<div className="space-y-1">
<label className="block font-medium">Academic Calendar</label>
<select
name="academic_calendar"
value={academic_calendar}
onChange={handleParentFieldChange}
className="w-full border rounded p-2"
>
<option value="semester">Semester</option>
<option value="quarter">Quarter</option>
<option value="trimester">Trimester</option>
<option value="other">Other</option>
</select>
</div>
{/* If Grad/Professional or other that needs credit_hours_required */}
{(program_type === 'Graduate/Professional Certificate' || {(program_type === 'Graduate/Professional Certificate' ||
program_type === 'First Professional Degree' || program_type === 'First Professional Degree' ||
program_type === 'Doctoral Degree') && ( program_type === 'Doctoral Degree') && (
@ -422,6 +448,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/> />
</div> </div>
{/* Tuition (auto or override) */}
<div className="space-y-1"> <div className="space-y-1">
<label className="block font-medium">Yearly Tuition</label> <label className="block font-medium">Yearly Tuition</label>
<input <input
@ -433,16 +460,26 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/> />
</div> </div>
{/* Annual Financial Aid with "Need Help?" Wizard button */}
<div className="space-y-1"> <div className="space-y-1">
<label className="block font-medium">(Estimated) Annual Financial Aid</label> <label className="block font-medium">(Estimated) Annual Financial Aid</label>
<input <div className="flex space-x-2">
type="number" <input
name="annual_financial_aid" type="number"
value={annual_financial_aid} name="annual_financial_aid"
onChange={handleParentFieldChange} value={annual_financial_aid}
placeholder="e.g. 2000" onChange={handleParentFieldChange}
className="w-full border rounded p-2" placeholder="e.g. 2000"
/> className="w-full border rounded p-2"
/>
<button
type="button"
onClick={() => setShowAidWizard(true)}
className="bg-gray-200 px-3 py-2 rounded"
>
Need Help?
</button>
</div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -457,9 +494,9 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/> />
</div> </div>
{/* If "currently_enrolled" show Hours Completed + Program Length */}
{college_enrollment_status === 'currently_enrolled' && ( {college_enrollment_status === 'currently_enrolled' && (
<> <>
<div className="space-y-1"> <div className="space-y-1">
<label className="block font-medium">Hours Completed</label> <label className="block font-medium">Hours Completed</label>
<input <input
@ -563,6 +600,22 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
Finish Onboarding Finish Onboarding
</button> </button>
</div> </div>
{/* RENDER THE MODAL WITH FINANCIAL AID WIZARD IF showAidWizard === true */}
{showAidWizard && (
<Modal onClose={() => setShowAidWizard(false)}>
<FinancialAidWizard
onAidEstimated={(estimate) => {
// Update the annual_financial_aid with the wizard's result
setData(prev => ({
...prev,
annual_financial_aid: estimate
}));
}}
onClose={() => setShowAidWizard(false)}
/>
</Modal>
)}
</div> </div>
); );
} }

View File

@ -1,11 +1,21 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
function SignIn({ setIsAuthenticated, setUser }) { function SignIn({ setIsAuthenticated, setUser }) {
const navigate = useNavigate(); const navigate = useNavigate();
const usernameRef = useRef(''); const usernameRef = useRef('');
const passwordRef = useRef(''); const passwordRef = useRef('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showSessionExpiredMsg, setShowSessionExpiredMsg] = useState(false);
const location = useLocation();
useEffect(() => {
// Check if the URL query param has ?session=expired
const query = new URLSearchParams(location.search);
if (query.get('session') === 'expired') {
setShowSessionExpiredMsg(true);
}
}, [location.search]);
const handleSignIn = async (event) => { const handleSignIn = async (event) => {
event.preventDefault(); event.preventDefault();
@ -68,7 +78,13 @@ function SignIn({ setIsAuthenticated, setUser }) {
}; };
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-4"> <div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-4">
{showSessionExpiredMsg && (
<div className="mb-4 p-2 bg-red-100 border border-red-300 text-red-700 rounded">
Your session has expired. Please sign in again.
</div>
)}
<div className="w-full max-w-sm rounded-md bg-white p-6 shadow-md"> <div className="w-full max-w-sm rounded-md bg-white p-6 shadow-md">
<h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1> <h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1>

View File

@ -0,0 +1,32 @@
// Modal.js
import React from 'react';
import { Button } from './button.js';
function Modal({ children, onClose }) {
return (
<div
className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50"
onClick={onClose}
>
{/* This div is the actual "modal" dialog.
We stop click propagation so a click *inside* doesn't close it. */}
<div
className="bg-white p-6 rounded shadow-md"
style={{ maxWidth: '600px', width: '90%' }}
onClick={e => e.stopPropagation()}
>
<Button
onClick={onClose}
className="float-right bg-red-600 text-gray-500 hover:text-gray-700"
>
</Button>
<div className="clear-both">
{children}
</div>
</div>
</div>
);
}
export default Modal;

View File

@ -142,8 +142,8 @@ export function simulateFinancialProjection(userProfile) {
interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO' interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO'
flatAnnualRate = 0.06, // 6% default if using FLAT flatAnnualRate = 0.06, // 6% default if using FLAT
monthlyReturnSamples = [], // if using historical-based random sampling monthlyReturnSamples = [], // if using historical-based random sampling
randomRangeMin = -0.03, // if using a random range approach randomRangeMin = -0.02, // if using a random range approach
randomRangeMax = 0.08, randomRangeMax = 0.02,
} = userProfile; } = userProfile;

Binary file not shown.