spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import { ArrowRight, Code2 } from 'lucide-react';2import Link from 'next/link';3import { FreshnessBadge } from '@/components/ui/badges';4import { Section } from '@/components/ui/section';5import { Unavailable } from '@/components/ui/unavailable';6import { fmtAgo } from '@/lib/format';7import { routes } from '@/lib/site';8import type { HomePayload } from '@/lib/types';910type Source = HomePayload['sources'][number];1112/** Freshness from the connector's own cadence: ≤ 2 intervals fresh, ≤ 6 aging, else stale. */13export function freshnessOf(s: Source, now = Date.now()): 'fresh' | 'aging' | 'stale' | 'unavailable' | 'not_enabled' {14 if (!s.enabled) return 'not_enabled';15 if (!s.last_success_at) return 'unavailable';16 const age = (now - new Date(s.last_success_at).getTime()) / 1000;17 const iv = s.interval_seconds ?? 86_400;18 if (age <= iv * 2) return 'fresh';19 if (age <= iv * 6) return 'aging';20 return 'stale';21}2223export function DataSources({ sources }: { sources: HomePayload['sources'] }) {24 const grouped = new Map<string, Source[]>();25 for (const s of sources) grouped.set(s.id, [...(grouped.get(s.id) ?? []), s]);26 const attributions = [...new Set(sources.map((s) => s.attribution_text).filter((t): t is string => Boolean(t)))];27 return (28 <Section eyebrow="Sources" title="Where the data comes from" action={{ href: routes.sources(), label: 'Sources & status' }} className="py-0">29 {grouped.size === 0 ? (30 <Unavailable what="Source status" />31 ) : (32 <ul className="divide-y divide-rule border-t border-rule">33 {[...grouped.entries()].map(([id, conns]) => {34 const first = conns[0]!;35 return (36 <li key={id} className="grid gap-2 py-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1.6fr)] md:gap-6">37 <div className="min-w-0">38 <p className="flex items-center gap-2 text-[15px] text-ink">39 {first.name}40 <span className="rounded border border-rule px-1.5 py-0.5 text-2xs text-ink-3">{first.official ? 'Official' : 'Public'}</span>41 </p>42 {first.attribution_text && <p className="mt-0.5 text-2xs text-ink-3">{first.attribution_text}</p>}43 </div>44 <ul className="space-y-1">45 {conns.map((c) => (46 <li key={c.connector ?? c.id} className="flex flex-wrap items-center justify-between gap-x-3 gap-y-0.5 text-xs">47 <span className="mono text-ink-2">{c.connector ?? '—'}</span>48 <span className="flex items-center gap-3">49 <span className="tnum text-ink-3">updated {fmtAgo(c.last_success_at)}</span>50 <FreshnessBadge status={freshnessOf(c)} />51 </span>52 </li>53 ))}54 </ul>55 </li>56 );57 })}58 </ul>59 )}60 {attributions.length > 0 && <p className="mt-4 text-2xs leading-relaxed text-ink-3">{attributions.join(' ')} Derived classifications follow the published <Link href={routes.methodology()} className="link">methodology</Link>.</p>}61 </Section>62 );63}6465export function ApiTeaser() {66 return (67 <Section eyebrow="Developers" title="Everything here is an API" className="py-0">68 <div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">69 <p className="max-w-xl text-sm leading-relaxed text-ink-2">70 Every number on this page is served by the public JSON API — catalogue, search, live SGP4 positions, constellations, operators,71 launches, debris, reentries, events and the statistics snapshot. Interactive OpenAPI docs at{' '}72 <code className="mono rounded bg-plane-2 px-1.5 py-0.5 text-xs text-ink">/api/v1/docs</code>.73 </p>74 <div className="mono rounded-lg border border-rule bg-plane/60 p-4 text-xs leading-6 text-ink-2">75 <p className="text-ink-3"># live position of the ISS</p>76 <p>GET /api/v1/satellites/25544/live</p>77 <p className="mt-2 text-ink-3"># all tracked objects, two SGP4 epochs</p>78 <p>GET /api/v1/orbit/positions</p>79 <p className="mt-2 text-ink-3"># full-text search across entities</p>80 <p>GET /api/v1/search?q=starlink</p>81 </div>82 </div>83 <Link href={routes.developers()} className="mt-4 inline-flex h-11 items-center gap-2 rounded-md border border-rule-strong px-4 text-sm text-ink hover:bg-plane-2">84 <Code2 className="size-4" aria-hidden /> API documentation <ArrowRight className="size-4" aria-hidden />85 </Link>86 </Section>87 );88}89