import type { EconomicServiceClient } from '@/generated/client/worldmonitor/economic/v1/service_client'; import { Panel } from './Panel'; import { t } from '@/services/i18n'; import { escapeHtml, unsafeRawHtml } from '@/utils/sanitize'; let _client: EconomicServiceClient | null = null; async function getEconomicClient(): Promise { if (!_client) { const { EconomicServiceClient } = await import('@/generated/client/worldmonitor/economic/v1/service_client'); const { getRpcBaseUrl } = await import('@/services/rpc-client'); _client = new EconomicServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) }); } return _client; } const COUNTRY_FLAGS: Record = { US: 'πŸ‡ΊπŸ‡Έ', GB: 'πŸ‡¬πŸ‡§', UK: 'πŸ‡¬πŸ‡§', EU: 'πŸ‡ͺπŸ‡Ί', EUR: 'πŸ‡ͺπŸ‡Ί', EA: 'πŸ‡ͺπŸ‡Ί', DE: 'πŸ‡©πŸ‡ͺ', FR: 'πŸ‡«πŸ‡·', JP: 'πŸ‡―πŸ‡΅', CN: 'πŸ‡¨πŸ‡³', CA: 'πŸ‡¨πŸ‡¦', AU: 'πŸ‡¦πŸ‡Ί', }; const EU_COUNTRIES = new Set(['EU', 'EA', 'EUR', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'PT', 'FI', 'IE', 'GR']); type RegionFilter = 'all' | 'us' | 'eu'; const IMPACT_COLORS: Record = { high: '#e74c3c', medium: '#f39c12', low: 'rgba(255,255,255,0.3)', }; interface EconomicEvent { event: string; country: string; date: string; impact: string; actual: string; estimate: string; previous: string; unit: string; } function groupByDate(events: EconomicEvent[]): Map { const map = new Map(); for (const ev of events) { const key = ev.date || 'Unknown'; if (!map.has(key)) map.set(key, []); map.get(key)!.push(ev); } return map; } function formatDateGroup(dateStr: string): string { if (!dateStr || dateStr === 'Unknown') return 'Unknown'; const d = new Date(`${dateStr}T00:00:00`); if (Number.isNaN(d.getTime())) return dateStr; return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); } function fmtVal(val: string, unit: string): string { if (!val) return 'β€”'; return unit ? `${val} ${unit}` : val; } function countdown(dateStr: string): string { const today = new Date(); today.setHours(0, 0, 0, 0); const d = new Date(`${dateStr}T00:00:00`); if (Number.isNaN(d.getTime())) return ''; const days = Math.round((d.getTime() - today.getTime()) / 86_400_000); if (days === 0) return 'today'; if (days === 1) return 'tomorrow'; if (days < 0) return Math.abs(days) < 14 ? `${Math.abs(days)}d ago` : `${Math.round(Math.abs(days) / 7)}w ago`; if (days < 14) return `in ${days}d`; return `in ${Math.round(days / 7)}w`; } export class EconomicCalendarPanel extends Panel { private _hasData = false; private _events: EconomicEvent[] = []; private _region: RegionFilter = 'all'; constructor() { super({ id: 'economic-calendar', title: 'Economic Calendar', showCount: false, infoTooltip: t('components.economicCalendar.infoTooltip') }); this.content.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('button[data-region]'); if (!btn) return; const region = btn.dataset.region as RegionFilter; if (region && region !== this._region) { this._region = region; this._render(); } }); } public async fetchData(): Promise { this.showLoading('Loading economic calendar...'); try { const client = await getEconomicClient(); const today = new Date(); const fromDate = today.toISOString().slice(0, 10); const toDate = new Date(today.getTime() + 30 * 86400_000).toISOString().slice(0, 10); const resp = await client.getEconomicCalendar({ fromDate, toDate }); if (resp.unavailable || !resp.events || resp.events.length === 0) { if (!this._hasData) this.showError('Economic calendar data unavailable.', () => void this.fetchData()); return false; } this._events = resp.events as EconomicEvent[]; this._hasData = true; this._render(); return true; } catch (err) { if (this.isAbortError(err)) return false; if (!this._hasData) this.showError('Failed to load economic calendar.', () => void this.fetchData()); return false; } } private _filterEvents(): EconomicEvent[] { if (this._region === 'us') return this._events.filter((e) => e.country === 'US'); if (this._region === 'eu') return this._events.filter((e) => EU_COUNTRIES.has(e.country)); return this._events; } private _renderRegionTabs(): string { const tabs: { key: RegionFilter; label: string }[] = [ { key: 'all', label: 'All' }, { key: 'us', label: 'US' }, { key: 'eu', label: 'EU' }, ]; return `
` + tabs.map(({ key, label }) => { const active = this._region === key; return ``; }).join('') + `
`; } private _render(): void { if (!this._hasData) { this.showError('No upcoming economic events.', () => void this.fetchData()); return; } const filtered = this._filterEvents(); const grouped = groupByDate(filtered); let bodyRows = ''; let isFirstGroup = true; for (const [date, events] of grouped) { const borderTop = isFirstGroup ? '' : 'border-top:1px solid rgba(255,255,255,0.06);'; isFirstGroup = false; bodyRows += ` ${escapeHtml(formatDateGroup(date))} `; for (const ev of events) { const impact = (ev.impact || 'low').toLowerCase(); const impactColor = IMPACT_COLORS[impact] ?? IMPACT_COLORS.low; const flag = COUNTRY_FLAGS[ev.country] ?? escapeHtml(ev.country); const isHigh = impact === 'high'; // Right column: actual value when released, countdown otherwise let rightLabel: string; let rightStyle: string; if (ev.actual) { rightLabel = escapeHtml(fmtVal(ev.actual, ev.unit)); rightStyle = 'color:var(--text);font-weight:600'; } else { rightLabel = escapeHtml(countdown(ev.date)); rightStyle = 'color:rgba(255,255,255,0.35);font-style:italic'; } bodyRows += ` ${flag}${escapeHtml(ev.event)} ${rightLabel} `; } } const emptyMsgText = this._region === 'all' ? 'No upcoming economic events' : 'No events for selected region'; const emptyMsg = filtered.length === 0 ? `${escapeHtml(emptyMsgText)}` : ''; const html = `${this._renderRegionTabs()}
${emptyMsg}${bodyRows}
EVENT
`; this.setSafeContent(unsafeRawHtml(html, 'legacy Panel.setContent() migration')); } }