setSearchInput(e.target.value)}
diff --git a/src/components/EducationalProgramsPage.js b/src/components/EducationalProgramsPage.js
index a2e3012..911f590 100644
--- a/src/components/EducationalProgramsPage.js
+++ b/src/components/EducationalProgramsPage.js
@@ -84,10 +84,14 @@ function EducationalProgramsPage() {
const [maxDistance, setMaxDistance] = useState(100);
const [inStateOnly, setInStateOnly] = useState(false);
const [careerTitle, setCareerTitle] = useState(location.state?.careerTitle || '');
+ const [selectedCareer, setSelectedCareer] = useState(location.state?.foundObj || '');
+ const [showSearch, setShowSearch] = useState(true);
// If user picks a new career from CareerSearch
const handleCareerSelected = (foundObj) => {
setCareerTitle(foundObj.title || '');
+ setSelectedCareer(foundObj);
+ localStorage.setItem('selectedCareer', JSON.stringify(foundObj));
let rawCips = Array.isArray(foundObj.cip_code) ? foundObj.cip_code : [foundObj.cip_code];
const cleanedCips = rawCips.map((code) => {
@@ -96,15 +100,23 @@ function EducationalProgramsPage() {
});
setCipCodes(cleanedCips);
setsocCode(foundObj.soc_code);
+ setShowSearch(false);
};
+ function handleChangeCareer() {
+ // Optionally remove from localStorage if the user is truly 'unselecting' it
+ localStorage.removeItem('selectedCareer');
+ setSelectedCareer(null);
+ setShowSearch(true);
+ }
+
// Fixed handleSelectSchool (removed extra brace)
const handleSelectSchool = (school) => {
const proceed = window.confirm(
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
);
if (proceed) {
- navigate('/milestone-tracker', { state: { selectedSchool: school } });
+ navigate('/career-roadmap', { state: { selectedSchool: school } });
}
};
@@ -147,50 +159,7 @@ function EducationalProgramsPage() {
loadKsaData();
}, []);
- async function fetchAiKsaFallback(socCode, careerTitle) {
- // Optionally show a “loading” indicator
- setLoadingKsa(true);
- setKsaError(null);
-
- try {
- const token = localStorage.getItem('token');
- if (!token) {
- throw new Error('No auth token found; cannot fetch AI-based KSAs.');
- }
-
- // Call the new endpoint in server3.js
- const resp = await fetch(
- `/api/premium/ksa/${socCode}?careerTitle=${encodeURIComponent(careerTitle)}`,
- {
- headers: {
- Authorization: `Bearer ${token}`
- }
- }
- );
-
- if (!resp.ok) {
- throw new Error(`AI KSA endpoint returned status ${resp.status}`);
- }
-
- const json = await resp.json();
- // Expect shape: { source: 'chatgpt' | 'db' | 'local', data: { knowledge, skills, abilities } }
-
- // The arrays from server may already be in the “IM/LV” format
- // so we can combine them into one array for display:
- const finalKsa = [...json.data.knowledge, ...json.data.skills, ...json.data.abilities];
- finalKsa.forEach(item => {
- item.onetSocCode = socCode;
- });
- const combined = combineIMandLV(finalKsa);
- setKsaForCareer(combined);
- } catch (err) {
- console.error('Error fetching AI-based KSAs:', err);
- setKsaError('Could not load AI-based KSAs. Please try again later.');
- setKsaForCareer([]);
- } finally {
- setLoadingKsa(false);
- }
-}
+
// Filter: only IM >=3, then combine IM+LV
useEffect(() => {
@@ -201,12 +170,8 @@ useEffect(() => {
}
if (!allKsaData.length) {
- // We haven't loaded local data yet (or it failed to load).
- // We can either wait, or directly try fallback now.
- // For example:
- fetchAiKsaFallback(socCode, careerTitle);
- return;
- }
+ return;
+}
// Otherwise, we have local data loaded:
let filtered = allKsaData.filter((r) => r.onetSocCode === socCode);
@@ -230,28 +195,46 @@ useEffect(() => {
// Load user profile
useEffect(() => {
- async function loadUserProfile() {
- try {
- const token = localStorage.getItem('token');
- if (!token) {
- console.warn('No token found, cannot load user-profile.');
- return;
- }
- const res = await fetch('/api/user-profile', {
- headers: { Authorization: `Bearer ${token}` },
- });
- if (!res.ok) {
- throw new Error('Failed to fetch user profile');
- }
- const data = await res.json();
- setUserZip(data.zipcode || '');
- setUserState(data.state || '');
- } catch (err) {
- console.error('Error loading user profile:', err);
+ async function loadUserProfile() {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ console.warn('No token found, cannot load user-profile.');
+ return;
}
+ const res = await fetch('/api/user-profile', {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!res.ok) throw new Error('Failed to fetch user profile');
+ const data = await res.json();
+ setUserZip(data.zipcode || '');
+ setUserState(data.state || '');
+ } catch (err) {
+ console.error('Error loading user profile:', err);
}
- loadUserProfile();
- }, []);
+
+ // Then handle localStorage:
+ const stored = localStorage.getItem('selectedCareer');
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ setSelectedCareer(parsed);
+ setCareerTitle(parsed.title || '');
+
+ // Re-set CIP code logic (like in handleCareerSelected)
+ let rawCips = Array.isArray(parsed.cip_code)
+ ? parsed.cip_code
+ : [parsed.cip_code];
+ const cleanedCips = rawCips.map((code) => code.toString().replace('.', '').slice(0, 4));
+ setCipCodes(cleanedCips);
+
+ setsocCode(parsed.soc_code);
+
+ setShowSearch(false);
+ }
+ }
+ loadUserProfile();
+}, []);
+
// Fetch schools once CIP codes are set
useEffect(() => {
@@ -519,100 +502,190 @@ useEffect(() => {
);
}
+ async function fetchAiKsaFallback(socCode, careerTitle) {
+ // Optionally show a “loading” indicator
+ setLoadingKsa(true);
+ setKsaError(null);
+
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('No auth token found; cannot fetch AI-based KSAs.');
+ }
+
+ // Call the new endpoint in server3.js
+ const resp = await fetch(
+ `/api/premium/ksa/${socCode}?careerTitle=${encodeURIComponent(careerTitle)}`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ }
+ );
+
+ if (!resp.ok) {
+ throw new Error(`AI KSA endpoint returned status ${resp.status}`);
+ }
+
+ const json = await resp.json();
+ // Expect shape: { source: 'chatgpt' | 'db' | 'local', data: { knowledge, skills, abilities } }
+
+ // The arrays from server may already be in the “IM/LV” format
+ // so we can combine them into one array for display:
+ const finalKsa = [...json.data.knowledge, ...json.data.skills, ...json.data.abilities];
+ finalKsa.forEach(item => {
+ item.onetSocCode = socCode;
+ });
+ const combined = combineIMandLV(finalKsa);
+ setKsaForCareer(combined);
+ } catch (err) {
+ console.error('Error fetching AI-based KSAs:', err);
+ setKsaError('Could not load AI-based KSAs. Please try again later.');
+ setKsaForCareer([]);
+ } finally {
+ setLoadingKsa(false);
+ }
+}
+
return (
-
- {/* KSA Section */}
- {renderKsaSection()}
+
+ {/* 1. If user is allowed to search for a career again, show CareerSearch */}
+ {showSearch && (
+
+
Find a Career
+
+
+ )}
- {/* School List */}
-
- Schools for: {careerTitle || 'Unknown Career'}
-
+ {/* 2. If the user has already selected a career and we're not showing search */}
+ {selectedCareer && !showSearch && (
+
+
+ Currently selected: {selectedCareer.title}
+
+
+
+ )}
- {/* Filter Bar */}
-
-
+ {/* If we’re loading or errored out, handle that up front */}
+ {loading && (
+
Loading skills and educational programs...
+ )}
+ {error && (
+
+ Error: {error}
+
+ )}
-
+ {/* 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 */}
+
+ Schools for: {careerTitle || 'Unknown Career'}
+
- {userState && (
-
+);
-
- {filteredAndSortedSchools.map((school, idx) => {
- // 1) Ensure the website has a protocol:
- const displayWebsite = ensureHttp(school['Website']);
-
- return (
-
-
- {school['Website'] ? (
-
- {school['INSTNM'] || 'Unnamed School'}
-
- ) : (
- school['INSTNM'] || 'Unnamed School'
- )}
-
-
Degree Type: {school['CREDDESC'] || 'N/A'}
-
In-State Tuition: ${school['In_state cost'] || 'N/A'}
-
Out-of-State Tuition: ${school['Out_state cost'] || 'N/A'}
-
Distance: {school.distance !== null ? `${school.distance} mi` : 'N/A'}
-
-
-
- );
- })}
-
-
- );
}
export default EducationalProgramsPage;
diff --git a/src/components/FinancialAidWizard.js b/src/components/FinancialAidWizard.js
new file mode 100644
index 0000000..d88b0c5
--- /dev/null
+++ b/src/components/FinancialAidWizard.js
@@ -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 (
+
+ {step === 1 && (
+
+
Basic Info
+
+
+
+ Approx. Income Range
+
+
+
+
+
+
+
+ )}
+
+ {step === 2 && (
+
+
Military / Veteran?
+
+ setIsVeteran(e.target.checked)}
+ />
+ I’m a veteran or have GI Bill benefits
+
+
+
Add Scholarships / Grants
+
+ Have a known scholarship, state grant, or other funding?
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {step === 3 && (
+
+
Your Estimated Financial Aid
+
+ Approx. Total Aid:{" "}
+ {`$${calculateAid().toLocaleString()}`}
+
+
+
+
Important Disclaimers:
+
+ - This is an approximation—final amounts require FAFSA and official award letters.
+ - Scholarship amounts can vary by school, state, or specific qualifications.
+ - If you have special circumstances (assets, unique tax situations), your actual aid may differ.
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default FinancialAidWizard;
diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js
index b8b8198..93771bf 100644
--- a/src/components/PremiumOnboarding/CollegeOnboarding.js
+++ b/src/components/PremiumOnboarding/CollegeOnboarding.js
@@ -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') ? (
+ {/* In District, In State, Online, etc. */}
Defer Loan Payments until Graduation?
+ {/* School / Program */}
School Name*
+ {/* Academic Calendar (just re-added) */}
+
+ Academic Calendar
+
+
+
+ {/* 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
/>
+ {/* Tuition (auto or override) */}
Yearly Tuition
+ {/* Annual Financial Aid with "Need Help?" Wizard button */}
@@ -457,9 +494,9 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/>
+ {/* If "currently_enrolled" show Hours Completed + Program Length */}
{college_enrollment_status === 'currently_enrolled' && (
<>
-
Hours Completed
+
+ {/* RENDER THE MODAL WITH FINANCIAL AID WIZARD IF showAidWizard === true */}
+ {showAidWizard && (
+
setShowAidWizard(false)}>
+ {
+ // Update the annual_financial_aid with the wizard's result
+ setData(prev => ({
+ ...prev,
+ annual_financial_aid: estimate
+ }));
+ }}
+ onClose={() => setShowAidWizard(false)}
+ />
+
+ )}
);
}
diff --git a/src/components/SignIn.js b/src/components/SignIn.js
index 9bf5e25..f68aa4e 100644
--- a/src/components/SignIn.js
+++ b/src/components/SignIn.js
@@ -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 (
+