spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
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