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 { 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