'use client'; import { ArrowDownRight, ArrowUpRight } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { t, tOpt } from '@/i18n'; import { clientAnalytics } from '@/lib/client-api-analytics'; import { cn } from '@/lib/cn'; import { severityLevel } from '@/lib/severity'; import { routes } from '@/lib/site'; import type { MoverCategory, MoverItem, MoverKindFilter, MoverWindow, MoversResponse } from '@/lib/types-analytics'; import { kindLabel } from '@/components/data/change-list'; import { CountryChip } from '@/components/data/country-chip'; import { Segmented } from '@/components/controls/indicator-select'; const WINDOWS: MoverWindow[] = [1, 5, 10]; const CATEGORIES: MoverCategory[] = ['all', 'economic', 'demographic', 'health', 'energy', 'climate', 'digital', 'housing', 'labor']; const KINDS_1: MoverKindFilter[] = ['all', 'improvement', 'deterioration', 'record', 'reversal', 'acceleration', 'structural']; const KINDS_N: MoverKindFilter[] = ['all', 'improvement', 'deterioration', 'increase', 'decrease']; /** * Biggest movers: window tabs (24 months / 5 years / 10 years), category and kind chips; rows show country, * indicator, ref → now, delta, severity dot and the templated headline. The server passes the first payload * (window 1, all); every change refetches `/movers` client-side with request cancellation. */ export function Movers({ initial, limit = 12, compact = false, initialWindow }: { initial: MoversResponse | null; limit?: number; compact?: boolean; initialWindow?: MoverWindow }) { const [win, setWin] = useState(initialWindow ?? initial?.window ?? 1); const [category, setCategory] = useState('all'); const [kind, setKind] = useState('all'); const [cache, setCache] = useState>(() => (initial ? { [`${initial.window}|all|all`]: initial } : {})); const [loading, setLoading] = useState(false); const abortRef = useRef(null); const key = `${win}|${category}|${kind}`; useEffect(() => { if (cache[key] !== undefined) return; abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); clientAnalytics .movers({ window: win, category, kind, limit }, ctrl.signal) .then((r) => setCache((c) => ({ ...c, [key]: r }))) .catch((e) => { if ((e as Error).name !== 'AbortError') setCache((c) => ({ ...c, [key]: null })); }) .finally(() => { if (!ctrl.signal.aborted) setLoading(false); }); return () => ctrl.abort(); }, [key, win, category, kind, limit, cache]); const data = cache[key]; const kinds = win === 1 ? KINDS_1 : KINDS_N; useEffect(() => { if (!kinds.includes(kind)) setKind('all'); }, [kinds, kind]); const items = useMemo(() => (data?.items ?? []).slice(0, limit), [data, limit]); return (
setWin(Number(v) as MoverWindow)} label={t('home.movers.window')} options={WINDOWS.map((w) => ({ value: String(w) as '1' | '5' | '10', label: t(`home.movers.window.${w}` as 'home.movers.window.1') }))} size="sm" /> {data?.filter_note ? {data.filter_note} : null}
    {kinds.map((k) => (
  • ))}
{data === null ? (

{t('common.errorHint')}

) : data && items.length === 0 ? (

{t('home.movers.none')}

) : (
    {items.map((m) => ( ))}
)}
{t('home.changes.all')} → {t('home.movers.extremes')} →
); } function MoverRow({ m }: { m: MoverItem }) { const up = m.direction === 'up'; const Icon = up ? ArrowUpRight : ArrowDownRight; const lvl = severityLevel(m.severity); const tone = m.interpretation === 'improvement' ? 'text-up' : m.interpretation === 'deterioration' ? 'text-down' : up ? 'text-inc' : 'text-dec'; const deltaText = m.delta_pct != null && ['currency', 'number', 'tonnes', 'kwh'].includes(m.indicator.format ?? '') ? `${m.delta_pct > 0 ? '+' : '−'}${Math.abs(m.delta_pct).toFixed(1)} %` : m.delta != null ? `${m.delta > 0 ? '+' : '−'}${Math.abs(m.delta).toFixed(1)}${m.indicator.format === 'percent' ? ' pts' : ''}` : ''; return (
  • {kindLabel(m.kind, null)} {m.interpretation ? {tOpt(`home.movers.interp.${m.interpretation}`, m.interpretation)} : null}
    {m.indicator.short_name ?? m.indicator.name} {m.formatted_ref ?? ''} → {m.formatted ?? ''} {deltaText} {m.ref_year}–{m.year}
    {m.headline ?

    {m.headline}

    : null}
  • ); }