SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
7.0 KB · 136 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { HBars } from '@/components/charts/charts';4import { ChipRow, EmptyRow, ScrollTable, entityMetadata, first, type Params } from '@/components/entities/shared';5import { Container, PageHeader } from '@/components/ui/section';6import { Unavailable } from '@/components/ui/unavailable';7import { api, safe } from '@/lib/api';8import { fmtInt, num } from '@/lib/format';9import { routes } from '@/lib/site';10import type { CountryRow } from '@/lib/types';1112type SearchParams = Record<string, string | string[] | undefined>;1314const SORTS: { value: string | undefined; label: string; key: keyof CountryRow; noun: string }[] = [15  { value: undefined, label: 'Active payloads', key: 'active_payloads', noun: 'active payloads' },16  { value: 'objects', label: 'Objects on orbit', key: 'objects_on_orbit', noun: 'objects on orbit' },17  { value: 'debris', label: 'Debris', key: 'debris_on_orbit', noun: 'debris fragments on orbit' },18  { value: 'launches', label: 'Launches', key: 'launches', noun: 'launches' },19  { value: 'name', label: 'Name', key: 'name', noun: 'name' },20];2122export async function generateMetadata({ searchParams }: { searchParams: Promise<SearchParams> }): Promise<Metadata> {23  const sort = first((await searchParams).sort);24  const s = SORTS.find((x) => (x.value ?? '') === (sort ?? '')) ?? SORTS[0]!;25  return entityMetadata({26    title: `Countries in orbit — ranked by ${s.noun}`,27    description: 'Every country with catalogued objects in Earth orbit, ranked by active payloads, objects on orbit, debris, rocket bodies, launches and operators — with regional breakdown.',28    path: routes.countries(),29  });30}3132export default async function CountriesPage({ searchParams }: { searchParams: Promise<SearchParams> }) {33  const sp = await searchParams;34  const params: Params = { sort: first(sp.sort) };35  const sortDef = SORTS.find((x) => (x.value ?? '') === (params.sort ?? '')) ?? SORTS[0]!;36  const res = await safe(api.countries(params.sort ?? 'active'));37  const rows = res?.data ?? [];38  const base = routes.countries();3940  const metricKey = sortDef.key === 'name' ? 'active_payloads' : sortDef.key;41  const metricNoun = sortDef.key === 'name' ? 'active payloads' : sortDef.noun;42  const top10 = rows.length ? [...rows].sort((a, b) => (num(b[metricKey] as never) ?? 0) - (num(a[metricKey] as never) ?? 0)).slice(0, 10).map((c) => ({ label: c.name, value: num(c[metricKey] as never) ?? 0, href: routes.country(c.slug) })) : [];43  const byRegion = new Map<string, number>();44  for (const c of rows) byRegion.set(c.region ?? 'Unattributed', (byRegion.get(c.region ?? 'Unattributed') ?? 0) + (num(c[metricKey] as never) ?? 0));45  const regionData = [...byRegion.entries()].map(([label, value], i) => ({ label, value, color: `var(--series-${(i % 8) + 1})` })).sort((a, b) => b.value - a.value);4647  const totals = rows.reduce(48    (t, c) => ({ active: t.active + (num(c.active_payloads) ?? 0), objects: t.objects + (num(c.objects_on_orbit) ?? 0), debris: t.debris + (num(c.debris_on_orbit) ?? 0) }),49    { active: 0, objects: 0, debris: 0 },50  );5152  return (53    <Container wide>54      <PageHeader55        eyebrow="Countries"56        title="Countries in orbit"57        lede={58          res ? (59            <>60              {fmtInt(rows.length)} countries and jurisdictions hold catalogued objects in orbit — together {fmtInt(totals.active)} active payloads, {fmtInt(totals.objects)} objects on orbit and {fmtInt(totals.debris)} tracked debris fragments. Attribution follows SATCAT owner codes mapped to ISO 3166 (joint programmes are listed under their lead entry); see the{' '}61              <Link href={routes.methodology()} className="text-accent hover:underline">methodology</Link>.62            </>63          ) : (64            'Country rankings are temporarily unavailable.'65          )66        }67      />6869      <div className="border-y border-rule py-2">70        <ChipRow label="Rank by" paramKey="sort" base={base} params={params} current={params.sort} options={SORTS.map((s) => ({ value: s.value, label: s.label }))} />71      </div>7273      {res && rows.length > 0 && (74        <div className="grid gap-8 border-b border-rule py-8 lg:grid-cols-2">75          <div>76            <p className="eyebrow mb-3">Top 10 · {metricNoun}</p>77            <HBars data={top10} barHeight={30} />78          </div>79          <div>80            <p className="eyebrow mb-3">By region · {metricNoun}</p>81            <HBars data={regionData} barHeight={30} />82            <p className="mt-3 text-xs text-ink-3">Regions aggregate the countries listed below; multinational organisations (e.g. ESA, Intelsat) appear as their own entry.</p>83          </div>84        </div>85      )}8687      <div className="py-6">{!res ? <Unavailable what="Country rankings" /> : <CountryTable rows={rows} />}</div>88    </Container>89  );90}9192function CountryTable({ rows }: { rows: CountryRow[] }) {93  return (94    <ScrollTable>95      <table className="data-table stack">96        <thead>97          <tr>98            <th className="num">#</th>99            <th>Country</th>100            <th>Region</th>101            <th className="num">Active payloads</th>102            <th className="num max-md:hidden!">Payloads on orbit</th>103            <th className="num">Objects on orbit</th>104            <th className="num">Debris</th>105            <th className="num max-md:hidden!">Rocket bodies</th>106            <th className="num">Launches</th>107            <th className="num max-md:hidden!">Operators</th>108            <th className="num">Payloads 365 d</th>109          </tr>110        </thead>111        <tbody>112          {rows.length === 0 && <EmptyRow colSpan={11}>No countries returned.</EmptyRow>}113          {rows.map((c, i) => (114            <tr key={c.code}>115              <td data-label="Rank" className="num mono text-xs text-ink-3">{i + 1}</td>116              <td className="primary" data-label="Country">117                <Link href={routes.country(c.slug)} className="link font-medium">{c.name}</Link>118                <span className="mono ml-2 text-xs text-ink-3">{c.iso3 ?? c.code}</span>119              </td>120              <td data-label="Region" className="text-ink-2">{c.region ?? '—'}</td>121              <td data-label="Active payloads" className="num tnum font-medium text-active">{fmtInt(c.active_payloads)}</td>122              <td data-label="Payloads on orbit" className="num tnum max-md:hidden!">{fmtInt(c.on_orbit_payloads)}</td>123              <td data-label="Objects on orbit" className="num tnum">{fmtInt(c.objects_on_orbit)}</td>124              <td data-label="Debris" className="num tnum text-warn">{fmtInt(c.debris_on_orbit)}</td>125              <td data-label="Rocket bodies" className="num tnum max-md:hidden!">{fmtInt(c.rocket_bodies_on_orbit)}</td>126              <td data-label="Launches" className="num tnum">{fmtInt(c.launches)}</td>127              <td data-label="Operators" className="num tnum max-md:hidden!">{fmtInt(c.operators)}</td>128              <td data-label="Payloads 365 d" className="num tnum">{fmtInt(c.payloads_last_365d)}</td>129            </tr>130          ))}131        </tbody>132      </table>133    </ScrollTable>134  );135}136