130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import type { MortgageSummary } from '@/lib/mortgage-engine'
|
|
import type { MortgageState } from '@/lib/mortgage-types'
|
|
|
|
interface Props {
|
|
state: MortgageState
|
|
summary: MortgageSummary
|
|
}
|
|
|
|
function fmt(n: number): string {
|
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n)
|
|
}
|
|
|
|
function fmtPct(n: number): string {
|
|
return `${n.toFixed(1)}%`
|
|
}
|
|
|
|
export default function ComparisonPanel({ state, summary }: Props) {
|
|
const repaidPct = state.totalPrincipal > 0
|
|
? (state.totalPrincipalPaid / state.totalPrincipal) * 100
|
|
: 0
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 text-sm">
|
|
{/* Progress */}
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Progress</h3>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<StatCard label="Principal Repaid" value={fmt(state.totalPrincipalPaid)} sub={fmtPct(repaidPct)} />
|
|
<StatCard label="Remaining" value={fmt(state.totalPrincipalRemaining)} />
|
|
<StatCard label="Interest Paid" value={fmt(state.totalInterestPaid)} color="text-amber-400" />
|
|
<StatCard label="Tranches Done" value={`${state.tranchesRepaid} / ${state.tranches.length}`} color="text-emerald-400" />
|
|
</div>
|
|
{/* Overall progress bar */}
|
|
<div className="mt-2">
|
|
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-sky-500 to-emerald-500 rounded-full transition-all duration-300"
|
|
style={{ width: `${Math.min(100, repaidPct)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Community Fund */}
|
|
{state.communityFundBalance > 0 && (
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Community Fund</h3>
|
|
<div className="bg-emerald-900/30 rounded p-3 border border-emerald-800/50">
|
|
<div className="text-lg font-mono text-emerald-400">{fmt(state.communityFundBalance)}</div>
|
|
<div className="text-xs text-slate-400 mt-1">Overflow directed to community resilience</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Comparison */}
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">
|
|
rMortgage vs Traditional
|
|
</h3>
|
|
<div className="space-y-3">
|
|
<CompareRow
|
|
label="Monthly Payment"
|
|
myco={fmt(summary.mycoMonthlyPayment)}
|
|
trad={fmt(summary.tradMonthlyPayment)}
|
|
/>
|
|
<CompareRow
|
|
label="Total Interest"
|
|
myco={fmt(summary.mycoTotalInterest)}
|
|
trad={fmt(summary.tradTotalInterest)}
|
|
highlight={summary.interestSaved > 0}
|
|
/>
|
|
<CompareRow
|
|
label="Payoff"
|
|
myco={`${Math.ceil(summary.mycoPayoffMonths / 12)}y`}
|
|
trad={`${Math.ceil(summary.tradPayoffMonths / 12)}y`}
|
|
highlight={summary.monthsSaved > 0}
|
|
/>
|
|
<CompareRow
|
|
label="Avg Lender Yield"
|
|
myco={fmtPct(summary.avgLenderYield)}
|
|
trad="N/A (bank)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Key insight */}
|
|
<div className="bg-gradient-to-br from-sky-900/40 to-emerald-900/40 rounded-lg p-4 border border-sky-800/30">
|
|
<h4 className="text-xs font-semibold text-sky-300 uppercase mb-2">Community Wealth</h4>
|
|
<div className="text-2xl font-mono text-emerald-400 mb-1">{fmt(summary.communityRetained)}</div>
|
|
<p className="text-xs text-slate-400">
|
|
Interest that stays in the community instead of flowing to a distant institution.
|
|
{summary.interestSaved > 0 && (
|
|
<> Plus {fmt(summary.interestSaved)} saved through distributed rates.</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StatCard({ label, value, sub, color }: { label: string; value: string; sub?: string; color?: string }) {
|
|
return (
|
|
<div className="bg-slate-800 rounded p-2">
|
|
<div className="text-[10px] text-slate-500 uppercase">{label}</div>
|
|
<div className={`text-sm font-mono ${color ?? 'text-white'}`}>
|
|
{value}
|
|
{sub && <span className="text-xs text-slate-400 ml-1">{sub}</span>}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CompareRow({ label, myco, trad, highlight }: { label: string; myco: string; trad: string; highlight?: boolean }) {
|
|
return (
|
|
<div className="grid grid-cols-3 gap-2 items-center">
|
|
<div className="text-xs text-slate-400">{label}</div>
|
|
<div className={`text-xs font-mono text-center px-2 py-1 rounded ${
|
|
highlight ? 'bg-emerald-900/40 text-emerald-400' : 'bg-slate-800 text-white'
|
|
}`}>
|
|
{myco}
|
|
</div>
|
|
<div className="text-xs font-mono text-center px-2 py-1 rounded bg-slate-800 text-slate-400">
|
|
{trad}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|