SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
4.8 KB · 89 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader, Section, Note } from '@/components/ui/section';4import { Badge } from '@/components/ui/badge';5import { EmptyState } from '@/components/ui/empty-state';6import { listMetrics } from '@/lib/queries/rankings';7import { humanize, unitLabel } from '@/lib/format';89export const metadata: Metadata = { title: 'Rankings', description: 'Transparent cancer rankings: one metric, one scope, one formula version per table, with lineage for every rank.' };10export const dynamic = 'force-dynamic';1112const CATEGORY_ORDER = ['burden', 'lethality', 'trials', 'research', 'trend', 'molecular', 'unmet_need', 'rarity', 'treatment', 'composite'];13const CATEGORY_LABEL: Record<string, string> = { burden: 'Burden', lethality: 'Lethality', trials: 'Clinical research', research: 'Research activity', trend: 'Trends', molecular: 'Molecular knowledge', unmet_need: 'Gap indexes', rarity: 'Rarity', treatment: 'Treatment', composite: 'Composite' };1415export default async function RankingsIndex() {16  const metrics = await listMetrics();17  const byCat = new Map<string, typeof metrics>();18  for (const m of metrics) byCat.set(m.category, [...(byCat.get(m.category) ?? []), m]);19  const cats = [...byCat.keys()].sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b));20  const computed = metrics.filter((m) => m.snapshot_count > 0).length;2122  return (23    <div>24      <PageHeader kicker="Rankings" title="Rankings" lede="Every ranking is one metric, in one scope (geography · sex · age · year · entity level), under one formula version. No composite 'worst cancer' score is published in this phase.">25        <p className="mt-3 text-[13px] text-ink-2">26          <span className="ci-num font-medium">{computed}</span> of <span className="ci-num">{metrics.length}</span> metrics currently have a computed snapshot.27        </p>28      </PageHeader>2930      <Note tone="warn">"Deadliest cancer" has no single answer. Annual deaths, age-standardized mortality rate, mortality-to-incidence ratio and 5-year survival each rank cancers differently and are published as separate metrics. Pick the question before reading the table.</Note>3132      {metrics.length === 0 ? (33        <div className="mt-6">34          <EmptyState title="Metric catalog not seeded">The metric definitions table is empty on this environment. Run the database seed.</EmptyState>35        </div>36      ) : (37        <div className="mt-6 space-y-8">38          {cats.map((cat) => (39            <Section key={cat} id={cat} kicker="Category" title={CATEGORY_LABEL[cat] ?? humanize(cat)}>40              <div className="ci-table-wrap">41                <table className="ci-table">42                  <thead>43                    <tr>44                      <th>Metric</th>45                      <th>Unit</th>46                      <th>Direction</th>47                      <th>Scopes</th>48                      <th className="num">Snapshots</th>49                      <th>Sources</th>50                      <th>Formula version</th>51                    </tr>52                  </thead>53                  <tbody>54                    {(byCat.get(cat) ?? []).map((m) => (55                      <tr key={m.slug}>56                        <td className="min-w-[220px]">57                          <Link className="ci-link font-medium" href={`/rankings/${m.slug}`}>58                            {m.name}59                          </Link>60                          {m.experimental ? <Badge tone="warn" className="ml-1">experimental</Badge> : null}61                          <p className="mt-0.5 max-w-[520px] text-[12px] text-ink-3">{m.description}</p>62                        </td>63                        <td>{unitLabel(m.unit)}</td>64                        <td className="text-[12.5px]">{m.higher_is_worse == null ? 'neutral' : m.higher_is_worse ? 'higher = worse' : 'higher = better'}</td>65                        <td className="text-[12px] text-ink-3">{m.valid_dimensions.join(', ')}</td>66                        <td className="num">{m.snapshot_count > 0 ? m.snapshot_count : <span className="text-ink-4" title="No snapshot computed yet">—</span>}</td>67                        <td className="text-[12px]">68                          {m.source_slugs.map((s) => (69                            <Link key={s} href={`/source/${s}`} className="ci-mono mr-1 text-ink-2 hover:text-accent">70                              {s}71                            </Link>72                          ))}73                        </td>74                        <td>75                          <span className="ci-mono text-[11.5px]">{m.formula_version}</span>76                        </td>77                      </tr>78                    ))}79                  </tbody>80                </table>81              </div>82            </Section>83          ))}84        </div>85      )}86    </div>87  );88}89