// 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(''); const [icData, setIcData] = useState([]); /* ── Simple form fields ─ */ const [degree, setDegree] = useState(''); const [tuitionType, setTuitionType] = useState('inState'); const [tuition, setTuition] = useState(''); const [tuitionManual, setTuitionManual] = useState(false); const [err, setErr] = useState(''); /* ── When “Continue” is pressed show true calculator ─ */ const [showCalc, setShowCalc] = useState(false); const navigate = useNavigate(); /* ─── helpers (declare FIRST so everything below can use them) ─── */ const currencyFmt = n => Number(n).toLocaleString('en-US', { style:'currency', currency:'USD', maximumFractionDigits:0 }); const pickField = ({ inState, outOfState, inStateGraduate, outStateGraduate }, isGrad, resid) => isGrad ? resid === 'inState' ? inStateGraduate : outStateGraduate : resid === 'inState' ? inState : outOfState; const getAnnualTuition = (schoolsArr, typed, isGrad, resid) => pickField(schoolsArr?.[0] ?? {}, isGrad, resid) || (typed ? Number(typed) : 0); /* ── memo’ed degree list for current school ── */ const schoolDegrees = useMemo(() => { if (!schoolSearch.trim()) return []; const list = cipData .filter(r => r.INSTNM.toLowerCase() === schoolSearch.toLowerCase()) .map(r => r.CREDDESC); return [...new Set(list)]; }, [schoolSearch, cipData]); const degreeMenu = schoolDegrees.length ? schoolDegrees : DEGREE_OPTS; const isGrad = /(Master|Doctoral|First Professional|Graduate|Certificate)/i.test(degree); const annualTuition= getAnnualTuition(schools, tuition, isGrad, tuitionType); const showUpsell = user && !user.is_premium && !user.is_pro_premium; useEffect(() => { if (!open) return; if (schools.length) return; if (!cipCodes.length) return; (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]); useEffect(() => { if (!open || icData.length) return; fetch('/ic2023_ay.csv') .then(r => r.text()) .then(text => { const [header, ...rows] = text.split('\n').map(l => l.split(',')); return rows.map(row => Object.fromEntries(row.map((v, i) => [header[i], v])) ); }) .then(setIcData) .catch(e => console.error('iPEDS load fail', e)); }, [open, icData.length]); /* ───────── auto-tuition when schoolSearch settles ───────── */ useEffect(() => { if (!schoolSearch.trim() || !icData.length) return; const rec = cipData.find(r => r.INSTNM.toLowerCase() === schoolSearch.toLowerCase()); if (!rec) return; const match = icData.find(r => r.UNITID === rec.UNITID); if (!match) return; const calc = () => { const grad = /(Master|Doctoral|First Professional|Graduate|Certificate)/i.test(degree); if (!grad) { return tuitionType === 'inState' ? parseFloat(match.TUITION1 || match.TUITION2 || '') : parseFloat(match.TUITION3 || ''); } return tuitionType === 'inState' ? parseFloat(match.TUITION5 || match.TUITION6 || '') : parseFloat(match.TUITION7 || ''); }; const est = calc(); if (est && !tuitionManual) setTuition(String(est)); }, [schoolSearch, tuitionType, degree, cipData, icData, tuitionManual]); /* ════════════════════════════════════════════════════ 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]); /* ─── “CONTINUE” HANDLER ───────────────────────────────────────── */ const handleContinue = () => { // Guard-rail: tuition required if (!tuition.trim()) { setErr('Please enter an annual tuition estimate.'); return; } setErr(''); // Selectively populate the ONE tuition field that matches const base = { inState: 0, outOfState: 0, inStateGraduate: 0, outStateGraduate: 0, }; if (!isGrad) { if (tuitionType === 'inState') base.inState = parseFloat(tuition); else base.outOfState = parseFloat(tuition); } else { if (tuitionType === 'inState') base.inStateGraduate = parseFloat(tuition); else base.outStateGraduate = parseFloat(tuition); } const stub = { name : schoolSearch || 'Unknown School', degreeType : degree || 'Unspecified', programLength : 4, tuition : parseFloat(tuition), ...base, }; setSchools([stub]); // overwrite – single-school calc setShowCalc(true); }; /* ════════════════════════════════════════════════════ RENDER ════════════════════════════════════════════════════ */ if (!open) return null; return (
{/* Header */}

Estimate Student-Loan Payments

{/* Content */}
{/* Free money first */}

Free money first

  1. Submit the FAFSA (it’s free).
  2. Check state grant / Promise / HOPE programs.
  3. Apply for scholarships (FastWeb, Bold.org, local).
  4. Take gift-aid first, loans last.
{/* STEP 1: QUICK FORM */} {!showCalc && (
{ e.preventDefault(); handleContinue(); }} > {/* School name (optional) */}
setSchoolSearch(e.target.value)} list="school-suggestions" placeholder="Start typing…" className="mt-1 w-full rounded border px-3 py-2 text-sm" onBlur={e => setSchoolSearch(e.target.value.trim())} /> {suggestions.map((s, i) => (
{/* Residency */}
{/* Degree */}
{/* Tuition */}
{ setTuition(e.target.value); setTuitionManual(true); // user has taken control }} />
{/* Error */} {err &&

{err}

}
)} {/* STEP 2: REAL CALCULATOR */} {showCalc && ( <> {/* small separator */}
{/* PREMIUM CTA */} {showUpsell ? ( /* ── user is NOT premium ───────────────────────────── */

Your estimate for  {degree || 'Unspecified program'}{tuitionType === 'inState' ? 'In-State' : 'Out-of-State'}  tuition is  {currencyFmt(annualTuition)}.

Unlock Premium to see exactly how this loan fits into a full financial plan — salary growth, living costs, retirement goals, and more.

) : ( /* ── user IS premium ───────────────────────────────── */

You’re on Premium — we’ve estimated your {tuitionType === 'inState' ? 'In-State' : 'Out-of-State'}{' '} tuition for a {degree || 'program'} at  {currencyFmt(annualTuition)}.
Head to Profile ▸ Premium Onboarding to put this into your plan.

)} )} {/* STEP 3 : results table */} {results?.length > 0 && (

Estimated payments

{results.map((r, i) => ( ))}
School Monthly Total Cost Net Gain
{r.name || r.INSTNM || '—'} {currencyFmt(r.totalMonthlyPayment)} {currencyFmt(r.totalLoanCost)} {currencyFmt(r.netGain)}
)}
); }