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%
6.9 KB · 139 lines tsx
Raw Blame History
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