Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
raw
history blame
8.56 kB
import apiClient from './apiClient.js';
import { escapeHtml } from '../shared/js/utils/sanitizer.js';
class NewsView {
constructor(section) {
this.section = section;
this.tableBody = section.querySelector('[data-news-body]');
this.filterInput = section.querySelector('[data-news-search]');
this.rangeSelect = section.querySelector('[data-news-range]');
this.symbolFilter = section.querySelector('[data-news-symbol]');
this.modalBackdrop = section.querySelector('[data-news-modal]');
this.modalContent = section.querySelector('[data-news-modal-content]');
this.closeModalBtn = section.querySelector('[data-close-news-modal]');
this.dataset = [];
this.datasetMap = new Map();
}
async init() {
this.tableBody.innerHTML = '<tr><td colspan="6">Loading news...</td></tr>';
await this.loadNews();
this.bindEvents();
}
bindEvents() {
if (this.filterInput) {
this.filterInput.addEventListener('input', () => this.renderRows());
}
if (this.rangeSelect) {
this.rangeSelect.addEventListener('change', () => this.renderRows());
}
if (this.symbolFilter) {
this.symbolFilter.addEventListener('input', () => this.renderRows());
}
if (this.closeModalBtn) {
this.closeModalBtn.addEventListener('click', () => this.hideModal());
}
if (this.modalBackdrop) {
this.modalBackdrop.addEventListener('click', (event) => {
if (event.target === this.modalBackdrop) {
this.hideModal();
}
});
}
}
async loadNews() {
const result = await apiClient.getLatestNews(40);
if (!result.ok) {
const errorMsg = escapeHtml(result.error || 'Failed to load news');
this.tableBody.innerHTML = `<tr><td colspan="6"><div class="inline-message inline-error">${errorMsg}</div></td></tr>`;
return;
}
this.dataset = result.data || [];
this.datasetMap.clear();
this.dataset.forEach((item, index) => {
const rowId = item.id || `${item.title}-${index}`;
this.datasetMap.set(rowId, item);
});
this.renderRows();
}
renderRows() {
const searchTerm = (this.filterInput?.value || '').toLowerCase();
const symbolFilter = (this.symbolFilter?.value || '').toLowerCase();
const range = this.rangeSelect?.value || '24h';
const rangeMap = { '24h': 86_400_000, '7d': 604_800_000, '30d': 2_592_000_000 };
const limit = rangeMap[range] || rangeMap['24h'];
const filtered = this.dataset.filter((item) => {
const matchesText = `${item.title} ${item.summary}`.toLowerCase().includes(searchTerm);
const matchesSymbol = symbolFilter
? (item.symbols || []).some((symbol) => symbol.toLowerCase().includes(symbolFilter))
: true;
const published = new Date(item.published_at || item.date || Date.now()).getTime();
const withinRange = Date.now() - published <= limit;
return matchesText && matchesSymbol && withinRange;
});
if (!filtered.length) {
this.tableBody.innerHTML = '<tr><td colspan="6">No news for selected filters.</td></tr>';
return;
}
this.tableBody.innerHTML = filtered
.map((news, index) => {
const rowId = news.id || `${escapeHtml(news.title || '')}-${index}`;
this.datasetMap.set(rowId, news);
// Sanitize all dynamic content
const source = escapeHtml(news.source || 'N/A');
const title = escapeHtml(news.title || '');
const symbols = (news.symbols || []).map(s => escapeHtml(s));
const sentiment = escapeHtml(news.sentiment || 'Unknown');
return `
<tr data-news-id="${escapeHtml(rowId)}">
<td>${new Date(news.published_at || news.date).toLocaleString()}</td>
<td>${source}</td>
<td>${title}</td>
<td>${symbols.map((s) => `<span class="chip">${s}</span>`).join(' ')}</td>
<td><span class="badge ${this.getSentimentClass(news.sentiment)}">${sentiment}</span></td>
<td>
<button class="ghost" data-news-summarize="${escapeHtml(rowId)}">Summarize</button>
</td>
</tr>
`;
})
.join('');
this.section.querySelectorAll('tr[data-news-id]').forEach((row) => {
row.addEventListener('click', () => {
const id = row.dataset.newsId;
const item = this.datasetMap.get(id);
if (item) {
this.showModal(item);
}
});
});
this.section.querySelectorAll('[data-news-summarize]').forEach((button) => {
button.addEventListener('click', (event) => {
event.stopPropagation();
const { newsSummarize } = button.dataset;
this.summarizeArticle(newsSummarize, button);
});
});
}
getSentimentClass(sentiment) {
switch ((sentiment || '').toLowerCase()) {
case 'bullish':
return 'badge-success';
case 'bearish':
return 'badge-danger';
default:
return 'badge-neutral';
}
}
async summarizeArticle(rowId, button) {
const item = this.datasetMap.get(rowId);
if (!item || !button) return;
button.disabled = true;
const original = button.textContent;
button.textContent = 'Summarizing…';
const payload = {
title: item.title,
body: item.body || item.summary || item.description || '',
source: item.source || '',
};
const result = await apiClient.summarizeNews(payload);
button.disabled = false;
button.textContent = original;
if (!result.ok) {
this.showModal(item, null, result.error);
return;
}
this.showModal(item, result.data?.analysis || result.data);
}
async showModal(item, analysis = null, errorMessage = null) {
if (!this.modalContent) return;
this.modalBackdrop.classList.add('active');
// Sanitize all user data before inserting into HTML
const title = escapeHtml(item.title || '');
const source = escapeHtml(item.source || '');
const summary = escapeHtml(item.summary || item.description || '');
const symbols = (item.symbols || []).map(s => escapeHtml(s));
this.modalContent.innerHTML = `
<h3>${title}</h3>
<p class="text-muted">${new Date(item.published_at || item.date).toLocaleString()}${source}</p>
<p>${summary}</p>
<div class="chip-row">${symbols.map((s) => `<span class="chip">${s}</span>`).join('')}</div>
<div class="ai-block">${analysis ? '' : errorMessage ? '' : 'Click Summarize to run AI insights.'}</div>
`;
const aiBlock = this.modalContent.querySelector('.ai-block');
if (!aiBlock) return;
if (errorMessage) {
aiBlock.innerHTML = `<div class="inline-message inline-error">${escapeHtml(errorMessage)}</div>`;
return;
}
if (!analysis) {
aiBlock.innerHTML = '<div class="inline-message inline-info">Use the Summarize button to request AI analysis.</div>';
return;
}
const sentiment = analysis.sentiment || analysis.analysis?.sentiment;
const analysisSummary = escapeHtml(analysis.summary || analysis.analysis?.summary || 'Model returned no summary.');
const sentimentLabel = escapeHtml(sentiment?.label || sentiment || 'Unknown');
const sentimentScore = sentiment?.score !== undefined ? escapeHtml(String(sentiment.score)) : '';
aiBlock.innerHTML = `
<h4>AI Summary</h4>
<p>${analysisSummary}</p>
<p><strong>Sentiment:</strong> ${sentimentLabel}${sentimentScore ? ` (${sentimentScore})` : ''}</p>
`;
}
hideModal() {
if (this.modalBackdrop) {
this.modalBackdrop.classList.remove('active');
}
}
}
export default NewsView;