dev1/src/components/LoanRepaymentDrawer.js

307 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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>
);
}