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%
4.6 KB · 89 lines tsx
Raw Blame History
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