spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import { ArrowDownRight, ArrowUpRight } from 'lucide-react';3import Link from 'next/link';4import { t } from '@/i18n';5import { cn } from '@/lib/cn';6import { fixed, formatValue } from '@/lib/format';7import { routes } from '@/lib/site';8import type { FormatSpec } from '@/lib/types';9import type { ExtremeFacet } from '@/lib/types-analytics';10import { SourceLine } from '@/components/charts/source-line';1112function changeText(row: ExtremeFacet['rows'][number], metric: ExtremeFacet['metric'], spec: FormatSpec): string {13 if (metric === 'pct' && row.delta_pct != null) return `${row.delta_pct >= 0 ? '+' : '−'}${fixed(Math.abs(row.delta_pct), 1)} %`;14 if (row.delta == null) return t('common.na');15 const sign = row.delta >= 0 ? '+' : '−';16 if (metric === 'points') return `${sign}${fixed(Math.abs(row.delta), spec.precision ?? 1)} pts`;17 return `${sign}${formatValue(Math.abs(row.delta), spec)}`;18}1920/** One extremes facet: title, direction, rows start → end with the change bar. Neutral colours (increase/decrease). */21export function ExtremeFacetBlock({ facet }: { facet: ExtremeFacet }) {22 const ind = facet.indicator;23 const spec: FormatSpec = { format: ind.format, unit: ind.unit, unit_short: ind.unit_short, precision: ind.precision, frequency: 'A', name: ind.short_name ?? ind.name, higher_is_better: ind.higher_is_better };24 const up = facet.direction === 'up';25 const Icon = up ? ArrowUpRight : ArrowDownRight;26 const mags = facet.rows.map((r) => Math.abs((facet.metric === 'pct' ? r.delta_pct : r.delta) ?? 0));27 const max = Math.max(1e-9, ...mags);28 const y0 = facet.rows[0]?.year_start;29 const y1 = facet.rows[0]?.year_end;30 const payload = { indicator: { slug: ind.slug, name: ind.name ?? ind.slug, format: ind.format, unit: ind.unit, unit_short: ind.unit_short, precision: ind.precision, frequency: 'A' as const, higher_is_better: ind.higher_is_better }, value: null, country: null, downloadHref: routes.indicatorDownload(ind.slug) };31 return (32 <section id={facet.id} className="hairline min-w-0 scroll-mt-28 pb-8 pt-6" aria-labelledby={`${facet.id}-h`}>33 <div className="mb-3 flex flex-wrap items-end justify-between gap-x-6 gap-y-2">34 <div className="min-w-0">35 <h2 id={`${facet.id}-h`} className="display text-xl text-ink md:text-2xl">36 {facet.title}37 </h2>38 <p className="mt-0.5 flex flex-wrap items-center gap-x-2 text-sm text-ink-2">39 <span className={cn('inline-flex items-center gap-0.5 font-medium', up ? 'text-inc' : 'text-dec')}>40 <Icon size={14} aria-hidden /> {up ? t('extremes.direction.up') : t('extremes.direction.down')}41 </span>42 <span className="text-ink-3">·</span>43 <Link href={routes.indicator(ind.slug)} className="link-quiet text-ink">44 {ind.name}45 </Link>46 {y0 && y1 ? (47 <span className="tnum text-ink-3">48 · {y0}–{y1} · {t('extremes.n', { n: facet.n })}49 </span>50 ) : null}51 </p>52 </div>53 <div className="flex flex-wrap gap-x-4 text-sm">54 <Link href={routes.ranking(ind.slug)} className="inline-flex min-h-[44px] items-center text-accent hover:underline md:min-h-[32px]">55 {t('extremes.openRanking')} →56 </Link>57 <Link href={routes.explore({ indicator: ind.slug, year: y1 ?? null })} className="inline-flex min-h-[44px] items-center text-accent hover:underline md:min-h-[32px]">58 {t('extremes.explore')} →59 </Link>60 </div>61 </div>62 <ol className="divide-y divide-rule">63 {facet.rows.map((r, i) => {64 const mag = Math.abs((facet.metric === 'pct' ? r.delta_pct : r.delta) ?? 0);65 const pct = (mag / max) * 100;66 return (67 <li key={r.country.id} className="grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5 py-1.5 sm:grid-cols-[1.5rem_minmax(0,12rem)_minmax(0,1fr)_minmax(9rem,auto)]">68 <span className="tnum text-right text-xs text-ink-3">{i + 1}</span>69 <Link href={routes.country(r.country.slug ?? r.country.id.toLowerCase())} className="link-quiet col-span-2 flex min-h-[40px] min-w-0 items-center gap-1.5 text-sm text-ink sm:col-span-1 sm:min-h-[32px]">70 <span aria-hidden>{r.country.flag}</span>71 <span className="truncate">{r.country.name}</span>72 </Link>73 <div className="col-start-2 row-start-2 h-2.5 min-w-0 self-center sm:col-start-3 sm:row-start-1 sm:h-3.5" aria-hidden>74 <div className={cn('h-full rounded-r-sm', up ? 'bg-inc' : 'bg-dec')} style={{ width: `${Math.max(1.5, pct)}%`, opacity: 0.85 }} />75 </div>76 <div className="tnum col-start-3 row-start-2 text-right text-sm sm:col-start-4 sm:row-start-1">77 <span className={cn('font-semibold', up ? 'text-inc' : 'text-dec')}>{changeText(r, facet.metric, spec)}</span>78 <span className="block text-2xs text-ink-3">79 {r.formatted_start ?? formatValue(r.value_start, spec)} <span aria-hidden>→</span> <span className="sr-only">{t('extremes.to')}</span> {r.formatted_end ?? formatValue(r.value_end, spec)}80 {r.year_start && r.year_end && (r.year_start !== y0 || r.year_end !== y1) ? ` (${r.year_start}–${r.year_end})` : ''}81 </span>82 </div>83 </li>84 );85 })}86 </ol>87 <div className="mt-2">88 <SourceLine provenance={facet.provenance} payload={payload} />89 </div>90 </section>91 );92}93