spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
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