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%
7.7 KB · 158 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { t } from '@/i18n';4import { api, isNotBuilt, safe } from '@/lib/api';5import { apiAnalytics } from '@/lib/api-analytics';6import { apiExplore } from '@/lib/api-explore';7import { routes } from '@/lib/site';8import { HEADLINE_INDICATORS } from '@/lib/topics';9import { RankedBars, rankedRowFromCountry } from '@/components/charts/ranked-bars';10import type { IndicatorOption } from '@/components/controls/indicator-select';11import { NotBuiltState } from '@/components/data/empty-state';12import { Section } from '@/components/data/section';13import { SimilarityPlayground } from '@/components/explore/similarity-playground';14import { CompareTeaser } from '@/components/home/compare-teaser';15import { IndicatorList } from '@/components/home/featured-indicators';16import { Hero } from '@/components/home/hero';17import { HeroMap, type HeroCountry } from '@/components/home/hero-map';18import { LatestUpdates } from '@/components/home/latest-updates';19import { Movers } from '@/components/home/movers';20import { SnapshotStrip } from '@/components/home/snapshot-strip';21import { TopicsGrid } from '@/components/home/topics-grid';22import { TrajectoryTeaser } from '@/components/home/trajectory-teaser';23import { Transparency } from '@/components/home/transparency';24import { WorldPulse } from '@/components/home/world-pulse';25import { baseFeatures } from '@/components/indicators/map-geometry';2627export const metadata: Metadata = { alternates: { canonical: '/' } };28export const revalidate = 900;2930const DEFAULT_MAP = 'gdp-per-capita-ppp';31const MAP_CHIPS = ['gdp-per-capita-ppp', 'population', 'gdp', 'gdp-growth', 'inflation', 'life-expectancy', 'fertility-rate', 'internet-users', 'co2-per-capita', 'renewable-electricity-share', 'unemployment-rate', 'population-growth'];32/** The three curated lists kept on the home page (the rest lives on /rankings). */33const LIST_KEYS = ['largest_economies', 'highest_life_expectancy', 'energy_transition_leaders'] as const;3435export default async function HomePage() {36  let home: Awaited<ReturnType<typeof api.home>> | null = null;37  let notBuilt = false;38  try {39    home = await api.home();40  } catch (e) {41    if (isNotBuilt(e)) notBuilt = true;42    else throw e;43  }44  if (notBuilt || !home) {45    return (46      <>47        <Hero />48        <NotBuiltState />49      </>50    );51  }52  const [countriesRes, frames, indicators, pulse, movers, scatter, updates, sources] = await Promise.all([53    safe(api.countries()),54    safe(apiAnalytics.indicatorFrames(DEFAULT_MAP)),55    safe(apiExplore.indicators({ with_data: true })),56    safe(apiAnalytics.pulse()),57    safe(apiAnalytics.movers({ window: 1, limit: 12 })),58    safe(apiAnalytics.scatter({ x: 'gdp-per-capita-ppp', y: 'life-expectancy', size: 'population' })),59    safe(apiAnalytics.updates()),60    safe(apiExplore.sources()),61  ]);62  const countries = countriesRes?.items ?? [];63  const { features, sphere } = countries.length ? baseFeatures(countries) : { features: [], sphere: '' };64  const heroCountries: HeroCountry[] = countries.map((c) => ({ id: c.id, slug: c.slug, name: c.name ?? c.id, flag: c.flag, region: c.region }));65  const options: IndicatorOption[] = (indicators?.items ?? [])66    .filter((i) => (i.n_countries ?? 0) >= 20 && i.frequency === 'A')67    .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 }));68  const chips = MAP_CHIPS.filter((s) => options.some((o) => o.slug === s));69  const s = home.snapshot;7071  return (72    <>73      <Hero />7475      {/* Global interactive hero map + time machine */}76      <section id="map" aria-labelledby="map-h" className="pb-8 md:pb-10">77        <h2 id="map-h" className="sr-only">78          {t('home.map.title')}79        </h2>80        {features.length ? <HeroMap geometry={features} sphere={sphere} initial={frames} options={options.length ? options : HEADLINE_INDICATORS.map((sl) => ({ slug: sl, name: sl }))} chips={chips.length ? chips : [DEFAULT_MAP]} countries={heroCountries} /> : null}81      </section>8283      <SnapshotStrip s={s} />8485      {pulse && pulse.items.length ? (86        <Section id="pulse" title={t('home.pulse.title')} subtitle={t('home.pulse.sub')} actions={<Link href={routes.changes()} className="text-accent hover:underline">{t('home.changes.all')} →</Link>}>87          <WorldPulse data={pulse} />88        </Section>89      ) : null}9091      <Section id="movers" title={t('home.movers.title')} subtitle={t('home.movers.sub')}>92        <Movers initial={movers} limit={12} compact />93      </Section>9495      <Section id="topics" title={t('home.topics.title')} subtitle={t('home.topics.sub')}>96        <TopicsGrid />97      </Section>9899      <div className="grid gap-x-10 lg:grid-cols-2">100        <Section id="compare" title={t('home.compare.title')} subtitle={t('home.compare.sub')}>101          <CompareTeaser countries={countries} />102        </Section>103        <Section id="similar" title={t('home.similar.title')} subtitle={t('home.similar.sub')}>104          <SimilarityPlayground />105        </Section>106      </div>107108      <Section id="rankings" title={t('nav.rankings')} subtitle={t('home.rankings.sub')} actions={<Link href={routes.rankings()} className="text-accent hover:underline">{t('common.seeAll')} →</Link>}>109        <div className="grid gap-x-10 gap-y-8 lg:grid-cols-3">110          {LIST_KEYS.map((key) => {111            const list = home.lists[key];112            if (!list || !list.rows?.length) return null;113            const latestYear = Math.max(0, ...list.rows.map((r) => r.year ?? 0));114            return (115              <div key={key} className="min-w-0">116                <div className="mb-2 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">117                  <h3 className="text-base font-semibold text-ink">{list.title}</h3>118                  <span className="text-xs text-ink-3">{list.description}</span>119                </div>120                {list.filter_note ? <p className="-mt-1 mb-2 text-xs text-ink-3">{list.filter_note}</p> : null}121                <RankedBars rows={list.rows.slice(0, 6).map((r) => rankedRowFromCountry(r.country, r.value, r.rank, null, r.year != null && latestYear - r.year >= 2 ? r.year : null))} spec={list.indicator} provenance={list.rows[0]?.provenance ?? null} />122                <Link href={routes.ranking(list.indicator.slug)} className="mt-2 inline-flex min-h-[36px] items-center text-sm text-accent hover:underline">123                  {t('common.seeFullRanking')} →124                </Link>125              </div>126            );127          })}128        </div>129      </Section>130131      {scatter && scatter.points.length ? (132        <Section id="trajectory" title={t('home.trajectory.title')} subtitle={t('home.trajectory.sub')}>133          <TrajectoryTeaser data={scatter} />134        </Section>135      ) : null}136137      <div className="grid gap-x-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">138        <Section id="featured" title={t('home.featured.title')} subtitle={t('home.featured.sub')}>139          <IndicatorList items={home.trending.length ? home.trending : home.featured_indicators} limit={10} />140        </Section>141        {updates ? (142          <Section id="updated" title={t('home.updates.title')} subtitle={t('home.updates.sub')}>143            <LatestUpdates data={updates} />144          </Section>145        ) : (146          <Section id="updated" title={t('home.updated.title')} subtitle={t('home.updated.sub')}>147            <IndicatorList items={home.recently_updated} showUpdated limit={8} />148          </Section>149        )}150      </div>151152      <Section id="transparency" title={t('home.transparency.title')} subtitle={t('home.transparency.sub')}>153        <Transparency sources={sources?.items ?? []} />154      </Section>155    </>156  );157}158