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