/* ================================================================ FinWise — Shared JavaScript Utilities, navigation, localStorage helpers ================================================================ */ // ── Page detection ──────────────────────────────────────────────── (function setActiveNav() { const page = location.pathname.split('/').pop() || 'index.html'; document.querySelectorAll('.nav-item, .bottom-nav-item').forEach(el => { const href = el.getAttribute('href') || ''; if (href === page || (page === '' && href === 'index.html')) { el.classList.add('active'); } }); })(); // ── LocalStorage Helpers ────────────────────────────────────────── const LS = { get(key, fallback = null) { try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; } catch { return fallback; } }, set(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); return true; } catch { return false; } } }; // ── Default Portfolio Data ──────────────────────────────────────── const DEFAULT_PORTFOLIO = { assets: [ { ticker: 'VOO', name: 'Vanguard S&P 500 ETF', pct: 30, price: 478.22, shares: 3.1, color: '#22d3ee', type: 'ETF' }, { ticker: 'QQQ', name: 'Invesco Nasdaq 100 ETF', pct: 20, price: 456.80, shares: 2.2, color: '#8b5cf6', type: 'ETF' }, { ticker: 'NVDA', name: 'NVIDIA Corporation', pct: 15, price: 875.40, shares: 0.85, color: '#10b981', type: 'Stock' }, { ticker: 'AAPL', name: 'Apple Inc.', pct: 12, price: 188.60, shares: 3.2, color: '#f59e0b', type: 'Stock' }, { ticker: 'BND', name: 'Vanguard Bond ETF', pct: 13, price: 73.40, shares: 8.8, color: '#6366f1', type: 'Bond' }, { ticker: 'GLD', name: 'SPDR Gold Trust', pct: 7, price: 218.10, shares: 1.6, color: '#f43f5e', type: 'Commodity' }, { ticker: 'AMZN', name: 'Amazon.com Inc.', pct: 3, price: 188.90, shares: 0.8, color: '#0ea5e9', type: 'Stock' }, ], totalInvested: 12500, riskProfile: 'Moderate', goals: ['Wealth Building'], lastUpdated: new Date().toISOString() }; function getPortfolio() { return LS.get('fw_portfolio', DEFAULT_PORTFOLIO); } function savePortfolio(p) { p.lastUpdated = new Date().toISOString(); LS.set('fw_portfolio', p); } // ── Simulated Market Data ───────────────────────────────────────── const MARKET_PRICES = { 'VOO': { price: 478.22, change: +1.24, changePct: +0.26 }, 'QQQ': { price: 456.80, change: -2.10, changePct: -0.46 }, 'NVDA': { price: 875.40, change: +18.5, changePct: +2.16 }, 'AAPL': { price: 188.60, change: +0.80, changePct: +0.43 }, 'BND': { price: 73.40, change: -0.05, changePct: -0.07 }, 'GLD': { price: 218.10, change: +3.20, changePct: +1.49 }, 'AMZN': { price: 188.90, change: +1.60, changePct: +0.86 }, 'VTI': { price: 240.30, change: +0.94, changePct: +0.39 }, 'TSLA': { price: 182.30, change: -4.20, changePct: -2.25 }, 'WMT': { price: 67.80, change: +0.30, changePct: +0.44 }, 'MCD': { price: 281.50, change: +1.10, changePct: +0.39 }, }; // ── Historical Performance Generator ───────────────────────────── function generateHistory(days = 180, startVal = 10000, volatility = 0.012) { const data = []; let val = startVal; const now = Date.now(); for (let i = days; i >= 0; i--) { const date = new Date(now - i * 86400000); const change = (Math.random() - 0.46) * volatility; val = val * (1 + change); data.push({ date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), value: Math.round(val * 100) / 100 }); } return data; } // ── Number Formatting ───────────────────────────────────────────── function fmt$(n) { return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtPct(n) { return (n > 0 ? '+' : '') + n.toFixed(2) + '%'; } function fmtK(n) { return n >= 1000000 ? '$' + (n/1000000).toFixed(2) + 'M' : n >= 1000 ? '$' + (n/1000).toFixed(1) + 'K' : '$' + n.toFixed(0); } // ── Animated Counter ────────────────────────────────────────────── function animateCounter(el, target, prefix = '', suffix = '', duration = 1200) { const start = parseFloat(el.textContent.replace(/[^0-9.-]/g, '')) || 0; const startTime = performance.now(); function step(now) { const p = Math.min((now - startTime) / duration, 1); const ease = 1 - Math.pow(1 - p, 3); const val = start + (target - start) * ease; el.textContent = prefix + val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + suffix; if (p < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } // ── Chart.js Defaults ───────────────────────────────────────────── function applyChartDefaults() { if (typeof Chart === 'undefined') return; Chart.defaults.color = '#8faac8'; Chart.defaults.borderColor = 'rgba(34,211,238,0.10)'; Chart.defaults.font.family = "'DM Sans', sans-serif"; } // ── Ticker Data for Sidebar ─────────────────────────────────────── const TICKERS = [ { sym: 'S&P 500', val: '5,308', chg: '+0.26%', up: true }, { sym: 'NASDAQ', val: '16,742', chg: '-0.46%', up: false }, { sym: 'BTC', val: '68,420', chg: '+2.14%', up: true }, { sym: 'GOLD', val: '2,318', chg: '+1.49%', up: true }, ]; function renderSidebarTickers() { const container = document.getElementById('sidebar-tickers'); if (!container) return; container.innerHTML = TICKERS.map(t => `
${t.sym} ${t.chg}
`).join(''); } // ── Risk Score Calculator ───────────────────────────────────────── function calcRiskScore(portfolio) { const weights = { ETF: 3, Stock: 5, Bond: 1, Commodity: 4 }; let score = 0; portfolio.assets.forEach(a => { score += (weights[a.type] || 3) * (a.pct / 100); }); return Math.round((score / 5) * 100); // 0-100 } // ── Diversification Score ───────────────────────────────────────── function calcDiversification(portfolio) { const types = [...new Set(portfolio.assets.map(a => a.type))].length; const count = portfolio.assets.length; const maxPct = Math.max(...portfolio.assets.map(a => a.pct)); const concentration = maxPct > 40 ? 0.6 : maxPct > 25 ? 0.8 : 1.0; return Math.round(((types / 4) * 0.4 + (Math.min(count, 7) / 7) * 0.4 + concentration * 0.2) * 100); } // ── Color Palette ───────────────────────────────────────────────── const ASSET_COLORS = ['#22d3ee','#10b981','#8b5cf6','#f59e0b','#f43f5e','#6366f1','#0ea5e9','#34d399','#a78bfa','#fb923c']; // ── Toast Notification ──────────────────────────────────────────── function showToast(msg, type = 'success') { const t = document.createElement('div'); t.style.cssText = ` position:fixed; bottom:90px; right:20px; z-index:9999; padding:12px 20px; border-radius:10px; font-size:13px; font-weight:600; background:${type === 'success' ? 'rgba(16,185,129,0.95)' : 'rgba(244,63,94,0.95)'}; color:#fff; box-shadow:0 8px 32px rgba(0,0,0,0.4); animation: fadeInUp 0.3s ease; max-width: 300px; `; t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), 3000); } // ── On DOM Ready ────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { applyChartDefaults(); renderSidebarTickers(); });