LoanRepayment reimplmementation in PreparingLanding

This commit is contained in:
Josh 2025-06-30 16:53:21 +00:00
parent a8247d63b2
commit 7b810ff2de
9 changed files with 969 additions and 214 deletions

View File

@ -30,6 +30,9 @@ import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import RetirementPlanner from './components/RetirementPlanner.js';
import ResumeRewrite from './components/ResumeRewrite.js';
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
export const ProfileCtx = React.createContext();
@ -177,7 +180,8 @@ function App() {
return (
<ProfileCtx.Provider
value={{ financialProfile, setFinancialProfile,
scenario, setScenario }}
scenario, setScenario,
user, }}
>
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
{/* Header */}
@ -348,6 +352,23 @@ function App() {
>
Financial Profile
</Link>
{canAccessPremium ? (
/* Premium users go straight to the wizard */
<Link
to="/premium-onboarding"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Premium Onboarding
</Link>
) : (
/* Free users are nudged to upgrade */
<Link
to="/paywall"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
College Planning&nbsp;<span className="text-xs">(Premium)</span>
</Link>
)}
</div>
</div>
</nav>
@ -431,6 +452,7 @@ function App() {
<Route path="/profile" element={<UserProfile />} />
<Route path="/planning" element={<PlanningLanding />} />
<Route path="/career-explorer" element={<CareerExplorer />} />
<Route path="/loan-repayment" element={<LoanRepaymentPage />}/>
<Route path="/educational-programs" element={<EducationalProgramsPage />} />
<Route path="/preparing" element={<PreparingLanding />} />

View File

@ -1,224 +1,252 @@
import React, { useState } from 'react';
import { Button } from './ui/button.js';
/**
* LoanRepayment
*
* If `schools.length === 0` we first show a quick-fill estimator
* that lets a free-tier user type a school name, choose degree type,
* and enter an annual-tuition guess. We then create a minimal
* school object and push it into `schools`, after which the full
* repayment form (your original code) appears.
*/
function LoanRepayment({
schools,
/* ORIGINAL PROPS */
schools = [],
salaryData,
setResults,
setLoading,
setPersistedROI,
programLength,
}) {
const [expectedSalary, setExpectedSalary] = useState(0);
const [tuitionType, setTuitionType] = useState('inState'); // Tuition type: inState or outOfState
const [interestRate, setInterestRate] = useState(5.5); // Interest rate
const [loanTerm, setLoanTerm] = useState(10); // Loan term in years
const [extraPayment, setExtraPayment] = useState(0); // Extra monthly payment
const [currentSalary, setCurrentSalary] = useState(0); // Current salary input
const [error, setError] = useState(null);
/* NEW: parent must hand us a setter so we can inject the stub */
setSchools,
}) {
/* ------------------------- quick-fill state ------------------------- */
const [scratch, setScratch] = useState({
school: '',
programType: '',
tuition: '',
});
/* ------------------------- calculator state ------------------------ */
const [expectedSalary, setExpectedSalary] = useState(0);
const [tuitionType, setTuitionType] = useState('inState');
const [interestRate, setInterestRate] = useState(5.5);
const [loanTerm, setLoanTerm] = useState(10);
const [extraPayment, setExtraPayment] = useState(0);
const [currentSalary, setCurrentSalary] = useState(0);
const [error, setError] = useState(null);
/* ------------------------- validation ------------------------------ */
const validateInputs = () => {
if (!schools || schools.length === 0) {
setError('School data is missing. Loan calculations cannot proceed.');
return false;
}
if (isNaN(interestRate) || interestRate <= 0) {
setError('Interest rate must be a valid number greater than 0.');
return false;
}
if (isNaN(loanTerm) || loanTerm <= 0) {
setError('Loan term must be a valid number greater than 0.');
return false;
}
if (isNaN(extraPayment) || extraPayment < 0) {
setError('Extra monthly payment cannot be negative.');
return false;
}
if (isNaN(currentSalary) || currentSalary < 0) {
setError('Current salary must be a valid number and cannot be negative.');
return false;
}
if (isNaN(expectedSalary) || expectedSalary < 0) {
setError('Expected salary must be a valid number and cannot be negative.');
return false;
}
if (!schools?.length) { setError('Missing school data.'); return false; }
if (isNaN(interestRate)||interestRate<=0) { setError('Interest rate > 0'); return false; }
if (isNaN(loanTerm)||loanTerm<=0) { setError('Loan term > 0'); return false; }
if (isNaN(extraPayment)||extraPayment<0) { setError('Extra pmt ≥ 0'); return false; }
if (isNaN(currentSalary)||currentSalary<0){ setError('Current salary ≥0');return false; }
if (isNaN(expectedSalary)||expectedSalary<0){setError('Expected salary ≥0');return false;}
setError(null);
return true;
};
/* ------------------------- main calculation ----------------------- */
const calculateLoanDetails = () => {
if (!validateInputs()) return;
setLoading(true);
setLoading?.(true);
const results = schools.map((school) => {
const programLength = Number(school.programLength);
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
let totalTuition = 0;
let undergraduateYears = 0;
let graduateYears = 0;
// ✅ Handle Associates (2 years total, all undergrad)
if (school.degreeType.includes("Associate")) {
undergraduateYears = 2;
graduateYears = 0;
}
// ✅ Handle Bachelor's (4 years total, all undergrad)
else if (school.degreeType.includes("Bachelor")) {
undergraduateYears = 4;
graduateYears = 0;
/* your existing repayment logic — unchanged */
const programLen = Number(school.programLength);
const tuition = tuitionType === 'inState' ? school.inState : school.outOfState;
let ugYears=0, gradYears=0;
if (school.degreeType.includes('Associate')) ugYears=2;
else if (school.degreeType.includes('Bachelor')) ugYears=4;
else if (school.degreeType.includes('Master')) { ugYears=4; gradYears=2; }
else if (school.degreeType.includes('First Professional')||school.degreeType.includes('Doctoral'))
{ ugYears=4; gradYears=4; }
else if (school.degreeType.includes('Certificate')) ugYears=1;
else { ugYears=Math.min(programLen,4); gradYears=Math.max(programLen-4,0); }
let totalTuition = ugYears*tuition;
if (gradYears>0) {
const gradTuit = tuitionType==='inState'?school.inStateGraduate:school.outStateGraduate;
totalTuition += gradYears*gradTuit;
}
// ✅ Handle Master's (4 undergrad + 2 graduate)
else if (school.degreeType.includes("Master")) {
undergraduateYears = 4;
graduateYears = 2;
const r = Number(interestRate)/12/100;
const n = Number(loanTerm)*12;
const pmtMin = totalTuition * (r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1);
const pmt = Number(pmtMin)+Number(extraPayment);
let bal=totalTuition, months=0;
while (bal>0 && months<n*2){
months++;
const interest = bal*r;
bal -= Math.max(pmt - interest,0);
}
// ✅ Handle First Professional & Doctoral (4 undergrad + 4 grad)
else if (school.degreeType.includes("First Professional") || school.degreeType.includes("Doctoral")) {
undergraduateYears = 4;
graduateYears = 4;
}
// ✅ Handle Certificate (default 1 year undergraduate)
else if (school.degreeType.includes("Certificate")) {
undergraduateYears = 1;
graduateYears = 0;
}
// ✅ Default fallback
else {
undergraduateYears = Math.min(programLength, 4);
graduateYears = Math.max(programLength - 4, 0);
}
totalTuition += undergraduateYears * tuition;
if (graduateYears > 0) {
const gradTuition = tuitionType === 'inState'
? school.inStateGraduate
: school.outStateGraduate;
totalTuition += graduateYears * gradTuition;
}
// Loan calculations
const monthlyRate = Number(interestRate) / 12 / 100;
const loanTermMonths = Number(loanTerm) * 12;
const minimumMonthlyPayment = totalTuition * (monthlyRate * Math.pow(1 + monthlyRate, loanTermMonths)) /
(Math.pow(1 + monthlyRate, loanTermMonths) - 1);
const extraMonthlyPayment = Number(minimumMonthlyPayment) + Number(extraPayment);
let remainingBalance = totalTuition;
let monthsWithExtra = 0;
while (remainingBalance > 0) {
monthsWithExtra++;
const interest = remainingBalance * monthlyRate;
const principal = Math.max(extraMonthlyPayment - interest, 0);
remainingBalance -= principal;
if (monthsWithExtra > loanTermMonths * 2) break;
}
const totalLoanCost = extraMonthlyPayment * monthsWithExtra;
// Safe Net Gain Calculation
let salary = Number(expectedSalary) || 0;
let netGain = (-totalLoanCost).toFixed(2);
let monthlySalary = (0).toFixed(2);
if (salary > 0) {
const currentSalaryNum = Number(currentSalary) || 0;
const totalSalary = salary * loanTerm;
const currentSalaryEarnings = currentSalaryNum * loanTerm * Math.pow(1.03, loanTerm);
if (!isNaN(totalSalary) && !isNaN(currentSalaryEarnings)) {
netGain = (totalSalary - totalLoanCost - currentSalaryEarnings).toFixed(2);
monthlySalary = (salary / 12).toFixed(2);
} else {
netGain = (-totalLoanCost).toFixed(2);
monthlySalary = (0).toFixed(2);
}
} else {
netGain = (-totalLoanCost).toFixed(2);
monthlySalary = (0).toFixed(2);
}
const totalCost = pmt*months;
/* safe net-gain estimate */
const salary = Number(expectedSalary)||0;
const cur = Number(currentSalary)||0;
const netGain = salary
? (salary*loanTerm - totalCost - cur*loanTerm*Math.pow(1.03,loanTerm)).toFixed(2)
: (-totalCost).toFixed(2);
return {
...school,
totalTuition: totalTuition.toFixed(2),
monthlyPayment: minimumMonthlyPayment.toFixed(2),
totalMonthlyPayment: extraMonthlyPayment.toFixed(2),
totalLoanCost: totalLoanCost.toFixed(2),
totalTuition : totalTuition.toFixed(2),
monthlyPayment: pmtMin.toFixed(2),
totalMonthlyPayment: pmt.toFixed(2),
totalLoanCost : totalCost.toFixed(2),
netGain,
monthlySalary,
monthlySalary : (salary/12).toFixed(2),
};
});
setResults(results);
setLoading(false);
};
setResults?.(results);
setLoading?.(false);
};
/* ================================================================= */
/* QUICK-FILL PANEL (only when schools.length === 0) */
/* ================================================================= */
if (!schools || schools.length === 0) {
const ready = scratch.tuition;
return (
<div className="border rounded p-6 space-y-4 max-w-md mx-auto">
<h2 className="text-lg font-semibold text-center">
Estimate student-loan payments
</h2>
<input
className="border rounded p-2 w-full"
placeholder="School name *"
value={scratch.school}
onChange={(e)=>setScratch({...scratch,school:e.target.value})}
/>
<select
className="border rounded p-2 w-full"
value={scratch.programType}
onChange={(e)=>setScratch({...scratch,programType:e.target.value})}
>
<option value="">Degree / program type *</option>
<option>Associate's Degree</option>
<option>Bachelor's Degree</option>
<option>Master's Degree</option>
<option>Doctoral Degree</option>
<option>Graduate/Professional Certificate</option>
<option>First Professional Degree</option>
</select>
<input
type="number"
className="border rounded p-2 w-full"
placeholder="Estimated annual tuition *"
value={scratch.tuition}
onChange={(e)=>setScratch({...scratch,tuition:e.target.value})}
/>
<Button
className="w-full"
disabled={!ready}
onClick={()=>{
const t = Number(scratch.tuition);
const stub = {
INSTNM : scratch.school || '(self-entered)', // ← if blank
degreeType: scratch.programType || 'Unknown', // ← if blank
programLength: 4,
inState: t,
outOfState: t,
inStateGraduate: t,
outStateGraduate: t,
};
setSchools([stub]); // now main form will appear
}}
>
Continue
</Button>
</div>
);
}
/* ================================================================= */
/* ORIGINAL DETAILED REPAYMENT FORM */
/* ================================================================= */
return (
<div className="loan-repayment-container">
<form onSubmit={(e) => { e.preventDefault(); calculateLoanDetails(); }}>
<div className="input-group">
<label>Tuition Type:</label>
<select value={tuitionType} onChange={(e) => setTuitionType(e.target.value)}>
<option value="inState">In-State</option>
<option value="outOfState">Out-of-State</option>
</select>
</div>
<div className="input-group">
<label>Interest Rate:</label>
<input type="number"
value={interestRate}
onChange={(e) => setInterestRate(e.target.value)}
placeholder="Enter the interest rate"
/>
</div>
<div className="input-group">
<label>Loan Term (years):</label>
<input type="number"
value={loanTerm}
onChange={(e) => setLoanTerm(e.target.value)}
placeholder="Enter the length of the loan repayment period"
/>
</div>
<div className="input-group">
<label>Extra Monthly Payment:</label>
<input type="number"
value={extraPayment}
onChange={(e) => setExtraPayment(e.target.value)}
placeholder="Enter any additional monthly payment"
/>
</div>
<div className="input-group">
<label>Current Salary:</label>
<input type="number"
value={currentSalary}
onChange={(e) => setCurrentSalary(e.target.value)}
placeholder="Enter your current salary"
/>
</div>
<div className="input-group">
<label>Expected Salary:</label>
<input
type="number"
value={expectedSalary}
onChange={(e) => setExpectedSalary(e.target.value)}
placeholder="Enter expected salary"
/>
<div className="loan-repayment-container max-w-xl mx-auto">
<form onSubmit={(e)=>{e.preventDefault();calculateLoanDetails();}}
className="space-y-4 border rounded p-6">
{/* Tuition type */}
<div className="input-group">
<label className="block font-medium">Tuition Type</label>
<select
value={tuitionType}
onChange={(e)=>setTuitionType(e.target.value)}
className="border rounded p-2 w-full"
>
<option value="inState">In-State</option>
<option value="outOfState">Out-of-State</option>
</select>
</div>
<div className="calculate-button-container">
<button type="submit">Calculate</button>
{/* Interest rate */}
<div className="input-group">
<label className="block font-medium">Interest Rate (%)</label>
<input type="number"
value={interestRate}
onChange={(e)=>setInterestRate(e.target.value)}
className="border rounded p-2 w-full"/>
</div>
{/* Loan term */}
<div className="input-group">
<label className="block font-medium">Loan Term (years)</label>
<input type="number"
value={loanTerm}
onChange={(e)=>setLoanTerm(e.target.value)}
className="border rounded p-2 w-full"/>
</div>
{/* Extra payment */}
<div className="input-group">
<label className="block font-medium">Extra Monthly Payment</label>
<input type="number"
value={extraPayment}
onChange={(e)=>setExtraPayment(e.target.value)}
className="border rounded p-2 w-full"/>
</div>
{/* Current salary */}
<div className="input-group">
<label className="block font-medium">Current Salary</label>
<input type="number"
value={currentSalary}
onChange={(e)=>setCurrentSalary(e.target.value)}
className="border rounded p-2 w-full"/>
</div>
{/* Expected salary */}
<div className="input-group">
<label className="block font-medium">Expected Salary</label>
<input type="number"
value={expectedSalary}
onChange={(e)=>setExpectedSalary(e.target.value)}
className="border rounded p-2 w-full"/>
</div>
{/* Submit */}
<div className="text-right">
<Button type="submit">Calculate</Button>
</div>
{error && <div className="text-red-600 text-sm">{error}</div>}
</form>
</div>
</form>
{error && <div className="error">{error}</div>}
</div>
);
}
export default LoanRepayment;
export default LoanRepayment;

View File

@ -0,0 +1,306 @@
// src/components/LoanRepaymentDrawer.js
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { X } from 'lucide-react';
import LoanRepayment from './LoanRepayment.js';
import { Button } from './ui/button.js';
import { getNearbySchools } from '../utils/getNearbySchools.js';
import UpsellSummary from './UpsellSummary.js';
import { useNavigate } from 'react-router-dom';
/* ───────────────────────── CONSTANTS ───────────────────────── */
const DEGREE_OPTS = [
"Associate's Degree",
"Bachelor's Degree",
"Master's Degree",
'Graduate / Professional Certificate',
'First Professional Degree',
'Doctoral Degree',
];
/* ──────────────────────── COMPONENT ────────────────────────── */
export default function LoanRepaymentDrawer({
open,
onClose,
schools, // comes from PreparingLanding
setSchools,
results,
setResults,
user,
cipCodes = [],
userZip = '',
userState='',
}) {
/* Hooks must always run return null later if !open */
/* ── Remote data for auto-suggest ─ */
const [cipData, setCipData] = useState([]);
const [schoolSearch, setSchoolSearch] = useState('');
/* ── Simple form fields ─ */
const [degree, setDegree] = useState('');
const [tuition, setTuition] = useState('');
const [err, setErr] = useState('');
/* ── When “Continue” is pressed show true calculator ─ */
const [showCalc, setShowCalc] = useState(false);
const navigate = useNavigate();
/* ▒▒▒ NEW auto-seed effect ▒▒▒ */
useEffect(() => {
// run once every time the drawer opens
if (!open) return; // drawer closed
if (schools.length) return; // already have data
if (!cipCodes.length) return; // no career → nothing to seed
(async () => {
try {
const seed = await getNearbySchools(cipCodes, userZip, userState);
if (seed.length) setSchools(seed);
} catch (e) {
console.warn('auto-seed schools failed:', e);
}
})();
}, [open, schools.length, cipCodes.join('-'), userZip, userState, setSchools]);
/*
FETCH CIP DATA (only once the drawer is ever opened)
*/
useEffect(() => {
if (!open || cipData.length) return;
fetch('/cip_institution_mapping_new.json')
.then(r => r.text())
.then(text =>
text
.split('\n')
.map(l => { try { return JSON.parse(l); } catch { return null; } })
.filter(Boolean)
)
.then(arr => setCipData(arr))
.catch(e => console.error('CIP fetch error', e));
}, [open, cipData.length]);
/*
SCHOOL AUTOCOMPLETE LIST (memoised)
*/
const suggestions = useMemo(() => {
if (!schoolSearch.trim()) return [];
const low = schoolSearch.toLowerCase();
const set = new Set(
cipData
.filter(r => r.INSTNM.toLowerCase().includes(low))
.map(r => r.INSTNM)
);
return [...set].slice(0, 10);
}, [schoolSearch, cipData]);
/*
ESC --> close convenience
*/
const escHandler = useCallback(e => {
if (e.key === 'Escape') onClose();
}, [onClose]);
useEffect(() => {
if (open) window.addEventListener('keydown', escHandler);
return () => window.removeEventListener('keydown', escHandler);
}, [open, escHandler]);
/*
HANDLE CONTINUE
*/
const handleContinue = () => {
// Tuition is the only truly required field
if (!tuition.trim()) {
setErr('Please enter an annual tuition estimate.');
return;
}
setErr('');
// Build a stub “school” object so LoanRepayment
// can work with the same shape it expects
const stub = {
name : schoolSearch || 'Unknown School',
degreeType : degree || 'Unspecified',
programLength : 4, // sensible default
inState : parseFloat(tuition),
outOfState : parseFloat(tuition),
inStateGraduate : parseFloat(tuition),
outStateGraduate: parseFloat(tuition),
};
setSchools([stub]); // overwrite single-school calc
setShowCalc(true);
};
const showUpsell = user && !user.is_premium && !user.is_pro_premium;
/*
RENDER
*/
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex justify-end bg-black/40">
<div className="h-full w-full max-w-md bg-white shadow-xl flex flex-col">
{/* Header */}
<div className="flex justify-between items-center px-5 py-4 border-b">
<h2 className="font-semibold">Estimate Student-Loan Payments</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
<X size={20} />
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-6 flex-1">
{/* Free money first */}
<section className="text-sm mb-6">
<h3 className="font-semibold mb-1">Free money first</h3>
<ol className="list-decimal list-inside space-y-1">
<li>Submit the <strong>FAFSA</strong> (its free).</li>
<li>Check state <strong>grant&nbsp;/ Promise&nbsp;/ HOPE</strong> programs.</li>
<li>Apply for <strong>scholarships</strong> (FastWeb, Bold.org, local).</li>
<li>Take gift-aid first, loans last.</li>
</ol>
</section>
{/* STEP 1: QUICK FORM */}
{!showCalc && (
<form
className="space-y-4"
onSubmit={e => { e.preventDefault(); handleContinue(); }}
>
{/* School name (optional) */}
<div>
<label className="text-sm font-medium">School name</label>
<input
type="text"
value={schoolSearch}
onChange={e => setSchoolSearch(e.target.value)}
list="school-suggestions"
placeholder="Start typing…"
className="mt-1 w-full rounded border px-3 py-2 text-sm"
/>
<datalist id="school-suggestions">
{suggestions.map((s, i) => (
<option key={i} value={s} />
))}
</datalist>
</div>
{/* Degree */}
<div>
<label className="text-sm font-medium">Degree&nbsp;/ program type</label>
<select
value={degree}
onChange={e => setDegree(e.target.value)}
className="mt-1 w-full rounded border px-3 py-2 text-sm"
>
<option value="">Select</option>
{DEGREE_OPTS.map((d, i) => <option key={i} value={d}>{d}</option>)}
</select>
</div>
{/* Tuition */}
<div>
<label className="text-sm font-medium">Estimated annual tuition *</label>
<input
type="number"
min="0"
step="100"
value={tuition}
onChange={e => setTuition(e.target.value)}
placeholder="e.g. 28000"
className="mt-1 w-full rounded border px-3 py-2 text-sm"
/>
</div>
{/* Error */}
{err && <p className="text-red-600 text-sm">{err}</p>}
<Button type="submit" className="w-full">
Continue
</Button>
</form>
)}
{/* STEP 2: REAL CALCULATOR */}
{showCalc && (
<>
<LoanRepayment
schools={schools}
setSchools={setSchools}
setResults={setResults}
results={results}
/>
{/* small separator */}
<hr className="my-6" />
{/* PREMIUM CTA */}
{showUpsell ? (
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded space-y-2 text-sm">
<p className="font-medium">
Want to see how this loan fits into a full financial plan?
</p>
<p>
Premium subscribers can model salary growth, living costs,
retirement goals, and more.
</p>
<Button
className="w-full"
onClick={() => {
onClose();
navigate('/paywall');
}}
>
Unlock Premium Features
</Button>
</div>
) : (
/* If already premium just show a helpful note */
<div className="bg-green-50 border-l-4 border-green-400 p-4 rounded text-sm">
<p className="font-medium">
Youre on Premium open the <strong>College Planning wizard</strong> to store this tuition in your plan
(Profile Premium Onboarding College Details).
</p>
</div>
)}
</>
)}
{/* STEP 3 : results table */}
{results?.length > 0 && (
<div className="mt-8">
<h4 className="font-semibold mb-2 text-center">
Estimated payments
</h4>
<table className="w-full text-sm border">
<thead className="bg-gray-100">
<tr>
<th className="p-2">School</th>
<th className="p-2">Monthly</th>
<th className="p-2">Total Cost</th>
<th className="p-2">Net Gain</th>
</tr>
</thead>
<tbody>
{results.map((r, i) => (
<tr key={i} className="border-t">
<td className="p-2">{r.name}</td>
<td className="p-2">${r.totalMonthlyPayment}</td>
<td className="p-2">${r.totalLoanCost}</td>
<td className="p-2">${r.netGain}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,11 @@
// src/pages/LoanRepaymentPage.js
import LoanRepayment from '../components/LoanRepayment.js';
export default function LoanRepaymentPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
{/* Pass data via props or context exactly as LoanRepayment expects */}
<LoanRepayment />
</div>
);
}

View File

@ -1,68 +1,106 @@
import React from 'react';
// src/components/PreparingLanding.js
import React, { useState, useCallback, useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from './ui/button.js';
import LoanRepaymentDrawer from './LoanRepaymentDrawer.js';
import { ProfileCtx } from '../App.js';
function PreparingLanding() {
const navigate = useNavigate();
const { user } = useContext(ProfileCtx);
/* ─── Drawer visibility ─────────────────────────────── */
const [showLoan, setShowLoan] = useState(false);
/* ─── Stub-school state lives here; Drawer mutates it ─ */
const [schools, setSchools] = useState([]); // [] → quick-fill form
const [cipCodes] = useState([]); // you may hold these at page level
const [userZip] = useState('');
const [loanResults, setLoanResults] = useState([]);
/* Esc -to-close convenience */
const escHandler = useCallback(e => {
if (e.key === 'Escape') setShowLoan(false);
}, []);
useEffect(() => {
if (showLoan) window.addEventListener('keydown', escHandler);
return () => window.removeEventListener('keydown', escHandler);
}, [showLoan, escHandler]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
<div className="max-w-3xl w-full bg-white shadow-lg rounded-lg p-8 space-y-6">
{/* ───────────────── TITLE / INTRO ───────────────── */}
<h1 className="text-3xl font-bold text-center">
Preparing for Your (Next) Career
</h1>
<p className="text-gray-600 text-center">
Build the right skills and plan your education so you can confidently
enter (or transition into) your new career.
enteror transition intoyour new career.
</p>
{/* Section: Choose Skills-Based vs. Formal Education */}
{/* ──────────────── 1) PATH CHOICE ──────────────── */}
<section className="space-y-4">
<h2 className="text-xl font-semibold">Which Path Fits You?</h2>
<p className="text-gray-700">
We can help you identify whether a <strong>skills-based program</strong>
(certifications, bootcamps, on-the-job training) or a more
<strong> formal education route</strong> (two-year or four-year college)
is the best fit. Whichever path you choose, our AI tools will guide
you from application to graduation.
We can help you identify whether a&nbsp;
<strong>skills-based program</strong> (certifications, bootcamps) or a&nbsp;
<strong>formal education route</strong> (two- or four-year college)
is the best fit. Whichever path you choose, AptivaAI will help you map next stepsfrom applying to graduating.
</p>
<div className="flex flex-col sm:flex-row gap-4">
{/* Explore Education Options (handles skill-based & formal) */}
<Button onClick={() => navigate('/educational-programs')}>
Plan My Education Path
</Button>
{/* How to Pay button (placeholder route) */}
<Button onClick={() => navigate('/how-to-pay')}>
<Button onClick={() => setShowLoan(true)}>
How to Pay for Education
</Button>
</div>
</section>
{/* Section: Tie-In to LoanRepayment or Additional Financial Planning */}
{/* ──────────────── 2) LOAN BLURB ──────────────── */}
<section className="space-y-4">
<h2 className="text-xl font-semibold">Financing Your Future</h2>
<p className="text-gray-700">
Already have an idea of where you want to enroll?
We can help you compare costs, estimate student loan repayments,
and map out work-study or part-time opportunities.
Our integrated <strong>LoanRepayment</strong> tools will show you
Already have an idea of where you want to enroll? Compare costs,
estimate student-loan repayments, and map out work-study or part-time
opportunities. Our integrated <strong>LoanRepayment</strong> tool shows
realistic monthly payments so you can make confident choices.
</p>
</section>
{/* Optional: Retake Interest Inventory */}
{/* ──────────────── 3) INTEREST INVENTORY ──────────────── */}
<section className="space-y-3">
<h2 className="text-xl font-semibold">Still Exploring?</h2>
<p className="text-gray-700">
If youd like to revisit career possibilities, feel free to retake
our Interest Inventory to see other matching paths.
Want to revisit career possibilities? Retake our Interest Inventory to
see other matching paths.
</p>
<Button onClick={() => navigate('/interest-inventory')}>
Retake Interest Inventory
</Button>
</section>
</div>
{/* ─────────────── DRAWER MOUNT ─────────────── */}
{showLoan && (
<LoanRepaymentDrawer
open={showLoan}
onClose={() => setShowLoan(false)}
schools={schools}
setSchools={setSchools}
results={loanResults}
setResults={setLoanResults}
isPremium={user?.is_premium || user?.is_pro_premium}
cipCodes={cipCodes}
userZip={userZip}
user={user}
/>
)}
</div>
);
}

View File

@ -0,0 +1,35 @@
// UpsellSummary.jsx
import { Button } from './ui/button.js';
export default function UpsellSummary({ row, onUpgrade, isPremium }) {
if (!row) return null;
const niceMoney = n => '$' + Number(n).toLocaleString();
return (
<div id="loan-results" className="mt-6 space-y-3 border-t pt-4">
<p className="text-sm text-gray-700">
With a payment of <strong>{niceMoney(row.totalMonthlyPayment)}/mo</strong>{' '}
youd spend <strong>{niceMoney(row.totalLoanCost)}</strong> over the loan
term and still net about{' '}
<strong className="text-green-600">
{niceMoney(row.netGain)}
</strong>{' '}
after pay-off.
</p>
{isPremium ? (
<Button className="w-full" onClick={onUpgrade}>
Open Detailed ROI Planner
</Button>
) : (
<Button
className="w-full bg-green-500 hover:bg-green-600"
onClick={onUpgrade}
>
See full ROI with Premium
</Button>
)}
</div>
);
}

View File

@ -0,0 +1,50 @@
import { fetchSchools, clientGeocodeZip, haversineDistance } from './apiUtils.js';
/**
* Returns the N cheapest schools within a radius.
* cipCodes array like ['1101','5202']
* userZip '30303' etc (may be '')
* userState 'GA' etc (may be '')
* maxDistMi number (default 100)
* limit number (default 10)
*/
export async function getNearbySchools(
cipCodes = [],
userZip = '',
userState = '',
{ maxDistMi = 100, limit = 10 } = {}
) {
const raw = await fetchSchools(cipCodes);
let userLat=null, userLng=null;
if (userZip) {
try {
const geo = await clientGeocodeZip(userZip);
userLat = geo.lat; userLng = geo.lng;
} catch { /* ignore treat as no-location user */ }
}
const withDist = raw.map(r => {
const lat = r.LATITUDE ? parseFloat(r.LATITUDE) : null;
const lon = r.LONGITUD ? parseFloat(r.LONGITUD) : null;
const dist = (userLat && userLng && lat && lon)
? haversineDistance(userLat,userLng,lat,lon)
: null;
return { ...r, distance: dist };
});
let cand = withDist
.filter(s => {
if (s.distance===null) return true;
return s.distance <= maxDistMi;
})
.sort((a,b)=>{
const aT = parseFloat(a['In_state cost'] || Infinity);
const bT = parseFloat(b['In_state cost'] || Infinity);
return aT - bT;
});
/* If user said “in-state only” we can filter here omitted for brevity */
return cand.slice(0, limit);
}

223
src/utils/ipedsTuition.js Normal file
View File

@ -0,0 +1,223 @@
// src/components/LoanRepaymentDrawer.jsx
import React, { useState, useMemo } from 'react';
import { X } from 'lucide-react';
import { Button } from './ui/button.js';
import LoanRepayment from './LoanRepayment.js';
/**
* Tiny helper so we dont repeat className clutter.
*/
const Field = ({ label, children }) => (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
{children}
</div>
);
export default function LoanRepaymentDrawer({ open, onClose }) {
/**************************************************************************
* 1Drawer visibility
**************************************************************************/
if (!open) return null;
/**************************************************************************
* 2Local state for the estimator form
**************************************************************************/
const [form, setForm] = useState({
schoolName: '',
programType: '',
annualTuition: '',
});
const [step, setStep] = useState(1); // 1 = “free-money”, 2 = estimator, 3 = results
const [showErrors, setShowErrors] = useState(false);
// Pass-throughs for <LoanRepayment/>
const [calcResults, setCalcResults] = useState(null);
const [calcLoading, setCalcLoading] = useState(false);
const onChange = (field) => (e) =>
setForm((prev) => ({ ...prev, [field]: e.target.value }));
const formValid =
form.programType &&
form.annualTuition &&
!Number.isNaN(Number(form.annualTuition)) &&
Number(form.annualTuition) > 0;
/**************************************************************************
* 3Derived school object handed to <LoanRepayment/>
**************************************************************************/
const derivedSchool = useMemo(() => {
if (!formValid) return null;
return {
// Minimal fields LoanRepayment expects
degreeType: form.programType,
// If user typed nothing → “Unknown school”
name: form.schoolName || 'Unknown school',
programLength: 4, // we cant know; LoanRepayment falls back if it differs
inState: Number(form.annualTuition),
outOfState: Number(form.annualTuition),
// grad tuition duplicates harmless for our purposes
inStateGraduate: Number(form.annualTuition),
outStateGraduate: Number(form.annualTuition),
};
}, [form, formValid]);
/**************************************************************************
* 4UI
**************************************************************************/
return (
<div className="fixed inset-0 z-50 flex justify-end bg-black/40">
{/* drawer panel */}
<div className="h-full w-full max-w-md bg-white shadow-xl overflow-y-auto p-6 relative">
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
>
<X size={20} />
</button>
{/* title bar */}
<h2 className="text-lg font-semibold mb-6 text-center">
Estimate Student-Loan Payments
</h2>
{/* ───────────────── STEP 1 free money first ───────────────── */}
{step === 1 && (
<>
<p className="text-gray-700 mb-4">
<strong>Before you borrow:</strong>
</p>
<ul className="list-disc list-inside text-gray-600 space-y-2 mb-6">
<li>
Complete the&nbsp;
<a
href="https://studentaid.gov/h/apply-for-aid/fafsa"
target="_blank"
rel="noreferrer"
className="text-blue-600 underline"
>
FAFSA
</a>{' '}
its free and unlocks grants, work-study, and low-interest
federal loans.
</li>
<li>Search local and departmental scholarships (they add up!).</li>
<li>
Compare tuition discounts for in-state, online, or employer
tuition-reimbursement programs.
</li>
</ul>
<Button className="w-full" onClick={() => setStep(2)}>
Continue
</Button>
</>
)}
{/* ───────────────── STEP 2 quick estimator form ─────────────── */}
{step === 2 && (
<>
<Field label="School name (optional)">
<input
type="text"
value={form.schoolName}
onChange={onChange('schoolName')}
placeholder="e.g. Georgia Tech"
className="w-full border rounded p-2"
/>
</Field>
<Field label="Degree / program type *">
<select
value={form.programType}
onChange={onChange('programType')}
className="w-full border rounded p-2 bg-white"
>
<option value="">Select</option>
<option>Certificate</option>
<option>Associate's Degree</option>
<option>Bachelor's Degree</option>
<option>Master's Degree</option>
<option>First Professional Degree</option>
<option>Doctoral Degree</option>
</select>
</Field>
<Field label="Estimated annual tuition *">
<input
type="number"
value={form.annualTuition}
onChange={onChange('annualTuition')}
placeholder="USD"
className="w-full border rounded p-2"
/>
</Field>
{showErrors && !formValid && (
<p className="text-red-600 text-sm mb-4">
Please fill in the required fields (degree type and tuition).
</p>
)}
<Button
className="w-full"
onClick={() => {
if (!formValid) {
setShowErrors(true);
return;
}
setShowErrors(false);
setStep(3);
}}
>
Estimate payments
</Button>
</>
)}
{/* ───────────────── STEP 3 results / LoanRepayment ──────────── */}
{step === 3 && derivedSchool && (
<>
<LoanRepayment
schools={[derivedSchool]}
salaryData={null}
setResults={setCalcResults}
setLoading={setCalcLoading}
setPersistedROI={() => {}}
programLength={4}
/>
{calcLoading && (
<p className="text-sm text-gray-500 mt-4">Calculating</p>
)}
{calcResults && (
<div className="mt-6 border-t pt-4">
<h3 className="font-semibold mb-2">Quick Summary</h3>
<pre className="whitespace-pre-wrap text-xs bg-gray-50 p-2 rounded max-h-48 overflow-auto">
{JSON.stringify(calcResults, null, 2)}
</pre>
</div>
)}
<Button
variant="secondary"
className="mt-6 w-full"
onClick={() => {
setCalcResults(null);
setStep(2);
}}
>
Back to inputs
</Button>
</>
)}
</div>
</div>
);
}

42
src/utils/tuitionCalc.js Normal file
View File

@ -0,0 +1,42 @@
/*
ONE place that owns the math
*/
export function calcAnnualTuition({
ipedsRows, schoolRow,
programType, creditHoursPerYear,
inState, inDistrict,
}) {
if (!ipedsRows?.length || !schoolRow || !programType) return 0;
const row = ipedsRows.find(r => r.UNITID === schoolRow.UNITID);
if (!row) return 0;
const grad = [
"Master's Degree", "Doctoral Degree",
"Graduate/Professional Certificate", "First Professional Degree",
].includes(programType);
const pick = (u1,u2,u3) => inDistrict ? row[u1] : inState ? row[u2] : row[u3];
const partTime = Number( grad ? pick('HRCHG5','HRCHG6','HRCHG7')
: pick('HRCHG1','HRCHG2','HRCHG3') );
const fullTime = Number( grad ? pick('TUITION5','TUITION6','TUITION7')
: pick('TUITION1','TUITION2','TUITION3') );
const ch = Number(creditHoursPerYear) || 0;
return (ch && ch < 24 && partTime) ? partTime * ch : fullTime;
}
export function calcProgramLength({ programType, hrsPerYear, hrsCompleted=0, hrsRequired=0 }) {
if (!programType || !hrsPerYear) return '0.00';
let need = hrsRequired;
switch (programType) {
case "Associate's Degree": need = 60; break;
case "Bachelor's Degree" : need = 120; break;
case "Master's Degree" : need = 180; break;
case "Doctoral Degree" : need = 240; break;
case "First Professional Degree": need = 180; break;
}
return ((need - hrsCompleted) / hrsPerYear).toFixed(2);
}