import type { Metadata } from 'next'; import { Search } from 'lucide-react'; import Link from 'next/link'; import { ChipRow, EmptyRow, ScrollTable, entityMetadata, first, type Params } from '@/components/entities/shared'; 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 { fmtDate, fmtInt, num, titleCase } from '@/lib/format'; import { routes } from '@/lib/site'; import type { OperatorRow } from '@/lib/types'; type SearchParams = Record; const SORTS: { value: string | undefined; label: string }[] = [ { value: undefined, label: 'Active payloads' }, { value: 'total', label: 'Total payloads' }, { value: 'growth', label: 'Growth 365 d' }, { value: 'name', label: 'Name' }, ]; const SORT_NOUN: Record = { '': 'active payloads', total: 'total payloads launched', growth: 'payloads launched in the last 365 days', name: 'name' }; const KINDS = ['operator', 'agency', 'military', 'manufacturer', 'launch_provider']; function parse(sp: SearchParams): Params { return { sort: first(sp.sort), kind: first(sp.kind), country: first(sp.country), q: first(sp.q)?.trim() || undefined, page: first(sp.page) }; } export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const p = parse(await searchParams); const bits = [p.kind && titleCase(p.kind), p.country && titleCase(p.country), p.q && `“${p.q}”`].filter(Boolean).join(' · '); return entityMetadata({ title: bits ? `Satellite operators — ${bits}` : 'Satellite operators, agencies and military programmes — ranked', description: 'Every organisation operating satellites, ranked by active payloads: commercial operators, space agencies and military programmes with fleet size, constellations, launches and 365-day growth.', path: routes.operators(), }); } export default async function OperatorsPage({ searchParams }: { searchParams: Promise }) { const params = parse(await searchParams); const page = Math.max(1, Number(params.page) || 1); const res = await safe(api.operators({ sort: params.sort, kind: params.kind, country: params.country, q: params.q, page })); const rows = res?.data ?? []; const total = res?.pagination.total ?? null; const base = routes.operators(); const activeSum = rows.reduce((s, r) => s + (num(r.active_payloads) ?? 0), 0); return ( {fmtInt(total)} organisations{params.q ? ` matching “${params.q}”` : params.kind || params.country ? ' in this selection' : ''} — commercial operators, agencies and military programmes — ranked by {SORT_NOUN[params.sort ?? ''] ?? 'active payloads'}. {rows.length > 0 && <> The {fmtInt(rows.length)} shown here operate {fmtInt(activeSum)} active payloads.} ) : ( 'Operator rankings are temporarily unavailable.' ) } >
{params.sort && } {params.kind && } {params.country && } {params.q && ( Clear )}
({ value: k, label: titleCase(k) }))]} /> {params.country && }
{!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 OperatorTable({ rows, offset }: { rows: OperatorRow[]; offset: number }) { return ( {rows.length === 0 && No operator matches this query.} {rows.map((o, i) => ( ))}
# Operator Kind Country Active On orbit Total Constellations Launches 365 d First launch Last launch
{offset + i + 1} {o.name} {titleCase(o.kind)} {o.country_slug ? {o.country_name} : o.country_name ?? '—'} {fmtInt(o.active_payloads)} {fmtInt(o.on_orbit_payloads)} {fmtInt(o.total_payloads)} {fmtInt(o.constellations)} {fmtInt(o.launches)} {fmtInt(o.payloads_last_365d)} {fmtDate(o.first_launch)} {fmtDate(o.last_launch)}
); }