spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import { ArrowDownRight, ArrowUpRight } from 'lucide-react';2import Link from 'next/link';3import { t } from '@/i18n';4import { cn } from '@/lib/cn';5import { formatValue, grouped } from '@/lib/format';6import { routes } from '@/lib/site';7import type { PulseItem, PulseResponse } from '@/lib/types-analytics';89/**10 * World Pulse: a dense editorial list of what is changing globally, one row per headline/featured indicator.11 * Direction uses the neutral increase/decrease colours (--inc/--dec); "improvement" tones apply only when the12 * indicator declares a direction. Share bar = countries up vs down; right column = biggest mover.13 */14export function WorldPulse({ data, limit = 14 }: { data: PulseResponse; limit?: number }) {15 const items = [...data.items].sort((a, b) => Math.max(b.share_up, b.share_down) - Math.max(a.share_up, a.share_down)).slice(0, limit);16 return (17 <div className="min-w-0">18 <ol className="divide-y divide-rule border-y border-rule">19 {items.map((it) => (20 <PulseRow key={it.indicator.slug} it={it} />21 ))}22 </ol>23 <p className="mt-3 text-xs text-ink-3">24 {t('home.pulse.footnote', { year: data.year_reference, n: grouped(data.summary.n_countries_reporting) })} {t('semantics.note')}25 </p>26 </div>27 );28}2930function PulseRow({ it }: { it: PulseItem }) {31 const up = it.share_up >= it.share_down;32 const Icon = up ? ArrowUpRight : ArrowDownRight;33 const spec = { format: it.indicator.format, unit: it.indicator.unit, unit_short: it.indicator.unit_short, precision: it.indicator.precision };34 const mover = up ? it.top_up : it.top_down;35 const total = it.n_up + it.n_down + it.n_flat || 1;36 return (37 <li className="grid gap-x-4 gap-y-1.5 py-3 md:grid-cols-[minmax(0,12rem)_minmax(0,1fr)_minmax(0,14rem)] md:items-center">38 <div className="flex min-w-0 items-center gap-2">39 <span className={cn('grid h-7 w-7 shrink-0 place-items-center rounded-sm', up ? 'bg-inc/15 text-inc' : 'bg-dec/15 text-dec')} aria-hidden>40 <Icon size={15} strokeWidth={2.25} />41 </span>42 <Link href={routes.indicator(it.indicator.slug)} className="link-quiet flex min-h-[44px] min-w-0 items-center text-sm font-semibold text-ink md:min-h-0">43 <span className="truncate">{it.indicator.short_name ?? it.indicator.name}</span>44 </Link>45 </div>46 <div className="min-w-0">47 <p className="text-sm leading-snug text-ink">{it.headline}</p>48 <div className="mt-1 flex items-center gap-2 text-2xs text-ink-3" aria-label={t('home.pulse.shareLabel', { up: it.n_up, down: it.n_down })}>49 <span className="tnum shrink-0 text-inc">↑ {it.n_up}</span>50 <span className="flex h-1.5 min-w-0 flex-1 overflow-hidden rounded-xs bg-surface-2" aria-hidden>51 <span className="h-full bg-inc" style={{ width: `${(it.n_up / total) * 100}%` }} />52 <span className="h-full bg-dec" style={{ width: `${(it.n_down / total) * 100}%` }} />53 </span>54 <span className="tnum shrink-0 text-dec">↓ {it.n_down}</span>55 {it.record_highs ? <span className="tnum hidden shrink-0 sm:inline">· {t('home.pulse.records', { n: it.record_highs })}</span> : null}56 </div>57 </div>58 <div className="min-w-0 text-xs text-ink-2">59 {mover ? (60 <Link href={mover.country.slug ? routes.country(mover.country.slug) : routes.indicator(it.indicator.slug)} className="link-quiet flex min-h-[32px] items-center gap-1.5">61 <span className="shrink-0 text-ink-3">{up ? t('home.pulse.largestRise') : t('home.pulse.largestFall')}</span>62 <span aria-hidden>{mover.country.flag}</span>63 <span className="truncate font-medium text-ink">{mover.country.name}</span>64 <span className="tnum shrink-0 text-ink-3">{formatValue(mover.value, spec)}</span>65 </Link>66 ) : null}67 </div>68 </li>69 );70}71