'use client'; import { AlertTriangle, ArrowDownRight, ArrowUpRight, GitCommitHorizontal, Repeat, SlidersHorizontal, TrendingDown, TrendingUp, Trophy, Waves, 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 { formatPeriod, grouped } from '@/lib/format'; import { WB_REGIONS } from '@/lib/regions'; import { severityLabel, severityLevel } from '@/lib/severity'; import { routes } from '@/lib/site'; import type { ChangeItem } from '@/lib/types'; import { BottomSheet } from '@/components/data/bottom-sheet'; import { kindLabel } from '@/components/data/change-list'; import { CountryChip } from '@/components/data/country-chip'; import { EntityPicker, type PickedEntity } from '@/components/explore/entity-picker'; 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, }; type Sev = 0 | 0.4 | 0.7; /** * Global change feed with filters (kind chips, indicator typeahead, region, minimum severity) in a bottom * sheet on phones and inline on desktop. Items are grouped by period (year). Server passes the first page; * changing a server-side filter (kind / indicator / severity) refetches `/api/v1/changes`; region filters * client-side (the API has no region parameter but every item carries the country's region). */ export function ChangesFeed({ initial, kinds, limit = 100, compact = false }: { initial: ChangeItem[]; kinds: string[]; limit?: number; compact?: boolean }) { const [kind, setKind] = useState(null); const [indicator, setIndicator] = useState(null); const [region, setRegion] = useState(null); const [minSev, setMinSev] = useState(0); const [items, setItems] = useState(initial); const [state, setState] = useState<'idle' | 'loading' | 'error'>('idle'); const [sheet, setSheet] = useState(false); const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; return; } const ctrl = new AbortController(); setState('loading'); clientExplore .changes({ limit, kind, indicator: indicator?.slug ?? null, min_severity: minSev || null }, ctrl.signal) .then((r) => { setItems(r.items); setState('idle'); }) .catch((e) => { if ((e as Error).name !== 'AbortError') setState('error'); }); return () => ctrl.abort(); }, [kind, indicator, minSev, limit]); const visible = useMemo(() => (region ? items.filter((c) => (c.country?.region ?? '').toUpperCase() === region) : items), [items, region]); const groups = useMemo(() => { const m = new Map(); for (const c of visible) { const key = c.period ? formatPeriod(c.period, 'A') : t('common.na'); if (!m.has(key)) m.set(key, []); m.get(key)!.push(c); } return Array.from(m.entries()).sort((a, b) => b[0].localeCompare(a[0])); }, [visible]); const active = (kind ? 1 : 0) + (indicator ? 1 : 0) + (region ? 1 : 0) + (minSev ? 1 : 0); const reset = () => { setKind(null); setIndicator(null); setRegion(null); setMinSev(0); }; const filters = (
{t('changes.filter.kind')}
  • setKind(null)}> {t('common.all')}
  • {kinds.map((k) => (
  • setKind(kind === k ? null : k)}> {kindLabel(k, 10)}
  • ))}
{t('changes.filter.indicator')}
{indicator ? ( {indicator.name} ) : ( )}
{!compact ? (
{t('changes.filter.region')}
  • setRegion(null)}> {t('common.all')}
  • {WB_REGIONS.map((r) => (
  • setRegion(region === r.id ? null : r.id)}> {r.short}
  • ))}
) : null}
{t('changes.filter.severity')}
    {( [ [0, t('changes.filter.severity.any')], [0.4, t('changes.filter.severity.notable')], [0.7, t('changes.filter.severity.major')], ] as Array<[Sev, string]> ).map(([v, label]) => (
  • setMinSev(v)}> {label}
  • ))}
); return (
{compact ? ( ) : null} {active ? ( ) : null} {state === 'loading' ? t('changes.loading') : t('changes.count', { n: grouped(visible.length) })}
{!compact ?
{filters}
: null} {state === 'error' ?

{t('changes.error')}

: null} {state !== 'error' && visible.length === 0 && state !== 'loading' ?

{t('changes.none')}

: null}
{groups.map(([period, rows]) => (

{period}

    {rows.map((c, i) => ( ))}
))}
setSheet(false)} side="center" title={t('common.filters')}> {filters}
); } function ChangeRow({ c }: { c: ChangeItem }) { const Icon = ICON[c.kind ?? ''] ?? ArrowUpRight; const lvl = severityLevel(c.severity); const ind = c.indicator as { id: string; slug?: string; name?: string | null; topic?: string | null }; const slug = ind.slug ?? ind.id; const seriesHref = c.country?.slug && ind.topic ? routes.countryIndicator(c.country.slug, ind.topic, slug) : routes.indicator(slug); return (
  • {c.country ? : null} {kindLabel(c.kind, c.window_years)} {severityLabel(c.severity)}

    {c.headline ?? `${ind.name ?? slug}: ${c.formatted ?? ''}`}

    {t('changes.series')} → {ind.name ?? slug}
  • ); } function Chip({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); }