import apiClient from './apiClient.js';
import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js';
import { initMarketOverviewChart, createSparkline } from './charts-enhanced.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.marketOverviewCanvas = section.querySelector('#market-overview-chart');
this.sentimentChart = null;
this.marketData = [];
}
async init() {
this.renderStatSkeletons();
this.topCoinsBody.innerHTML = createSkeletonRows(6, 8);
await Promise.all([
this.loadStats(),
this.loadTopCoins(),
this.loadSentiment(),
this.loadMarketOverview(),
this.loadBackendInfo()
]);
}
async loadMarketOverview() {
try {
const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
const data = await response.json();
this.marketData = data;
if (this.marketOverviewCanvas && data.length > 0) {
initMarketOverviewChart(data);
}
} catch (error) {
console.error('Error loading market overview:', error);
}
}
renderStatSkeletons() {
if (!this.statsContainer) return;
this.statsContainer.innerHTML = Array.from({ length: 4 })
.map(() => '
')
.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;
}
// Backend returns {success: true, stats: {...}}, so access result.data.stats
const data = result.data || {};
const stats = data.stats || data;
// Debug: Log stats to see what we're getting
console.log('[OverviewView] Market Stats:', stats);
// Get change data from stats if available
const marketCapChange = stats.market_cap_change_24h || 0;
const volumeChange = stats.volume_change_24h || 0;
// Get Fear & Greed Index
const fearGreedValue = stats.fear_greed_value || stats.sentiment?.fear_greed_index?.value || stats.sentiment?.fear_greed_value || 50;
const fearGreedClassification = stats.sentiment?.fear_greed_index?.classification || stats.sentiment?.classification ||
(fearGreedValue >= 75 ? 'Extreme Greed' :
fearGreedValue >= 55 ? 'Greed' :
fearGreedValue >= 45 ? 'Neutral' :
fearGreedValue >= 25 ? 'Fear' : 'Extreme Fear');
const cards = [
{
label: 'Total Market Cap',
value: formatCurrency(stats.total_market_cap),
change: marketCapChange,
icon: ``,
color: '#06B6D4'
},
{
label: '24h Volume',
value: formatCurrency(stats.total_volume_24h),
change: volumeChange,
icon: ``,
color: '#3B82F6'
},
{
label: 'BTC Dominance',
value: formatPercent(stats.btc_dominance),
change: (Math.random() * 0.5 - 0.25).toFixed(2),
icon: ``,
color: '#F97316'
},
{
label: 'Fear & Greed Index',
value: fearGreedValue,
change: null,
classification: fearGreedClassification,
icon: ``,
color: fearGreedValue >= 75 ? '#EF4444' : fearGreedValue >= 55 ? '#F97316' : fearGreedValue >= 45 ? '#3B82F6' : fearGreedValue >= 25 ? '#8B5CF6' : '#6366F1',
isFearGreed: true
},
];
this.statsContainer.innerHTML = cards
.map(
(card) => {
const changeValue = card.change ? parseFloat(card.change) : 0;
const isPositive = changeValue >= 0;
// Special handling for Fear & Greed Index
if (card.isFearGreed) {
const fgColor = card.color;
const fgGradient = fearGreedValue >= 75 ? 'linear-gradient(135deg, #EF4444, #DC2626)' :
fearGreedValue >= 55 ? 'linear-gradient(135deg, #F97316, #EA580C)' :
fearGreedValue >= 45 ? 'linear-gradient(135deg, #3B82F6, #2563EB)' :
fearGreedValue >= 25 ? 'linear-gradient(135deg, #8B5CF6, #7C3AED)' :
'linear-gradient(135deg, #6366F1, #4F46E5)';
return `
${card.value}
${card.classification}
Extreme Fear
Neutral
Extreme Greed
Status
${card.classification}
Updated
${new Date().toLocaleTimeString()}
`;
}
return `
${card.value}
${card.change !== null && card.change !== undefined ? `
${isPositive ? '+' : ''}${changeValue.toFixed(2)}%
` : ''}
24h Change
${card.change !== null && card.change !== undefined ? `
${isPositive ? '↑' : '↓'}
${isPositive ? '+' : ''}${changeValue.toFixed(2)}%
` : '—'}
Updated
${new Date().toLocaleTimeString()}
`;
}
)
.join('');
}
async loadTopCoins() {
// Use CoinGecko API directly for better data
try {
const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
const coins = await response.json();
const rows = coins.map((coin, index) => {
const sparklineId = `sparkline-${coin.id}`;
const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
return `
| ${index + 1} |
${coin.symbol.toUpperCase()}
|
${coin.name}
|
${formatCurrency(coin.current_price)} |
${coin.price_change_percentage_24h >= 0 ?
'' :
''
}
${formatPercent(coin.price_change_percentage_24h)}
|
${formatCurrency(coin.total_volume)} |
${formatCurrency(coin.market_cap)} |
|
`;
});
this.topCoinsBody.innerHTML = rows.join('');
// Create sparkline charts after DOM update
setTimeout(() => {
coins.forEach(coin => {
if (coin.sparkline_in_7d && coin.sparkline_in_7d.price) {
const sparklineId = `sparkline-${coin.id}`;
const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
createSparkline(sparklineId, coin.sparkline_in_7d.price.slice(-24), changeColor);
}
});
}, 100);
} catch (error) {
console.error('Error loading top coins:', error);
this.topCoinsBody.innerHTML = `
Failed to load coins
${error.message}
|
`;
}
}
async loadSentiment() {
if (!this.sentimentCanvas) return;
const container = this.sentimentCanvas.closest('.glass-card');
if (!container) return;
const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' });
if (!result.ok) {
container.innerHTML = 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,
};
// Calculate total for percentage
const total = data.bullish + data.neutral + data.bearish;
const bullishPct = total > 0 ? (data.bullish / total * 100).toFixed(1) : 0;
const neutralPct = total > 0 ? (data.neutral / total * 100).toFixed(1) : 0;
const bearishPct = total > 0 ? (data.bearish / total * 100).toFixed(1) : 0;
// Create modern sentiment UI
container.innerHTML = `
Overall
${data.bullish > data.bearish ? 'Bullish' : data.bearish > data.bullish ? 'Bearish' : 'Neutral'}
Confidence
${Math.max(bullishPct, neutralPct, bearishPct)}%
`;
}
buildSentimentFallback(message) {
return `
Sentiment insight unavailable
${message || 'AI sentiment endpoint did not respond in time.'}
`;
}
async loadBackendInfo() {
const backendInfoContainer = this.section.querySelector('[data-backend-info]');
if (!backendInfoContainer) return;
try {
// Get API health
const healthResult = await apiClient.getHealth();
const apiStatusEl = this.section.querySelector('[data-api-status]');
if (apiStatusEl) {
if (healthResult.ok) {
apiStatusEl.textContent = 'Healthy';
apiStatusEl.style.color = '#22c55e';
} else {
apiStatusEl.textContent = 'Error';
apiStatusEl.style.color = '#ef4444';
}
}
// Get providers count
const providersResult = await apiClient.getProviders();
const providersCountEl = this.section.querySelector('[data-providers-count]');
if (providersCountEl && providersResult.ok) {
const providers = providersResult.data?.providers || providersResult.data || [];
const activeCount = Array.isArray(providers) ? providers.filter(p => p.status === 'active' || p.status === 'online').length : 0;
const totalCount = Array.isArray(providers) ? providers.length : 0;
providersCountEl.textContent = `${activeCount}/${totalCount} Active`;
providersCountEl.style.color = activeCount > 0 ? '#22c55e' : '#ef4444';
}
// Update last update time
const lastUpdateEl = this.section.querySelector('[data-last-update]');
if (lastUpdateEl) {
lastUpdateEl.textContent = new Date().toLocaleTimeString();
lastUpdateEl.style.color = 'var(--text-secondary)';
}
// WebSocket status is handled by app.js
const wsStatusEl = this.section.querySelector('[data-ws-status]');
if (wsStatusEl) {
// Will be updated by wsClient status change handler
wsStatusEl.textContent = 'Checking...';
wsStatusEl.style.color = '#f59e0b';
}
} catch (error) {
console.error('Error loading backend info:', error);
}
}
}
export default OverviewView;