/* ================================================================ 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 => `