spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import { AlertTriangle, ArrowDownRight, ArrowUpRight, GitCommitHorizontal, Repeat, TrendingDown, TrendingUp, Trophy, Waves } from 'lucide-react';2import Link from 'next/link';3import { t, tOpt } from '@/i18n';4import { cn } from '@/lib/cn';5import { formatPeriod } from '@/lib/format';6import { severityLabel, severityLevel } from '@/lib/severity';7import { routes } from '@/lib/site';8import type { ChangeItem } from '@/lib/types';910const ICON: Record<string, typeof ArrowUpRight> = {11 yoy_jump: ArrowUpRight,12 yoy_drop: ArrowDownRight,13 record_high: Trophy,14 record_low: AlertTriangle,15 n_year_high: TrendingUp,16 n_year_low: TrendingDown,17 sign_flip: Repeat,18 accelerating: TrendingUp,19 decelerating: TrendingDown,20 structural_break: GitCommitHorizontal,21 trend_reversal: Repeat,22 volatility_spike: Waves,23};2425export function kindLabel(kind: string | null | undefined, windowYears?: number | null): string {26 if (!kind) return '';27 return tOpt(`change.kind.${kind}`, kind.replace(/_/g, ' '), { n: windowYears ?? 10 });28}2930/**31 * Feed of detected changes/events: kind icon + label, headline, country (optional), period, severity chip.32 * Server component. `showCountry` for the global feed; `compact` for the timeline.33 */34export function ChangeList({ items, showCountry = false, className, emptyText }: { items: ChangeItem[]; showCountry?: boolean; className?: string; emptyText?: string }) {35 if (items.length === 0) return <p className={cn('py-4 text-sm text-ink-3', className)}>{emptyText ?? t('country.changes.none')}</p>;36 return (37 <ol className={cn('divide-y divide-rule', className)}>38 {items.map((c, i) => {39 const Icon = ICON[c.kind ?? ''] ?? ArrowUpRight;40 const lvl = severityLevel(c.severity);41 const indSlug = (c.indicator as { slug?: string; id: string }).slug ?? c.indicator.id;42 const href = c.country?.slug ? routes.country(c.country.slug) : routes.indicator(indSlug);43 return (44 <li key={c.id ?? `${indSlug}-${i}`} className="flex gap-3 py-3">45 <span className={cn('mt-0.5 grid h-7 w-7 shrink-0 place-items-center rounded-sm', lvl === 'high' ? 'bg-accent-soft text-accent' : 'bg-surface-2 text-ink-2')} aria-hidden>46 <Icon size={15} />47 </span>48 <div className="min-w-0 flex-1">49 <div className="flex flex-wrap items-baseline gap-x-2 text-2xs text-ink-3">50 <span className="font-medium uppercase tracking-wide">{kindLabel(c.kind, c.window_years)}</span>51 {c.period ? <span className="tnum">{formatPeriod(c.period, 'A')}</span> : null}52 <span className={cn('rounded-xs px-1 py-px', lvl === 'high' ? 'bg-accent-soft text-accent' : 'bg-surface-2 text-ink-3')}>{severityLabel(c.severity)}</span>53 </div>54 <Link href={href} className="link-quiet mt-0.5 block text-sm leading-snug text-ink">55 {showCountry && c.country ? (56 <span className="mr-1.5 text-ink-2">57 <span aria-hidden>{c.country.flag} </span>58 {c.country.name}59 </span>60 ) : null}61 {c.headline ?? `${'name' in c.indicator ? c.indicator.name : indSlug}: ${c.formatted ?? ''}`}62 </Link>63 </div>64 </li>65 );66 })}67 </ol>68 );69}70