spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { FreshnessBadge, MissionLabel, OrbitBadge, StatusBadge, TypeBadge } from '@/components/ui/badges';3import { fmt1, fmtAgo, fmtDate, fmtDateTime, fmtDeg, fmtInt, fmtKm, fmtMinutes, num } from '@/lib/format';4import { routes } from '@/lib/site';5import type { SatelliteDetail } from '@/lib/types';6import { DL, Derived, Row } from './primitives';78export const OPS_STATUS: Record<string, string> = {9 '+': 'Operational',10 '-': 'Non-operational',11 P: 'Partially operational',12 B: 'Backup / standby',13 S: 'Spare',14 X: 'Extended mission',15 D: 'Decayed',16 '?': 'Unknown',17};1819function KeyStat({ label, value, unit, hint }: { label: string; value: string; unit?: string; hint?: string }) {20 return (21 <div className="min-w-0 border-l border-rule pl-3">22 <p className="eyebrow">{label}</p>23 <p className="mono tnum mt-1 truncate text-lg font-medium text-ink md:text-2xl">24 {value}25 {unit && value !== '—' && <span className="ml-1 text-xs text-ink-3 md:text-sm">{unit}</span>}26 </p>27 {hint && <p className="mt-0.5 truncate text-2xs text-ink-3">{hint}</p>}28 </div>29 );30}3132export function Hero({ d }: { d: SatelliteDetail }) {33 const os = d.orbital_state;34 const live = d.live && d.live.error == null ? d.live : null;35 const epochAge = live?.epoch_age_hours ?? (os ? (Date.now() - new Date(os.epoch).getTime()) / 3.6e6 : null);36 return (37 <header className="pb-6 pt-6 md:pb-8 md:pt-10">38 <p className="eyebrow mono">39 {d.object_type === 'PAYLOAD' || d.object_type === 'STATION' || d.object_type === 'CREWED' ? 'Satellite' : d.object_type === 'ROCKET_BODY' ? 'Rocket body' : d.object_type === 'DEBRIS' ? 'Debris object' : 'Catalogued object'}40 {d.norad_id !== null && <> · NORAD {d.norad_id}</>}41 {d.cospar_id && <> · COSPAR {d.cospar_id}</>}42 </p>43 <h1 className="display mt-2 break-words text-3xl md:text-5xl">{d.name}</h1>4445 <div className="mt-4 flex flex-wrap items-center gap-2">46 <StatusBadge status={d.status} size="md" />47 <TypeBadge type={d.object_type} />48 <OrbitBadge orbitClass={d.orbit_class} />49 <span className="inline-flex items-center gap-1.5 text-xs text-ink-2">50 <MissionLabel mission={d.mission_type} /> <Derived />51 </span>52 </div>5354 <ul className="mt-4 flex flex-wrap gap-x-5 gap-y-1.5 text-sm">55 <li>56 <span className="text-ink-3">Operator </span>57 {d.operator_slug ? <Link className="link" href={routes.operator(d.operator_slug)}>{d.operator_name}</Link> : <span className="text-ink-2">{d.owner_name ?? d.owner_code ?? '—'}</span>}58 </li>59 <li>60 <span className="text-ink-3">Constellation </span>61 {d.constellation_slug ? <Link className="link" href={routes.constellation(d.constellation_slug)}>{d.constellation_name}</Link> : <span className="text-ink-2">—</span>}62 </li>63 <li>64 <span className="text-ink-3">Country </span>65 {d.country_slug ? <Link className="link" href={routes.country(d.country_slug)}>{d.country_name}</Link> : <span className="text-ink-2">{d.owner_code === 'ISS' ? 'International' : '—'}</span>}66 </li>67 </ul>6869 <dl className="mt-7 grid grid-cols-2 gap-x-3 gap-y-5 sm:grid-cols-3 lg:grid-cols-6">70 <KeyStat label="Altitude now" value={live ? fmt1(live.altitude_km) : '—'} unit="km" hint={live ? 'SGP4 propagation' : d.status === 'DECAYED' ? 'object decayed' : 'no element set'} />71 <KeyStat label="Velocity" value={live ? live.velocity_km_s.toFixed(2) : '—'} unit="km/s" />72 <KeyStat label="Inclination" value={os ? fmtDeg(os.inclination) : fmtDeg(d.inclination_deg)} />73 <KeyStat label="Period" value={fmtMinutes(os?.period_minutes ?? d.period_minutes)} hint={num(os?.period_minutes ?? d.period_minutes) !== null ? `${fmt1(os?.period_minutes ?? d.period_minutes)} min` : undefined} />74 <KeyStat label="Perigee" value={fmtInt(os?.perigee_km ?? d.perigee_km)} unit="km" />75 <KeyStat label="Apogee" value={fmtInt(os?.apogee_km ?? d.apogee_km)} unit="km" />76 </dl>7778 <p className="mt-5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-ink-3">79 <FreshnessBadge status={d.freshness.orbit.status} label={`orbit ${d.freshness.orbit.status}`} />80 {os ? (81 <span>82 Orbit updated {fmtAgo(os.updated_at)} · element epoch <span className="mono text-ink-2">{fmtDateTime(os.epoch)}</span>83 {epochAge !== null && <> ({fmt1(epochAge)} h old)</>}84 </span>85 ) : (86 <span>No orbital element set on file{d.decay_date ? ` — decayed ${fmtDate(d.decay_date)}` : ''}.</span>87 )}88 <span>· Metadata updated {fmtAgo(d.freshness.metadata.updated_at)}</span>89 </p>90 </header>91 );92}9394/** Right-hand "terminal" panel: a compact readout of the object's state. Sticky on desktop. */95export function TelemetryPanel({ d }: { d: SatelliteDetail }) {96 const os = d.orbital_state;97 const live = d.live && d.live.error == null ? d.live : null;98 return (99 <div className="panel p-4 md:p-5">100 <div className="flex items-center justify-between gap-3 border-b border-rule pb-3">101 <p className="eyebrow">Telemetry readout</p>102 <FreshnessBadge status={d.freshness.orbit.status} />103 </div>104 <DL>105 <Row label="Status" mono={false} value={<span className="inline-flex flex-wrap justify-end gap-1.5"><StatusBadge status={d.status} />{d.ops_status_code && <span className="text-xs text-ink-3" title="SATCAT operational status code">{OPS_STATUS[d.ops_status_code] ?? d.ops_status_code}</span>}</span>} />106 <Row label="Object type" mono={false} value={<TypeBadge type={d.object_type} />} />107 <Row label={<>Orbit class <Derived /></>} mono={false} value={<OrbitBadge orbitClass={d.orbit_class} />} />108 {live && <Row label="Altitude" value={fmtKm(live.altitude_km, 1)} />}109 {live && <Row label="Velocity" value={`${live.velocity_km_s.toFixed(3)} km/s`} />}110 <Row label="Perigee × apogee" value={os ? `${fmtInt(os.perigee_km)} × ${fmtInt(os.apogee_km)} km` : d.perigee_km !== null ? `${fmtInt(d.perigee_km)} × ${fmtInt(d.apogee_km)} km` : '—'} />111 <Row label="Inclination" value={fmtDeg(os?.inclination ?? d.inclination_deg)} />112 <Row label="Period" value={fmtMinutes(os?.period_minutes ?? d.period_minutes)} />113 <Row label="Eccentricity" value={os ? os.eccentricity.toFixed(6) : '—'} />114 <Row label="Element epoch" value={os ? fmtDateTime(os.epoch) : '—'} />115 {d.rcs_m2 !== null && <Row label="Radar cross-section" value={`${fmt1(d.rcs_m2)} m²`} />}116 <Row label="Launched" value={fmtDate(d.launch_date)} />117 {d.decay_date && <Row label="Decayed" value={fmtDate(d.decay_date)} />}118 <Row label="First indexed" value={fmtDate(d.first_seen_at)} />119 <Row label="Last seen in sources" value={fmtAgo(d.last_seen_at)} />120 </DL>121 <div className="mt-4 border-t border-rule pt-3 text-2xs text-ink-3">122 <p className="mono">NORAD {d.norad_id ?? '—'} · {d.cospar_id ?? 'no COSPAR id'}</p>123 <p className="mt-1">124 Sources: {Array.from(new Set(d.sources.map((s) => s.name))).join(', ') || 'none linked to this record'} · <Link href={routes.methodology()} className="hover:text-accent">methodology</Link>125 </p>126 </div>127 </div>128 );129}130