feat(rMortgage): aggregate pool viz, earnings comparison, fewer tranches
- Pool summary cards clickable → aggregate breakdown vessel showing total outstanding/repaid/reinvested with earnings comparison bar - Lender detail: earnings bar (interest vs reinvestment), advantage callout showing % gain from reinvesting returns - Borrower options reduced from 6 to 3 tranches (10yr, 20yr, 30yr) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d955d18af4
commit
a1f8702988
|
|
@ -166,7 +166,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
|
|
||||||
// Borrower options state
|
// Borrower options state
|
||||||
private borrowerMonthlyBudget = 1500;
|
private borrowerMonthlyBudget = 1500;
|
||||||
private borrowerOptionsVisible = false;
|
private showPoolOverview = false;
|
||||||
|
|
||||||
// Tour engine
|
// Tour engine
|
||||||
private _tour!: TourEngine;
|
private _tour!: TourEngine;
|
||||||
|
|
@ -5155,6 +5155,18 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
this.render();
|
this.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.shadow.querySelector('[data-action="toggle-pool"]')?.addEventListener('click', () => {
|
||||||
|
this.showPoolOverview = !this.showPoolOverview;
|
||||||
|
this.selectedLenderId = null;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.shadow.querySelector('[data-action="close-pool"]')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.showPoolOverview = false;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
this.shadow.querySelector('[data-action="update-borrower"]')?.addEventListener('click', () => {
|
this.shadow.querySelector('[data-action="update-borrower"]')?.addEventListener('click', () => {
|
||||||
const budgetEl = this.shadow.querySelector('[data-borrower="budget"]') as HTMLInputElement;
|
const budgetEl = this.shadow.querySelector('[data-borrower="budget"]') as HTMLInputElement;
|
||||||
if (budgetEl) this.borrowerMonthlyBudget = Math.max(parseFloat(budgetEl.value) || 100, 100);
|
if (budgetEl) this.borrowerMonthlyBudget = Math.max(parseFloat(budgetEl.value) || 100, 100);
|
||||||
|
|
@ -5278,15 +5290,35 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compute aggregate pool stats across all positions */
|
||||||
|
private computePoolStats() {
|
||||||
|
const reinvestApy = this.reinvestmentPositions.length > 0
|
||||||
|
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 4.0;
|
||||||
|
let totalBorrowed = 0, totalRepaid = 0, totalReinvested = 0, totalLoanEarnings = 0, totalReinvestEarnings = 0;
|
||||||
|
for (const m of this.mortgagePositions) {
|
||||||
|
if (m.status !== 'active' && m.status !== 'paid-off') continue;
|
||||||
|
const me = Math.floor((Date.now() - m.startDate) / (86400000 * 30));
|
||||||
|
const paid = m.monthlyPayment * me;
|
||||||
|
const intPaid = paid - (paid * m.principal / (m.monthlyPayment * m.termMonths));
|
||||||
|
const prinRepaid = Math.min(paid - intPaid, m.principal);
|
||||||
|
const idle = prinRepaid * 0.6;
|
||||||
|
totalBorrowed += m.principal;
|
||||||
|
totalRepaid += prinRepaid;
|
||||||
|
totalReinvested += idle;
|
||||||
|
totalLoanEarnings += intPaid;
|
||||||
|
totalReinvestEarnings += idle * (reinvestApy / 100) * (me / 12);
|
||||||
|
}
|
||||||
|
const deployed = this.reinvestmentPositions.reduce((s, r) => s + r.deposited, 0);
|
||||||
|
const yieldValue = this.reinvestmentPositions.reduce((s, r) => s + (r.currentValue - r.deposited), 0);
|
||||||
|
return { totalBorrowed, totalRepaid, totalReinvested, totalLoanEarnings, totalReinvestEarnings, deployed, yieldValue, reinvestApy };
|
||||||
|
}
|
||||||
|
|
||||||
private renderMortgageTab(): string {
|
private renderMortgageTab(): string {
|
||||||
if (this.loading) return '<div class="flows-loading">Loading mortgage data...</div>';
|
if (this.loading) return '<div class="flows-loading">Loading mortgage data...</div>';
|
||||||
|
|
||||||
const totalPool = this.mortgagePositions.reduce((s, m) => s + m.principal, 0);
|
const pool = this.computePoolStats();
|
||||||
const deployed = this.reinvestmentPositions.reduce((s, r) => s + r.deposited, 0);
|
|
||||||
const currentYieldValue = this.reinvestmentPositions.reduce((s, r) => s + (r.currentValue - r.deposited), 0);
|
|
||||||
const avgApy = this.reinvestmentPositions.length > 0
|
const avgApy = this.reinvestmentPositions.length > 0
|
||||||
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length
|
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
const selectedLender = this.selectedLenderId
|
const selectedLender = this.selectedLenderId
|
||||||
? this.mortgagePositions.find(m => m.id === this.selectedLenderId) || null
|
? this.mortgagePositions.find(m => m.id === this.selectedLenderId) || null
|
||||||
|
|
@ -5300,14 +5332,16 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
<span style="background: rgba(16,185,129,0.15); color: #10b981; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">BETA</span>
|
<span style="background: rgba(16,185,129,0.15); color: #10b981; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">BETA</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pool Summary Cards -->
|
<!-- Pool Summary — clickable to toggle aggregate view -->
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px;">
|
<div data-action="toggle-pool" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; cursor: pointer;" title="Click to view aggregate pool breakdown">
|
||||||
${this.renderPoolCard('Total Pool', this.fmtUsd(totalPool), 'Aggregate mortgage capital', '#3b82f6')}
|
${this.renderPoolCard('Total Borrowed', this.fmtUsd(pool.totalBorrowed), `${this.mortgagePositions.filter(m => m.status === 'active').length} active positions`, '#3b82f6')}
|
||||||
${this.renderPoolCard('Deployed to DeFi', this.fmtUsd(deployed), 'Idle capital reinvested', '#10b981')}
|
${this.renderPoolCard('Total Repaid', this.fmtUsd(pool.totalRepaid), `${Math.round(pool.totalRepaid / (pool.totalBorrowed || 1) * 100)}% of principal`, '#60a5fa')}
|
||||||
${this.renderPoolCard('Yield Earned', this.fmtUsd(currentYieldValue), 'From reinvestment positions', '#f59e0b')}
|
${this.renderPoolCard('Reinvested', this.fmtUsd(pool.totalReinvested), `Earning ${avgApy.toFixed(1)}% APY`, '#10b981')}
|
||||||
${this.renderPoolCard('Avg APY', avgApy.toFixed(2) + '%', 'Weighted pool return', '#8b5cf6')}
|
${this.renderPoolCard('Total Earnings', this.fmtUsd(pool.totalLoanEarnings + pool.totalReinvestEarnings), `Interest + yield`, '#f59e0b')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this.showPoolOverview ? this.renderPoolOverview(pool) : ''}
|
||||||
|
|
||||||
<!-- Active Mortgages -->
|
<!-- Active Mortgages -->
|
||||||
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid var(--rs-border, #2a2a3e);">
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid var(--rs-border, #2a2a3e);">
|
||||||
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Active Mortgages</h3>
|
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Active Mortgages</h3>
|
||||||
|
|
@ -5336,7 +5370,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
<!-- Borrower Options -->
|
<!-- Borrower Options -->
|
||||||
${this.renderBorrowerOptions()}
|
${this.renderBorrowerOptions()}
|
||||||
|
|
||||||
<!-- Reinvestment Tracker -->
|
<!-- Reinvestment & Rates -->
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px;">
|
||||||
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
|
||||||
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Reinvestment Positions</h3>
|
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Reinvestment Positions</h3>
|
||||||
|
|
@ -5354,8 +5388,6 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
`).join('')}
|
`).join('')}
|
||||||
${this.reinvestmentPositions.length === 0 ? '<div style="color: var(--rs-text-secondary); font-size: 14px;">No reinvestment positions yet</div>' : ''}
|
${this.reinvestmentPositions.length === 0 ? '<div style="color: var(--rs-text-secondary); font-size: 14px;">No reinvestment positions yet</div>' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Live Rates -->
|
|
||||||
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
|
||||||
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Live DeFi Rates</h3>
|
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Live DeFi Rates</h3>
|
||||||
${this.liveRates.map(r => `
|
${this.liveRates.map(r => `
|
||||||
|
|
@ -5401,6 +5433,75 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderPoolOverview(pool: ReturnType<typeof FolkFlowsApp.prototype.computePoolStats>): string {
|
||||||
|
const total = pool.totalBorrowed || 1;
|
||||||
|
const repaidPct = (pool.totalRepaid / total) * 100;
|
||||||
|
const reinvestedPct = (pool.totalReinvested / total) * 100;
|
||||||
|
const outstandingPct = Math.max(100 - repaidPct, 0);
|
||||||
|
const totalEarnings = pool.totalLoanEarnings + pool.totalReinvestEarnings;
|
||||||
|
// Earnings comparison — show reinvestment dominance
|
||||||
|
const loanPct = totalEarnings > 0 ? (pool.totalLoanEarnings / totalEarnings) * 100 : 50;
|
||||||
|
const reinvPct = totalEarnings > 0 ? (pool.totalReinvestEarnings / totalEarnings) * 100 : 50;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #8b5cf6;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||||
|
<h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Aggregate Pool Breakdown</h3>
|
||||||
|
<button data-action="close-pool" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 180px 1fr; gap: 24px;">
|
||||||
|
<!-- Aggregate vessel -->
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||||
|
<svg viewBox="0 0 120 200" width="140" height="200" style="margin-bottom: 8px;">
|
||||||
|
<rect x="10" y="10" width="100" height="180" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/>
|
||||||
|
<!-- Outstanding (empty) — top -->
|
||||||
|
<rect x="12" y="12" width="96" height="${outstandingPct * 1.76}" rx="10" fill="rgba(255,255,255,0.04)"/>
|
||||||
|
<!-- Repaid (blue) — fills from bottom -->
|
||||||
|
<rect x="12" y="${12 + outstandingPct * 1.76}" width="96" height="${(repaidPct - reinvestedPct) * 1.76}" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
<!-- Reinvested (green) — bottom portion of repaid -->
|
||||||
|
<rect x="12" y="${12 + (100 - reinvestedPct) * 1.76}" width="96" height="${reinvestedPct * 1.76}" rx="10" fill="rgba(16,185,129,0.5)"/>
|
||||||
|
${outstandingPct > 12 ? `<text x="60" y="${12 + outstandingPct * 0.88}" text-anchor="middle" fill="var(--rs-text-secondary)" font-size="10">${Math.round(outstandingPct)}% Outstanding</text>` : ''}
|
||||||
|
${(repaidPct - reinvestedPct) > 12 ? `<text x="60" y="${12 + outstandingPct * 1.76 + (repaidPct - reinvestedPct) * 0.88}" text-anchor="middle" fill="#60a5fa" font-size="10" font-weight="600">${Math.round(repaidPct - reinvestedPct)}% Repaid</text>` : ''}
|
||||||
|
${reinvestedPct > 8 ? `<text x="60" y="${12 + (100 - reinvestedPct * 0.5) * 1.76}" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">${Math.round(reinvestedPct)}% Reinvested</text>` : ''}
|
||||||
|
</svg>
|
||||||
|
<div style="font-size: 11px; color: var(--rs-text-secondary);">${this.fmtUsd(pool.totalBorrowed)} total</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aggregate stats + earnings comparison -->
|
||||||
|
<div>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 20px;">
|
||||||
|
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
|
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Outstanding</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(pool.totalBorrowed - pool.totalRepaid)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
|
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(pool.totalRepaid)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
|
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 700; color: #10b981;">${this.fmtUsd(pool.totalReinvested)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Earnings comparison bar -->
|
||||||
|
<div style="margin-bottom: 8px; font-size: 13px; font-weight: 600; color: var(--rs-text-primary);">Earnings Breakdown: ${this.fmtUsd(totalEarnings)}</div>
|
||||||
|
<div style="height: 32px; border-radius: 8px; overflow: hidden; display: flex; margin-bottom: 8px;">
|
||||||
|
<div style="width: ${loanPct}%; background: #f59e0b; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;">${loanPct > 15 ? 'Interest ' + this.fmtUsd(pool.totalLoanEarnings) : ''}</div>
|
||||||
|
<div style="width: ${reinvPct}%; background: #10b981; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;">${reinvPct > 15 ? 'Reinvestment ' + this.fmtUsd(pool.totalReinvestEarnings) : ''}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 16px; font-size: 12px;">
|
||||||
|
<span style="display: flex; align-items: center; gap: 4px;"><span style="width: 10px; height: 10px; border-radius: 2px; background: #f59e0b; display: inline-block;"></span> Loan interest: ${this.fmtUsd(pool.totalLoanEarnings)}</span>
|
||||||
|
<span style="display: flex; align-items: center; gap: 4px;"><span style="width: 10px; height: 10px; border-radius: 2px; background: #10b981; display: inline-block;"></span> Reinvestment yield: ${this.fmtUsd(pool.totalReinvestEarnings)}</span>
|
||||||
|
</div>
|
||||||
|
${pool.totalReinvestEarnings > pool.totalLoanEarnings
|
||||||
|
? `<div style="margin-top: 12px; padding: 10px 14px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 8px; font-size: 12px; color: #10b981;">Reinvesting returns earns ${((pool.totalReinvestEarnings / (pool.totalLoanEarnings || 1) - 1) * 100).toFixed(0)}% more than interest alone. Compounding idle capital is the most profitable strategy.</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderPoolCard(title: string, value: string, subtitle: string, color: string): string {
|
private renderPoolCard(title: string, value: string, subtitle: string, color: string): string {
|
||||||
return `
|
return `
|
||||||
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e); border-top: 3px solid ${color};">
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e); border-top: 3px solid ${color};">
|
||||||
|
|
@ -5427,26 +5528,32 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLenderDetail(m: MortgagePosition): string {
|
private renderLenderDetail(m: MortgagePosition): string {
|
||||||
// Calculate repayment progress
|
|
||||||
const monthsElapsed = Math.floor((Date.now() - m.startDate) / (86400000 * 30));
|
const monthsElapsed = Math.floor((Date.now() - m.startDate) / (86400000 * 30));
|
||||||
const totalPaid = m.monthlyPayment * monthsElapsed;
|
const totalPaid = m.monthlyPayment * monthsElapsed;
|
||||||
const interestPaid = totalPaid - (totalPaid * m.principal / (m.monthlyPayment * m.termMonths));
|
const interestPaid = totalPaid - (totalPaid * m.principal / (m.monthlyPayment * m.termMonths));
|
||||||
const principalRepaid = Math.min(totalPaid - interestPaid, m.principal);
|
const principalRepaid = Math.min(totalPaid - interestPaid, m.principal);
|
||||||
const remaining = Math.max(m.principal - principalRepaid, 0);
|
|
||||||
|
|
||||||
// Simulate reinvestment of idle capital from this position
|
const idleCapital = principalRepaid * 0.6;
|
||||||
const idleCapital = principalRepaid * 0.6; // 60% of repaid principal goes to reinvestment
|
|
||||||
const reinvestApy = this.reinvestmentPositions.length > 0
|
const reinvestApy = this.reinvestmentPositions.length > 0
|
||||||
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length
|
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 4.0;
|
||||||
: 4.0;
|
|
||||||
const reinvestEarnings = idleCapital * (reinvestApy / 100) * (monthsElapsed / 12);
|
const reinvestEarnings = idleCapital * (reinvestApy / 100) * (monthsElapsed / 12);
|
||||||
const loanEarnings = interestPaid;
|
const loanEarnings = interestPaid;
|
||||||
|
const totalEarnings = loanEarnings + reinvestEarnings;
|
||||||
|
|
||||||
// Vessel proportions
|
// Vessel: outstanding at top (empty), repaid-idle in middle (blue), reinvested at bottom (green)
|
||||||
const vesselTotal = m.principal;
|
const total = m.principal || 1;
|
||||||
const repaidPct = (principalRepaid / vesselTotal) * 100;
|
const outstandingPct = Math.max(((m.principal - principalRepaid) / total) * 100, 0);
|
||||||
const reinvestedPct = (idleCapital / vesselTotal) * 100;
|
const repaidIdlePct = ((principalRepaid - idleCapital) / total) * 100;
|
||||||
const emptyPct = Math.max(100 - repaidPct - reinvestedPct, 0);
|
const reinvestedPct = (idleCapital / total) * 100;
|
||||||
|
|
||||||
|
// Earnings comparison
|
||||||
|
const earningsTotal = totalEarnings || 1;
|
||||||
|
const loanPct = (loanEarnings / earningsTotal) * 100;
|
||||||
|
const reinvPct = (reinvestEarnings / earningsTotal) * 100;
|
||||||
|
|
||||||
|
// Project: what if they reinvest ALL repaid principal vs none
|
||||||
|
const noReinvestEarnings = loanEarnings;
|
||||||
|
const fullReinvestEarnings = loanEarnings + (principalRepaid * (reinvestApy / 100) * (monthsElapsed / 12));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #3b82f6;">
|
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #3b82f6;">
|
||||||
|
|
@ -5454,48 +5561,60 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
<h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Lender Pool: ${this.esc(m.borrower)}</h3>
|
<h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Lender Pool: ${this.esc(m.borrower)}</h3>
|
||||||
<button data-action="close-lender" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">×</button>
|
<button data-action="close-lender" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 160px 1fr; gap: 24px;">
|
||||||
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 24px;">
|
<!-- Vessel -->
|
||||||
<!-- Vessel visualization -->
|
|
||||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||||
<svg viewBox="0 0 120 180" width="120" height="180" style="margin-bottom: 8px;">
|
<svg viewBox="0 0 120 200" width="130" height="200" style="margin-bottom: 8px;">
|
||||||
<!-- Vessel outline -->
|
<rect x="10" y="10" width="100" height="180" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/>
|
||||||
<rect x="10" y="10" width="100" height="160" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/>
|
<!-- Outstanding (empty) -->
|
||||||
<!-- Empty (remaining principal) -->
|
<rect x="12" y="12" width="96" height="${outstandingPct * 1.76}" rx="10" fill="rgba(255,255,255,0.04)"/>
|
||||||
<rect x="12" y="${12 + (repaidPct + reinvestedPct) * 1.56}" width="96" height="${emptyPct * 1.56}" rx="10" fill="rgba(255,255,255,0.05)"/>
|
<!-- Repaid idle (blue) -->
|
||||||
|
<rect x="12" y="${12 + outstandingPct * 1.76}" width="96" height="${repaidIdlePct * 1.76}" fill="rgba(59,130,246,0.4)"/>
|
||||||
<!-- Reinvested (green) -->
|
<!-- Reinvested (green) -->
|
||||||
<rect x="12" y="${12 + repaidPct * 1.56}" width="96" height="${reinvestedPct * 1.56}" rx="${reinvestedPct > 0 ? 0 : 10}" fill="rgba(16,185,129,0.4)"/>
|
<rect x="12" y="${12 + (outstandingPct + repaidIdlePct) * 1.76}" width="96" height="${reinvestedPct * 1.76}" rx="10" fill="rgba(16,185,129,0.5)"/>
|
||||||
<!-- Repaid (blue) -->
|
${outstandingPct > 15 ? `<text x="60" y="${12 + outstandingPct * 0.88}" text-anchor="middle" fill="var(--rs-text-secondary)" font-size="10">${Math.round(outstandingPct)}%</text>` : ''}
|
||||||
<rect x="12" y="12" width="96" height="${repaidPct * 1.56}" rx="10" fill="rgba(59,130,246,0.4)"/>
|
${repaidIdlePct > 12 ? `<text x="60" y="${12 + outstandingPct * 1.76 + repaidIdlePct * 0.88}" text-anchor="middle" fill="#60a5fa" font-size="10" font-weight="600">${Math.round(repaidIdlePct)}% Idle</text>` : ''}
|
||||||
<!-- Labels -->
|
${reinvestedPct > 10 ? `<text x="60" y="${12 + (outstandingPct + repaidIdlePct) * 1.76 + reinvestedPct * 0.88}" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">${Math.round(reinvestedPct)}% DeFi</text>` : ''}
|
||||||
${repaidPct > 10 ? `<text x="60" y="${12 + repaidPct * 0.78}" text-anchor="middle" fill="#60a5fa" font-size="11" font-weight="600">${Math.round(repaidPct)}% Repaid</text>` : ''}
|
|
||||||
${reinvestedPct > 10 ? `<text x="60" y="${12 + repaidPct * 1.56 + reinvestedPct * 0.78}" text-anchor="middle" fill="#10b981" font-size="11" font-weight="600">${Math.round(reinvestedPct)}% Reinvested</text>` : ''}
|
|
||||||
</svg>
|
</svg>
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-align: center;">Pool: ${this.fmtUsd(vesselTotal)}</div>
|
<div style="font-size: 11px; color: var(--rs-text-secondary);">${this.fmtUsd(m.principal)} pool</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats breakdown -->
|
<div>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
<!-- Stats row -->
|
||||||
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 16px;">
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid Principal</div>
|
<div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
<div style="font-size: 20px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(principalRepaid)}</div>
|
<div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(principalRepaid)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
|
<div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700; color: #10b981;">${this.fmtUsd(idleCapital)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
||||||
|
<div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Outstanding</div>
|
||||||
|
<div style="font-size: 16px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(m.principal - principalRepaid)}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested Capital</div>
|
<!-- Earnings comparison bar -->
|
||||||
<div style="font-size: 20px; font-weight: 700; color: #10b981;">${this.fmtUsd(idleCapital)}</div>
|
<div style="font-size: 13px; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 6px;">Earnings: ${this.fmtUsd(totalEarnings)} <span style="font-size: 11px; color: var(--rs-text-secondary); font-weight: 400;">${monthsElapsed}mo of ${m.termMonths}mo</span></div>
|
||||||
|
<div style="height: 28px; border-radius: 6px; overflow: hidden; display: flex; margin-bottom: 6px;">
|
||||||
|
<div style="width: ${loanPct}%; background: #f59e0b; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: 600;">${loanPct > 20 ? this.fmtUsd(loanEarnings) : ''}</div>
|
||||||
|
<div style="width: ${reinvPct}%; background: #10b981; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: 600;">${reinvPct > 20 ? this.fmtUsd(reinvestEarnings) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
<div style="display: flex; gap: 14px; font-size: 11px; color: var(--rs-text-secondary); margin-bottom: 12px;">
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Loan Interest Earned</div>
|
<span><span style="width: 8px; height: 8px; border-radius: 2px; background: #f59e0b; display: inline-block; margin-right: 4px;"></span>Interest ${this.fmtUsd(loanEarnings)}</span>
|
||||||
<div style="font-size: 20px; font-weight: 700; color: #f59e0b;">${this.fmtUsd(loanEarnings)}</div>
|
<span><span style="width: 8px; height: 8px; border-radius: 2px; background: #10b981; display: inline-block; margin-right: 4px;"></span>Reinvestment ${this.fmtUsd(reinvestEarnings)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
|
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvestment Yield</div>
|
<!-- Reinvestment advantage callout -->
|
||||||
<div style="font-size: 20px; font-weight: 700; color: #10b981;">${this.fmtUsd(reinvestEarnings)}</div>
|
<div style="padding: 10px 14px; background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.25); border-radius: 8px; font-size: 12px;">
|
||||||
</div>
|
<div style="color: #10b981; font-weight: 600; margin-bottom: 4px;">Reinvestment advantage</div>
|
||||||
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px; grid-column: span 2;">
|
<div style="color: var(--rs-text-secondary);">
|
||||||
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Total Earnings</div>
|
Without reinvestment: <strong style="color: var(--rs-text-primary);">${this.fmtUsd(noReinvestEarnings)}</strong>
|
||||||
<div style="font-size: 24px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(loanEarnings + reinvestEarnings)}</div>
|
→ With full reinvestment: <strong style="color: #10b981;">${this.fmtUsd(fullReinvestEarnings)}</strong>
|
||||||
<div style="font-size: 12px; color: var(--rs-text-secondary);">${monthsElapsed} months elapsed of ${m.termMonths}mo term</div>
|
<span style="color: #10b981; font-weight: 600;"> (+${((fullReinvestEarnings / (noReinvestEarnings || 1) - 1) * 100).toFixed(0)}%)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5503,7 +5622,7 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderBorrowerOptions(): string {
|
private renderBorrowerOptions(): string {
|
||||||
const termOptions = [60, 120, 180, 240, 300, 360]; // 5yr, 10yr, 15yr, 20yr, 25yr, 30yr
|
const termOptions = [120, 240, 360]; // 10yr, 20yr, 30yr
|
||||||
const budget = this.borrowerMonthlyBudget;
|
const budget = this.borrowerMonthlyBudget;
|
||||||
|
|
||||||
// Build lender pool: each active lender has available capital (simulated from repaid principal)
|
// Build lender pool: each active lender has available capital (simulated from repaid principal)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue