SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.3 KB · 70 lines tsx
Raw Blame History
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