'use client'; import { Check, Copy, Download, X } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { t } from '@/i18n'; import { clientPlatform } from '@/lib/client-api-platform'; import { cn } from '@/lib/cn'; import { grouped } from '@/lib/format'; import { routes } from '@/lib/site'; import { parseList, useUrlState } from '@/lib/url-state'; import type { CountryLite } from '@/lib/types-compare'; import { seriesVar } from '@/components/charts/palette'; import { CountryTypeahead } from '@/components/compare/country-picker'; import { IndicatorSelect, Segmented, type IndicatorOption } from '@/components/controls/indicator-select'; const MAX_COUNTRIES = 20; const MAX_INDICATORS = 20; const MIN_YEAR = 1960; interface Preset { key: 'g7' | 'brics' | 'climate'; countries: string[]; indicators: string[]; } const PRESETS: Preset[] = [ { key: 'g7', countries: ['canada', 'france', 'germany', 'italy', 'japan', 'united-kingdom', 'united-states'], indicators: ['population', 'gdp', 'gdp-per-capita', 'gdp-growth', 'inflation', 'unemployment-rate', 'life-expectancy', 'general-government-gross-debt-pct-gdp'] }, { key: 'brics', countries: ['brazil', 'russia', 'india', 'china', 'south-africa'], indicators: ['gdp', 'gdp-per-capita-ppp', 'gdp-growth', 'inflation', 'trade-pct-gdp', 'population'] }, { key: 'climate', countries: ['china', 'united-states', 'india', 'russia', 'japan', 'germany', 'iran', 'saudi-arabia'], indicators: ['co2-emissions', 'co2-per-capita', 'renewable-electricity-share', 'energy-use-per-capita', 'carbon-intensity-electricity'] }, ]; /** * Dataset builder: countries (≤ 20) × indicators (≤ 20) × year range × format → the exact * `/api/v1/compare/download.{fmt}` URL, with a live row estimate for small selections. State lives in the URL * (`?countries=&indicators=&from=&to=&format=`) so a build is shareable. */ export function DownloadBuilder({ countries, indicators, maxYear }: { countries: CountryLite[]; indicators: IndicatorOption[]; maxYear: number }) { const { get, getNum, set } = useUrlState(); const bySlug = useMemo(() => new Map(countries.map((c) => [c.slug, c])), [countries]); const byInd = useMemo(() => new Map(indicators.map((i) => [i.slug, i])), [indicators]); const selCountries = parseList(get('countries'), MAX_COUNTRIES).filter((s) => bySlug.has(s)); const selIndicators = parseList(get('indicators'), MAX_INDICATORS).filter((s) => byInd.has(s)); const from = getNum('from'); const to = getNum('to'); const format = (get('format') === 'json' ? 'json' : 'csv') as 'csv' | 'json'; const forecast = get('forecast') === '1'; const [copied, setCopied] = useState(false); const [estimate, setEstimate] = useState<{ key: string; rows: number } | null>(null); const [estimating, setEstimating] = useState(false); const abortRef = useRef(null); const ids = selCountries.map((s) => bySlug.get(s)!.id); const ready = ids.length > 0 && selIndicators.length > 0; const q = ready ? routes.compareDownload(ids, selIndicators, { from, to }, format) + (forecast ? '' : '&include_forecast=false') : null; const estKey = `${ids.join(',')}|${selIndicators.join(',')}|${from ?? ''}|${to ?? ''}|${forecast}`; useEffect(() => { if (!ready || ids.length * selIndicators.length > 60) { setEstimate(null); return; } if (estimate?.key === estKey) return; abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setEstimating(true); const timer = setTimeout(() => { clientPlatform .seriesBundle(ids, selIndicators.slice(0, 12), { from, to }, ctrl.signal) .then((r) => { const rows = r.series.reduce((a, s) => a + s.values.filter((v) => forecast || !v.is_forecast).length, 0); const scale = selIndicators.length > 12 ? selIndicators.length / 12 : 1; setEstimate({ key: estKey, rows: Math.round(rows * scale) }); }) .catch(() => setEstimate(null)) .finally(() => { if (!ctrl.signal.aborted) setEstimating(false); }); }, 250); return () => { clearTimeout(timer); ctrl.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [estKey, ready]); const years: number[] = []; for (let y = maxYear; y >= MIN_YEAR; y--) years.push(y); const copy = async () => { if (!q) return; try { await navigator.clipboard.writeText(`${window.location.origin}${q}`); setCopied(true); setTimeout(() => setCopied(false), 1600); } catch { /* clipboard unavailable */ } }; const setCountries = (next: string[]) => set({ countries: next.join(',') || null }, 0); const setIndicators = (next: string[]) => set({ indicators: next.join(',') || null }, 0); const excl = new Set(selCountries); const pickerOptions = indicators.filter((i) => !selIndicators.includes(i.slug)); return (
{/* Quick starts */}
{t('download.presetHint')} {PRESETS.map((p) => ( ))}
{/* Countries */}
{t('download.countries')}
{selCountries.length}/{MAX_COUNTRIES}
{selCountries.length < MAX_COUNTRIES ? setCountries([...selCountries, c.slug])} placeholder={t('download.addCountry')} /> :

{t('download.max', { n: MAX_COUNTRIES })}

}
    {selCountries.map((s, i) => { const c = bySlug.get(s)!; return (
  • {c.flag} {c.name}
  • ); })}
{/* Indicators */}
{t('download.indicators')}
{selIndicators.length}/{MAX_INDICATORS}
{selIndicators.length < MAX_INDICATORS ? setIndicators([...selIndicators, slug])} label={t('download.addIndicator')} size="sm" /> :

{t('download.max', { n: MAX_INDICATORS })}

}
    {selIndicators.map((s) => { const i = byInd.get(s)!; return (
  • {i.short_name ?? i.name}
  • ); })}
{/* Years · format */}
{t('download.years')}
{t('download.format')}
set({ format: v === 'csv' ? null : v }, 0)} options={[{ value: 'csv', label: 'CSV' }, { value: 'json', label: 'JSON' }]} label={t('download.format')} />
{selCountries.length || selIndicators.length ? ( ) : null}
{/* Result */}
{t('download.get', { fmt: format.toUpperCase() })} {ready ? t('download.selection', { c: selCountries.length, i: selIndicators.length }) : t('download.needBoth')} {ready && (estimate?.key === estKey || estimating) ? · {estimating && estimate?.key !== estKey ? t('download.estimating') : t('download.estimate', { n: grouped(estimate?.rows ?? 0) })} : null}
{q ? (
{t('download.url')}
{q}
{ready && estimate ?

{t('download.estimateNote')}

: null}
) : null}
); }