import apiClient from './apiClient.js'; import { formatCurrency, formatPercent, createSkeletonRows } from './uiUtils.js'; class MarketView { constructor(section, wsClient) { this.section = section; this.wsClient = wsClient; this.tableBody = section.querySelector('[data-market-body]'); this.searchInput = section.querySelector('[data-market-search]'); this.timeframeButtons = section.querySelectorAll('[data-timeframe]'); this.liveToggle = section.querySelector('[data-live-toggle]'); this.drawer = section.querySelector('[data-market-drawer]'); this.drawerClose = section.querySelector('[data-close-drawer]'); this.drawerSymbol = section.querySelector('[data-drawer-symbol]'); this.drawerStats = section.querySelector('[data-drawer-stats]'); this.drawerNews = section.querySelector('[data-drawer-news]'); this.chartWrapper = section.querySelector('[data-chart-wrapper]'); this.chartCanvas = this.chartWrapper?.querySelector('#market-detail-chart'); this.chart = null; this.coins = []; this.filtered = []; this.currentTimeframe = '7d'; this.liveUpdates = false; } async init() { this.tableBody.innerHTML = createSkeletonRows(10, 7); await this.loadCoins(); this.bindEvents(); } bindEvents() { if (this.searchInput) { this.searchInput.addEventListener('input', () => this.filterCoins()); } this.timeframeButtons.forEach((btn) => { btn.addEventListener('click', () => { this.timeframeButtons.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); this.currentTimeframe = btn.dataset.timeframe; if (this.drawer?.classList.contains('active') && this.drawerSymbol?.dataset.symbol) { this.openDrawer(this.drawerSymbol.dataset.symbol); } }); }); if (this.liveToggle) { this.liveToggle.addEventListener('change', (event) => { this.liveUpdates = event.target.checked; if (this.liveUpdates) { this.wsSubscription = this.wsClient.subscribe('price_update', (payload) => this.applyLiveUpdate(payload)); } else if (this.wsSubscription) { this.wsSubscription(); } }); } if (this.drawerClose) { this.drawerClose.addEventListener('click', () => this.drawer.classList.remove('active')); } } async loadCoins() { const result = await apiClient.getTopCoins(50); if (!result.ok) { this.tableBody.innerHTML = `
Unable to load coins

${result.error}

`; return; } this.coins = result.data || []; this.filtered = [...this.coins]; this.renderTable(); } filterCoins() { const term = this.searchInput.value.toLowerCase(); this.filtered = this.coins.filter((coin) => { const name = `${coin.name} ${coin.symbol}`.toLowerCase(); return name.includes(term); }); this.renderTable(); } renderTable() { this.tableBody.innerHTML = this.filtered .map( (coin, index) => ` ${index + 1}
${coin.symbol || '—'}
${coin.name || 'Unknown'} ${formatCurrency(coin.price)} ${formatPercent(coin.change_24h)} ${formatCurrency(coin.volume_24h)} ${formatCurrency(coin.market_cap)} `, ) .join(''); this.section.querySelectorAll('.market-row').forEach((row) => { row.addEventListener('click', () => this.openDrawer(row.dataset.symbol)); }); } async openDrawer(symbol) { if (!symbol) return; this.drawerSymbol.textContent = symbol; this.drawerSymbol.dataset.symbol = symbol; this.drawer.classList.add('active'); this.drawerStats.innerHTML = '

Loading...

'; this.drawerNews.innerHTML = '

Loading news...

'; await Promise.all([this.loadCoinDetails(symbol), this.loadCoinNews(symbol)]); } async loadCoinDetails(symbol) { const [details, chart] = await Promise.all([ apiClient.getCoinDetails(symbol), apiClient.getPriceChart(symbol, this.currentTimeframe), ]); if (!details.ok) { this.drawerStats.innerHTML = `
${details.error}
`; } else { const coin = details.data || {}; this.drawerStats.innerHTML = `

Price

${formatCurrency(coin.price)}

24h Change

${formatPercent(coin.change_24h)}

High / Low

${formatCurrency(coin.high_24h)} / ${formatCurrency(coin.low_24h)}

Market Cap

${formatCurrency(coin.market_cap)}

`; } if (!chart.ok) { if (this.chartWrapper) { this.chartWrapper.innerHTML = `
${chart.error}
`; } } else { this.renderChart(chart.data || []); } } renderChart(points) { if (!this.chartWrapper) return; if (!this.chartCanvas || !this.chartWrapper.contains(this.chartCanvas)) { this.chartWrapper.innerHTML = ''; this.chartCanvas = this.chartWrapper.querySelector('#market-detail-chart'); } const labels = points.map((point) => point.time || point.timestamp); const data = points.map((point) => point.price || point.value); if (this.chart) { this.chart.destroy(); } this.chart = new Chart(this.chartCanvas, { type: 'line', data: { labels, datasets: [ { label: `${this.drawerSymbol.textContent} Price`, data, fill: false, borderColor: '#38bdf8', tension: 0.3, }, ], }, options: { animation: false, scales: { x: { ticks: { color: 'var(--text-muted)' } }, y: { ticks: { color: 'var(--text-muted)' } }, }, plugins: { legend: { display: false } }, }, }); } async loadCoinNews(symbol) { const result = await apiClient.getLatestNews(5); if (!result.ok) { this.drawerNews.innerHTML = `
${result.error}
`; return; } const related = (result.data || []).filter((item) => (item.symbols || []).includes(symbol)); if (!related.length) { this.drawerNews.innerHTML = '

No related headlines available.

'; return; } this.drawerNews.innerHTML = related .map( (news) => `

${news.title}

${news.summary || ''}

${new Date(news.published_at || news.date).toLocaleString()}
`, ) .join(''); } applyLiveUpdate(payload) { if (!this.liveUpdates) return; const symbol = payload.symbol || payload.ticker; if (!symbol) return; const row = this.section.querySelector(`tr[data-symbol="${symbol}"]`); if (!row) return; const priceCell = row.children[3]; const changeCell = row.children[4]; if (payload.price) { priceCell.textContent = formatCurrency(payload.price); } if (payload.change_24h) { changeCell.textContent = formatPercent(payload.change_24h); changeCell.classList.toggle('text-success', payload.change_24h >= 0); changeCell.classList.toggle('text-danger', payload.change_24h < 0); } row.classList.add('flash'); setTimeout(() => row.classList.remove('flash'), 600); } } export default MarketView;