'use client'; import { X } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { t } from '@/i18n'; import { clientExplore } from '@/lib/client-api-explore'; import { cn } from '@/lib/cn'; import { routes } from '@/lib/site'; import type { FormatSpec, Series } from '@/lib/types'; import { LineChart, type LineSeries } from '@/components/charts/line-chart'; import { pointsFromSeries } from '@/components/charts/scales'; import { seriesVar } from '@/components/charts/palette'; import { EmptyState } from '@/components/data/empty-state'; import type { ProvenancePayload } from '@/components/data/provenance-context'; import { EntityPicker, type PickedEntity } from '@/components/explore/entity-picker'; export interface CompareCountry { id: string; slug: string; name: string; flag: string | null; } const MAX = 5; /** * Country comparison teaser on an indicator page: chips for the selected countries (≤ 5), a typeahead to add * one, and a multi-series LineChart from `/series?country=&indicator=`. Colour slot follows the country's * position in the selection (stable while it stays selected). */ export function IndicatorCompare({ slug, spec, initialCountries, initialSeries, payload }: { slug: string; spec: FormatSpec; initialCountries: CompareCountry[]; initialSeries: Series[]; payload: ProvenancePayload | null }) { const [countries, setCountries] = useState(initialCountries); const [seriesById, setSeriesById] = useState>(() => Object.fromEntries(initialSeries.map((s) => [s.country.id, s]))); const [loading, setLoading] = useState(false); const abortRef = useRef(null); useEffect(() => { const missing = countries.filter((c) => seriesById[c.id] === undefined).map((c) => c.id); if (missing.length === 0) return; abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); clientExplore .seriesBundle(missing, slug, ctrl.signal) .then((r) => { const next: Record = {}; for (const id of missing) next[id] = r.series.find((s) => s.country.id === id) ?? null; setSeriesById((m) => ({ ...m, ...next })); }) .catch((e) => { if ((e as Error).name !== 'AbortError') setSeriesById((m) => ({ ...m, ...Object.fromEntries(missing.map((id) => [id, null])) })); }) .finally(() => { if (!ctrl.signal.aborted) setLoading(false); }); return () => ctrl.abort(); }, [countries, slug, seriesById]); const add = (e: PickedEntity) => { if (countries.length >= MAX || countries.some((c) => c.id === e.id)) return; setCountries((cs) => [...cs, { id: e.id, slug: e.slug, name: e.name, flag: e.flag ?? null }]); }; const remove = (id: string) => setCountries((cs) => cs.filter((c) => c.id !== id)); const lines: LineSeries[] = useMemo(() => { const out: LineSeries[] = []; countries.forEach((c, i) => { const s = seriesById[c.id]; if (!s) return; const points = pointsFromSeries(s.values); if (points.length) out.push({ id: c.id, name: c.name, points, colorIndex: i }); }); return out; }, [countries, seriesById]); const firstProv = countries.map((c) => seriesById[c.id]?.provenance).find(Boolean) ?? null; return (
{countries.map((c, i) => ( {c.flag ? {c.flag} : null} {c.name} ))} {countries.length < MAX ? ( c.id)} className="w-56" size="sm" /> ) : ( {t('indicator.compare.max')} )}
{lines.length === 0 && !loading ? ( ) : lines.length ? ( ) : (
{t('common.loading')}
)}
{countries.length >= 2 ? ( c.slug))} className="mt-2 inline-flex min-h-[40px] items-center text-sm text-accent hover:underline"> {t('indicator.compare.open')} → ) : null}
); }