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.0 KB · 94 lines tsx
Raw Blame History
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { Unavailable } from '@/components/ui/unavailable';4import { fmtInt, num, titleCase } from '@/lib/format';5import { ORBIT_CLASS_COLORS, routes } from '@/lib/site';6import type { HomePayload } from '@/lib/types';78interface RankRow {9  key: string;10  href: string;11  label: string;12  sub?: string | null;13  value: number;14  color?: string;15}1617/** Ranked list with proportional bars where every label is a link (the shared HBars is not linkable). */18export function RankedBars({ rows, valueLabel }: { rows: RankRow[]; valueLabel: string }) {19  if (!rows.length) return <Unavailable what="Ranking" />;20  const max = Math.max(1, ...rows.map((r) => r.value));21  return (22    <ol className="divide-y divide-rule border-t border-rule">23      {rows.map((r, i) => (24        <li key={r.key}>25          <Link href={r.href} className="group grid min-h-[48px] grid-cols-[1.6rem_minmax(0,1fr)_auto] items-center gap-x-3 py-2">26            <span className="mono text-2xs text-ink-3">{String(i + 1).padStart(2, '0')}</span>27            <span className="min-w-0">28              <span className="flex items-baseline gap-2">29                <span className="truncate text-sm text-ink group-hover:text-accent">{r.label}</span>30                {r.sub && <span className="hidden truncate text-2xs text-ink-3 sm:inline">{r.sub}</span>}31              </span>32              <span className="mt-1 block h-[4px] w-full overflow-hidden rounded-full bg-plane-2">33                <span className="block h-full rounded-full" style={{ width: `${Math.max(1.5, (r.value / max) * 100)}%`, background: r.color ?? 'var(--series-1)' }} />34              </span>35            </span>36            <span className="tnum text-right text-sm text-ink">37              {fmtInt(r.value)} <span className="sr-only">{valueLabel}</span>38            </span>39          </Link>40        </li>41      ))}42    </ol>43  );44}4546export function TopConstellations({ items }: { items: HomePayload['top_constellations'] }) {47  const rows: RankRow[] = items.map((c) => ({48    key: c.id,49    href: routes.constellation(c.slug),50    label: c.name,51    sub: [c.service_type ? titleCase(c.service_type) : null, c.orbit_class].filter(Boolean).join(' · '),52    value: num(c.active) ?? 0,53    color: ORBIT_CLASS_COLORS[c.orbit_class ?? 'OTHER'] ?? 'var(--series-1)',54  }));55  return (56    <Section eyebrow="Constellations" title="Largest constellations" action={{ href: routes.constellations(), label: 'All constellations' }} className="py-0">57      <p className="mb-3 text-xs text-ink-3">Active satellites · membership is derived (see <Link href={routes.methodology()} className="link">methodology</Link>)</p>58      <RankedBars rows={rows} valueLabel="active satellites" />59    </Section>60  );61}6263export function TopCountries({ items }: { items: HomePayload['top_countries'] }) {64  const rows: RankRow[] = items.slice(0, 10).map((c) => ({65    key: c.code,66    href: routes.country(c.slug),67    label: c.name,68    sub: `${fmtInt(c.objects_on_orbit)} objects on orbit`,69    value: num(c.active_payloads) ?? 0,70    color: 'var(--series-6)',71  }));72  return (73    <Section eyebrow="Countries" title="Active payloads by country" action={{ href: routes.countries(), label: 'All countries' }} className="py-0">74      <RankedBars rows={rows} valueLabel="active payloads" />75    </Section>76  );77}7879export function TopOperators({ items }: { items: HomePayload['top_operators'] }) {80  const rows: RankRow[] = items.slice(0, 10).map((o) => ({81    key: o.id,82    href: routes.operator(o.slug),83    label: o.name,84    sub: [o.country_code, num(o.payloads_last_365d) ? `${fmtInt(o.payloads_last_365d)} launched in 365 d` : null].filter(Boolean).join(' · '),85    value: num(o.active_payloads) ?? 0,86    color: 'var(--series-2)',87  }));88  return (89    <Section eyebrow="Operators" title="Largest operators" action={{ href: routes.operators(), label: 'All operators' }} className="py-0">90      <RankedBars rows={rows} valueLabel="active payloads" />91    </Section>92  );93}94