WM01 / shared.js
AndyKandy26's picture
Upload 9 files
00e4c29 verified
/* ================================================================
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 => `
<div class="ticker-item">
<span class="ticker-name">${t.sym}</span>
<span class="${t.up ? 'up' : 'down'}">${t.chg}</span>
</div>
`).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();
});