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.8 KB · 121 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Suspense } from 'react';4import { t } from '@/i18n';5import { api, safe } from '@/lib/api';6import { apiExplore } from '@/lib/api-explore';7import { formatDate } from '@/lib/format';8import { SITE_URL, routes } from '@/lib/site';9import { toCountryLite } from '@/lib/types-compare';10import type { IndicatorOption } from '@/components/controls/indicator-select';11import { Section } from '@/components/data/section';12import { CodeBlock } from '@/components/explore/copy-button';13import { DownloadPicker } from '@/components/explore/download-picker';14import { PageHeader } from '@/components/explore/page-header';15import { FooterCredits } from '@/components/layout/site-footer';16import { DownloadBuilder } from '@/components/platform/download-builder';1718export const revalidate = 3600;1920export const metadata: Metadata = {21  title: t('download.title'),22  description: t('download.description'),23  alternates: { canonical: routes.download() },24};2526const COLUMNS = ['country_id', 'country_name', 'indicator_id', 'indicator_name', 'period', 'year', 'frequency', 'value', 'unit', 'is_estimate', 'is_forecast', 'status', 'source', 'source_name', 'dataset', 'series_code', 'retrieved_at', 'source_updated_at', 'url', 'licence'];2728export default async function DownloadPage() {29  const [sources, countriesRes, indicatorsRes] = await Promise.all([safe(apiExplore.sources()), safe(api.countries()), safe(apiExplore.indicators({ with_data: true }))]);30  const items = sources?.items ?? [];31  const names = items.map((s) => s.name ?? s.id);32  const built = sources?.meta.built_at ?? null;33  const countries = (countriesRes?.items ?? []).filter((c) => c.kind !== 'aggregate').map(toCountryLite).sort((a, b) => a.name.localeCompare(b.name));34  // Plain data for the client builder (the helper in indicator-select.tsx is a client module; map here on the server).35  const indicators: IndicatorOption[] = (indicatorsRes?.items ?? []).map((i) => ({ slug: i.slug, name: i.name ?? i.slug, short_name: i.short_name, topic: i.topic, unit: i.unit, featured: i.featured, first_year: i.first_year, last_year: i.last_year, n_countries: i.n_countries }));36  const maxYear = Math.max(new Date().getUTCFullYear(), ...indicators.map((i) => i.last_year ?? 0));37  const bulk = `# list indicators, then fetch each dataset\ncurl -s "${SITE_URL}/api/v1/indicators" | jq -r '.items[].slug' | while read slug; do\n  curl -s -o "$slug.csv" "${SITE_URL}/api/v1/indicators/$slug/download.csv"; sleep 0.6\ndone`;3839  return (40    <>41      <PageHeader title={t('download.title')} lede={t('download.sub')} meta={built && sources?.meta.run_id ? t('download.snapshot', { run: sources.meta.run_id, date: formatDate(built) }) : undefined} />4243      <Section id="builder" title={t('download.builder.title')} subtitle={t('download.builder.sub')} className="border-t-0">44        <Suspense fallback={null}>45          <DownloadBuilder countries={countries} indicators={indicators} maxYear={maxYear} />46        </Suspense>47      </Section>4849      <Section id="quick" title={t('download.quick.title')} subtitle={t('download.quick.sub')}>50        <div className="grid gap-x-10 gap-y-6 lg:grid-cols-2">51          <div>52            <h3 className="text-sm font-semibold text-ink">{t('download.quick.country')}</h3>53            <p className="mb-2 text-xs text-ink-3">{t('download.quick.countrySub')}</p>54            <DownloadPicker type="country" />55          </div>56          <div>57            <h3 className="text-sm font-semibold text-ink">{t('download.quick.indicator')}</h3>58            <p className="mb-2 text-xs text-ink-3">{t('download.quick.indicatorSub')}</p>59            <DownloadPicker type="indicator" />60          </div>61        </div>62      </Section>6364      <div className="grid gap-x-10 lg:grid-cols-2">65        <Section id="bulk" title={t('download.bulk.title')} subtitle={t('download.bulk.sub')}>66          <p className="max-w-prose text-sm leading-relaxed text-ink-2">{t('download.bulk.text')}</p>67          <CodeBlock code={bulk} lang="bash" className="mt-3" />68        </Section>69        <Section id="columns" title={t('download.columns.title')} subtitle={t('download.columns.sub')}>70          <ul className="flex flex-wrap gap-1.5">71            {COLUMNS.map((c) => (72              <li key={c} className="rounded-xs border border-rule bg-surface px-1.5 py-0.5 font-mono text-xs text-ink-2">73                {c}74              </li>75            ))}76          </ul>77          <h3 className="mt-6 text-sm font-semibold text-ink">{t('download.api.title')}</h3>78          <p className="mt-1 max-w-prose text-sm leading-relaxed text-ink-2">{t('download.api.text', { base: `${SITE_URL}/api/v1` })}</p>79          <Link href={routes.api()} className="mt-1 inline-flex min-h-[44px] items-center text-sm text-accent hover:underline md:min-h-[36px]">80            {t('download.api.link')} →81          </Link>82        </Section>83      </div>8485      <Section id="licence" title={t('download.licence.title')} subtitle={t('download.licence.sub')}>86        <div className="max-w-prose space-y-3 text-sm leading-relaxed text-ink-2">87          <p>{t('download.licence.compilation')}</p>88          <p>{t('download.licence.sources')}</p>89        </div>90        {items.length ? (91          <table className="mt-4 w-full max-w-3xl border-collapse text-sm">92            <thead>93              <tr className="border-b border-rule text-left text-xs text-ink-3">94                <th scope="col" className="py-1.5 pr-3 font-medium">{t('download.licence.source')}</th>95                <th scope="col" className="py-1.5 pr-3 font-medium">{t('download.licence.licence')}</th>96                <th scope="col" className="hidden py-1.5 font-medium sm:table-cell">{t('download.licence.attribution')}</th>97              </tr>98            </thead>99            <tbody className="divide-y divide-rule">100              {items.map((s) => (101                <tr key={s.id}>102                  <td className="py-2 pr-3">103                    <Link href={routes.source(s.id)} className="link-quiet inline-flex min-h-[44px] items-center text-ink md:min-h-[32px]">104                      {s.name ?? s.id}105                    </Link>106                  </td>107                  <td className="py-2 pr-3 text-ink-2">{s.licence ?? t('common.na')}</td>108                  <td className="hidden py-2 text-xs text-ink-3 sm:table-cell">{s.attribution ?? ''}</td>109                </tr>110              ))}111            </tbody>112          </table>113        ) : null}114        <h3 className="mt-5 text-sm font-semibold text-ink">{t('download.licence.example')}</h3>115        <blockquote className="mt-1 max-w-prose border-l-2 border-rule pl-3 text-sm text-ink-2">{t('download.licence.exampleText', { sources: names.slice(0, 4).join(', ') || 'World Bank, IMF, OECD', date: formatDate(built ?? new Date().toISOString()) })}</blockquote>116        <FooterCredits className="mt-6 text-xs text-ink-2" />117      </Section>118    </>119  );120}121