import { Panel } from './Panel'; import { t } from '@/services/i18n'; import { escapeHtml, unsafeRawHtml } from '@/utils/sanitize'; import { getHydratedData } from '@/services/bootstrap'; import { createLazyClient, getRpcBaseUrl, rpcFetch } from '@/services/rpc-client'; import type { ListBigMacPricesResponse } from '@/generated/client/worldmonitor/economic/v1/service_client'; import { EconomicServiceClient } from '@/services/generated-rpc-clients'; const getEconomicClient = createLazyClient(() => new EconomicServiceClient(getRpcBaseUrl(), { fetch: rpcFetch })); export class BigMacPanel extends Panel { constructor() { super({ id: 'bigmac', title: t('panels.bigmac'), infoTooltip: t('components.bigmac.infoTooltip') }); } public async fetchData(): Promise { try { const hydrated = getHydratedData('bigmac') as ListBigMacPricesResponse | undefined; if (hydrated?.countries?.length) { if (!this.element?.isConnected) return; this.renderIndex(hydrated); void getEconomicClient().listBigMacPrices({}).then(data => { if (!this.element?.isConnected || !data.countries?.length) return; this.renderIndex(data); }).catch(() => {}); return; } const data = await getEconomicClient().listBigMacPrices({}); if (!this.element?.isConnected) return; this.renderIndex(data); } catch (err) { if (this.isAbortError(err)) return; if (!this.element?.isConnected) return; this.showError(t('common.failedMarketData'), () => void this.fetchData()); } } private renderIndex(data: ListBigMacPricesResponse): void { if (!data.countries?.length) { this.showError(t('common.failedMarketData'), () => void this.fetchData()); return; } const sorted = [...data.countries] .filter((c): c is typeof c & { usdPrice: number } => c.usdPrice != null && c.usdPrice > 0) .sort((a, b) => b.usdPrice - a.usdPrice); const maxCode = sorted[0]?.code; const minCode = sorted[sorted.length - 1]?.code; const showWow = data.wowAvailable && data.wowAvgPct !== undefined; const wowHeader = showWow ? `${t('panels.bigmacWow')}` : ''; const rows = sorted.map(c => { const cls = c.code === minCode ? 'gb-cheapest' : c.code === maxCode ? 'gb-priciest' : ''; let wowCell = ''; if (showWow) { const pct = c.wowPct ?? null; if (pct == null) { wowCell = `—`; } else { const sign = pct >= 0 ? '▲' : '▼'; const wowCls = pct >= 0 ? 'bm-wow-up' : 'bm-wow-down'; wowCell = `${sign}${Math.abs(pct).toFixed(1)}%`; } } return ` ${escapeHtml(c.flag)} ${escapeHtml(c.name)} $${c.usdPrice.toFixed(2)} ${wowCell} `; }).join(''); let wowSummary = ''; if (showWow) { const avg = data.wowAvgPct; const sign = avg >= 0 ? '▲' : '▼'; const cls = avg >= 0 ? 'bm-wow-up' : 'bm-wow-down'; wowSummary = `
Global avg: ${sign}${Math.abs(avg).toFixed(1)}% ${t('panels.bigmacWow')}
`; } const updatedAt = data.fetchedAt ? new Date(data.fetchedAt).toLocaleDateString() : ''; const html = `
${wowSummary}
${wowHeader} ${rows}
${t('panels.bigmacCountry')} USD
${updatedAt ? `
${t('components.status.updatedAt', { time: updatedAt })}
` : ''}
`; this.setSafeContent(unsafeRawHtml(html, 'legacy Panel.setContent() migration')); } }