import type { Metadata } from 'next'; import Link from 'next/link'; import { EventTimeline } from '@/components/events/event-timeline'; import { eventLabel } from '@/components/events/event-badge'; import { Chips, Note } from '@/components/stats/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 { fmtAgo, fmtInt } from '@/lib/format'; import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; export const revalidate = 60; type Search = Promise<{ page?: string | string[]; type?: string | string[]; since?: string | string[] }>; const PAGE_SIZE = 50; const WINDOWS = [ { key: '24h', label: 'Last 24 h', hours: 24 }, { key: '7d', label: 'Last 7 d', hours: 24 * 7 }, { key: '30d', label: 'Last 30 d', hours: 24 * 30 }, ] as const; const pick = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v); const TITLE = 'Orbital events timeline — launches, decays, orbit changes'; const DESC = 'Chronological feed of what changed in Earth orbit: newly catalogued payloads, decays and reentries, orbit changes and decommissions, with source and derived confidence for every event.'; export async function generateMetadata({ searchParams }: { searchParams: Search }): Promise { const sp = await searchParams; const type = pick(sp.type); const title = type ? `${eventLabel(type)} events — timeline` : TITLE; const url = `${SITE_URL}${type ? routes.events(`type=${encodeURIComponent(type)}`) : routes.events()}`; return { title, description: DESC, alternates: { canonical: url }, openGraph: { title: `${title} | ${SITE_NAME}`, description: DESC, url, type: 'website', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title: `${title} | ${SITE_NAME}`, description: DESC }, }; } function buildHref(params: { page?: number; type?: string; since?: string }): string { const p = new URLSearchParams(); if (params.type) p.set('type', params.type); if (params.since) p.set('since', params.since); if (params.page && params.page > 1) p.set('page', String(params.page)); const s = p.toString(); return routes.events(s || undefined); } export default async function EventsPage({ searchParams }: { searchParams: Search }) { const sp = await searchParams; const page = Math.max(1, Number(pick(sp.page) ?? 1) || 1); const type = pick(sp.type) || undefined; const sinceRaw = pick(sp.since) || undefined; const since = sinceRaw && !Number.isNaN(new Date(sinceRaw).getTime()) ? sinceRaw : undefined; const base = { page, page_size: PAGE_SIZE, type }; let payload = await safe(api.events({ ...base, since })); let sinceFailed = false; if (!payload && since) { // The API currently rejects `since` (500) — degrade honestly to the unfiltered feed and say so. sinceFailed = true; payload = await safe(api.events(base)); } const types = payload?.types ?? []; const now = Date.now(); const typeChips = [{ href: buildHref({ since }), label: 'All types', active: !type, count: types.length ? fmtInt(types.reduce((s, t) => s + (Number(t.count) || 0), 0)) : undefined }, ...types.map((t) => ({ href: buildHref({ type: t.type, since }), label: eventLabel(t.type), active: type === t.type, count: fmtInt(t.count) }))]; const windowChips = [{ href: buildHref({ type }), label: 'All time', active: !since }, ...WINDOWS.map((w) => { const iso = new Date(now - w.hours * 3600_000).toISOString().slice(0, 19) + 'Z'; // active if current `since` is within ±1 h of this window const active = !!since && Math.abs(new Date(since).getTime() - (now - w.hours * 3600_000)) < 3600_000; return { href: buildHref({ type, since: iso }), label: w.label, active }; })]; return ( {payload && payload.data[0] &&

Latest event {fmtAgo(payload.data[0].event_time)} · {fmtInt(payload.pagination.total)} events in view

}
{sinceFailed && ( The time-window filter is unavailable from the API right now — showing the unfiltered feed instead. Clear window )} {!payload ? (
) : payload.data.length === 0 ? (
Only event types produced by connected sources appear here. Reset filters
) : ( <> buildHref({ page: p, type, since: sinceFailed ? undefined : since })} /> )} Times are UTC. Confidence is a derived score, not a measurement. Event types that no connected source produces (e.g. regulatory approvals) are simply absent — nothing is simulated.
); }