'use client'; import { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { t } from '@/i18n'; import { clientApi } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import { displayValue, formatPeriod } from '@/lib/format'; import { routes } from '@/lib/site'; import type { MetricValue, SeriesResponse } from '@/lib/types'; import { LineChart } from '@/components/charts/line-chart'; import { pointsFromSeries, pointsFromSpark } from '@/components/charts/scales'; import { ChangeChip } from '@/components/data/change-chip'; import { EmptyState } from '@/components/data/empty-state'; import { payloadFor, type MetricCountry } from '@/components/data/metric'; import { useProvenance } from '@/components/data/provenance-context'; import { RankBadge } from '@/components/data/rank-badge'; const CHART_H = 220; /** * One indicator on a topic page: name, latest value + change + rank, full-history LineChart (dashed forecast), * source line, Compare / Ranking / Indicator links. `series` (server-fetched) renders immediately; otherwise * the chart mounts on scroll (IntersectionObserver) and fetches `/countries/{id}/series/{slug}` client-side. * The chart area reserves its height so nothing shifts. */ export function IndicatorRow({ metric, country, regionName, series: initialSeries, eager = false }: { metric: MetricValue; country: MetricCountry; regionName?: string | null; series?: SeriesResponse | null; eager?: boolean }) { const { open } = useProvenance(); const m = metric; const ref = useRef(null); const [series, setSeries] = useState(initialSeries); const [visible, setVisible] = useState(eager || !!initialSeries); const [error, setError] = useState(false); useEffect(() => { if (visible || !ref.current) return; const el = ref.current; if (typeof IntersectionObserver === 'undefined') { setVisible(true); return; } const io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) { setVisible(true); io.disconnect(); } }, { rootMargin: '400px 0px' }, ); io.observe(el); return () => io.disconnect(); }, [visible]); useEffect(() => { if (!visible || series !== undefined || !m.has_data) return; const ctrl = new AbortController(); clientApi .countrySeries(country.id, m.indicator, ctrl.signal) .then((r) => setSeries(r)) .catch((e) => { if ((e as Error).name !== 'AbortError') { setError(true); setSeries(null); } }); return () => ctrl.abort(); }, [visible, series, m.has_data, m.indicator, country.id]); const name = m.indicator_name ?? m.indicator; const points = series ? pointsFromSeries(series.values) : pointsFromSpark(m.sparkline); const spec = { format: m.format, unit: m.unit, unit_short: m.unit_short, frequency: m.frequency, name, higher_is_better: m.higher_is_better, precision: series?.indicator.precision ?? null }; const payload = payloadFor(m, country, { name: series?.indicator.name ?? name }); return (

{series?.indicator.name ?? name}

{m.unit ?

{m.unit}

: null} {m.has_data ? ( ) : null}
{t('topic.compareLink')} {t('topic.rankingLink')} {t('topic.indicatorLink')}
{!m.has_data ? ( ) : error ? ( ) : points.length >= 2 ? ( ) : (
{t('common.loading')}
)}
); }