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%
10.0 KB · 250 lines tsx
Raw Blame History
1/** Reusable data tables for entity detail pages — all `.data-table.stack` (stacked cards under 768 px). */2import Link from 'next/link';3import { OrbitBadge, StatusBadge, TypeBadge } from '@/components/ui/badges';4import { MissionLabel } from '@/components/ui/badges';5import { fmtDate, fmtInt, fmtKm, fmtDeg, titleCase } from '@/lib/format';6import { routes } from '@/lib/site';7import type { Num } from '@/lib/types';8import { EmptyRow, ScrollTable } from './shared';910// ---------------------------------------------------------------------------------------------------- launches11export interface LaunchLike {12  id: string;13  cospar_launch_id: string;14  launch_date: string | null;15  site_name: string | null;16  site_slug: string | null;17  satellites?: Num;18  payload_count?: Num;19  active?: Num;20  primary_name?: string | null;21}2223export function LaunchesTable({ rows, showActive = false, showPrimary = false }: { rows: LaunchLike[]; showActive?: boolean; showPrimary?: boolean }) {24  const cols = 4 + (showActive ? 1 : 0) + (showPrimary ? 1 : 0);25  return (26    <ScrollTable>27      <table className="data-table stack">28        <thead>29          <tr>30            <th>Launch</th>31            <th>Date</th>32            {showPrimary && <th>Primary payload</th>}33            <th>Site</th>34            <th className="num">{showPrimary ? 'Payloads' : 'Satellites'}</th>35            {showActive && <th className="num">Active</th>}36          </tr>37        </thead>38        <tbody>39          {rows.length === 0 && <EmptyRow colSpan={cols}>No launches recorded.</EmptyRow>}40          {rows.map((l) => (41            <tr key={l.id}>42              <td className="primary" data-label="Launch">43                <Link href={routes.launch(l.cospar_launch_id)} className="link mono">{l.cospar_launch_id}</Link>44              </td>45              <td data-label="Date" className="tnum text-ink-2">{fmtDate(l.launch_date)}</td>46              {showPrimary && <td data-label="Primary payload" className="text-ink-2">{l.primary_name ?? '—'}</td>}47              <td data-label="Site" className="max-w-[360px] text-ink-2">48                {l.site_slug ? <Link href={routes.launchSite(l.site_slug)} className="link">{l.site_name ?? l.site_slug}</Link> : l.site_name ?? '—'}49              </td>50              <td data-label={showPrimary ? 'Payloads' : 'Satellites'} className="num tnum">{fmtInt(l.satellites ?? l.payload_count)}</td>51              {showActive && <td data-label="Active" className="num tnum text-active">{fmtInt(l.active)}</td>}52            </tr>53          ))}54        </tbody>55      </table>56    </ScrollTable>57  );58}5960// ------------------------------------------------------------------------------------------------ launch sites61export interface SiteLike {62  code: string;63  name: string;64  slug: string;65  country_code?: string | null;66  launches: Num;67  satellites?: Num;68  last_launch?: string | null;69}7071export function SitesTable({ rows, showSatellites = false, showLast = false }: { rows: SiteLike[]; showSatellites?: boolean; showLast?: boolean }) {72  const cols = 3 + (showSatellites ? 1 : 0) + (showLast ? 1 : 0);73  return (74    <ScrollTable>75      <table className="data-table stack">76        <thead>77          <tr>78            <th>Site</th>79            <th>Code</th>80            <th className="num">Launches</th>81            {showSatellites && <th className="num">Satellites</th>}82            {showLast && <th>Last launch</th>}83          </tr>84        </thead>85        <tbody>86          {rows.length === 0 && <EmptyRow colSpan={cols}>No launch sites recorded.</EmptyRow>}87          {rows.map((s) => (88            <tr key={s.code}>89              <td className="primary" data-label="Site">90                <Link href={routes.launchSite(s.slug)} className="link">{s.name}</Link>91                {s.country_code && <span className="mono ml-2 text-xs text-ink-3">{s.country_code}</span>}92              </td>93              <td data-label="Code" className="mono text-xs text-ink-2">{s.code}</td>94              <td data-label="Launches" className="num tnum">{fmtInt(s.launches)}</td>95              {showSatellites && <td data-label="Satellites" className="num tnum">{fmtInt(s.satellites)}</td>}96              {showLast && <td data-label="Last launch" className="tnum text-ink-2">{fmtDate(s.last_launch)}</td>}97            </tr>98          ))}99        </tbody>100      </table>101    </ScrollTable>102  );103}104105// -------------------------------------------------------------------------------------------------- satellites106export interface SatLike {107  id: string;108  slug: string;109  name: string;110  norad_id: number | null;111  status: string;112  object_type?: string;113  orbit_class?: string | null;114  mission_type?: string | null;115  launch_date: string | null;116  perigee_km?: Num;117  apogee_km?: Num;118  inclination_deg?: Num;119}120121export function SatellitesTable({ rows, columns }: { rows: SatLike[]; columns: ('type' | 'orbit' | 'mission' | 'perigee' | 'apogee' | 'inclination')[] }) {122  const has = (c: (typeof columns)[number]) => columns.includes(c);123  const cols = 4 + columns.length;124  return (125    <ScrollTable>126      <table className="data-table stack">127        <thead>128          <tr>129            <th>Satellite</th>130            <th>NORAD</th>131            <th>Status</th>132            {has('type') && <th>Type</th>}133            {has('orbit') && <th>Orbit</th>}134            {has('mission') && <th>Mission</th>}135            <th>Launched</th>136            {has('perigee') && <th className="num">Perigee</th>}137            {has('apogee') && <th className="num">Apogee</th>}138            {has('inclination') && <th className="num">Incl.</th>}139          </tr>140        </thead>141        <tbody>142          {rows.length === 0 && <EmptyRow colSpan={cols}>No satellites recorded.</EmptyRow>}143          {rows.map((s) => (144            <tr key={s.id}>145              <td className="primary" data-label="Satellite">146                <Link href={routes.satellite(s.slug)} className="link font-medium">{s.name}</Link>147              </td>148              <td data-label="NORAD" className="mono text-xs text-ink-2">{s.norad_id ?? '—'}</td>149              <td data-label="Status"><StatusBadge status={s.status} /></td>150              {has('type') && <td data-label="Type"><TypeBadge type={s.object_type} /></td>}151              {has('orbit') && <td data-label="Orbit"><OrbitBadge orbitClass={s.orbit_class} /></td>}152              {has('mission') && <td data-label="Mission" className="text-ink-2"><MissionLabel mission={s.mission_type} /></td>}153              <td data-label="Launched" className="tnum text-ink-2">{fmtDate(s.launch_date)}</td>154              {has('perigee') && <td data-label="Perigee" className="num tnum">{fmtKm(s.perigee_km)}</td>}155              {has('apogee') && <td data-label="Apogee" className="num tnum">{fmtKm(s.apogee_km)}</td>}156              {has('inclination') && <td data-label="Incl." className="num tnum">{fmtDeg(s.inclination_deg)}</td>}157            </tr>158          ))}159        </tbody>160      </table>161    </ScrollTable>162  );163}164165// ------------------------------------------------------------------------------------------ constellation lists166export interface ConstellationLike {167  id: string;168  slug: string;169  name: string;170  service_type: string | null;171  orbit_class: string | null;172  active: Num;173  total: Num;174  launched_last_365d?: Num;175}176177export function ConstellationsMiniTable({ rows }: { rows: ConstellationLike[] }) {178  const showGrowth = rows.some((r) => r.launched_last_365d !== undefined);179  return (180    <ScrollTable>181      <table className="data-table stack">182        <thead>183          <tr>184            <th>Constellation</th>185            <th>Service</th>186            <th>Orbit</th>187            <th className="num">Active</th>188            <th className="num">Total</th>189            {showGrowth && <th className="num">365 d</th>}190          </tr>191        </thead>192        <tbody>193          {rows.length === 0 && <EmptyRow colSpan={showGrowth ? 6 : 5}>No constellations linked.</EmptyRow>}194          {rows.map((c) => (195            <tr key={c.id}>196              <td className="primary" data-label="Constellation"><Link href={routes.constellation(c.slug)} className="link font-medium">{c.name}</Link></td>197              <td data-label="Service" className="text-ink-2">{titleCase(c.service_type)}</td>198              <td data-label="Orbit"><OrbitBadge orbitClass={c.orbit_class} /></td>199              <td data-label="Active" className="num tnum text-active">{fmtInt(c.active)}</td>200              <td data-label="Total" className="num tnum">{fmtInt(c.total)}</td>201              {showGrowth && <td data-label="Launched 365 d" className="num tnum">{fmtInt(c.launched_last_365d)}</td>}202            </tr>203          ))}204        </tbody>205      </table>206    </ScrollTable>207  );208}209210// ----------------------------------------------------------------------------------------------- operator lists211export interface OperatorLike {212  id: string;213  slug: string;214  name: string;215  kind: string;216  active_payloads: Num;217  total_payloads: Num;218  payloads_last_365d: Num;219}220221export function OperatorsMiniTable({ rows }: { rows: OperatorLike[] }) {222  return (223    <ScrollTable>224      <table className="data-table stack">225        <thead>226          <tr>227            <th>Operator</th>228            <th>Kind</th>229            <th className="num">Active</th>230            <th className="num">Total</th>231            <th className="num">365 d</th>232          </tr>233        </thead>234        <tbody>235          {rows.length === 0 && <EmptyRow colSpan={5}>No operators linked.</EmptyRow>}236          {rows.map((o) => (237            <tr key={o.id}>238              <td className="primary" data-label="Operator"><Link href={routes.operator(o.slug)} className="link font-medium">{o.name}</Link></td>239              <td data-label="Kind" className="text-ink-2">{titleCase(o.kind)}</td>240              <td data-label="Active" className="num tnum text-active">{fmtInt(o.active_payloads)}</td>241              <td data-label="Total" className="num tnum">{fmtInt(o.total_payloads)}</td>242              <td data-label="Launched 365 d" className="num tnum">{fmtInt(o.payloads_last_365d)}</td>243            </tr>244          ))}245        </tbody>246      </table>247    </ScrollTable>248  );249}250