| <!DOCTYPE html> |
| <html lang="en" data-theme="light"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="description" content="Crypto Intelligence Hub - Modern Dashboard with 40+ Data Sources"> |
| <title>Dashboard | Crypto Intelligence Hub</title> |
| |
| |
| <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2322d3ee'/%3E%3Cstop offset='100%25' stop-color='%236366f1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23g)'/%3E%3C/svg%3E"> |
| |
| |
| <link rel="stylesheet" href="/static/shared/css/theme-modern.css"> |
| <link rel="stylesheet" href="/static/shared/css/sidebar-modern.css"> |
| |
| <style> |
| |
| body { |
| margin: 0; |
| padding: 0; |
| background: var(--bg-secondary); |
| } |
| |
| .app-layout { |
| display: flex; |
| min-height: 100vh; |
| } |
| |
| .main-content { |
| flex: 1; |
| margin-left: var(--sidebar-width); |
| transition: margin-left var(--transition-base); |
| padding: var(--space-6); |
| } |
| |
| .sidebar-modern.collapsed ~ .main-content { |
| margin-left: var(--sidebar-collapsed-width); |
| } |
| |
| |
| .page-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: var(--space-8); |
| padding-bottom: var(--space-6); |
| border-bottom: 1px solid var(--border-primary); |
| } |
| |
| .page-title h1 { |
| font-size: var(--text-4xl); |
| font-weight: var(--font-bold); |
| background: var(--accent-gradient); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: var(--space-2); |
| } |
| |
| .page-subtitle { |
| color: var(--text-tertiary); |
| font-size: var(--text-lg); |
| } |
| |
| .page-actions { |
| display: flex; |
| gap: var(--space-3); |
| } |
| |
| .btn { |
| padding: var(--space-3) var(--space-6); |
| border-radius: var(--radius-lg); |
| font-weight: var(--font-semibold); |
| font-size: var(--text-sm); |
| cursor: pointer; |
| transition: all var(--transition-base); |
| border: none; |
| display: inline-flex; |
| align-items: center; |
| gap: var(--space-2); |
| } |
| |
| .btn-primary { |
| background: var(--accent-gradient); |
| color: white; |
| box-shadow: var(--shadow-md); |
| } |
| |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-lg); |
| } |
| |
| .btn-secondary { |
| background: var(--surface-secondary); |
| color: var(--text-primary); |
| border: 1px solid var(--border-primary); |
| } |
| |
| .btn-secondary:hover { |
| background: var(--surface-hover); |
| } |
| |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
| gap: var(--space-6); |
| margin-bottom: var(--space-8); |
| } |
| |
| .stat-card { |
| background: var(--surface-primary); |
| border-radius: var(--radius-xl); |
| padding: var(--space-6); |
| border: 1px solid var(--border-primary); |
| box-shadow: var(--shadow-sm); |
| transition: all var(--transition-base); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .stat-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: var(--accent-gradient); |
| opacity: 0; |
| transition: opacity var(--transition-fast); |
| } |
| |
| .stat-card:hover { |
| transform: translateY(-4px); |
| box-shadow: var(--shadow-md); |
| } |
| |
| .stat-card:hover::before { |
| opacity: 1; |
| } |
| |
| .stat-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: var(--space-4); |
| } |
| |
| .stat-icon { |
| width: 48px; |
| height: 48px; |
| border-radius: var(--radius-lg); |
| background: var(--accent-gradient); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3); |
| } |
| |
| .stat-icon svg { |
| width: 24px; |
| height: 24px; |
| color: white; |
| } |
| |
| .stat-badge { |
| padding: var(--space-1) var(--space-3); |
| border-radius: var(--radius-full); |
| font-size: var(--text-xs); |
| font-weight: var(--font-bold); |
| text-transform: uppercase; |
| } |
| |
| .stat-badge.success { |
| background: rgba(16, 185, 129, 0.1); |
| color: var(--color-success); |
| } |
| |
| .stat-badge.warning { |
| background: rgba(245, 158, 11, 0.1); |
| color: var(--color-warning); |
| } |
| |
| .stat-value { |
| font-size: var(--text-4xl); |
| font-weight: var(--font-extrabold); |
| color: var(--text-primary); |
| margin-bottom: var(--space-2); |
| } |
| |
| .stat-label { |
| font-size: var(--text-sm); |
| color: var(--text-tertiary); |
| font-weight: var(--font-medium); |
| } |
| |
| .stat-change { |
| margin-top: var(--space-3); |
| display: flex; |
| align-items: center; |
| gap: var(--space-2); |
| font-size: var(--text-sm); |
| font-weight: var(--font-semibold); |
| } |
| |
| .stat-change.positive { |
| color: var(--color-success); |
| } |
| |
| .stat-change.negative { |
| color: var(--color-danger); |
| } |
| |
| |
| .card { |
| background: var(--surface-primary); |
| border-radius: var(--radius-xl); |
| border: 1px solid var(--border-primary); |
| box-shadow: var(--shadow-sm); |
| overflow: hidden; |
| margin-bottom: var(--space-6); |
| } |
| |
| .card-header { |
| padding: var(--space-6); |
| border-bottom: 1px solid var(--border-primary); |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| } |
| |
| .card-title { |
| font-size: var(--text-xl); |
| font-weight: var(--font-bold); |
| color: var(--text-primary); |
| display: flex; |
| align-items: center; |
| gap: var(--space-3); |
| } |
| |
| .card-body { |
| padding: var(--space-6); |
| } |
| |
| |
| .loading { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: var(--space-8); |
| color: var(--text-tertiary); |
| } |
| |
| .spinner { |
| width: 40px; |
| height: 40px; |
| border: 3px solid var(--border-primary); |
| border-top-color: var(--accent-primary); |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| |
| .news-list { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-4); |
| } |
| |
| .news-item { |
| display: flex; |
| gap: var(--space-4); |
| padding: var(--space-4); |
| border-radius: var(--radius-lg); |
| transition: all var(--transition-fast); |
| cursor: pointer; |
| } |
| |
| .news-item:hover { |
| background: var(--surface-hover); |
| } |
| |
| .news-source { |
| font-size: var(--text-xs); |
| color: var(--text-tertiary); |
| font-weight: var(--font-semibold); |
| text-transform: uppercase; |
| } |
| |
| .news-title { |
| font-size: var(--text-base); |
| font-weight: var(--font-semibold); |
| color: var(--text-primary); |
| margin: var(--space-2) 0; |
| } |
| |
| .news-time { |
| font-size: var(--text-sm); |
| color: var(--text-tertiary); |
| } |
| |
| |
| .fng-gauge { |
| width: 200px; |
| height: 200px; |
| margin: 0 auto; |
| position: relative; |
| } |
| |
| .fng-circle { |
| width: 100%; |
| height: 100%; |
| border-radius: 50%; |
| background: conic-gradient( |
| from 180deg, |
| var(--color-danger) 0deg 45deg, |
| var(--color-warning) 45deg 90deg, |
| var(--color-info) 90deg 135deg, |
| var(--color-success) 135deg 180deg |
| ); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .fng-inner { |
| width: 80%; |
| height: 80%; |
| background: var(--bg-primary); |
| border-radius: 50%; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .fng-value { |
| font-size: var(--text-5xl); |
| font-weight: var(--font-extrabold); |
| background: var(--accent-gradient); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .fng-label { |
| font-size: var(--text-sm); |
| color: var(--text-tertiary); |
| font-weight: var(--font-semibold); |
| margin-top: var(--space-2); |
| } |
| |
| |
| @media (max-width: 1024px) { |
| .main-content { |
| margin-left: 0; |
| } |
| |
| .sidebar-modern.collapsed ~ .main-content { |
| margin-left: 0; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .main-content { |
| padding: var(--space-4); |
| } |
| |
| .page-header { |
| flex-direction: column; |
| align-items: flex-start; |
| gap: var(--space-4); |
| } |
| |
| .stats-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| |
| <script src="/static/js/api-config.js"></script> |
| <script> |
| |
| window.apiReady = new Promise((resolve) => { |
| if (window.apiClient) { |
| console.log('✅ API Client ready'); |
| resolve(window.apiClient); |
| } else { |
| console.error('❌ API Client not loaded'); |
| } |
| }); |
| </script> |
|
|
| </head> |
| <body> |
| <div class="app-layout"> |
| |
| <div id="sidebar-container"></div> |
|
|
| |
| <main class="main-content"> |
| |
| <header class="page-header"> |
| <div class="page-title"> |
| <h1>Dashboard</h1> |
| <p class="page-subtitle">Real-time crypto market intelligence</p> |
| </div> |
| <div class="page-actions"> |
| <button class="btn btn-secondary" id="refresh-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/> |
| <path d="M21 3v5h-5"/> |
| </svg> |
| Refresh |
| </button> |
| <button class="btn btn-primary" id="theme-toggle"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="4"/> |
| <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/> |
| </svg> |
| Toggle Theme |
| </button> |
| </div> |
| </header> |
|
|
| |
| <div class="stats-grid"> |
| |
| <div class="stat-card" id="btc-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/> |
| </svg> |
| </div> |
| <span class="stat-badge success">Live</span> |
| </div> |
| <div class="stat-value" id="btc-price">Loading...</div> |
| <div class="stat-label">Bitcoin (BTC)</div> |
| <div class="stat-change positive" id="btc-change"> |
| <span>↑</span> |
| <span>--</span> |
| </div> |
| </div> |
|
|
| |
| <div class="stat-card" id="eth-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polygon points="12 2 3 12 12 16 21 12"/> |
| <polygon points="12 22 3 14 12 18 21 14"/> |
| </svg> |
| </div> |
| <span class="stat-badge success">Live</span> |
| </div> |
| <div class="stat-value" id="eth-price">Loading...</div> |
| <div class="stat-label">Ethereum (ETH)</div> |
| <div class="stat-change positive" id="eth-change"> |
| <span>↑</span> |
| <span>--</span> |
| </div> |
| </div> |
|
|
| |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M3 3v18h18"/> |
| <path d="M18 7l-5 5-4-4-5 5"/> |
| </svg> |
| </div> |
| <span class="stat-badge warning">24h</span> |
| </div> |
| <div class="stat-value" id="total-cap">$2.1T</div> |
| <div class="stat-label">Total Market Cap</div> |
| <div class="stat-change positive"> |
| <span>↑</span> |
| <span>2.3%</span> |
| </div> |
| </div> |
|
|
| |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M4 11a9 9 0 0 1 9 9"/> |
| <path d="M4 4a16 16 0 0 1 16 16"/> |
| <circle cx="5" cy="19" r="2"/> |
| </svg> |
| </div> |
| <span class="stat-badge success" id="api-status-badge">Online</span> |
| </div> |
| <div class="stat-value" id="api-success-rate">98%</div> |
| <div class="stat-label">API Success Rate</div> |
| <div class="stat-change positive" id="api-stats"> |
| <span>40+</span> |
| <span>sources active</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div style="display: grid; grid-template-columns: 2fr 1fr; gap: var(--space-6);"> |
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/> |
| </svg> |
| Latest News |
| </div> |
| <span id="news-count" style="color: var(--text-tertiary); font-size: var(--text-sm);">Loading...</span> |
| </div> |
| <div class="card-body"> |
| <div class="news-list" id="news-list"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="card-header"> |
| <div class="card-title"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"/> |
| <path d="M8 14s1.5 2 4 2 4-2 4-2"/> |
| </svg> |
| Fear & Greed |
| </div> |
| </div> |
| <div class="card-body"> |
| <div class="fng-gauge" id="fng-gauge"> |
| <div class="fng-circle"> |
| <div class="fng-inner"> |
| <div class="fng-value" id="fng-value">--</div> |
| <div class="fng-label" id="fng-label">Loading...</div> |
| </div> |
| </div> |
| </div> |
| <div style="text-align: center; margin-top: var(--space-6); color: var(--text-tertiary); font-size: var(--text-sm);" id="fng-source"> |
| Source: -- |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <script type="module"> |
| import apiClient from '/static/shared/js/api-client-comprehensive.js'; |
| import sidebarManager from '/static/shared/js/sidebar-manager.js'; |
| |
| |
| fetch('/static/shared/layouts/sidebar-modern.html') |
| .then(r => r.text()) |
| .then(html => { |
| document.getElementById('sidebar-container').innerHTML = html; |
| }); |
| |
| |
| document.getElementById('theme-toggle').addEventListener('click', () => { |
| const html = document.documentElement; |
| const current = html.getAttribute('data-theme') || 'light'; |
| const next = current === 'light' ? 'dark' : 'light'; |
| html.setAttribute('data-theme', next); |
| localStorage.setItem('theme', next); |
| }); |
| |
| |
| const savedTheme = localStorage.getItem('theme') || 'light'; |
| document.documentElement.setAttribute('data-theme', savedTheme); |
| |
| |
| async function loadDashboard() { |
| try { |
| |
| const btc = await apiClient.getMarketPrice('bitcoin'); |
| document.getElementById('btc-price').textContent = `$${btc.price.toLocaleString()}`; |
| if (btc.change24h) { |
| const changeEl = document.getElementById('btc-change'); |
| changeEl.innerHTML = `<span>${btc.change24h > 0 ? '↑' : '↓'}</span><span>${Math.abs(btc.change24h).toFixed(2)}%</span>`; |
| changeEl.className = `stat-change ${btc.change24h > 0 ? 'positive' : 'negative'}`; |
| } |
| |
| |
| const eth = await apiClient.getMarketPrice('ethereum'); |
| document.getElementById('eth-price').textContent = `$${eth.price.toLocaleString()}`; |
| if (eth.change24h) { |
| const changeEl = document.getElementById('eth-change'); |
| changeEl.innerHTML = `<span>${eth.change24h > 0 ? '↑' : '↓'}</span><span>${Math.abs(eth.change24h).toFixed(2)}%</span>`; |
| changeEl.className = `stat-change ${eth.change24h > 0 ? 'positive' : 'negative'}`; |
| } |
| |
| |
| const fng = await apiClient.getSentiment(); |
| document.getElementById('fng-value').textContent = fng.value; |
| document.getElementById('fng-label').textContent = fng.classification; |
| document.getElementById('fng-source').textContent = `Source: ${fng.source}`; |
| |
| |
| const news = await apiClient.getNews(10); |
| document.getElementById('news-count').textContent = `${news.length} articles`; |
| const newsList = document.getElementById('news-list'); |
| newsList.innerHTML = news.map(item => ` |
| <div class="news-item" onclick="window.open('${item.link}', '_blank')"> |
| <div style="flex: 1;"> |
| <div class="news-source">${item.source}</div> |
| <div class="news-title">${item.title}</div> |
| <div class="news-time">${new Date(item.publishedAt || Date.now()).toLocaleString()}</div> |
| </div> |
| </div> |
| `).join(''); |
| |
| |
| const stats = apiClient.getStats(); |
| document.getElementById('api-success-rate').textContent = stats.successRate; |
| document.getElementById('api-stats').innerHTML = `<span>${stats.successful}/${stats.total}</span><span>requests</span>`; |
| |
| console.log('✅ Dashboard loaded successfully'); |
| console.log('API Stats:', stats); |
| } catch (error) { |
| console.error('❌ Failed to load dashboard:', error); |
| } |
| } |
| |
| |
| document.getElementById('refresh-btn').addEventListener('click', () => { |
| apiClient.clearCache(); |
| loadDashboard(); |
| }); |
| |
| |
| loadDashboard(); |
| |
| |
| setInterval(() => { |
| loadDashboard(); |
| }, 120000); |
| </script> |
| </body> |
| </html> |
|
|
|
|