/** * Current Route tab — shows the primary lane's chokepoints, transit/freight * estimates, disruption score, and war risk tier. Shows a noModeledLane * empty state when the origin/destination clusters have no shared route. */ import type { GetRouteExplorerLaneResponse, ChokepointExposureSummary, } from '@/generated/server/worldmonitor/supply_chain/v1/service_server'; import { formatTransitRange, formatFreightRange, formatExposurePct, formatDisruptionScore, disruptionScoreClass, warRiskTierLabel, warRiskTierClass, escapeHtml, } from './route-utils'; import { setTrustedHtml, trustedHtml } from '@/utils/dom-utils'; export interface CurrentRouteTabOptions { onChokepointSelect?: (chokepointId: string) => void; } export class CurrentRouteTab { public readonly element: HTMLDivElement; private opts: CurrentRouteTabOptions; constructor(opts: CurrentRouteTabOptions = {}) { this.opts = opts; this.element = document.createElement('div'); this.element.className = 're-tab re-tab--current'; this.element.setAttribute('role', 'tabpanel'); this.renderEmpty(); } public update(data: GetRouteExplorerLaneResponse | null): void { if (!data || data.noModeledLane) { this.renderNoModeledLane(); return; } this.renderData(data); } private renderEmpty(): void { setTrustedHtml(this.element, trustedHtml('
Pick a country pair and product to see the current route.
', "legacy direct innerHTML migration")); } private renderNoModeledLane(): void { setTrustedHtml(this.element, trustedHtml('
' + '

No modeled lane

' + '

WorldMonitor does not have a modeled maritime route between these two countries. ' + 'This may mean the pair shares no major trade corridor in our dataset, or one country is landlocked.

' + '
', "legacy direct innerHTML migration")); } private renderData(data: GetRouteExplorerLaneResponse): void { const summaryHtml = this.renderSummary(data); const chokepointsHtml = this.renderChokepointList(data.chokepointExposures); setTrustedHtml(this.element, trustedHtml(`${summaryHtml}${chokepointsHtml}`, "legacy direct innerHTML migration")); this.attachChokepointListeners(); } private renderSummary(data: GetRouteExplorerLaneResponse): string { const riskCls = warRiskTierClass(data.warRiskTier); const disruptCls = disruptionScoreClass(data.disruptionScore); return [ '
', `
`, ` Transit`, ` ${formatTransitRange(data.estTransitDaysRange)}`, `
`, `
`, ` Freight (est.)`, ` ${formatFreightRange(data.estFreightUsdPerTeuRange, data.cargoType)}`, `
`, `
`, ` Disruption`, ` ${formatDisruptionScore(data.disruptionScore)}`, `
`, `
`, ` War Risk`, ` ${escapeHtml(warRiskTierLabel(data.warRiskTier))}`, `
`, '
', ].join('\n'); } private renderChokepointList(exposures: ChokepointExposureSummary[]): string { if (exposures.length === 0) { return '
No chokepoint exposures on this route.
'; } const rows = exposures.map( (e, i) => `` + `${i + 1}` + `${escapeHtml(e.chokepointName)}` + `${formatExposurePct(e.exposurePct)}` + ``, ); return [ '', ' ', ` ${rows.join('')}`, '
#ChokepointExposure
', ].join('\n'); } private attachChokepointListeners(): void { const rows = this.element.querySelectorAll('.re-current__cp-row'); rows.forEach((row) => { const cpId = row.dataset.cpId; if (!cpId) return; const select = () => this.opts.onChokepointSelect?.(cpId); row.addEventListener('click', select); row.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); select(); } }); }); } }