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

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

View File

@ -31,8 +31,8 @@ import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import 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,7 +42,7 @@ function App() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Define premium paths (including /enhancing and /retirement)
// Premium paths (including /enhancing, /retirement)
const premiumPaths = [
'/career-roadmap',
'/paywall',
@ -52,27 +52,32 @@ function App() {
'/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"
@ -183,24 +196,24 @@ 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('/enhancing')}
>
Enhancing Your Career
{!canAccessPremium && (
<span className="text-xs ml-1 text-gray-600">(Premium)</span>
<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"
@ -208,32 +221,24 @@ function App() {
>
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"
@ -247,31 +252,26 @@ 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('/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>
@ -279,33 +279,28 @@ 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
min-w-0
max-w-[90px]
truncate
`}
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>
{/* 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"
@ -314,23 +309,22 @@ function App() {
</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>
);

View File

@ -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
@ -893,10 +893,11 @@ function CareerExplorer() {
</option>
))}
</select>
<Button
onClick={() => {
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;
}
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
@ -905,7 +906,14 @@ function CareerExplorer() {
>
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>
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
<CareerSuggestions

View File

@ -171,9 +171,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</thead>
<tbody>
<tr>
<td className="px-3 py-2 border-b font-semibold">
Current Jobs
</td>
<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()}
@ -186,9 +184,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)}
</tr>
<tr>
<td className="px-3 py-2 border-b font-semibold">
Jobs in 10 yrs
</td>
<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()}
@ -214,9 +210,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)}
</tr>
<tr>
<td className="px-3 py-2 border-b font-semibold">
Annual Openings
</td>
<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()}
@ -230,7 +224,19 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</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,
its possible that some tasks or responsibilities could be automated
over time.
</p>
)}
</div>
</div>
</div>
</div>

View File

@ -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 dont see your exact title, please choose the closest matchthis 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 dont see your exact title, please choose the closest matchthis helps us provide the most accurate guidance.</h5>
<input
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}

View File

@ -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(
'Youre about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
);
if (proceed) {
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;
}
}
// Otherwise, we have local data loaded:
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
@ -240,18 +205,36 @@ useEffect(() => {
const res = await fetch('/api/user-profile', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
throw new Error('Failed to fetch user profile');
}
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);
}
// 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,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 (
<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 were 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()}
{/* School List */}
@ -575,11 +639,10 @@ useEffect(() => {
)}
</div>
{/* Display the sorted/filtered schools */}
<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>
@ -611,8 +674,18 @@ useEffect(() => {
);
})}
</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>
);
}
export default EducationalProgramsPage;

View File

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

View File

@ -1,6 +1,6 @@
// CollegeOnboarding.js
import React, { useState, useEffect } from 'react';
import 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,8 +460,10 @@ 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>
<div className="flex space-x-2">
<input
type="number"
name="annual_financial_aid"
@ -443,6 +472,14 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
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>
);
}

View File

@ -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>

View File

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

View File

@ -142,8 +142,8 @@ export function simulateFinancialProjection(userProfile) {
interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO'
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;

Binary file not shown.