File size: 8,695 Bytes
00e4c29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/* ================================================================
   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();
});