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 { Suspense } from 'react';4import { ActiveFilters, FacetChips, FilterForm, href, parseQuery, titleFor, type SatQuery } from '@/components/satellite/explorer-filters';5import { Derived } from '@/components/satellite/primitives';6import { ResultsTable } from '@/components/satellite/results-table';7import { SearchBox } from '@/components/satellite/search-box';8import { Pagination } from '@/components/ui/pagination';9import { Container, PageHeader } from '@/components/ui/section';10import { Unavailable } from '@/components/ui/unavailable';11import { api, safe } from '@/lib/api';12import { fmtInt } from '@/lib/format';13import { routes, SITE_URL } from '@/lib/site';1415const PAGE_SIZE = 50;16const FACET_KEYS = ['status', 'object_type', 'orbit_class', 'mission_type', 'country', 'operator', 'constellation', 'on_orbit', 'q'] as const;1718type Props = { searchParams: Promise<Record<string, string | string[] | undefined>> };1920function apiQuery(q: SatQuery) {21 const page = Math.max(1, Number(q.page ?? '1') || 1);22 return { ...q, page, page_size: PAGE_SIZE, sort: q.sort ?? 'launch_date' };23}24function facetQuery(q: SatQuery) {25 return Object.fromEntries(FACET_KEYS.map((k) => [k, q[k]]));26}2728export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {29 const q = parseQuery(await searchParams);30 const filtered = Object.keys(q).filter((k) => k !== 'page' && k !== 'sort').length > 0;31 // Same request as the page body → deduplicated by the fetch cache, so labels (e.g. "Starlink") are real, not slugs.32 const facets = filtered ? ((await safe(api.satelliteFacets(facetQuery(q))))?.data ?? null) : null;33 const title = filtered ? `${titleFor(q, facets)} — Satellite explorer` : 'Satellite explorer — every catalogued object in Earth orbit';34 const description = 'Filter 70,000+ catalogued objects by status, object type, orbit class, mission, country, constellation and launch date. Real counts, live element sets, honest gaps.';35 const canonical = href(q, {}, false);36 return {37 title,38 description,39 alternates: { canonical },40 robots: q.page || q.q ? { index: false, follow: true } : undefined,41 openGraph: { title, description, url: `${SITE_URL}${canonical}` },42 twitter: { card: 'summary_large_image', title, description },43 };44}4546export default async function SatellitesPage({ searchParams }: Props) {47 const q = parseQuery(await searchParams);48 const [list, facetsRes] = await Promise.all([safe(api.satellites(apiQuery(q))), safe(api.satelliteFacets(facetQuery(q)))]);49 const facets = facetsRes?.data ?? null;50 const total = list?.pagination.total ?? null;51 const filtered = Object.keys(q).filter((k) => k !== 'page' && k !== 'sort').length > 0;5253 return (54 <Container wide>55 <PageHeader56 eyebrow={<>Catalogue · {total !== null ? <span className="tnum">{fmtInt(total)} objects</span> : 'count unavailable'}</>}57 title={filtered ? titleFor(q, facets) : 'Satellites & catalogued objects'}58 lede={filtered ? undefined : 'Every object in the public catalogue — payloads, rocket bodies, debris and stations — with live element sets where they exist. Filters are plain URL parameters: share, bookmark, script.'}59 >60 <div className="mt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">61 <Suspense fallback={<div className="h-11 w-full max-w-md rounded-md border border-rule bg-plane-2" />}>62 <SearchBox initial={q.q ?? ''} />63 </Suspense>64 <p className="text-xs text-ink-3">65 Orbit class, mission and constellation are <Derived /> · <Link href={routes.methodology()} className="hover:text-accent">methodology</Link>66 </p>67 </div>68 </PageHeader>6970 <div className="space-y-5 pb-4">71 <FilterForm q={q} />72 <ActiveFilters q={q} />73 {facets ? <FacetChips facets={facets} q={q} /> : <Unavailable what="Facet counts" compact />}74 </div>7576 <section className="py-6" aria-label="Results">77 {list === null ? (78 <Unavailable what="Catalogue results" />79 ) : list.data.length === 0 ? (80 <div className="rounded-lg border border-dashed border-rule-strong px-5 py-10 text-center">81 <p className="text-sm text-ink-2">No catalogued object matches these filters.</p>82 <p className="mt-1 text-xs text-ink-3">The count is real — nothing is hidden. Try removing a filter{q.launched_after || q.launched_before ? ' (launch-date filters depend on SATCAT launch dates, which are missing for some objects)' : ''}.</p>83 <Link href={routes.satellites()} 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 all filters</Link>84 </div>85 ) : (86 <>87 <Pagination page={list.pagination.page} pages={list.pagination.pages} total={list.pagination.total} pageSize={list.pagination.page_size} makeHref={(p) => href(q, { page: String(p) }, false)} className="mb-3" />88 <ResultsTable rows={list.data} />89 <Pagination page={list.pagination.page} pages={list.pagination.pages} total={list.pagination.total} pageSize={list.pagination.page_size} makeHref={(p) => href(q, { page: String(p) }, false)} className="mt-4" />90 </>91 )}92 </section>9394 <p className="pb-10 text-2xs text-ink-3">95 Catalogue rows come from CelesTrak SATCAT; perigee, apogee and inclination shown here are catalogue values (the detail page shows the latest element set). Facet counts are computed live over the filtered set.96 </p>97 </Container>98 );99}100