const { useState } = React; const formatCurrency = (val) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(val); function calculateMonthly(price, downPct, rate) { const principal = price * (1 - downPct / 100); if (principal <= 0) return 0; const monthlyRate = rate / 100 / 12; if (monthlyRate === 0) return principal / 360; return (principal * monthlyRate * Math.pow(1 + monthlyRate, 360)) / (Math.pow(1 + monthlyRate, 360) - 1); } function SliderInput({ label, value, onChange, min, max, step, format, suffix, unit }) { const [localText, setLocalText] = useState(null); const pct = ((value - min) / (max - min)) * 100; return (
{unit === "prefix" && $} setLocalText(String(value))} onBlur={() => { const num = parseFloat(localText); if (!isNaN(num)) onChange(Math.min(max, Math.max(min, num))); setLocalText(null); }} onChange={(e) => setLocalText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }} style={{ fontSize: 26, fontFamily: "'Libre Baskerville', serif", fontWeight: 400, color: "#1a1a1a", background: "transparent", border: "none", borderBottom: "1.5px solid #ddd", outline: "none", textAlign: "right", width: unit === "prefix" ? 170 : 80, padding: "2px 0", transition: "border-color 0.2s", }} /> {suffix && {suffix}}
onChange(parseFloat(e.target.value))} style={{ position: "absolute", width: "100%", height: 32, opacity: 0, cursor: "pointer", margin: 0, }} />
{unit === "prefix" ? formatCurrency(min) : `${min}${suffix || ""}`} {unit === "prefix" ? formatCurrency(max) : `${max}${suffix || ""}`}
); } function MortgageCalculator() { const [price, setPrice] = useState(4000000); const [downPct, setDownPct] = useState(20); const [rate, setRate] = useState(6); const [monthlyFees, setMonthlyFees] = useState(4400); const [tableIncludeFees, setTableIncludeFees] = useState(true); const monthly = calculateMonthly(price, downPct, rate); const principal = price * (1 - downPct / 100); const totalInterest = monthly * 360 - principal; const totalMonthly = monthly + monthlyFees; const prices = []; for (let p = 3400000; p <= 4100000; p += 50000) prices.push(p); const rates = []; for (let r = 5.25; r <= 6.25; r = Math.round((r + 0.125) * 1000) / 1000) rates.push(r); const isActivePrice = (p) => p === price; const isActiveRate = (r) => Math.abs(r - rate) < 0.001; return (

Mortgage Calculator

30-year fixed rate

v.toLocaleString()} unit="prefix" />
{ const num = parseFloat(e.target.value); if (!isNaN(num)) setDownPct(Math.min(100, Math.max(0, num))); else if (e.target.value === "") setDownPct(0); }} style={{ fontSize: 26, fontFamily: "'Libre Baskerville', serif", color: "#1a1a1a", background: "transparent", border: "none", borderBottom: "1.5px solid #ddd", outline: "none", textAlign: "right", width: 60, padding: "2px 0", }} /> %

{formatCurrency(price * downPct / 100)} down ยท {formatCurrency(principal)} loan

v.toFixed(3)} /> v.toLocaleString()} unit="prefix" />

Monthly Mortgage

{formatCurrency(monthly)}

Total Interest
{formatCurrency(totalInterest)}
Total Cost
{formatCurrency(monthly * 360)}

Total Monthly

{formatCurrency(totalMonthly)}

Mortgage + Fees
{formatCurrency(monthly)} + {formatCurrency(monthlyFees)}
{/* Sensitivity Table */}

Sensitivity Table

{tableIncludeFees ? "Total monthly" : "Mortgage only"} at {downPct}% down

{rates.map((r) => ( ))} {prices.map((p) => ( {rates.map((r) => { const val = calculateMonthly(p, downPct, r) + (tableIncludeFees ? monthlyFees : 0); const highlight = isActivePrice(p) && isActiveRate(r); const rowHL = isActivePrice(p); const colHL = isActiveRate(r); return ( ); })} ))}
Price \ Rate {r % 0.25 === 0 ? r.toFixed(2) : r.toFixed(3)}%
{formatCurrency(p)} {formatCurrency(Math.round(val))}
{/* Closing Costs Table */}

Cash Required at Close

Estimated NYC condo closing costs

{(() => { const loanAmount = price * (1 - downPct / 100); const downPayment = price * downPct / 100; const mansionTax = (() => { if (price < 1000000) return 0; if (price < 2000000) return price * 0.01; if (price < 3000000) return price * 0.0125; if (price < 5000000) return price * 0.0150; if (price < 10000000) return price * 0.0225; if (price < 15000000) return price * 0.0325; if (price < 20000000) return price * 0.0350; if (price < 25000000) return price * 0.0375; return price * 0.039; })(); const mortgageRecordingTax = loanAmount >= 500000 ? loanAmount * 0.01925 : loanAmount * 0.018; const titleInsurance = price * 0.0045; const attorneyFees = 5000; const bankFees = 3500; const recordingFees = 750; const misc = 1500; const closingCostsTotal = mansionTax + mortgageRecordingTax + titleInsurance + attorneyFees + bankFees + recordingFees + misc; const totalCash = downPayment + closingCostsTotal; const rows = [ { label: "Mansion Tax", amount: mansionTax, note: `NY state, progressive scale` }, { label: "Mortgage Recording Tax", amount: mortgageRecordingTax, note: `1.925% of loan (over $500K)` }, { label: "Title Insurance", amount: titleInsurance, note: `~0.45% of purchase price` }, { label: "Buyer's Attorney", amount: attorneyFees, note: "Flat fee" }, { label: "Bank Fees & Appraisal", amount: bankFees, note: "Lender charges" }, { label: "Recording Fees", amount: recordingFees, note: "Deed & mortgage filing" }, { label: "Miscellaneous", amount: misc, note: "Inspections, adjustments" }, ]; return ( {rows.map((row, i) => ( ))}
{row.label} {row.note} {formatCurrency(Math.round(row.amount))}
CLOSING COSTS SUBTOTAL {formatCurrency(Math.round(closingCostsTotal))}
Down Payment {downPct}% of purchase price {formatCurrency(Math.round(downPayment))}
TOTAL CASH AT CLOSE {formatCurrency(Math.round(totalCash))}
); })()}
); } ReactDOM.createRoot(document.getElementById('root')).render();