import type { Metadata } from 'next'; import Link from 'next/link'; import { ChipRow, Derived, entityMetadata, first, ScrollTable, EmptyRow, type Params } from '@/components/entities/shared'; import { OrbitBadge } from '@/components/ui/badges'; import { Pagination } from '@/components/ui/pagination'; import { Container, PageHeader } from '@/components/ui/section'; import { Unavailable } from '@/components/ui/unavailable'; import { api, safe } from '@/lib/api'; import { fmt1, fmtDate, fmtInt, fmtKm, num, titleCase } from '@/lib/format'; import { routes } from '@/lib/site'; import type { ConstellationRow } from '@/lib/types'; type Search = Record; const SORTS: { value: string | undefined; label: string }[] = [ { value: undefined, label: 'Active' }, { value: 'total', label: 'Total' }, { value: 'growth', label: 'Growth 365 d' }, { value: 'activity', label: 'Activity' }, { value: 'name', label: 'Name' }, ]; const SORT_NOUN: Record = { '': 'active satellites', total: 'total satellites launched', growth: 'satellites launched in the last 365 days', activity: 'activity score', name: 'name' }; const SERVICES = ['communications', 'earth-observation', 'navigation', 'iot', 'weather', 'military', 'technology', 'station', 'science']; const ORBITS = ['LEO', 'MEO', 'GEO', 'HEO', 'MIXED']; function parse(sp: Search): Params { return { sort: first(sp.sort), service: first(sp.service), orbit: first(sp.orbit), page: first(sp.page) }; } export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const p = parse(await searchParams); const bits = [p.service && titleCase(p.service), p.orbit].filter(Boolean).join(' ยท '); return entityMetadata({ title: bits ? `${bits} satellite constellations` : 'Satellite constellations โ€” ranked by active satellites', description: 'Every tracked satellite constellation ranked by active satellites, fleet size, 365-day growth and activity score, with orbit class, operator, median altitude and launch history.', path: routes.constellations(), }); } export default async function ConstellationsPage({ searchParams }: { searchParams: Promise }) { const params = parse(await searchParams); const page = Math.max(1, Number(params.page) || 1); const res = await safe(api.constellations({ sort: params.sort, service: params.service, orbit: params.orbit, page })); const rows = res?.data ?? []; const total = res?.pagination.total ?? null; const base = routes.constellations(); const activeSum = rows.reduce((s, r) => s + (num(r.active) ?? 0), 0); return ( {fmtInt(total)} constellations tracked{params.service || params.orbit ? ' in this selection' : ''}, ranked by {SORT_NOUN[params.sort ?? ''] ?? 'active satellites'}. {rows.length > 0 && <> The {fmtInt(rows.length)} shown on this page account for {fmtInt(activeSum)} active satellites.} Membership is derived from curated name patterns and CelesTrak groups โ€” see the methodology. ) : ( 'Constellation rankings are temporarily unavailable.' ) } />
({ value: s, label: titleCase(s) }))]} /> ({ value: o, label: o }))]} />
{!res ? : } {res && ( { const q = new URLSearchParams(); for (const [k, v] of Object.entries(params)) if (v && k !== 'page') q.set(k, v); if (p > 1) q.set('page', String(p)); const s = q.toString(); return s ? `${base}?${s}` : base; }} /> )}
); } function ConstellationTable({ rows, offset }: { rows: ConstellationRow[]; offset: number }) { return ( {rows.length === 0 && No constellation matches these filters.} {rows.map((c, i) => ( ))}
# Constellation Operator Service Orbit Active On orbit Total 365 d 30 d Activity Median perigee First launch Last launch
{offset + i + 1} {c.name} {c.country_code && {c.country_code}} {c.operator_slug ? {c.operator_name} : c.operator_name ?? 'โ€”'} {titleCase(c.service_type)} {fmtInt(c.active)} {fmtInt(c.on_orbit)} {fmtInt(c.total)} {fmtInt(c.launched_last_365d)} {fmtInt(c.launched_last_30d)} {fmt1(c.activity_score)} {fmtKm(c.median_perigee_km)} {fmtDate(c.first_launch)} {fmtDate(c.last_launch)}
); }