From 0c8cd4a969d2aaa5e5cee1bb5493b64832a8d3fb Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 2 Jun 2025 16:06:57 +0000 Subject: [PATCH] State save for EducationalProgramsPage to keep it from wiping the career on refresh. And added FinancialAidWizard. --- src/App.js | 342 ++++++++-------- src/components/CareerExplorer.js | 86 ++-- src/components/CareerModal.js | 162 ++++---- src/components/CareerSearch.js | 4 +- src/components/EducationalProgramsPage.js | 385 +++++++++++------- src/components/FinancialAidWizard.js | 210 ++++++++++ .../PremiumOnboarding/CollegeOnboarding.js | 91 ++++- src/components/SignIn.js | 20 +- src/components/ui/modal.js | 32 ++ src/utils/FinancialProjectionService.js | 4 +- user_profile.db | Bin 151552 -> 151552 bytes 11 files changed, 862 insertions(+), 474 deletions(-) create mode 100644 src/components/FinancialAidWizard.js create mode 100644 src/components/ui/modal.js diff --git a/src/App.js b/src/App.js index d20dab9..1717726 100644 --- a/src/App.js +++ b/src/App.js @@ -31,8 +31,8 @@ import Paywall from './components/Paywall.js'; import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js'; import MultiScenarioView from './components/MultiScenarioView.js'; -// 1) Import your ResumeRewrite component -import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed +// Import your ResumeRewrite component +import ResumeRewrite from './components/ResumeRewrite.js'; // adjust path if needed function App() { const navigate = useNavigate(); @@ -42,37 +42,42 @@ function App() { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); - // Define premium paths (including /enhancing and /retirement) - const premiumPaths = [ - '/career-roadmap', - '/paywall', - '/financial-profile', - '/multi-scenario', - '/premium-onboarding', - '/enhancing', - '/retirement', - '/resume-optimizer', -]; + // Premium paths (including /enhancing, /retirement) + const premiumPaths = [ + '/career-roadmap', + '/paywall', + '/financial-profile', + '/multi-scenario', + '/premium-onboarding', + '/enhancing', + '/retirement', + '/resume-optimizer', + ]; const showPremiumCTA = !premiumPaths.includes(location.pathname); // We'll define "canAccessPremium" for user const canAccessPremium = user?.is_premium || user?.is_pro_premium; - // Rehydrate user if there's a token + // ==================== + // Single Rehydrate UseEffect + // ==================== useEffect(() => { const token = localStorage.getItem('token'); + + // No token? -> not authenticated if (!token) { setIsLoading(false); return; } - // Validate token/fetch user profile + // Token exists, let's check with the server fetch('https://dev1.aptivaai.com/api/user-profile', { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => { if (!res.ok) { - throw new Error('Token invalid/expired'); + // For example, 401 => invalid or expired token + throw new Error('Token invalid on server side'); } return res.json(); }) @@ -82,14 +87,19 @@ function App() { }) .catch((err) => { console.error(err); + // Server says token invalid -> remove from localStorage localStorage.removeItem('token'); + // Force user to sign in again + navigate('/signin?session=expired'); }) .finally(() => { setIsLoading(false); }); - }, []); + }, [navigate]); - // Logout + // ==================== + // Logout Handler + // ==================== const handleLogout = () => { localStorage.removeItem('token'); localStorage.removeItem('careerSuggestionsCache'); @@ -98,6 +108,7 @@ function App() { navigate('/signin'); }; + // If we're still verifying the token, show a loading indicator if (isLoading) { return (
@@ -106,6 +117,9 @@ function App() { ); } + // ==================== + // Main App Render + // ==================== return (
{/* Header */} @@ -122,15 +136,15 @@ function App() {
- {/* Only Educational Programs as submenu */} {/* 3) Enhancing Your Career (Premium) */} -
- +
+ - Enhancing Your Career - {!canAccessPremium && ( - (Premium) - )} - - - {/* SUBMENU */} -
- - Career Roadmap - - - {/* Optimize Resume */} - - Optimize Resume - - - {/* Networking (placeholder) */} - - Networking - - - {/* Interview Help (placeholder) */} - - Interview Help - - - {/* Job Search (placeholder) */} - - Job Search - -
+ Career Roadmap + + + Optimize Resume + + + Networking + + + Interview Help + + + Job Search +
+
{/* 4) Retirement Planning (Premium) */}
- {/* Example retirement submenu item */} - {/* - Financial Tools - */} - {/* Add more retirement submenu items here if needed */} + {/* Add more retirement submenu items if needed */}
{/* 5) Profile */} -
- +
+ - Profile - - - {/* DROPDOWN MENU FOR PROFILE */} -
- {/* Account (Links to UserProfile.js) */} - - Account - - - {/* Financial Profile (Links to FinancialProfileForm.js) */} - - Financial Profile - -
+ Account + + + Financial Profile +
- +
{/* LOGOUT + UPGRADE BUTTONS */}
{showPremiumCTA && !canAccessPremium && (
); diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index 4a2e063..1b133eb 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -748,8 +748,8 @@ function CareerExplorer() { /> )} -
-

+
+

Explore Careers - use these tools to find your best fit

- + + + + + + + {/* Legend container with less internal gap, plus a left margin */} +
+ ⚠️ + = Limited Data for this career path +
+
- - -

{/* Now we pass the *filteredCareers* into the CareerSuggestions component */} {/* Economic Projections */} -
-

Economic Projections

- - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - -
- {careerDetails.economicProjections.state.area} - National
- Current Jobs - - {careerDetails.economicProjections.state.base.toLocaleString()} - - {careerDetails.economicProjections.national.base.toLocaleString()} -
- Jobs in 10 yrs - - {careerDetails.economicProjections.state.projection.toLocaleString()} - - {careerDetails.economicProjections.national.projection.toLocaleString()} -
Growth % - {careerDetails.economicProjections.state.percentChange}% - - {careerDetails.economicProjections.national.percentChange}% -
- Annual Openings - - {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} - - {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} -
-
+
+

Economic Projections

+ + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + +
+ {careerDetails.economicProjections.state.area} + National
Current Jobs + {careerDetails.economicProjections.state.base.toLocaleString()} + + {careerDetails.economicProjections.national.base.toLocaleString()} +
Jobs in 10 yrs + {careerDetails.economicProjections.state.projection.toLocaleString()} + + {careerDetails.economicProjections.national.projection.toLocaleString()} +
Growth % + {careerDetails.economicProjections.state.percentChange}% + + {careerDetails.economicProjections.national.percentChange}% +
Annual Openings + {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} + + {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} +
+ + {/* Conditional disclaimer when AI risk is Moderate or High */} + {(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && ( +

+ Note: These 10-year projections may change if AI-driven + tools significantly affect {careerDetails.title} tasks. + With a {aiRisk.riskLevel.toLowerCase()} AI risk, + it’s possible that some tasks or responsibilities could be automated + over time. +

+ )} +
+
diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js index b009162..6b58d77 100644 --- a/src/components/CareerSearch.js +++ b/src/components/CareerSearch.js @@ -64,8 +64,8 @@ const CareerSearch = ({ onCareerSelected }) => { return (
-

Search for Career (select from suggestions)

-

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.

+

Search for Career (select from suggestions)

+
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.
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

+
+

Are you considered a dependent or independent student?

+ + +
+ +
+ + +
+ +
+ +
+
+ )} + + {step === 2 && ( +
+

Military / Veteran?

+
+ setIsVeteran(e.target.checked)} + /> + +
+ +

Add Scholarships / Grants

+

+ Have a known scholarship, state grant, or other funding? +

+
+ {scholarships.map((sch, i) => ( +
+ updateScholarship(i, 'name', e.target.value)} + className="border p-2 rounded w-1/2" + /> + updateScholarship(i, 'amount', e.target.value)} + className="border p-2 rounded w-1/3" + /> + {/* Remove scholarship button */} + +
+ ))} +
+ + + +
+ + +
+
+ )} + + {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 */}
+ {/* Academic Calendar (just re-added) */} +
+ + +
+ + {/* 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) */}
+ {/* 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' && ( <> -
+ + {/* 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 ( +
+ {showSessionExpiredMsg && ( +
+ Your session has expired. Please sign in again. +
+ )}

Sign In

diff --git a/src/components/ui/modal.js b/src/components/ui/modal.js new file mode 100644 index 0000000..d8712de --- /dev/null +++ b/src/components/ui/modal.js @@ -0,0 +1,32 @@ +// Modal.js +import React from 'react'; +import { Button } from './button.js'; + +function Modal({ children, onClose }) { + return ( +
+ {/* This div is the actual "modal" dialog. + We stop click propagation so a click *inside* doesn't close it. */} +
e.stopPropagation()} + > + +
+ {children} +
+
+
+ ); +} + +export default Modal; diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index fc3bb81..9449fa4 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -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; diff --git a/user_profile.db b/user_profile.db index 036dddf0cf43790af6bdd08d1026c7566f8bfa23..a66615ed561a31b9764b4581d310e04a6729d9e0 100644 GIT binary patch delta 1390 zcmY+E&x;&I6vs20u&~*|_K-ke5_}-yVP}S!Stq!A+ORq3A%b_s)KtIiF1Na>wtn<( zM`W_-3K0*65C(Eez^jKqBzW`UQNcgJKft5-i53i@B7r;eY|$} z@!HqD&a?YhcRSCHZ+~~Y^Uc3kyV=3H>|u70eV_fBeVhH3{gwTgJ<0B6zhs|2J$^sC zvVOkT8BYh}$<|gkK5uRfwnpRe`ty6)$|tXOFRVT~_sUP*$y&4e=;`s7tCvnb=w9qi zzxZ(D@@F4yp1%0tzP>cS#f0+o0j6?r!5D2l9YIjYS|U=Fx#jMqF^i;6dMz#}@W3haYnO+{_M zSk2Knp^84_bTTcvlG(Hpb>^ZFo=K>+r?ny6L??>YLXJl21{_icJfkLW4;kh?S`w5D z&$SG#K3$`Q0(D`TDM_`I&6yltm<&-Fu)Jo9lX;K4R9fmnpc}$1){sd@`N5&^B?z@= z8HE|Rk9r*XU{Ys~E^6O^Jqqompnzgl&ajkDt1(&;z8Nn57_RIre)zj{dEN(KBGnUQ z14>bpV1?TUy$42n%1}@^DFS*A>L3+bHWN~K64|ydEi&%_E3`%QLj)}6x}0bp5WJ4v zfa}UgJcGMb2FBymXX6Zw6REU4TP7qr2p_GQvRUi)-m3Z}%JbTCa% zMt-;#^>S32I;z6aT@a27x={*u&*}t1sS5H%J5>Z$;7fH{BQ05Uzth3RSO0XbEXKcg z-nbScN)wK;^Ed>d@<7x!#Mxr!&rWY1x0h&%%8*FV3O?4eC1??4)sAuq@90CME0stE z8jFkhrvHza16}#t4XD5pn>3l!J!VUmqy&oxL6DxEGE^+$P-|Hev-B?!n?+UhjSi8` YBt3`7;2G*h+tas+y62Pr%FnO=2QYi~WB>pF delta 76 zcmV-S0JHyqpb3DW36L8BvXLA^0kVN$wO|3H{}>7ni2x6k54jKB53LXB5A_el51|i< i57DuKZ4bAJ4+47-2m%Z;01nLzGO>Xs47V~Y0?s2NoEi%N