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;
}
// Backend returns {success: true, coins: [...], count: ...}, so access result.data.coins
const data = result.data || {};
this.coins = data.coins || 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)} |
${coin.change_24h >= 0 ?
'' :
''
}
${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 {
// Backend returns {success: true, data: [...], ...}, so access result.data.data
const chartData = chart.data || {};
const points = chartData.data || chartData || [];
this.renderChart(points);
}
}
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;