WM01 / portfolio.html
AndyKandy26's picture
Upload 9 files
00e4c29 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FinWise — Portfolio Builder</title>
<link rel="stylesheet" href="shared.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
.builder-steps {
display: flex;
gap: 8px;
margin-bottom: 28px;
background: var(--bg3);
border-radius: var(--r-sm);
padding: 4px;
}
.step-btn {
flex: 1;
padding: 10px 8px;
border-radius: 6px;
border: none;
background: transparent;
color: var(--text2);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
text-align: center;
white-space: nowrap;
}
.step-btn.active {
background: var(--card);
color: var(--cyan);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.step-num {
display: inline-flex;
width: 20px; height: 20px;
background: var(--bg3);
border-radius: 50%;
align-items: center; justify-content: center;
font-size: 11px;
margin-right: 6px;
}
.step-btn.active .step-num { background: var(--cyan); color: var(--bg); }
.step-panel { display: none; }
.step-panel.active { display: block; }
.goal-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.goal-card {
background: var(--bg3);
border: 2px solid var(--border);
border-radius: var(--r);
padding: 20px 16px;
text-align: center;
cursor: pointer;
transition: all var(--transition);
}
.goal-card:hover { border-color: var(--border2); transform: translateY(-2px); }
.goal-card.selected { border-color: var(--cyan); background: rgba(34,211,238,0.08); box-shadow: var(--glow-c); }
.goal-icon { font-size: 32px; margin-bottom: 8px; }
.goal-title { font-size: 13px; font-weight: 700; margin-bottom: 4px; }
.goal-desc { font-size: 11px; color: var(--text2); }
.risk-slider-wrap {
padding: 20px;
background: var(--bg3);
border-radius: var(--r);
margin-bottom: 20px;
}
.risk-labels {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text2);
margin-top: 10px;
}
.risk-profile-display {
text-align: center;
padding: 16px;
background: rgba(34,211,238,0.06);
border-radius: var(--r-sm);
border: 1px solid var(--border2);
margin-top: 16px;
}
.rp-icon { font-size: 36px; margin-bottom: 6px; }
.rp-label { font-family: var(--font-head); font-size: 20px; font-weight: 700; }
.rp-desc { font-size: 13px; color: var(--text2); margin-top: 4px; }
.asset-card {
display: flex;
align-items: center;
gap: 14px;
padding: 16px;
background: var(--bg3);
border-radius: var(--r-sm);
margin-bottom: 10px;
border: 1px solid var(--border);
transition: all var(--transition);
}
.asset-card:hover { border-color: var(--border2); }
.asset-logo {
width: 42px; height: 42px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-weight: 800;
font-size: 11px;
font-family: var(--font-mono);
flex-shrink: 0;
color: var(--bg);
}
.asset-info { flex: 1; min-width: 0; }
.asset-ticker { font-weight: 700; font-size: 15px; }
.asset-name { font-size: 12px; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.asset-type { margin-top: 3px; }
.asset-sliders { flex: 2; }
.asset-pct-val {
font-family: var(--font-mono);
font-size: 15px;
font-weight: 700;
color: var(--cyan);
min-width: 46px;
text-align: right;
}
.asset-dollar { font-size: 11px; color: var(--text2); text-align: right; margin-top: 2px; }
.asset-remove {
background: rgba(244,63,94,0.1);
border: none;
color: var(--rose);
border-radius: 6px;
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
font-size: 14px;
transition: background var(--transition);
flex-shrink: 0;
}
.asset-remove:hover { background: rgba(244,63,94,0.25); }
.add-asset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
margin-top: 16px;
}
.add-asset-btn {
padding: 10px 8px;
background: var(--bg3);
border: 1px dashed var(--border2);
border-radius: var(--r-sm);
color: var(--text2);
font-size: 12px;
font-weight: 600;
cursor: pointer;
text-align: center;
transition: all var(--transition);
font-family: var(--font-body);
}
.add-asset-btn:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.05); }
.add-asset-btn.in-portfolio { border-style: solid; border-color: var(--emerald); color: var(--emerald); background: rgba(16,185,129,0.06); }
.portfolio-summary-side {
position: sticky;
top: 20px;
}
.pie-wrap { height: 220px; position: relative; }
.portfolio-total {
text-align: center;
padding: 16px;
background: var(--bg3);
border-radius: var(--r-sm);
margin: 16px 0;
}
.pt-label { font-size: 12px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.06em; }
.pt-value { font-family: var(--font-head); font-size: 28px; font-weight: 800; color: var(--cyan); }
.balance-warning {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: var(--r-sm);
font-size: 13px;
margin-bottom: 16px;
}
.balance-warning.ok { background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); color: var(--emerald); }
.balance-warning.warn { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); }
.balance-warning.err { background: rgba(244,63,94,0.1); border: 1px solid rgba(244,63,94,0.3); color: var(--rose); }
.rebal-btn {
width: 100%;
padding: 10px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--text2);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
font-family: var(--font-body);
margin-bottom: 8px;
}
.rebal-btn:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.05); }
</style>
</head>
<body>
<div class="app-shell">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-logo">
<div class="logo-mark">
<div class="logo-icon">📈</div>
<div><div class="logo-text">FinWise</div><div class="logo-sub">Smart Investing</div></div>
</div>
</div>
<div class="nav-section">
<div class="nav-label">Main</div>
<a href="index.html" class="nav-item"><span class="nav-icon">🏠</span> Dashboard</a>
<a href="portfolio.html" class="nav-item"><span class="nav-icon">📊</span> Portfolio Builder</a>
<a href="risk.html" class="nav-item"><span class="nav-icon">🎯</span> Risk Analyzer</a>
<a href="tracker.html" class="nav-item"><span class="nav-icon">📈</span> Tracker</a>
<div class="nav-label">Tools</div>
<a href="calculators.html" class="nav-item"><span class="nav-icon">🧮</span> Calculators</a>
<a href="insights.html" class="nav-item"><span class="nav-icon">💡</span> Insights</a>
</div>
<div class="sidebar-footer">
<div class="market-ticker">Live Market</div>
<div id="sidebar-tickers"></div>
</div>
</nav>
<main class="main-content">
<div class="page-header fade-in">
<div class="page-title">Portfolio <span>Builder</span></div>
<div class="page-subtitle">Build a smart, diversified portfolio in 3 easy steps</div>
</div>
<!-- Step Nav -->
<div class="builder-steps fade-in">
<button class="step-btn active" data-step="1"><span class="step-num">1</span>Your Goals</button>
<button class="step-btn" data-step="2"><span class="step-num">2</span>Risk Profile</button>
<button class="step-btn" data-step="3"><span class="step-num">3</span>Build & Tune</button>
</div>
<div class="grid-60-40">
<!-- Left: Steps -->
<div>
<!-- Step 1: Goals -->
<div class="step-panel active" id="step-1">
<div class="card fade-in">
<div class="section-title">🎯 What are you investing for?</div>
<div class="text-muted text-sm" style="margin-bottom:20px">Select one or more goals (you can always change later)</div>
<div class="goal-cards" id="goal-cards">
<div class="goal-card" data-goal="Retirement"><div class="goal-icon">🏖️</div><div class="goal-title">Retirement</div><div class="goal-desc">Long-term wealth for a comfortable retirement</div></div>
<div class="goal-card" data-goal="Wealth Building"><div class="goal-icon">💰</div><div class="goal-title">Wealth Building</div><div class="goal-desc">Grow your money over time</div></div>
<div class="goal-card" data-goal="Down Payment"><div class="goal-icon">🏠</div><div class="goal-title">Home Purchase</div><div class="goal-desc">Save for a down payment</div></div>
<div class="goal-card" data-goal="Education"><div class="goal-icon">📚</div><div class="goal-title">Education</div><div class="goal-desc">Fund for college or courses</div></div>
<div class="goal-card" data-goal="Emergency Fund"><div class="goal-icon">🛡️</div><div class="goal-title">Emergency Fund</div><div class="goal-desc">Safety net for unexpected expenses</div></div>
<div class="goal-card" data-goal="Passive Income"><div class="goal-icon">💸</div><div class="goal-title">Passive Income</div><div class="goal-desc">Earn dividends & interest</div></div>
</div>
<div class="slider-wrap">
<div class="slider-header">
<span class="slider-label">💵 How much to invest?</span>
<span class="slider-val" id="invest-display">$5,000</span>
</div>
<input type="range" id="invest-slider" min="500" max="100000" value="5000" step="500">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text3);margin-top:4px">
<span>$500</span><span>$100K</span>
</div>
</div>
<button class="btn btn-primary" onclick="goStep(2)">Next: Set Risk Profile →</button>
</div>
</div>
<!-- Step 2: Risk -->
<div class="step-panel" id="step-2">
<div class="card fade-in">
<div class="section-title">🎯 Your Risk Tolerance</div>
<div class="text-muted text-sm" style="margin-bottom:16px">Drag the slider to find your comfort level</div>
<div class="risk-slider-wrap">
<div class="slider-header">
<span class="slider-label">Risk Level</span>
<span class="slider-val" id="risk-val-display">Moderate</span>
</div>
<input type="range" id="risk-slider" min="1" max="5" value="3" step="1">
<div class="risk-labels">
<span>🐢 Very Conservative</span>
<span>🦁 Very Aggressive</span>
</div>
</div>
<div class="risk-profile-display" id="risk-profile-box">
<div class="rp-icon">⚖️</div>
<div class="rp-label">Moderate Investor</div>
<div class="rp-desc">Balanced mix of growth and stability. You're OK with some market swings for better long-term returns.</div>
</div>
<div class="flex gap-12" style="margin-top:20px">
<button class="btn btn-ghost" onclick="goStep(1)">← Back</button>
<button class="btn btn-primary" onclick="goStep(3);buildPortfolio()">Generate Portfolio →</button>
</div>
</div>
</div>
<!-- Step 3: Build -->
<div class="step-panel" id="step-3">
<div class="card fade-in">
<div class="flex justify-between items-center" style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:0">🏗️ Your Portfolio</div>
<div class="flex gap-8">
<button class="rebal-btn" style="width:auto;padding:8px 14px" onclick="autoRebalance()">⚖️ Auto-rebalance</button>
</div>
</div>
<div id="asset-balance-warning" class="balance-warning ok" style="margin-bottom:14px">
✅ Portfolio is balanced at 100%
</div>
<div id="asset-list"></div>
<div class="divider"></div>
<div class="section-title" style="font-size:14px">➕ Add Assets</div>
<div class="add-asset-grid" id="add-asset-grid"></div>
<div class="flex gap-12" style="margin-top:20px">
<button class="btn btn-ghost" onclick="goStep(2)">← Back</button>
<button class="btn btn-emerald btn-full" onclick="savePortfolio_()">💾 Save Portfolio</button>
</div>
</div>
</div>
</div>
<!-- Right: Live Preview -->
<div class="portfolio-summary-side">
<div class="card fade-in-1">
<div class="card-title">🔴 Live Preview</div>
<div class="pie-wrap">
<canvas id="previewPieChart"></canvas>
</div>
<div class="portfolio-total">
<div class="pt-label">Total Value</div>
<div class="pt-value" id="preview-total">$5,000</div>
</div>
<div id="preview-legend" style="margin-top:8px"></div>
<div class="divider"></div>
<div class="card-title">📊 Portfolio Metrics</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px">
<div style="background:var(--bg3);border-radius:var(--r-sm);padding:12px;text-align:center">
<div style="font-size:11px;color:var(--text2);margin-bottom:4px">Risk Score</div>
<div style="font-family:var(--font-head);font-size:20px;font-weight:800;color:var(--amber)" id="preview-risk"></div>
</div>
<div style="background:var(--bg3);border-radius:var(--r-sm);padding:12px;text-align:center">
<div style="font-size:11px;color:var(--text2);margin-bottom:4px">Diversification</div>
<div style="font-family:var(--font-head);font-size:20px;font-weight:800;color:var(--emerald)" id="preview-div"></div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Mobile Bottom Nav -->
<nav class="bottom-nav">
<div class="bottom-nav-inner">
<a href="index.html" class="bottom-nav-item"><span class="bnav-icon">🏠</span>Home</a>
<a href="portfolio.html" class="bottom-nav-item"><span class="bnav-icon">📊</span>Portfolio</a>
<a href="risk.html" class="bottom-nav-item"><span class="bnav-icon">🎯</span>Risk</a>
<a href="tracker.html" class="bottom-nav-item"><span class="bnav-icon">📈</span>Track</a>
<a href="calculators.html" class="bottom-nav-item"><span class="bnav-icon">🧮</span>Calc</a>
<a href="insights.html" class="bottom-nav-item"><span class="bnav-icon">💡</span>Insights</a>
</div>
</nav>
<script src="shared.js"></script>
<script>
// ── State ─────────────────────────────────────────────────────────
let portfolio = getPortfolio();
let investAmount = portfolio.totalInvested || 5000;
let riskLevel = 3; // 1-5
let selectedGoals = portfolio.goals || ['Wealth Building'];
let previewChart = null;
const RISK_PROFILES = {
1: { icon:'🐢', label:'Very Conservative', desc:'Capital preservation is your #1 priority. Heavy bonds & cash equivalents.', template:[30,0,5,0,50,10,5] },
2: { icon:'🛡️', label:'Conservative', desc:'Modest growth with low risk. Bonds-heavy with some equity exposure.', template:[20,10,10,5,40,10,5] },
3: { icon:'⚖️', label:'Moderate', desc:'Balanced growth & stability. OK with some swings for better returns.', template:[30,20,15,12,13,7,3] },
4: { icon:'🚀', label:'Aggressive', desc:'Growth-focused. Higher potential returns, higher volatility accepted.', template:[35,30,20,12,0,3,0] },
5: { icon:'🦁', label:'Very Aggressive', desc:'Maximum growth. Concentrated tech & equities. High-risk, high-reward.', template:[20,35,30,15,0,0,0] },
};
const ALL_ASSETS = [
{ ticker:'VOO', name:'Vanguard S&P 500 ETF', price:478.22, color:'#22d3ee', type:'ETF' },
{ ticker:'VTI', name:'Vanguard Total Market', price:240.30, color:'#0ea5e9', type:'ETF' },
{ ticker:'QQQ', name:'Invesco Nasdaq 100', price:456.80, color:'#8b5cf6', type:'ETF' },
{ ticker:'NVDA', name:'NVIDIA Corp', price:875.40, color:'#10b981', type:'Stock' },
{ ticker:'AAPL', name:'Apple Inc.', price:188.60, color:'#f59e0b', type:'Stock' },
{ ticker:'AMZN', name:'Amazon.com Inc.', price:188.90, color:'#0891b2', type:'Stock' },
{ ticker:'TSLA', name:'Tesla Inc.', price:182.30, color:'#f43f5e', type:'Stock' },
{ ticker:'WMT', name:'Walmart Inc.', price: 67.80, color:'#34d399', type:'Stock' },
{ ticker:'MCD', name:"McDonald's Corp", price:281.50, color:'#fb923c', type:'Stock' },
{ ticker:'BND', name:'Vanguard Bond ETF', price: 73.40, color:'#6366f1', type:'Bond' },
{ ticker:'GLD', name:'SPDR Gold Trust', price:218.10, color:'#fbbf24', type:'Commodity' },
{ ticker:'SLV', name:'iShares Silver Trust', price: 28.60, color:'#94a3b8', type:'Commodity' },
];
// ── Step Navigation ───────────────────────────────────────────────
function goStep(n) {
document.querySelectorAll('.step-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active'));
document.getElementById('step-' + n).classList.add('active');
document.querySelector(`.step-btn[data-step="${n}"]`).classList.add('active');
}
document.querySelectorAll('.step-btn').forEach(btn => {
btn.addEventListener('click', () => {
const s = parseInt(btn.dataset.step);
if (s === 3) buildPortfolio();
goStep(s);
});
});
// ── Goal Cards ────────────────────────────────────────────────────
document.querySelectorAll('.goal-card').forEach(card => {
card.addEventListener('click', () => {
const goal = card.dataset.goal;
const idx = selectedGoals.indexOf(goal);
if (idx >= 0) selectedGoals.splice(idx, 1);
else selectedGoals.push(goal);
card.classList.toggle('selected');
});
});
// Pre-select saved goals
selectedGoals.forEach(g => {
const c = document.querySelector(`.goal-card[data-goal="${g}"]`);
if (c) c.classList.add('selected');
});
// ── Investment Slider ─────────────────────────────────────────────
const investSlider = document.getElementById('invest-slider');
investSlider.value = investAmount;
document.getElementById('invest-display').textContent = '$' + investAmount.toLocaleString();
investSlider.addEventListener('input', () => {
investAmount = parseInt(investSlider.value);
document.getElementById('invest-display').textContent = '$' + investAmount.toLocaleString();
document.getElementById('preview-total').textContent = '$' + investAmount.toLocaleString();
updatePortfolioAmounts();
});
// ── Risk Slider ───────────────────────────────────────────────────
const riskSlider = document.getElementById('risk-slider');
riskSlider.addEventListener('input', () => {
riskLevel = parseInt(riskSlider.value);
updateRiskDisplay();
});
function updateRiskDisplay() {
const profile = RISK_PROFILES[riskLevel];
const box = document.getElementById('risk-profile-box');
box.querySelector('.rp-icon').textContent = profile.icon;
box.querySelector('.rp-label').textContent = profile.label;
box.querySelector('.rp-desc').textContent = profile.desc;
document.getElementById('risk-val-display').textContent = profile.label;
}
// ── Build Portfolio from template ─────────────────────────────────
function buildPortfolio() {
const template = RISK_PROFILES[riskLevel].template;
const baseAssets = [
ALL_ASSETS.find(a=>a.ticker==='VOO'),
ALL_ASSETS.find(a=>a.ticker==='QQQ'),
ALL_ASSETS.find(a=>a.ticker==='NVDA'),
ALL_ASSETS.find(a=>a.ticker==='AAPL'),
ALL_ASSETS.find(a=>a.ticker==='BND'),
ALL_ASSETS.find(a=>a.ticker==='GLD'),
ALL_ASSETS.find(a=>a.ticker==='AMZN'),
];
portfolio.assets = baseAssets.map((a, i) => ({
...a,
pct: template[i],
shares: parseFloat(((investAmount * template[i] / 100) / a.price).toFixed(3))
})).filter(a => a.pct > 0);
portfolio.totalInvested = investAmount;
portfolio.goals = selectedGoals;
portfolio.riskProfile = RISK_PROFILES[riskLevel].label;
renderAssetList();
renderAddGrid();
renderPreviewChart();
}
// ── Render Asset List ─────────────────────────────────────────────
function renderAssetList() {
const container = document.getElementById('asset-list');
container.innerHTML = '';
portfolio.assets.forEach((asset, idx) => {
const dollarVal = (investAmount * asset.pct / 100).toFixed(0);
const div = document.createElement('div');
div.className = 'asset-card';
div.innerHTML = `
<div class="asset-logo" style="background:${asset.color}">${asset.ticker}</div>
<div class="asset-info">
<div class="asset-ticker">${asset.ticker}</div>
<div class="asset-name">${asset.name}</div>
<span class="badge badge-${asset.type==='ETF'?'cyan':asset.type==='Bond'?'violet':asset.type==='Commodity'?'amber':'emerald'}">${asset.type}</span>
</div>
<div class="asset-sliders">
<input type="range" min="0" max="80" value="${asset.pct}" step="1"
oninput="updateAssetPct(${idx}, this.value)"
style="width:100%;margin-bottom:4px">
</div>
<div>
<div class="asset-pct-val" id="pct-${idx}">${asset.pct}%</div>
<div class="asset-dollar" id="dollar-${idx}">$${parseInt(dollarVal).toLocaleString()}</div>
</div>
<button class="asset-remove" onclick="removeAsset(${idx})">✕</button>
`;
container.appendChild(div);
});
updateBalanceWarning();
updateMetrics();
}
function updateAssetPct(idx, val) {
portfolio.assets[idx].pct = parseInt(val);
const dollarVal = (investAmount * parseInt(val) / 100).toFixed(0);
document.getElementById('pct-' + idx).textContent = val + '%';
document.getElementById('dollar-' + idx).textContent = '$' + parseInt(dollarVal).toLocaleString();
updateBalanceWarning();
renderPreviewChart();
updateMetrics();
}
function updatePortfolioAmounts() {
portfolio.assets.forEach((a, i) => {
const dEl = document.getElementById('dollar-' + i);
if (dEl) dEl.textContent = '$' + parseInt(investAmount * a.pct / 100).toLocaleString();
});
renderPreviewChart();
}
function updateBalanceWarning() {
const total = portfolio.assets.reduce((s, a) => s + a.pct, 0);
const warn = document.getElementById('asset-balance-warning');
if (total === 100) {
warn.className = 'balance-warning ok';
warn.innerHTML = '✅ Portfolio is balanced at 100%';
} else if (total < 100) {
warn.className = 'balance-warning warn';
warn.innerHTML = `⚠️ Under-allocated: ${total}% used, ${100-total}% remaining`;
} else {
warn.className = 'balance-warning err';
warn.innerHTML = `❌ Over-allocated: ${total}% total (exceeds 100% by ${total-100}%)`;
}
}
function updateMetrics() {
document.getElementById('preview-risk').textContent = calcRiskScore(portfolio) + '/100';
document.getElementById('preview-div').textContent = calcDiversification(portfolio) + '/100';
}
function removeAsset(idx) {
portfolio.assets.splice(idx, 1);
renderAssetList();
renderAddGrid();
renderPreviewChart();
}
function autoRebalance() {
const total = portfolio.assets.reduce((s, a) => s + a.pct, 0);
const perAsset = Math.floor(100 / portfolio.assets.length);
let rem = 100 - perAsset * portfolio.assets.length;
portfolio.assets.forEach((a, i) => { a.pct = perAsset + (i === 0 ? rem : 0); });
renderAssetList();
renderPreviewChart();
showToast('Portfolio auto-rebalanced to equal weights!');
}
// ── Add Asset Grid ────────────────────────────────────────────────
function renderAddGrid() {
const grid = document.getElementById('add-asset-grid');
grid.innerHTML = ALL_ASSETS.map(a => {
const inPortfolio = portfolio.assets.some(pa => pa.ticker === a.ticker);
return `<button class="add-asset-btn ${inPortfolio?'in-portfolio':''}"
onclick="toggleAsset('${a.ticker}')" title="${a.name}">
${inPortfolio ? '✓ ' : '+'} ${a.ticker}
</button>`;
}).join('');
}
function toggleAsset(ticker) {
const exists = portfolio.assets.findIndex(a => a.ticker === ticker);
if (exists >= 0) {
removeAsset(exists);
} else {
if (portfolio.assets.length >= 10) { showToast('Max 10 assets per portfolio', 'error'); return; }
const asset = ALL_ASSETS.find(a => a.ticker === ticker);
portfolio.assets.push({ ...asset, pct: 5, shares: 0 });
renderAssetList();
renderPreviewChart();
renderAddGrid();
}
}
// ── Preview Pie Chart ─────────────────────────────────────────────
function renderPreviewChart() {
const ctx = document.getElementById('previewPieChart').getContext('2d');
if (previewChart) previewChart.destroy();
const validAssets = portfolio.assets.filter(a => a.pct > 0);
previewChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: validAssets.map(a => a.ticker),
datasets: [{
data: validAssets.map(a => a.pct),
backgroundColor: validAssets.map(a => a.color),
borderColor: 'rgba(5,13,26,0.8)',
borderWidth: 3,
hoverOffset: 8
}]
},
options: {
responsive: true, maintainAspectRatio: false,
cutout: '68%',
plugins: { legend: { display: false } }
}
});
const leg = document.getElementById('preview-legend');
leg.innerHTML = validAssets.map(a => `
<div class="legend-item">
<div class="legend-dot" style="background:${a.color}"></div>
<span class="legend-name">${a.ticker}</span>
<span class="legend-pct">${a.pct}%</span>
</div>
`).join('');
document.getElementById('preview-total').textContent = '$' + investAmount.toLocaleString();
}
// ── Save Portfolio ────────────────────────────────────────────────
function savePortfolio_() {
const total = portfolio.assets.reduce((s,a) => s + a.pct, 0);
if (total !== 100) { showToast(`Total must be 100% (currently ${total}%)`, 'error'); return; }
portfolio.assets.forEach(a => {
a.shares = parseFloat(((investAmount * a.pct / 100) / a.price).toFixed(3));
});
savePortfolio(portfolio);
showToast('✅ Portfolio saved successfully!');
setTimeout(() => window.location.href = 'index.html', 1500);
}
// ── Init ──────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
applyChartDefaults();
renderAddGrid();
renderAssetList();
renderPreviewChart();
updateMetrics();
});
</script>
</body>
</html>