spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { t, tOpt } from '@/i18n';4import { isNotBuilt, safe } from '@/lib/api';5import { apiAnalytics } from '@/lib/api-analytics';6import { apiExplore } from '@/lib/api-explore';7import { fixed, formatValue, grouped } from '@/lib/format';8import { routes } from '@/lib/site';9import type { RegionCompareResponse } from '@/lib/types-analytics';10import { NotBuiltState } from '@/components/data/empty-state';11import { Section } from '@/components/data/section';12import { PageHeader } from '@/components/explore/page-header';13import { GroupComparePicker } from '@/components/regions/group-compare-picker';14import { GroupHistory } from '@/components/regions/group-history';1516export const revalidate = 900;17type SP = Record<string, string | string[] | undefined>;1819function slugOf(v: string | string[] | undefined, fallback: string): string {20 const s = (Array.isArray(v) ? v[0] : v) ?? '';21 return /^[a-z0-9][a-z0-9-]*$/.test(s) ? s : fallback;22}2324export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {25 const sp = await searchParams;26 const a = slugOf(sp.a, 'g7');27 const b = slugOf(sp.b, 'brics');28 const data = await safe(apiAnalytics.regionsCompare(a, b));29 const names = data ? data.groups.map((g) => g.name ?? g.id) : [a, b];30 const title = t('regions.compare.title', { a: names[0] ?? a, b: names[1] ?? b });31 const canonical = routes.regionCompare(a, b);32 return { title, description: t('regions.compare.description', { a: names[0] ?? a, b: names[1] ?? b }), alternates: { canonical }, robots: a === 'g7' && b === 'brics' ? undefined : { index: false, follow: true } };33}3435export default async function RegionsComparePage({ searchParams }: { searchParams: Promise<SP> }) {36 const sp = await searchParams;37 const a = slugOf(sp.a, 'g7');38 const b = slugOf(sp.b, 'brics');39 let data: RegionCompareResponse | null = null;40 try {41 data = await apiAnalytics.regionsCompare(a, b);42 } catch (e) {43 if (isNotBuilt(e)) return <NotBuiltState />;44 data = null;45 }46 const regions = await safe(apiExplore.regions());47 const groups = regions?.items ?? [];48 const names: Record<string, string> = data ? Object.fromEntries(data.groups.map((g) => [g.id, g.name ?? g.id])) : {};49 const ids = data ? data.groups.map((g) => g.id) : [];5051 return (52 <>53 <PageHeader crumbs={[{ href: routes.regions(), label: t('regions.title') }]} title={data ? t('regions.compare.heading', { a: names[ids[0] ?? ''] ?? a, b: names[ids[1] ?? ''] ?? b }) : t('regions.compare.pageTitle')} lede={t('regions.compare.lede')} />54 <div className="pb-4">55 <GroupComparePicker groups={groups} a={a} b={b} />56 </div>57 {!data ? (58 <p className="py-8 text-sm text-ink-3">{t('regions.compare.unavailable')}</p>59 ) : (60 <>61 <Section id="shares" title={t('regions.compare.shares')} subtitle={t('regions.compare.sharesSub')} className="border-t-0">62 <div className="grid gap-x-10 gap-y-4 sm:grid-cols-2">63 {(['population_share_pct', 'gdp_share_pct'] as const).map((k) => (64 <div key={k}>65 <div className="eyebrow mb-2">{k === 'gdp_share_pct' ? t('regions.compare.gdpShare') : t('regions.compare.popShare')}</div>66 <ol className="space-y-2">67 {data.groups.map((g, i) => {68 const v = data.shares[g.id]?.[k] ?? null;69 return (70 <li key={g.id} className="grid grid-cols-[minmax(0,9rem)_minmax(0,1fr)_4rem] items-center gap-x-3 text-sm">71 <Link href={routes.region(g.slug ?? g.id)} className="link-quiet truncate font-medium text-ink">72 {g.name}73 </Link>74 <span className="h-3 overflow-hidden rounded-xs bg-surface-2" aria-hidden>75 <span className="block h-full" style={{ width: `${Math.min(100, v ?? 0)}%`, background: `var(--series-${i + 1})` }} />76 </span>77 <span className="tnum text-right text-ink">{v != null ? `${fixed(v, 1)} %` : t('common.na')}</span>78 </li>79 );80 })}81 </ol>82 </div>83 ))}84 </div>85 </Section>8687 <Section id="table" title={t('regions.compare.table')} subtitle={t('regions.compare.tableSub')}>88 <table className="w-full border-collapse text-sm">89 <caption className="sr-only">{t('regions.compare.table')}</caption>90 <thead>91 <tr className="border-b border-rule text-left text-2xs uppercase tracking-wide text-ink-3">92 <th scope="col" className="py-2 pr-3 font-medium">93 {t('common.indicator')}94 </th>95 {data.groups.map((g, i) => (96 <th key={g.id} scope="col" className="py-2 pr-3 text-right font-medium">97 <span className="inline-flex items-center gap-1.5">98 <span aria-hidden className="inline-block h-2 w-2 rounded-full" style={{ background: `var(--series-${i + 1})` }} />99 {g.name}100 </span>101 </th>102 ))}103 </tr>104 </thead>105 <tbody className="divide-y divide-rule">106 {data.rows.map((r) => (107 <tr key={r.indicator.slug}>108 <th scope="row" className="py-2 pr-3 text-left font-normal">109 <Link href={routes.indicator(r.indicator.slug)} className="link-quiet block text-ink">110 {r.indicator.short_name ?? r.indicator.name}111 </Link>112 <span className="block text-2xs text-ink-3">113 {tOpt(`regions.${r.kind}`, r.kind)} · {r.indicator.unit}114 </span>115 </th>116 {data.groups.map((g) => {117 const v = r.values[g.id];118 return (119 <td key={g.id} className="tnum py-2 pr-3 text-right align-top">120 <span className="block text-base font-medium text-ink">{v ? formatValue(v.value, r.indicator) : t('common.na')}</span>121 {v ? <span className="block text-2xs text-ink-3">{t('region.aggregates.n', { n: grouped(v.n ?? 0), year: v.year ?? '' })}</span> : null}122 </td>123 );124 })}125 </tr>126 ))}127 </tbody>128 </table>129 </Section>130131 <Section id="history" title={t('regions.compare.history')} subtitle={t('regions.compare.historySub')}>132 <GroupHistory data={data} ids={ids} names={names} />133 </Section>134 </>135 )}136 </>137 );138}139