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.8 KB · 116 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { HBars } from '@/components/charts/charts';4import { DEFAULT_METRIC, findMetric, METRICS, type Row } from '@/components/stats/rankings-config';5import { Chips, Derived, InlineRank, Note } from '@/components/stats/shared';6import { Container, PageHeader, Section } from '@/components/ui/section';7import { Unavailable } from '@/components/ui/unavailable';8import { api, safe } from '@/lib/api';9import { fmtAgo, fmtInt } from '@/lib/format';10import { routes, SITE_NAME, SITE_URL } from '@/lib/site';1112export const revalidate = 600;1314type Search = Promise<{ metric?: string | string[] }>;1516function pick(v: string | string[] | undefined): string | undefined {17  return Array.isArray(v) ? v[0] : v;18}1920export async function generateMetadata({ searchParams }: { searchParams: Search }): Promise<Metadata> {21  const m = findMetric(pick((await searchParams).metric) ?? DEFAULT_METRIC);22  const url = `${SITE_URL}${m.key === DEFAULT_METRIC ? routes.rankings() : routes.rankings(m.key)}`;23  const title = `${m.title} — Rankings`;24  return {25    title,26    description: m.description,27    alternates: { canonical: url },28    openGraph: { title: `${title} | ${SITE_NAME}`, description: m.description, url, type: 'website', siteName: SITE_NAME },29    twitter: { card: 'summary_large_image', title: `${title} | ${SITE_NAME}`, description: m.description },30  };31}3233export default async function RankingsPage({ searchParams }: { searchParams: Search }) {34  const metricKey = pick((await searchParams).metric) ?? DEFAULT_METRIC;35  const m = findMetric(metricKey);36  const res = await safe(api.rankings(m.key, 50));37  const rows: Row[] = res?.data.rows ?? [];38  const top = rows.slice(0, 10).map((r) => ({ label: m.name(r), value: m.primary(r), href: m.href(r) ?? undefined }));39  const chips = METRICS.map((x) => ({ href: x.key === DEFAULT_METRIC ? routes.rankings() : routes.rankings(x.key), label: x.label, active: x.key === m.key }));40  const hasDerived = m.columns.some((c) => c.derived);4142  return (43    <Container>44      <PageHeader eyebrow="Rankings" title={m.title} lede={m.description}>45        <div className="mt-6">46          <Chips items={chips} ariaLabel="Ranking metric" />47        </div>48      </PageHeader>4950      {m.warn && <Note tone="warn" className="mb-6">{m.warn}</Note>}5152      {!res ? (53        <div className="py-6">54          <Unavailable what="Ranking" />55        </div>56      ) : rows.length === 0 ? (57        <div className="py-6">58          <Unavailable what="Ranking rows" />59        </div>60      ) : (61        <>62          <Section eyebrow="Top 10" title={m.primaryLabel} className="pt-0 md:pt-0">63            <HBars data={top} valueFormat={m.primaryFormat ?? fmtInt} />64          </Section>6566          <Section eyebrow={`${fmtInt(rows.length)} rows`} title="Full ranking" className="pt-0 md:pt-0">67            <div className="overflow-x-hidden">68              <table className="data-table stack text-sm">69                <thead>70                  <tr>71                    {m.columns.map((c) => (72                      <th key={c.key} className={c.num ? 'num' : undefined}>73                        {c.label}74                        {c.derived && <Derived className="ml-1.5 normal-case" />}75                      </th>76                    ))}77                  </tr>78                </thead>79                <tbody>80                  {rows.map((r, i) => (81                    <tr key={`${m.key}-${i}`}>82                      {m.columns.map((c, ci) => (83                        <td key={c.key} data-label={ci === 0 ? undefined : c.label} className={[c.num ? 'num tnum' : '', ci === 0 ? 'primary' : ''].join(' ').trim() || undefined}>84                          {ci === 0 ? (85                            <div className="flex items-start">86                              <InlineRank n={i + 1} />87                              {c.render(r)}88                            </div>89                          ) : (90                            c.render(r)91                          )}92                        </td>93                      ))}94                    </tr>95                  ))}96                </tbody>97              </table>98            </div>99            {(m.note || hasDerived) && (100              <Note className="mt-4">101                {m.note ?? (102                  <>103                    Columns marked <Derived className="mx-1" /> are computed by SatelliteIndex.104                  </>105                )}{' '}106                <Link href={routes.methodology()} className="text-accent hover:underline">Methodology</Link>107              </Note>108            )}109            <p className="mono mt-3 text-xs text-ink-3">Generated {fmtAgo(res.meta.generated_at)} · limit 50 · source: SatelliteIndex canonical catalogue (CelesTrak SATCAT + GP)</p>110          </Section>111        </>112      )}113    </Container>114  );115}116