| import apiClient from './apiClient.js'; | |
| import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js'; | |
| class OverviewView { | |
| constructor(section) { | |
| this.section = section; | |
| this.statsContainer = section.querySelector('[data-overview-stats]'); | |
| this.topCoinsBody = section.querySelector('[data-top-coins-body]'); | |
| this.sentimentCanvas = section.querySelector('#sentiment-chart'); | |
| this.sentimentChart = null; | |
| } | |
| async init() { | |
| this.renderStatSkeletons(); | |
| this.topCoinsBody.innerHTML = createSkeletonRows(6, 6); | |
| await Promise.all([this.loadStats(), this.loadTopCoins(), this.loadSentiment()]); | |
| } | |
| renderStatSkeletons() { | |
| if (!this.statsContainer) return; | |
| this.statsContainer.innerHTML = Array.from({ length: 4 }) | |
| .map(() => '<div class="glass-card stat-card skeleton" style="height: 140px;"></div>') | |
| .join(''); | |
| } | |
| async loadStats() { | |
| if (!this.statsContainer) return; | |
| const result = await apiClient.getMarketStats(); | |
| if (!result.ok) { | |
| renderMessage(this.statsContainer, { | |
| state: 'error', | |
| title: 'Unable to load market stats', | |
| body: result.error || 'Unknown error', | |
| }); | |
| return; | |
| } | |
| const stats = result.data || {}; | |
| const cards = [ | |
| { label: 'Total Market Cap', value: formatCurrency(stats.total_market_cap) }, | |
| { label: '24h Volume', value: formatCurrency(stats.total_volume_24h) }, | |
| { label: 'BTC Dominance', value: formatPercent(stats.btc_dominance) }, | |
| { label: 'ETH Dominance', value: formatPercent(stats.eth_dominance) }, | |
| ]; | |
| this.statsContainer.innerHTML = cards | |
| .map( | |
| (card) => ` | |
| <div class="glass-card stat-card"> | |
| <h3>${card.label}</h3> | |
| <div class="stat-value">${card.value}</div> | |
| <div class="stat-trend">Updated ${new Date().toLocaleTimeString()}</div> | |
| </div> | |
| `, | |
| ) | |
| .join(''); | |
| } | |
| async loadTopCoins() { | |
| const result = await apiClient.getTopCoins(10); | |
| if (!result.ok) { | |
| this.topCoinsBody.innerHTML = ` | |
| <tr><td colspan="7"> | |
| <div class="inline-message inline-error"> | |
| <strong>Failed to load coins</strong> | |
| <p>${result.error}</p> | |
| </div> | |
| </td></tr>`; | |
| return; | |
| } | |
| const rows = (result.data || []).map( | |
| (coin, index) => ` | |
| <tr> | |
| <td>${index + 1}</td> | |
| <td>${coin.symbol || coin.ticker || '—'}</td> | |
| <td>${coin.name || 'Unknown'}</td> | |
| <td>${formatCurrency(coin.price)}</td> | |
| <td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}"> | |
| ${formatPercent(coin.change_24h)} | |
| </td> | |
| <td>${formatCurrency(coin.volume_24h)}</td> | |
| <td>${formatCurrency(coin.market_cap)}</td> | |
| </tr> | |
| `); | |
| this.topCoinsBody.innerHTML = rows.join(''); | |
| } | |
| async loadSentiment() { | |
| if (!this.sentimentCanvas) return; | |
| const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' }); | |
| if (!result.ok) { | |
| this.sentimentCanvas.replaceWith(this.buildSentimentFallback(result.error)); | |
| return; | |
| } | |
| const payload = result.data || {}; | |
| const sentiment = payload.sentiment || payload.data || {}; | |
| const data = { | |
| bullish: sentiment.bullish ?? 40, | |
| neutral: sentiment.neutral ?? 35, | |
| bearish: sentiment.bearish ?? 25, | |
| }; | |
| if (this.sentimentChart) { | |
| this.sentimentChart.destroy(); | |
| } | |
| this.sentimentChart = new Chart(this.sentimentCanvas, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Bullish', 'Neutral', 'Bearish'], | |
| datasets: [ | |
| { | |
| data: [data.bullish, data.neutral, data.bearish], | |
| backgroundColor: ['#22c55e', '#38bdf8', '#ef4444'], | |
| borderWidth: 0, | |
| }, | |
| ], | |
| }, | |
| options: { | |
| cutout: '65%', | |
| plugins: { | |
| legend: { | |
| labels: { color: 'var(--text-primary)', usePointStyle: true }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| } | |
| buildSentimentFallback(message) { | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'inline-message inline-info'; | |
| wrapper.innerHTML = ` | |
| <strong>Sentiment insight unavailable</strong> | |
| <p>${message || 'AI sentiment endpoint did not respond in time.'}</p> | |
| `; | |
| return wrapper; | |
| } | |
| } | |
| export default OverviewView; | |