spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import Link from 'next/link';3import { t } from '@/i18n';4import { formatValue, grouped, ordinal } from '@/lib/format';5import { routes } from '@/lib/site';6import type { FormatSpec } from '@/lib/types';7import type { StoryItem, StoryResponse } from '@/lib/types-analytics';8import { LineChart } from '@/components/charts/line-chart';9import { useProvenance } from '@/components/data/provenance-context';1011/**12 * "How {name} changed": one small multiple per long-run indicator (full annual history), the templated sentence13 * computed server-side underneath, first/last labels, peak/trough and rank change. Every chart opens provenance.14 */15export function CountryStory({ data, country }: { data: StoryResponse; country: { id: string; slug: string | null; name: string; flag?: string | null } }) {16 const { open } = useProvenance();17 if (!data.items.length) return <p className="py-4 text-sm text-ink-3">{t('country.story.none')}</p>;18 return (19 <div className="grid gap-x-6 gap-y-7 sm:grid-cols-2 lg:grid-cols-3">20 {data.items.map((it) => (21 <StoryCell key={it.indicator.slug} it={it} country={country} onProvenance={() => open({ indicator: { slug: it.indicator.slug, name: it.indicator.name ?? it.indicator.slug, format: it.indicator.format, unit: it.indicator.unit, unit_short: it.indicator.unit_short, precision: it.indicator.precision, frequency: it.indicator.frequency, higher_is_better: it.indicator.higher_is_better }, value: { value: it.last.value, formatted: it.last.formatted, period: `${it.last.year}-01-01`, year: it.last.year, unit: it.indicator.unit, provenance: it.provenance }, country })} />22 ))}23 </div>24 );25}2627function StoryCell({ it, country, onProvenance }: { it: StoryItem; country: { slug: string | null }; onProvenance: () => void }) {28 const spec: FormatSpec = { format: it.indicator.format, unit: it.indicator.unit, unit_short: it.indicator.unit_short, precision: it.indicator.precision, name: it.indicator.short_name ?? it.indicator.name, higher_is_better: it.indicator.higher_is_better };29 const points = it.series.map(([year, value]) => ({ period: `${year}-01-01`, year, value }));30 const up = (it.change_abs ?? 0) >= 0;31 const title = it.indicator.short_name ?? it.indicator.name ?? it.indicator.slug;32 return (33 <article className="min-w-0 border-t border-rule pt-3" aria-labelledby={`story-${it.indicator.slug}`}>34 <div className="flex items-baseline justify-between gap-2">35 <h3 id={`story-${it.indicator.slug}`} className="min-w-0 truncate text-sm font-semibold text-ink">36 <Link href={it.indicator.topic && country.slug ? routes.countryIndicator(country.slug, it.indicator.topic, it.indicator.slug) : routes.indicator(it.indicator.slug)} className="link-quiet">37 {title}38 </Link>39 </h3>40 <span className={`tnum shrink-0 text-xs font-medium ${up ? 'text-inc' : 'text-dec'}`}>41 {it.change_pct != null && Math.abs(it.change_pct) < 10000 && ['currency', 'number', 'tonnes', 'kwh'].includes(it.indicator.format ?? '') ? `${up ? '+' : '−'}${grouped(Math.abs(it.change_pct))} %` : it.change_abs != null ? `${up ? '+' : '−'}${formatValue(Math.abs(it.change_abs), { ...spec, format: spec.format === 'percent' ? 'percent' : spec.format }).replace(/^(US\$|intl \$)/, '$1')}` : ''}42 </span>43 </div>44 <div className="tnum -mt-0.5 flex justify-between text-2xs text-ink-3">45 <span>46 {it.first.year} · {it.first.formatted ?? formatValue(it.first.value, spec)}47 </span>48 <span>49 {it.last.year} · <span className="font-medium text-ink">{it.last.formatted ?? formatValue(it.last.value, spec)}</span>50 </span>51 </div>52 <LineChart series={[{ id: it.indicator.slug, name: title, points }]} spec={spec} subject={title} height={150} endLabels={false} margin={{ left: 40, right: 14, top: 22, bottom: 22 }} defaultWidth={360} className="mt-1 [&_figcaption]:hidden" provenance={it.provenance} payload={{ indicator: { slug: it.indicator.slug, name: it.indicator.name ?? title, format: it.indicator.format, unit: it.indicator.unit, unit_short: it.indicator.unit_short, precision: it.indicator.precision, frequency: it.indicator.frequency, higher_is_better: it.indicator.higher_is_better }, value: { value: it.last.value, formatted: it.last.formatted, period: `${it.last.year}-01-01`, year: it.last.year, unit: it.indicator.unit, provenance: it.provenance }, country: null }} />53 <button type="button" onClick={onProvenance} className="mt-1 text-left text-xs leading-snug text-ink-2 hover:text-ink" aria-label={t('common.openProvenance')}>54 {it.text.replace(/^[^:]+:\s*/, '')}55 </button>56 <p className="tnum mt-1 text-2xs text-ink-3">57 {it.peak && it.peak.year !== it.last.year ? `${t('country.story.peak', { value: formatValue(it.peak.value, spec), year: it.peak.year })}` : ''}58 {it.rank_first && it.rank_last ? `${it.peak && it.peak.year !== it.last.year ? ' · ' : ''}${t('country.story.rank', { r0: ordinal(it.rank_first.rank), y0: it.rank_first.year, r1: ordinal(it.rank_last.rank), n: it.rank_last.n, y1: it.rank_last.year })}` : ''}59 </p>60 </article>61 );62}63