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.8 KB · 71 lines tsx
Raw Blame History
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