SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
7.3 KB · 130 lines tsx
Raw Blame History
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