spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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