State save for EducationalProgramsPage to keep it from wiping the career on refresh. And added FinancialAidWizard.
This commit is contained in:
parent
81a28f42f8
commit
0c8cd4a969
342
src/App.js
342
src/App.js
@ -31,8 +31,8 @@ import Paywall from './components/Paywall.js';
|
||||
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
||||
import MultiScenarioView from './components/MultiScenarioView.js';
|
||||
|
||||
// 1) Import your ResumeRewrite component
|
||||
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed
|
||||
// Import your ResumeRewrite component
|
||||
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust path if needed
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
@ -42,37 +42,42 @@ function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Define premium paths (including /enhancing and /retirement)
|
||||
const premiumPaths = [
|
||||
'/career-roadmap',
|
||||
'/paywall',
|
||||
'/financial-profile',
|
||||
'/multi-scenario',
|
||||
'/premium-onboarding',
|
||||
'/enhancing',
|
||||
'/retirement',
|
||||
'/resume-optimizer',
|
||||
];
|
||||
// Premium paths (including /enhancing, /retirement)
|
||||
const premiumPaths = [
|
||||
'/career-roadmap',
|
||||
'/paywall',
|
||||
'/financial-profile',
|
||||
'/multi-scenario',
|
||||
'/premium-onboarding',
|
||||
'/enhancing',
|
||||
'/retirement',
|
||||
'/resume-optimizer',
|
||||
];
|
||||
const showPremiumCTA = !premiumPaths.includes(location.pathname);
|
||||
|
||||
// We'll define "canAccessPremium" for user
|
||||
const canAccessPremium = user?.is_premium || user?.is_pro_premium;
|
||||
|
||||
// Rehydrate user if there's a token
|
||||
// ====================
|
||||
// Single Rehydrate UseEffect
|
||||
// ====================
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// No token? -> not authenticated
|
||||
if (!token) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate token/fetch user profile
|
||||
// Token exists, let's check with the server
|
||||
fetch('https://dev1.aptivaai.com/api/user-profile', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((res) => {
|
||||
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();
|
||||
})
|
||||
@ -82,14 +87,19 @@ function App() {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
// Server says token invalid -> remove from localStorage
|
||||
localStorage.removeItem('token');
|
||||
// Force user to sign in again
|
||||
navigate('/signin?session=expired');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
}, [navigate]);
|
||||
|
||||
// Logout
|
||||
// ====================
|
||||
// Logout Handler
|
||||
// ====================
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('careerSuggestionsCache');
|
||||
@ -98,6 +108,7 @@ function App() {
|
||||
navigate('/signin');
|
||||
};
|
||||
|
||||
// If we're still verifying the token, show a loading indicator
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
@ -106,6 +117,9 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// ====================
|
||||
// Main App Render
|
||||
// ====================
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
||||
{/* Header */}
|
||||
@ -122,15 +136,15 @@ function App() {
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
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
|
||||
`}
|
||||
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
|
||||
@ -155,21 +169,20 @@ function App() {
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
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
|
||||
`}
|
||||
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 for Your Career
|
||||
</Button>
|
||||
<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
|
||||
to="/educational-programs"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
@ -180,157 +193,138 @@ function App() {
|
||||
</div>
|
||||
|
||||
{/* 3) Enhancing Your Career (Premium) */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
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')}
|
||||
<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"
|
||||
>
|
||||
Enhancing Your Career
|
||||
{!canAccessPremium && (
|
||||
<span className="text-xs ml-1 text-gray-600">(Premium)</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* SUBMENU */}
|
||||
<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"
|
||||
>
|
||||
Career Roadmap
|
||||
</Link>
|
||||
|
||||
{/* Optimize Resume */}
|
||||
<Link
|
||||
to="/resume-optimizer"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Optimize Resume
|
||||
</Link>
|
||||
|
||||
{/* Networking (placeholder) */}
|
||||
<Link
|
||||
to="/networking"
|
||||
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>
|
||||
Career Roadmap
|
||||
</Link>
|
||||
<Link
|
||||
to="/resume-optimizer"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Optimize Resume
|
||||
</Link>
|
||||
<Link
|
||||
to="/networking"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Networking
|
||||
</Link>
|
||||
<Link
|
||||
to="/interview-help"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Interview Help
|
||||
</Link>
|
||||
<Link
|
||||
to="/job-search"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Job Search
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4) Retirement Planning (Premium) */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
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
|
||||
`}
|
||||
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
|
||||
{!canAccessPremium && (
|
||||
<span className="text-xs ml-1 text-gray-600">(Premium)</span>
|
||||
<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">
|
||||
{/* Example retirement submenu item */}
|
||||
{/* <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 */}
|
||||
{/* Add more retirement submenu items if needed */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5) Profile */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
style={{ color: '#1f2937' }}
|
||||
className={`
|
||||
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
|
||||
`}
|
||||
<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"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
|
||||
{/* DROPDOWN MENU FOR PROFILE */}
|
||||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||||
{/* Account (Links to UserProfile.js) */}
|
||||
<Link
|
||||
to="/profile"
|
||||
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>
|
||||
Account
|
||||
</Link>
|
||||
<Link
|
||||
to="/financial-profile"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Financial Profile
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* LOGOUT + UPGRADE BUTTONS */}
|
||||
<div className="flex items-center space-x-4 ml-4 relative z-10">
|
||||
{showPremiumCTA && !canAccessPremium && (
|
||||
<Button
|
||||
className="
|
||||
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
|
||||
"
|
||||
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')}
|
||||
>
|
||||
@ -348,7 +342,6 @@ function App() {
|
||||
)}
|
||||
</header>
|
||||
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 p-6">
|
||||
<Routes>
|
||||
@ -359,12 +352,13 @@ function App() {
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<SignIn setIsAuthenticated={setIsAuthenticated} setUser={setUser} />
|
||||
<SignIn
|
||||
setIsAuthenticated={setIsAuthenticated}
|
||||
setUser={setUser}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
|
||||
{/* Paywall (public) */}
|
||||
<Route path="/paywall" element={<Paywall />} />
|
||||
|
||||
{/* Authenticated routes */}
|
||||
@ -376,13 +370,13 @@ function App() {
|
||||
<Route path="/profile" element={<UserProfile />} />
|
||||
<Route path="/planning" element={<PlanningLanding />} />
|
||||
<Route path="/career-explorer" element={<CareerExplorer />} />
|
||||
<Route path="/educational-programs" element={<EducationalProgramsPage />} />
|
||||
<Route
|
||||
path="/educational-programs"
|
||||
element={<EducationalProgramsPage />}
|
||||
/>
|
||||
<Route path="/preparing" element={<PreparingLanding />} />
|
||||
|
||||
{/*
|
||||
1) EnhancingLanding is premium-only
|
||||
2) RetirementLanding is premium-only
|
||||
*/}
|
||||
{/* Premium-only routes */}
|
||||
<Route
|
||||
path="/enhancing"
|
||||
element={
|
||||
@ -399,8 +393,6 @@ function App() {
|
||||
</PremiumRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Other Premium-only routes */}
|
||||
<Route
|
||||
path="/career-roadmap"
|
||||
element={
|
||||
@ -433,8 +425,6 @@ function App() {
|
||||
</PremiumRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Resume Optimizer route */}
|
||||
<Route
|
||||
path="/resume-optimizer"
|
||||
element={
|
||||
@ -451,7 +441,7 @@ function App() {
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
{/* Session Handler */}
|
||||
{/* Session Handler (Optional) */}
|
||||
<SessionExpiredHandler />
|
||||
</div>
|
||||
);
|
||||
|
@ -748,8 +748,8 @@ function CareerExplorer() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
Explore Careers - use these tools to find your best fit
|
||||
</h2>
|
||||
<CareerSearch
|
||||
@ -868,44 +868,52 @@ function CareerExplorer() {
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 mb-4">
|
||||
<select
|
||||
className="border px-3 py-1 rounded"
|
||||
value={selectedJobZone}
|
||||
onChange={(e) => setSelectedJobZone(e.target.value)}
|
||||
>
|
||||
<option value="">All Preparation Levels</option>
|
||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||
<option key={zone} value={zone}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="border px-3 py-1 rounded"
|
||||
value={selectedJobZone}
|
||||
onChange={(e) => setSelectedJobZone(e.target.value)}
|
||||
>
|
||||
<option value="">All Preparation Levels</option>
|
||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||
<option key={zone} value={zone}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</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 */}
|
||||
<CareerSuggestions
|
||||
|
@ -153,84 +153,90 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</div>
|
||||
|
||||
{/* Economic Projections */}
|
||||
<div className="md:w-1/2 overflow-x-auto">
|
||||
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
||||
<table className="w-full text-left border border-gray-300 rounded">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="px-3 py-2 border-b"></th>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<th className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.area}
|
||||
</th>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<th className="px-3 py-2 border-b">National</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Current Jobs
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.base.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.base.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Jobs in 10 yrs
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.projection.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.percentChange}%
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.percentChange}%
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">
|
||||
Annual Openings
|
||||
</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="md:w-1/2 overflow-x-auto">
|
||||
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
||||
<table className="w-full text-left border border-gray-300 rounded">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="px-3 py-2 border-b"></th>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<th className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.area}
|
||||
</th>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<th className="px-3 py-2 border-b">National</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Current Jobs</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.base.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.base.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.projection.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.percentChange}%
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.percentChange}%
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
|
||||
{careerDetails.economicProjections.state && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
{careerDetails.economicProjections.national && (
|
||||
<td className="px-3 py-2 border-b">
|
||||
{careerDetails.economicProjections.national.annualOpenings.toLocaleString()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
||||
{(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
||||
<p className="text-sm text-red-600 mt-2">
|
||||
Note: These 10-year projections may change if AI-driven
|
||||
tools significantly affect {careerDetails.title} tasks.
|
||||
With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk,
|
||||
it’s possible that some tasks or responsibilities could be automated
|
||||
over time.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,8 +64,8 @@ const CareerSearch = ({ onCareerSelected }) => {
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<h2>Search for Career (select from suggestions)</h2>
|
||||
<h3>We have an extensive database with thousands of recognized job titles. If you don’t see your exact title, please choose the closest match—this helps us provide the most accurate guidance.</h3>
|
||||
<h4>Search for Career (select from suggestions)</h4>
|
||||
<h5>We have an extensive database with thousands of recognized job titles. If you don’t see your exact title, please choose the closest match—this helps us provide the most accurate guidance.</h5>
|
||||
<input
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
|
@ -84,10 +84,14 @@ function EducationalProgramsPage() {
|
||||
const [maxDistance, setMaxDistance] = useState(100);
|
||||
const [inStateOnly, setInStateOnly] = useState(false);
|
||||
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
|
||||
const handleCareerSelected = (foundObj) => {
|
||||
setCareerTitle(foundObj.title || '');
|
||||
setSelectedCareer(foundObj);
|
||||
localStorage.setItem('selectedCareer', JSON.stringify(foundObj));
|
||||
let rawCips = Array.isArray(foundObj.cip_code) ? foundObj.cip_code : [foundObj.cip_code];
|
||||
|
||||
const cleanedCips = rawCips.map((code) => {
|
||||
@ -96,15 +100,23 @@ function EducationalProgramsPage() {
|
||||
});
|
||||
setCipCodes(cleanedCips);
|
||||
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)
|
||||
const handleSelectSchool = (school) => {
|
||||
const proceed = window.confirm(
|
||||
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
|
||||
);
|
||||
if (proceed) {
|
||||
navigate('/milestone-tracker', { state: { selectedSchool: school } });
|
||||
navigate('/career-roadmap', { state: { selectedSchool: school } });
|
||||
}
|
||||
};
|
||||
|
||||
@ -147,50 +159,7 @@ function EducationalProgramsPage() {
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -201,12 +170,8 @@ useEffect(() => {
|
||||
}
|
||||
|
||||
if (!allKsaData.length) {
|
||||
// We haven't loaded local data yet (or it failed to load).
|
||||
// We can either wait, or directly try fallback now.
|
||||
// For example:
|
||||
fetchAiKsaFallback(socCode, careerTitle);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we have local data loaded:
|
||||
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
|
||||
@ -230,28 +195,46 @@ useEffect(() => {
|
||||
|
||||
// Load user profile
|
||||
useEffect(() => {
|
||||
async function loadUserProfile() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.warn('No token found, cannot load user-profile.');
|
||||
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);
|
||||
async function loadUserProfile() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.warn('No token found, cannot load user-profile.');
|
||||
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);
|
||||
}
|
||||
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
|
||||
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 (
|
||||
<div className="p-4">
|
||||
{/* KSA Section */}
|
||||
{renderKsaSection()}
|
||||
<div className="p-4">
|
||||
{/* 1. If user is allowed to search for a career again, show CareerSearch */}
|
||||
{showSearch && (
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold mb-2">Find a Career</h2>
|
||||
<CareerSearch onCareerSelected={handleCareerSelected} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* School List */}
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Schools for: {careerTitle || 'Unknown Career'}
|
||||
</h2>
|
||||
{/* 2. If the user has already selected a career and we're not showing search */}
|
||||
{selectedCareer && !showSearch && (
|
||||
<div className="mb-4">
|
||||
<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 */}
|
||||
<div className="mb-4 flex flex-wrap items-center space-x-4">
|
||||
<label className="text-sm text-gray-600">
|
||||
Sort:
|
||||
<select
|
||||
className="ml-2 rounded border px-2 py-1 text-sm"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
>
|
||||
<option value="tuition">Tuition</option>
|
||||
<option value="distance">Distance</option>
|
||||
</select>
|
||||
</label>
|
||||
{/* If we’re loading or errored out, handle that up front */}
|
||||
{loading && (
|
||||
<div>Loading skills and educational programs...</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-red-600">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{/* 3. Display CIP-based data only if we have CIP codes (means we have a known career) */}
|
||||
{cipCodes.length > 0 ? (
|
||||
<>
|
||||
{/* KSA section */}
|
||||
{renderKsaSection()}
|
||||
|
||||
<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>
|
||||
{/* School List */}
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Schools for: {careerTitle || 'Unknown Career'}
|
||||
</h2>
|
||||
|
||||
{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>
|
||||
{/* Filter Bar */}
|
||||
<div className="mb-4 flex flex-wrap items-center space-x-4">
|
||||
<label className="text-sm text-gray-600">
|
||||
Sort:
|
||||
<select
|
||||
className="ml-2 rounded border px-2 py-1 text-sm"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
>
|
||||
<option value="tuition">Tuition</option>
|
||||
<option value="distance">Distance</option>
|
||||
</select>
|
||||
</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 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;
|
||||
|
210
src/components/FinancialAidWizard.js
Normal file
210
src/components/FinancialAidWizard.js
Normal 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>I’m 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 approximation—final 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;
|
@ -1,6 +1,6 @@
|
||||
// CollegeOnboarding.js
|
||||
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 }) {
|
||||
// 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 [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 {
|
||||
college_enrollment_status = '',
|
||||
selected_school = '',
|
||||
selected_program = '',
|
||||
program_type = '',
|
||||
academic_calendar = 'semester',
|
||||
academic_calendar = 'semester', // <-- ACADEMIC CALENDAR
|
||||
annual_financial_aid = '',
|
||||
is_online = false,
|
||||
existing_college_debt = '',
|
||||
@ -34,7 +37,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
tuition_paid = '',
|
||||
} = 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 [autoTuition, setAutoTuition] = useState(0);
|
||||
|
||||
@ -67,7 +70,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
setManualProgramLength(e.target.value);
|
||||
};
|
||||
|
||||
// CIP fetch
|
||||
// Fetch CIP data (example)
|
||||
useEffect(() => {
|
||||
async function fetchCipData() {
|
||||
try {
|
||||
@ -85,7 +88,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
fetchCipData();
|
||||
}, []);
|
||||
|
||||
// iPEDS fetch
|
||||
// Fetch iPEDS data (example)
|
||||
useEffect(() => {
|
||||
async function fetchIpedsData() {
|
||||
try {
|
||||
@ -104,7 +107,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
fetchIpedsData();
|
||||
}, []);
|
||||
|
||||
// handleSchoolChange
|
||||
// Handle school name input
|
||||
const handleSchoolChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setData(prev => ({
|
||||
@ -167,7 +170,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
setAutoProgramLength('0.00');
|
||||
};
|
||||
|
||||
// once we have school+program, load possible program types
|
||||
// once we have school + program, load possible program types
|
||||
useEffect(() => {
|
||||
if (!selected_program || !selected_school || !schoolData.length) return;
|
||||
const possibleTypes = schoolData
|
||||
@ -184,8 +187,11 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
if (!icTuitionData.length) 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;
|
||||
|
||||
const unitId = found.UNITID;
|
||||
if (!unitId) return;
|
||||
|
||||
@ -283,6 +289,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
nextStep();
|
||||
};
|
||||
|
||||
// displayedTuition / displayedProgramLength
|
||||
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
||||
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 === 'prospective_student') ? (
|
||||
<div className="space-y-4">
|
||||
{/* In District, In State, Online, etc. */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -337,6 +345,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
<label className="font-medium">Defer Loan Payments until Graduation?</label>
|
||||
</div>
|
||||
|
||||
{/* School / Program */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">School Name*</label>
|
||||
<input
|
||||
@ -394,6 +403,23 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</select>
|
||||
</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 === 'First Professional Degree' ||
|
||||
program_type === 'Doctoral Degree') && (
|
||||
@ -422,6 +448,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tuition (auto or override) */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Yearly Tuition</label>
|
||||
<input
|
||||
@ -433,16 +460,26 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Annual Financial Aid with "Need Help?" Wizard button */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">(Estimated) Annual Financial Aid</label>
|
||||
<input
|
||||
type="number"
|
||||
name="annual_financial_aid"
|
||||
value={annual_financial_aid}
|
||||
onChange={handleParentFieldChange}
|
||||
placeholder="e.g. 2000"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
name="annual_financial_aid"
|
||||
value={annual_financial_aid}
|
||||
onChange={handleParentFieldChange}
|
||||
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 className="space-y-1">
|
||||
@ -457,9 +494,9 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* If "currently_enrolled" show Hours Completed + Program Length */}
|
||||
{college_enrollment_status === 'currently_enrolled' && (
|
||||
<>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Hours Completed</label>
|
||||
<input
|
||||
@ -563,6 +600,22 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
Finish Onboarding
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,21 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
function SignIn({ setIsAuthenticated, setUser }) {
|
||||
const navigate = useNavigate();
|
||||
const usernameRef = useRef('');
|
||||
const passwordRef = useRef('');
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
@ -68,7 +78,13 @@ function SignIn({ setIsAuthenticated, setUser }) {
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<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">
|
||||
<h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1>
|
||||
|
||||
|
32
src/components/ui/modal.js
Normal file
32
src/components/ui/modal.js
Normal 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;
|
@ -142,8 +142,8 @@ export function simulateFinancialProjection(userProfile) {
|
||||
interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO'
|
||||
flatAnnualRate = 0.06, // 6% default if using FLAT
|
||||
monthlyReturnSamples = [], // if using historical-based random sampling
|
||||
randomRangeMin = -0.03, // if using a random range approach
|
||||
randomRangeMax = 0.08,
|
||||
randomRangeMin = -0.02, // if using a random range approach
|
||||
randomRangeMax = 0.02,
|
||||
|
||||
} = userProfile;
|
||||
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user