spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { cn } from '@/lib/cn';3import { EVENT_TYPE_LABELS, routes } from '@/lib/site';4import type { EventEntity } from '@/lib/types';56/** Event type → colour token. Launch = accent, decay/reentry = warn, orbit change = accent-2, decommission = danger, activation = active. */7export const EVENT_TYPE_COLORS: Record<string, string> = {8 SATELLITE_LAUNCH: 'var(--accent)',9 DECAY: 'var(--warn)',10 REENTRY: 'var(--warn)',11 ORBIT_CHANGE: 'var(--accent-2)',12 SATELLITE_DECOMMISSION: 'var(--danger)',13 SATELLITE_ACTIVATION: 'var(--active)',14 CONSTELLATION_EXPANSION: 'var(--accent)',15 REGULATORY_APPROVAL: 'var(--series-4)',16 NEW_LICENSE: 'var(--series-4)',17 COMPANY_ANNOUNCEMENT: 'var(--inactive)',18};1920export function eventColor(type: string): string {21 return EVENT_TYPE_COLORS[type] ?? 'var(--inactive)';22}23export function eventLabel(type: string): string {24 return EVENT_TYPE_LABELS[type] ?? type.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());25}2627export function EventTypeBadge({ type, className, size = 'sm' }: { type: string; className?: string; size?: 'sm' | 'md' }) {28 const color = eventColor(type);29 return (30 <span className={cn('inline-flex items-center gap-1.5 whitespace-nowrap rounded border font-medium', size === 'sm' ? 'px-1.5 py-0.5 text-[11px]' : 'px-2.5 py-1 text-xs', className)} style={{ color, borderColor: `color-mix(in oklab, ${color} 35%, transparent)`, background: `color-mix(in oklab, ${color} 10%, transparent)` }}>31 <span className="dot" aria-hidden />32 {eventLabel(type)}33 </span>34 );35}3637/** Confidence as a mono percentage; tooltip explains it is derived. */38export function Confidence({ value, className }: { value: number | null | undefined; className?: string }) {39 if (value === null || value === undefined || !Number.isFinite(value)) return <span className={cn('mono text-xs text-ink-3', className)}>—</span>;40 const pct = Math.round(value * 100);41 const color = pct >= 90 ? 'var(--active)' : pct >= 60 ? 'var(--warn)' : 'var(--danger)';42 return (43 <span className={cn('mono text-xs', className)} title="Derived confidence: how sure SatelliteIndex is about this event given its source and dedupe rules" style={{ color }}>44 {pct}%45 </span>46 );47}4849export function entityHref(e: EventEntity): string | null {50 if (!e.slug) return null;51 switch (e.type) {52 case 'satellite':53 return routes.satellite(e.slug);54 case 'launch':55 return routes.launch(e.slug);56 case 'constellation':57 return routes.constellation(e.slug);58 case 'operator':59 return routes.operator(e.slug);60 case 'country':61 return routes.country(e.slug);62 case 'launch_site':63 return routes.launchSite(e.slug);64 default:65 return null;66 }67}6869export function entityName(e: EventEntity): string {70 if (e.type === 'launch') return `Launch ${e.slug ?? e.id}`;71 return e.name ?? e.slug ?? e.id;72}7374/** Inline list of entity links: satellite → /satellite/<slug>, launch → /launch/<cospar>. */75export function EntityLinks({ entities, className, max = 6 }: { entities: EventEntity[] | null | undefined; className?: string; max?: number }) {76 if (!entities?.length) return null;77 const shown = entities.slice(0, max);78 return (79 <ul className={cn('flex flex-wrap items-center gap-x-2 gap-y-1 text-xs', className)}>80 {shown.map((e) => {81 const href = entityHref(e);82 const label = entityName(e);83 return (84 <li key={`${e.type}-${e.id}`} className="inline-flex items-center gap-1">85 <span className="mono text-[10px] uppercase tracking-wider text-ink-3">{e.type.replace('_', ' ')}</span>86 {href ? (87 <Link href={href} className="link">{label}</Link>88 ) : (89 <span className="text-ink-2">{label}</span>90 )}91 {e.norad_id ? <span className="mono text-ink-3">#{e.norad_id}</span> : null}92 </li>93 );94 })}95 {entities.length > max && <li className="text-ink-3">+{entities.length - max} more</li>}96 </ul>97 );98}99