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
204
src/App.js
204
src/App.js
@ -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,7 +42,7 @@ 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',
|
||||||
@ -58,21 +58,26 @@ function App() {
|
|||||||
// 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"
|
||||||
@ -183,24 +196,24 @@ 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('/enhancing')}
|
onClick={() => navigate('/enhancing')}
|
||||||
>
|
>
|
||||||
Enhancing Your Career
|
Enhancing Your Career
|
||||||
{!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>
|
||||||
|
|
||||||
{/* SUBMENU */}
|
|
||||||
<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">
|
||||||
<Link
|
<Link
|
||||||
to="/career-roadmap"
|
to="/career-roadmap"
|
||||||
@ -208,32 +221,24 @@ function App() {
|
|||||||
>
|
>
|
||||||
Career Roadmap
|
Career Roadmap
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Optimize Resume */}
|
|
||||||
<Link
|
<Link
|
||||||
to="/resume-optimizer"
|
to="/resume-optimizer"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Optimize Resume
|
Optimize Resume
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Networking (placeholder) */}
|
|
||||||
<Link
|
<Link
|
||||||
to="/networking"
|
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"
|
||||||
>
|
>
|
||||||
Networking
|
Networking
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Interview Help (placeholder) */}
|
|
||||||
<Link
|
<Link
|
||||||
to="/interview-help"
|
to="/interview-help"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Interview Help
|
Interview Help
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Job Search (placeholder) */}
|
|
||||||
<Link
|
<Link
|
||||||
to="/job-search"
|
to="/job-search"
|
||||||
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"
|
||||||
@ -247,31 +252,26 @@ 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('/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>
|
||||||
|
|
||||||
@ -279,33 +279,28 @@ 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',
|
||||||
min-w-0
|
'min-w-0',
|
||||||
max-w-[90px]
|
'max-w-[90px]',
|
||||||
truncate
|
'truncate'
|
||||||
`}
|
)}
|
||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</Button>
|
</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">
|
<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
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Account
|
Account
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Financial Profile (Links to FinancialProfileForm.js) */}
|
|
||||||
<Link
|
<Link
|
||||||
to="/financial-profile"
|
to="/financial-profile"
|
||||||
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"
|
||||||
@ -314,23 +309,22 @@ function App() {
|
|||||||
</Link>
|
</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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
@ -893,10 +893,11 @@ function CareerExplorer() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!userProfile?.interest_inventory_answers) {
|
if (!userProfile?.interest_inventory_answers) {
|
||||||
alert('No interest inventory answers. Complete the interest inventory first!');
|
alert('Please complete the Interest Inventory to get suggestions.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
|
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
|
||||||
@ -905,7 +906,14 @@ function CareerExplorer() {
|
|||||||
>
|
>
|
||||||
Reload Career Suggestions
|
Reload Career Suggestions
|
||||||
</Button>
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
|
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
|
||||||
<CareerSuggestions
|
<CareerSuggestions
|
||||||
|
@ -171,9 +171,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</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
|
|
||||||
</td>
|
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.base.toLocaleString()}
|
{careerDetails.economicProjections.state.base.toLocaleString()}
|
||||||
@ -186,9 +184,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">
|
<td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
|
||||||
Jobs in 10 yrs
|
|
||||||
</td>
|
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
||||||
@ -214,9 +210,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">
|
<td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
|
||||||
Annual Openings
|
|
||||||
</td>
|
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
||||||
@ -230,7 +224,19 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 don’t see your exact title, please choose the closest match—this helps us provide the most accurate guidance.</h3>
|
<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
|
<input
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(e) => setSearchInput(e.target.value)}
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
|
@ -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(
|
||||||
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
|
'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) {
|
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,10 +170,6 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!allKsaData.length) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,19 +205,37 @@ useEffect(() => {
|
|||||||
const res = await fetch('/api/user-profile', {
|
const res = await fetch('/api/user-profile', {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error('Failed to fetch user profile');
|
||||||
throw new Error('Failed to fetch user profile');
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setUserZip(data.zipcode || '');
|
setUserZip(data.zipcode || '');
|
||||||
setUserState(data.state || '');
|
setUserState(data.state || '');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading user profile:', err);
|
console.error('Error loading user profile:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
loadUserProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Fetch schools once CIP codes are set
|
// Fetch schools once CIP codes are set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cipCodes.length) return;
|
if (!cipCodes.length) return;
|
||||||
@ -519,9 +502,90 @@ 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 */}
|
||||||
|
{showSearch && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Find a Career</h2>
|
||||||
|
<CareerSearch onCareerSelected={handleCareerSelected} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 3. Display CIP-based data only if we have CIP codes (means we have a known career) */}
|
||||||
|
{cipCodes.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{/* KSA section */}
|
||||||
{renderKsaSection()}
|
{renderKsaSection()}
|
||||||
|
|
||||||
{/* School List */}
|
{/* School List */}
|
||||||
@ -575,11 +639,10 @@ useEffect(() => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Display the sorted/filtered schools */}
|
||||||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
|
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
|
||||||
{filteredAndSortedSchools.map((school, idx) => {
|
{filteredAndSortedSchools.map((school, idx) => {
|
||||||
// 1) Ensure the website has a protocol:
|
|
||||||
const displayWebsite = ensureHttp(school['Website']);
|
const displayWebsite = ensureHttp(school['Website']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="rounded border p-3 text-sm">
|
<div key={idx} className="rounded border p-3 text-sm">
|
||||||
<strong>
|
<strong>
|
||||||
@ -611,8 +674,18 @@ useEffect(() => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EducationalProgramsPage;
|
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 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 => ({
|
||||||
@ -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,8 +460,10 @@ 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>
|
||||||
|
<div className="flex space-x-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="annual_financial_aid"
|
name="annual_financial_aid"
|
||||||
@ -443,6 +472,14 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
|||||||
placeholder="e.g. 2000"
|
placeholder="e.g. 2000"
|
||||||
className="w-full border rounded p-2"
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
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'
|
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;
|
||||||
|
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user