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 { LaunchFilterForm, launchHref, launchTitle, parseLaunchQuery } from '@/components/launches/launch-filters';4import { LaunchTimelineCharts } from '@/components/launches/launch-timeline';5import { LaunchesTable } from '@/components/launches/launches-table';6import { Pagination } from '@/components/ui/pagination';7import { Container, PageHeader } from '@/components/ui/section';8import { Unavailable } from '@/components/ui/unavailable';9import { api, safe } from '@/lib/api';10import { fmtInt } from '@/lib/format';11import { routes, SITE_URL } from '@/lib/site';1213const PAGE_SIZE = 50;14type Props = { searchParams: Promise<Record<string, string | string[] | undefined>> };1516export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {17 const q = parseLaunchQuery(await searchParams);18 const filtered = Object.keys(q).some((k) => k !== 'page' && k !== 'sort');19 // Same cached request as the page body → real site names in the title instead of slugs.20 const sites = q.site ? ((await safe(api.launchTimeline()))?.data.sites ?? null) : null;21 const title = filtered ? `${launchTitle(q, sites)} — Launches` : 'Launches — every orbital launch since 1957, derived from the catalogue';22 const description = 'Orbital launches reconstructed from SATCAT international designators: date, site, payload count, catalogued objects still on orbit, owners. Filter by year, site, country and owner.';23 const canonical = launchHref(q, {}, false);24 return { title, description, alternates: { canonical }, robots: q.page || q.q ? { index: false, follow: true } : undefined, openGraph: { title, description, url: `${SITE_URL}${canonical}` }, twitter: { card: 'summary_large_image', title, description } };25}2627export default async function LaunchesPage({ searchParams }: Props) {28 const q = parseLaunchQuery(await searchParams);29 const page = Math.max(1, Number(q.page ?? '1') || 1);30 const [list, timelineRes] = await Promise.all([safe(api.launches({ ...q, page, page_size: PAGE_SIZE, sort: q.sort ?? 'date' })), safe(api.launchTimeline())]);31 const timeline = timelineRes?.data ?? null;32 const filtered = Object.keys(q).some((k) => k !== 'page' && k !== 'sort');33 const total = list?.pagination.total ?? null;3435 return (36 <Container wide>37 <PageHeader38 eyebrow={<>Launch record · {total !== null ? <span className="tnum">{fmtInt(total)} launches{filtered ? ' match' : ''}</span> : 'count unavailable'}</>}39 title={filtered ? launchTitle(q, timeline?.sites ?? null) : 'Orbital launches'}40 lede={filtered ? undefined : 'Every launch that left at least one catalogued object in orbit, grouped by international designator. Counts of payloads, rocket bodies and debris are taken from the live catalogue, so "on orbit" changes as objects decay.'}41 />4243 <section className="pb-8" aria-label="Launch timeline">44 {timeline ? <LaunchTimelineCharts timeline={timeline} /> : <Unavailable what="Launch timeline" />}45 </section>4647 <div className="space-y-4 pb-4">48 <LaunchFilterForm q={q} timeline={timeline} />49 {filtered && (50 <ul className="flex flex-wrap gap-1.5">51 {(Object.keys(q) as (keyof typeof q)[]).filter((k) => k !== 'page' && k !== 'sort').map((k) => (52 <li key={k}>53 <Link href={launchHref(q, { [k]: undefined })} className="inline-flex min-h-9 items-center gap-1.5 rounded-md border border-accent/40 bg-accent-soft px-2.5 py-1 text-xs text-accent hover:bg-accent/20" title={`Remove ${k} filter`}>54 <span className="text-accent/70">{k.replace(/_/g, ' ')}:</span> {q[k]} <span aria-hidden>×</span>55 </Link>56 </li>57 ))}58 </ul>59 )}60 </div>6162 <section className="py-6" aria-label="Launches">63 {list === null ? (64 <Unavailable what={q.after || q.before ? 'Launch list (the after/before date filter is currently failing upstream)' : 'Launch list'} />65 ) : list.data.length === 0 ? (66 <div className="rounded-lg border border-dashed border-rule-strong px-5 py-10 text-center">67 <p className="text-sm text-ink-2">No launch matches these filters.</p>68 <Link href={routes.launches()} className="mt-4 inline-flex h-10 items-center rounded-md border border-rule px-4 text-sm text-ink hover:bg-plane-2">Clear filters</Link>69 </div>70 ) : (71 <>72 <Pagination page={list.pagination.page} pages={list.pagination.pages} total={list.pagination.total} pageSize={list.pagination.page_size} makeHref={(p) => launchHref(q, { page: String(p) }, false)} className="mb-3" />73 <LaunchesTable rows={list.data} ownerHref={(c) => launchHref(q, { owner: c })} />74 <Pagination page={list.pagination.page} pages={list.pagination.pages} total={list.pagination.total} pageSize={list.pagination.page_size} makeHref={(p) => launchHref(q, { page: String(p) }, false)} className="mt-4" />75 </>76 )}77 </section>7879 <p className="pb-10 text-2xs leading-relaxed text-ink-3">80 Launches are <em>derived</em>: SatelliteIndex groups catalogued objects by the launch part of their COSPAR international designator (e.g. 1998-067) and takes the earliest launch date and the launch site from SATCAT. Launches that placed nothing in the catalogue (failures below orbit, suborbital flights) do not appear. Region split uses the launch-site country. See the <Link href={routes.methodology()} className="hover:text-accent">methodology</Link>.81 </p>82 </Container>83 );84}85