/** * Market Page - Real-time Market Data (IMPROVED) * - Added SVG coin icons with fallback * - Added Chart button next to View button * - Improved metric cards visibility */ import { APIHelper } from '../../shared/js/utils/api-helper.js'; class MarketPage { constructor() { this.marketData = []; this.allMarketData = []; this.sortColumn = 'market_cap'; this.sortDirection = 'desc'; this.currentLimit = 50; } /** * Get coin image with SVG fallback * @param {Object} coin - Coin data * @returns {string} Image HTML with fallback */ getCoinImage(coin) { const imageUrl = coin.image || `https://assets.coingecko.com/coins/images/1/small/${coin.id}.png`; const symbol = (coin.symbol || '?').charAt(0).toUpperCase(); const colors = { 'B': '#F7931A', // Bitcoin orange 'E': '#627EEA', // Ethereum blue 'S': '#14F195', // Solana green 'C': '#3C3C3D', // Generic crypto 'default': '#94a3b8' }; const color = colors[symbol] || colors['default']; const fallbackSvg = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Ccircle cx='16' cy='16' r='15' fill='${encodeURIComponent(color)}'/%3E%3Ctext x='16' y='21' text-anchor='middle' fill='white' font-size='14' font-weight='bold' font-family='Arial'%3E${symbol}%3C/text%3E%3C/svg%3E`; return `${coin.name || 'Coin'}`; } async init() { try { console.log('[Market] Initializing...'); // Show loading state const tbody = document.querySelector('#market-table tbody'); if (tbody) { tbody.innerHTML = '

Loading market data...

'; } this.bindEvents(); await this.loadMarketData(); // Auto-refresh every 30 seconds (only when tab is visible) setInterval(() => { if (!document.hidden) { this.loadMarketData(this.currentLimit); } }, 30000); this.showToast('Market data loaded', 'success'); } catch (error) { console.error('[Market] Init error:', error); this.showToast('Failed to initialize market page', 'error'); } } bindEvents() { // Refresh button document.getElementById('refresh-btn')?.addEventListener('click', () => { this.loadMarketData(this.currentLimit); }); // Search functionality document.getElementById('search-input')?.addEventListener('input', (e) => { this.filterMarketData(e.target.value); }); // Category filter buttons document.querySelectorAll('.category-filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.category-filter-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); this.filterByCategory(e.target.dataset.category); }); }); // Timeframe buttons (Top 10, Top 25, Top 50, All) document.querySelectorAll('[data-timeframe]').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('[data-timeframe]').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); const timeframe = e.target.dataset.timeframe; this.applyLimitFilter(timeframe); }); }); // Sort dropdown document.getElementById('sort-select')?.addEventListener('change', (e) => { this.sortMarketData(e.target.value); }); // Export button document.getElementById('export-btn')?.addEventListener('click', () => { this.exportData(); }); // Table header sorting document.querySelectorAll('.sortable-header').forEach(header => { header.addEventListener('click', () => { const column = header.dataset.column; this.toggleSort(column); }); }); } async loadMarketData(limit = 50) { try { let data = []; // Try backend API first try { const json = await APIHelper.fetchAPI(`/api/coins/top?limit=${limit}`); // Handle various response formats data = APIHelper.extractArray(json, ['markets', 'coins', 'data']); if (Array.isArray(data) && data.length > 0) { console.log('[Market] Data loaded from backend API:', data.length, 'coins'); } } catch (e) { console.warn('[Market] Primary API unavailable, trying CoinGecko', e); } // Fallback to CoinGecko if no data if (!Array.isArray(data) || data.length === 0) { try { const response = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&per_page=${limit}&price_change_percentage=7d&sparkline=true`); if (response.ok) { data = await response.json(); console.log('[Market] Data loaded from CoinGecko:', data.length, 'coins'); } } catch (e) { console.warn('[Market] Fallback API also unavailable', e); } } // If all APIs fail, show error - NO DEMO DATA if (!Array.isArray(data) || data.length === 0) { console.error('[Market] All APIs failed - no data available'); this.marketData = []; this.allMarketData = []; this.renderMarketTable(); this.showToast('Unable to load market data. Please check your connection.', 'error'); return; } this.marketData = Array.isArray(data) ? data : []; this.allMarketData = [...this.marketData]; // Keep a copy for filtering this.renderMarketTable(); this.updateMarketStats(); this.updateTimestamp(); } catch (error) { console.error('[Market] Load error:', error); this.marketData = []; this.allMarketData = []; this.renderMarketTable(); this.showToast('Error loading market data. Please try again later.', 'error'); } } renderMarketTable() { const tbody = document.querySelector('#market-table tbody'); if (!tbody) return; // Update market stats this.updateMarketStats(); if (this.marketData.length === 0) { tbody.innerHTML = '

Loading market data...

'; return; } tbody.innerHTML = this.marketData.map((coin, index) => { const change = coin.price_change_percentage_24h || 0; const change7d = coin.price_change_percentage_7d_in_currency || 0; const changeClass = change >= 0 ? 'positive' : 'negative'; const change7dClass = change7d >= 0 ? 'positive' : 'negative'; const arrow = change >= 0 ? '↑' : '↓'; const arrow7d = change7d >= 0 ? '↑' : '↓'; return ` ${index + 1} ${this.getCoinImage(coin)}
${coin.name || 'Unknown'} ${(coin.symbol || 'N/A').toUpperCase()}
$${coin.current_price?.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 6})} ${arrow} ${Math.abs(change).toFixed(2)}% ${arrow7d} ${Math.abs(change7d).toFixed(2)}% $${(coin.market_cap / 1e9).toFixed(2)}B $${(coin.total_volume / 1e6).toFixed(2)}M `; }).join(''); } filterMarketData(query) { if (!query || query.trim() === '') { // Reset to all data this.marketData = [...this.allMarketData]; this.renderMarketTable(); return; } if (!Array.isArray(this.allMarketData)) { this.marketData = []; return; } const searchTerm = query.toLowerCase().trim(); const filtered = this.allMarketData.filter(coin => (coin.name && coin.name.toLowerCase().includes(searchTerm)) || (coin.symbol && coin.symbol.toLowerCase().includes(searchTerm)) || (coin.id && coin.id.toLowerCase().includes(searchTerm)) ); this.marketData = filtered; this.renderMarketTable(); // Show result count if (filtered.length === 0) { this.showToast('No coins found matching your search', 'info'); } } viewChart(coinId) { const coin = this.marketData.find(c => c.id === coinId); if (!coin) return; // Redirect to chart page or open chart modal window.location.href = `/static/pages/chart/index.html?symbol=${coin.symbol.toUpperCase()}`; } viewDetails(coinId) { const coin = this.marketData.find(c => c.id === coinId) || this.allMarketData.find(c => c.id === coinId); if (!coin) { this.showToast('Coin not found', 'error'); return; } const modal = document.getElementById('coin-modal'); if (!modal) { // Create modal if it doesn't exist const newModal = document.createElement('div'); newModal.id = 'coin-modal'; newModal.className = 'modal'; newModal.setAttribute('aria-hidden', 'true'); newModal.innerHTML = ` `; document.body.appendChild(newModal); return this.viewDetails(coinId); // Retry with new modal } const change = coin.price_change_percentage_24h || 0; const change7d = coin.price_change_percentage_7d_in_currency || 0; const changeClass = change >= 0 ? 'positive' : 'negative'; // Update modal document.getElementById('modal-title').textContent = `${coin.name || 'Unknown'} (${(coin.symbol || 'N/A').toUpperCase()})`; const modalBody = document.getElementById('modal-body'); modalBody.innerHTML = `
${this.getCoinImage(coin)}
$${coin.current_price?.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 8}) || '0.00'} ${change >= 0 ? '↑' : '↓'} ${Math.abs(change).toFixed(2)}% (24h) ${change7d >= 0 ? '↑' : '↓'} ${Math.abs(change7d).toFixed(2)}% (7d)
Market Cap $${(coin.market_cap / 1e9).toFixed(2)}B
24h Volume $${(coin.total_volume / 1e6).toFixed(2)}M
Market Cap Rank #${coin.market_cap_rank || 'N/A'}
Circulating Supply ${coin.circulating_supply ? (coin.circulating_supply / 1e6).toFixed(2) + 'M' : 'N/A'}
${coin.total_supply ? `
Total Supply ${(coin.total_supply / 1e6).toFixed(2)}M
` : ''} ${coin.ath ? `
All-Time High $${coin.ath.toLocaleString()}
` : ''}

Price chart coming soon

`; // Show modal modal.classList.add('active'); modal.setAttribute('aria-hidden', 'false'); // Close handlers const closeBtn = modal.querySelector('.modal-close'); const backdrop = modal.querySelector('.modal-backdrop'); const closeModal = () => { modal.classList.remove('active'); modal.setAttribute('aria-hidden', 'true'); }; closeBtn?.addEventListener('click', closeModal); backdrop?.addEventListener('click', closeModal); } filterByCategory(category) { console.log('[Market] Filter by category:', category); // Can be extended with real category filtering this.renderMarketTable(); } /** * Apply limit filter (Top 10, Top 25, Top 50, All) * @param {string} timeframe - Filter value from button */ applyLimitFilter(timeframe) { let limit = 50; switch(timeframe) { case '1D': limit = 10; break; case '7D': limit = 25; break; case '30D': limit = 50; break; case '1Y': limit = 100; break; default: limit = 50; } this.currentLimit = limit; this.loadMarketData(limit); this.showToast(`Showing Top ${limit} coins`, 'info'); } sortMarketData(sortBy) { if (!Array.isArray(this.marketData)) { this.marketData = []; return; } const sorted = [...this.marketData].sort((a, b) => { switch (sortBy) { case 'price_desc': return (b.current_price || 0) - (a.current_price || 0); case 'price_asc': return (a.current_price || 0) - (b.current_price || 0); case 'change_desc': return (b.price_change_percentage_24h || 0) - (a.price_change_percentage_24h || 0); case 'change_asc': return (a.price_change_percentage_24h || 0) - (b.price_change_percentage_24h || 0); case 'volume': return (b.total_volume || 0) - (a.total_volume || 0); case 'rank': default: return (a.market_cap_rank || 999) - (b.market_cap_rank || 999); } }); this.marketData = sorted; this.renderMarketTable(); } toggleSort(column) { if (!Array.isArray(this.marketData)) { this.marketData = []; return; } if (this.sortColumn === column) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = column; this.sortDirection = 'desc'; } const sorted = [...this.marketData].sort((a, b) => { const aVal = a[column] || 0; const bVal = b[column] || 0; return this.sortDirection === 'asc' ? aVal - bVal : bVal - aVal; }); this.marketData = sorted; this.renderMarketTable(); } updateMarketStats() { if (!Array.isArray(this.marketData) || this.marketData.length === 0) return; // Calculate totals const totalMcap = this.marketData.reduce((sum, coin) => sum + (coin.market_cap || 0), 0); const totalVolume = this.marketData.reduce((sum, coin) => sum + (coin.total_volume || 0), 0); // Get BTC data const btcCoin = this.marketData.find(c => c.symbol.toLowerCase() === 'btc'); const btcMcap = btcCoin?.market_cap || 0; const btcDominance = totalMcap > 0 ? (btcMcap / totalMcap) * 100 : 0; // Update DOM with improved styling const totalMcapEl = document.getElementById('total-mcap'); const totalVolumeEl = document.getElementById('total-volume'); const btcDominanceEl = document.getElementById('btc-dominance'); const activeCoinsEl = document.getElementById('active-coins'); if (totalMcapEl) { totalMcapEl.textContent = `$${(totalMcap / 1e12).toFixed(2)}T`; totalMcapEl.style.fontWeight = '700'; totalMcapEl.style.fontSize = '1.5rem'; } if (totalVolumeEl) { totalVolumeEl.textContent = `$${(totalVolume / 1e9).toFixed(2)}B`; totalVolumeEl.style.fontWeight = '700'; totalVolumeEl.style.fontSize = '1.5rem'; } if (btcDominanceEl) { btcDominanceEl.textContent = `${btcDominance.toFixed(1)}%`; btcDominanceEl.style.fontWeight = '700'; btcDominanceEl.style.fontSize = '1.5rem'; btcDominanceEl.style.color = btcDominance > 50 ? '#10b981' : '#f59e0b'; } if (activeCoinsEl) { activeCoinsEl.textContent = this.marketData.length.toString(); activeCoinsEl.style.fontWeight = '700'; activeCoinsEl.style.fontSize = '1.5rem'; } } exportData() { const csv = [ ['Rank', 'Name', 'Symbol', 'Price', '24h Change', 'Market Cap', 'Volume'], ...this.marketData.map((coin, idx) => [ idx + 1, coin.name, coin.symbol.toUpperCase(), coin.current_price, coin.price_change_percentage_24h, coin.market_cap, coin.total_volume ]) ].map(row => row.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `market_data_${Date.now()}.csv`; a.click(); URL.revokeObjectURL(url); this.showToast('Market data exported', 'success'); } updateTimestamp() { const el = document.getElementById('last-update'); if (el) { el.textContent = `Updated: ${new Date().toLocaleTimeString()}`; } } showToast(message, type = 'info') { APIHelper.showToast(message, type); } } // Export for module import export default MarketPage; // Also create instance for direct access if (typeof window !== 'undefined') { const marketPage = new MarketPage(); window.marketPage = marketPage; // Auto-init if DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => marketPage.init()); } else { marketPage.init(); } }