import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { CurrentHome, NewHome, Debt, CalculationResult } from './types';
import NumberInput from './components/NumberInput';
import { Icons, COLORS } from './constants';
import { getAIAnalysis } from './services/geminiService';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
const App: React.FC = () => {
// --- State ---
const [currentHome, setCurrentHome] = useState({
value: 400000,
mortgageBalance: 250000,
interestRate: 3.0,
termYears: 30,
taxInsuranceEstimate: 450,
});
const [newHome, setNewHome] = useState({
purchasePrice: 550000,
interestRate: 7.0,
termYears: 30,
taxInsuranceEstimate: 625,
});
const [debts, setDebts] = useState([
{ id: '1', name: 'Car Loan', balance: 25000, monthlyPayment: 550, payOff: true },
{ id: '2', name: 'Credit Card A', balance: 12000, monthlyPayment: 360, payOff: true },
{ id: '3', name: 'Student Loan', balance: 35000, monthlyPayment: 400, payOff: false },
]);
const [sellingCostsPercent, setSellingCostsPercent] = useState(6);
const [aiAnalysis, setAiAnalysis] = useState('');
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const [toast, setToast] = useState(null);
// --- URL Hydration ---
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const data = params.get('data');
if (data) {
try {
const decoded = JSON.parse(atob(data));
if (decoded.currentHome) setCurrentHome(decoded.currentHome);
if (decoded.newHome) setNewHome(decoded.newHome);
if (decoded.debts) setDebts(decoded.debts);
if (decoded.sellingCostsPercent) setSellingCostsPercent(decoded.sellingCostsPercent);
} catch (e) {
console.error("Failed to hydrate state from URL", e);
}
}
}, []);
// --- Toast logic ---
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
// --- Calculations ---
const calculateMortgage = (principal: number, annualRate: number, years: number) => {
if (annualRate === 0) return principal / (years * 12);
const monthlyRate = annualRate / 100 / 12;
const numberOfPayments = years * 12;
return (
(principal * monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
(Math.pow(1 + monthlyRate, numberOfPayments) - 1)
);
};
const results = useMemo((): CalculationResult => {
const sellingCosts = currentHome.value * (sellingCostsPercent / 100);
const netProceeds = currentHome.value - currentHome.mortgageBalance - sellingCosts;
const debtsToPayOff = debts.filter((d) => d.payOff);
const totalDebtBalanceEliminated = debtsToPayOff.reduce((acc, d) => acc + d.balance, 0);
const downPayment = Math.max(0, netProceeds - totalDebtBalanceEliminated);
const newMortgageAmount = Math.max(0, newHome.purchasePrice - downPayment);
const currentPI = calculateMortgage(currentHome.mortgageBalance, currentHome.interestRate, currentHome.termYears);
const currentDebtPayments = debts.reduce((acc, d) => acc + d.monthlyPayment, 0);
const currentTotal = currentPI + currentDebtPayments + currentHome.taxInsuranceEstimate;
const newPI = calculateMortgage(newMortgageAmount, newHome.interestRate, newHome.termYears);
const remainingDebts = debts.filter((d) => !d.payOff);
const remainingDebtPayments = remainingDebts.reduce((acc, d) => acc + d.monthlyPayment, 0);
const newTotal = newPI + newHome.taxInsuranceEstimate + remainingDebtPayments;
return {
currentMonthlyTotal: currentTotal,
newMonthlyTotal: newTotal,
monthlySavings: currentTotal - newTotal,
equityGained: currentHome.value - currentHome.mortgageBalance,
netProceeds,
downPayment,
newMortgageAmount,
debtsPaidOff: totalDebtBalanceEliminated,
};
}, [currentHome, newHome, debts, sellingCostsPercent]);
// --- Handlers ---
const handleDebtToggle = (id: string) => {
setDebts((prev) =>
prev.map((d) => (d.id === id ? { ...d, payOff: !d.payOff } : d))
);
};
const addDebt = () => {
const newDebt: Debt = {
id: Date.now().toString(),
name: 'New Debt',
balance: 0,
monthlyPayment: 0,
payOff: false,
};
setDebts([...debts, newDebt]);
};
const removeDebt = (id: string) => {
setDebts(debts.filter((d) => d.id !== id));
};
const updateDebt = (id: string, field: keyof Debt, value: any) => {
setDebts((prev) =>
prev.map((d) => (d.id === id ? { ...d, [field]: value } : d))
);
};
const runAI = useCallback(async () => {
setIsAnalyzing(true);
const analysis = await getAIAnalysis(currentHome, newHome, debts, results);
setAiAnalysis(analysis);
setIsAnalyzing(false);
}, [currentHome, newHome, debts, results]);
const generateShareLink = () => {
const state = { currentHome, newHome, debts, sellingCostsPercent };
const serialized = btoa(JSON.stringify(state));
const url = new URL(window.location.href);
url.searchParams.set('data', serialized);
return url.toString();
};
const copyToClipboard = (text: string, msg: string) => {
navigator.clipboard.writeText(text);
setToast(msg);
};
const shareSummary = () => {
const savings = Math.round(results.monthlySavings);
const debtPaid = Math.round(results.debtsPaidOff);
const summary = `Check out this Louisville Home Strategy I put together for you!
By using your existing home equity, we can:
- Eliminate $${debtPaid.toLocaleString()} in high-interest debt
- Get you into your new $${newHome.purchasePrice.toLocaleString()} home
- ${savings >= 0 ? `SAVE you $${Math.abs(savings).toLocaleString()} per month` : `Adjust your monthly outlay to $${Math.round(results.newMonthlyTotal).toLocaleString()}`}
See the full breakdown here: ${generateShareLink()}`;
copyToClipboard(summary, "Summary copied! Paste in text or DM.");
};
// --- Chart Data ---
const chartData = [
{
name: 'Current Outlay',
total: Math.round(results.currentMonthlyTotal),
mortgage: Math.round(calculateMortgage(currentHome.mortgageBalance, currentHome.interestRate, currentHome.termYears) + currentHome.taxInsuranceEstimate),
debt: Math.round(debts.reduce((acc, d) => acc + d.monthlyPayment, 0)),
},
{
name: 'Proposed Plan',
total: Math.round(results.newMonthlyTotal),
mortgage: Math.round(calculateMortgage(results.newMortgageAmount, newHome.interestRate, newHome.termYears) + newHome.taxInsuranceEstimate),
debt: Math.round(debts.filter(d => !d.payOff).reduce((acc, d) => acc + d.monthlyPayment, 0)),
},
];
return (
{/* Toast Notification */}
{toast && (
{toast}
)}
{/* Header */}
{/* Share Modal */}
{showShareModal && (
)}
{/* Left Column: Inputs */}
);
};
export default App;Louisville Equity Unlocker
Debt Consolidation & Home Upgrade Strategy
Potential Monthly Change
= 0 ? 'text-green-400' : 'text-red-400'}`}> {results.monthlySavings >= 0 ? '+' : ''}${Math.abs(Math.round(results.monthlySavings)).toLocaleString()} /mo
Share This Plan
Send these specific calculations to your client via text, DM, or email.
{/* Current Situation */}
setCurrentHome({ ...currentHome, value: v })}
prefix="$"
/>
setCurrentHome({ ...currentHome, mortgageBalance: v })}
prefix="$"
/>
setCurrentHome({ ...currentHome, interestRate: v })}
suffix="%"
step={0.1}
/>
setCurrentHome({ ...currentHome, taxInsuranceEstimate: v })}
prefix="$"
/>
setSellingCostsPercent(v)}
suffix="%"
step={0.5}
/>
{/* New Home Target */}
setNewHome({ ...newHome, purchasePrice: v })}
prefix="$"
/>
setNewHome({ ...newHome, interestRate: v })}
suffix="%"
step={0.125}
/>
setNewHome({ ...newHome, taxInsuranceEstimate: v })}
prefix="$"
/>
{/* Center/Right Column: Debt Management & Results */}
Current Situation
New Home Purchase
{/* Debt Console */}
{/* Results Visuals */}
{isAnalyzing ? (
) : (
)}
)}
{/* Detailed Breakdown Table */}
Existing Debts to Consolidate
{debts.map((debt) => (
))}
{debts.length === 0 &&
updateDebt(debt.id, 'name', e.target.value)}
/>
updateDebt(debt.id, 'balance', parseFloat(e.target.value))}
/>
updateDebt(debt.id, 'monthlyPayment', parseFloat(e.target.value))}
/>
No debts added. Add them to see the consolidation impact!
}
{/* Visual Chart */}
`$${value.toLocaleString()}`}
/>
{/* Quick Stats Grid */}
{/* AI Strategy Summary */}
{(aiAnalysis || isAnalyzing) && (
Monthly Expense Comparison
Net Proceeds
${Math.round(results.netProceeds).toLocaleString()}
Debt Paid Off
${Math.round(results.debtsPaidOff).toLocaleString()}
New Down Payment
${Math.round(results.downPayment).toLocaleString()}
Loan-to-Value
{Math.round((results.newMortgageAmount / newHome.purchasePrice) * 100)}%
Louisville Realtor AI Analysis
{aiAnalysis}
Full Monthly Budget Breakdown
| Expense Type | Current ({currentHome.interestRate}%) | Proposed ({newHome.interestRate}%) | Change |
|---|---|---|---|
| Mortgage (P&I) | ${Math.round(calculateMortgage(currentHome.mortgageBalance, currentHome.interestRate, currentHome.termYears)).toLocaleString()} | ${Math.round(calculateMortgage(results.newMortgageAmount, newHome.interestRate, newHome.termYears)).toLocaleString()} | +${Math.round(calculateMortgage(results.newMortgageAmount, newHome.interestRate, newHome.termYears) - calculateMortgage(currentHome.mortgageBalance, currentHome.interestRate, currentHome.termYears)).toLocaleString()} |
| Taxes & Insurance | ${currentHome.taxInsuranceEstimate.toLocaleString()} | ${newHome.taxInsuranceEstimate.toLocaleString()} | = currentHome.taxInsuranceEstimate ? 'text-red-500' : 'text-green-500'}`}> {newHome.taxInsuranceEstimate >= currentHome.taxInsuranceEstimate ? '+' : '-'}${Math.abs(newHome.taxInsuranceEstimate - currentHome.taxInsuranceEstimate).toLocaleString()} |
| Non-Mortgage Debt | ${debts.reduce((acc, d) => acc + d.monthlyPayment, 0).toLocaleString()} | ${debts.filter(d => !d.payOff).reduce((acc, d) => acc + d.monthlyPayment, 0).toLocaleString()} | -${(debts.filter(d => d.payOff).reduce((acc, d) => acc + d.monthlyPayment, 0)).toLocaleString()} |
| Total Monthly Outlay | ${Math.round(results.currentMonthlyTotal).toLocaleString()} | ${Math.round(results.newMonthlyTotal).toLocaleString()} | = 0 ? 'text-green-600' : 'text-red-600'}`}> {results.monthlySavings >= 0 ? 'Savings: ' : 'Increase: '} ${Math.abs(Math.round(results.monthlySavings)).toLocaleString()} |
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(
);
}