'use client'; import { Activity, AlertTriangle, ArrowDownRight, ArrowUpRight, GitCommitHorizontal, Repeat, TrendingDown, TrendingUp, Trophy, Waves } from 'lucide-react'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import { severityLevel } from '@/lib/severity'; import { routes } from '@/lib/site'; import { topicById } from '@/lib/topics'; import type { ChangeItem } from '@/lib/types'; import { kindLabel } from '@/components/data/change-list'; const ICON: Record = { yoy_jump: ArrowUpRight, yoy_drop: ArrowDownRight, record_high: Trophy, record_low: AlertTriangle, n_year_high: TrendingUp, n_year_low: TrendingDown, sign_flip: Repeat, accelerating: TrendingUp, decelerating: TrendingDown, structural_break: GitCommitHorizontal, trend_reversal: Repeat, volatility_spike: Waves, }; /** Topic filter chips → indicator topics. */ const FILTERS: Array<{ id: string; topics: string[] }> = [ { id: 'all', topics: [] }, { id: 'economy', topics: ['economy', 'government', 'trade', 'income'] }, { id: 'population', topics: ['population'] }, { id: 'health', topics: ['health'] }, { id: 'energy', topics: ['energy'] }, { id: 'climate', topics: ['climate', 'environment'] }, { id: 'digital', topics: ['digital', 'innovation'] }, ]; const PAGE = 40; /** * Country timeline 2.0: a vertical chronological rail grouped by decade → year, topic filters, kind glyphs and * severity emphasis (record highs/lows, sharp changes, reversals, structural breaks, volatility). Newest first. */ export function Timeline({ items, slug }: { items: ChangeItem[]; slug: string }) { const [filter, setFilter] = useState('all'); const [shown, setShown] = useState(PAGE); const filtered = useMemo(() => { const f = FILTERS.find((x) => x.id === filter) ?? FILTERS[0]!; const rows = f.topics.length ? items.filter((it) => f.topics.includes(('topic' in it.indicator ? it.indicator.topic : null) ?? '')) : items; return [...rows].sort((a, b) => (b.year ?? 0) - (a.year ?? 0) || (b.severity ?? 0) - (a.severity ?? 0)); }, [items, filter]); const visible = filtered.slice(0, shown); const decades = useMemo(() => { const m = new Map>(); for (const it of visible) { const y = it.year ?? 0; const d = Math.floor(y / 10) * 10; if (!m.has(d)) m.set(d, new Map()); const ym = m.get(d)!; if (!ym.has(y)) ym.set(y, []); ym.get(y)!.push(it); } return Array.from(m.entries()).sort((a, b) => b[0] - a[0]); }, [visible]); const counts = useMemo(() => Object.fromEntries(FILTERS.map((f) => [f.id, f.topics.length ? items.filter((it) => f.topics.includes(('topic' in it.indicator ? it.indicator.topic : null) ?? '')).length : items.length])), [items]); if (items.length === 0) return

{t('country.timeline.none')}

; return (
    {FILTERS.filter((f) => (counts[f.id] ?? 0) > 0 || f.id === 'all').map((f) => (
  • ))}
    {(['record_high', 'yoy_jump', 'sign_flip', 'structural_break', 'volatility_spike'] as const).map((k) => { const Icon = ICON[k]!; return (
  • {kindLabel(k, 10)}
  • ); })}
    {decades.map(([decade, years]) => (
  1. {t('country.timeline.decade', { d: decade })}
      {Array.from(years.entries()).map(([year, events]) => (
    1. {year}
        {events.map((e, i) => { const ind = e.indicator; const indSlug = (ind as { slug?: string; id: string }).slug ?? ind.id; const topic = 'topic' in ind ? topicById(ind.topic ?? '')?.id : undefined; const href = topic ? routes.countryIndicator(slug, topic, indSlug) : routes.indicator(indSlug); const lvl = severityLevel(e.severity); const Icon = ICON[e.kind ?? ''] ?? Activity; return (
      • {kindLabel(e.kind, e.window_years)} {e.headline}
      • ); })}
    2. ))}
  2. ))}
{filtered.length > shown ? (
) : null}
); }