import { notFound } from "next/navigation"; import Link from "next/link"; import type { Metadata } from "next"; import { graph } from "@/lib/graph"; import { KIND_META, KINDS, routeFor, type Kind } from "@/lib/schema"; import { Container, PageHeader } from "@/components/ui"; import { rankInstitutions } from "@/lib/ranking"; import { EntityBrowser } from "@/components/EntityBrowser"; import { FrontSchematic } from "@/components/FrontSchematic"; import { BottleneckMap } from "@/components/BottleneckMap"; import { CancerIcon, CancerIconDefs } from "@/components/CancerIcon"; import { FrontIcon } from "@/components/FrontIcon"; import { TermSchematic } from "@/components/TermSchematic"; import { logoSrc } from "@/lib/logos"; import { kindTitle, pageMeta } from "@/lib/seo"; import { KindName, T } from "@/components/T"; import { RankGlyph } from "@/components/RankGlyph"; import { cap } from "@/lib/kind-browser"; import { isPagedKind, kindBrowser, pageKindRows } from "@/lib/tables/kinds"; import { KIND_PAGE } from "@/lib/static-tables"; import { MyCancerPin, MyCancerTrialsFilter } from "@/components/MyCancer"; import { myCancerTiles } from "@/lib/my-cancer-list"; const ROUTE_TO_KIND: Record = Object.fromEntries(KINDS.map((k) => [KIND_META[k].route, k])) as Record; export function generateStaticParams() { return KINDS.map((k) => ({ kind: KIND_META[k].route })); } /** Index page title is the public kind name ("Treatments & tests", "Cancers"); the description is the KIND_META blurb with the live count. */ export async function generateMetadata({ params }: { params: Promise<{ kind: string }> }): Promise { const { kind } = await params; const k = ROUTE_TO_KIND[kind]; if (!k) return {}; const meta = KIND_META[k]; const n = graph().kind(k).length; return pageMeta({ title: kindTitle(k), description: `${meta.blurb} ${n} ${meta.plural}, each with a plain-English TL;DR and sources.`, path: `/${meta.route}/` }); } export default async function KindIndex({ params }: { params: Promise<{ kind: string }> }) { const { kind } = await params; const k = ROUTE_TO_KIND[kind]; if (!k) notFound(); const meta = KIND_META[k]; const title = k === "section" ? "Fronts of the war on cancer" : k === "term" ? "Glossary" : k === "bottleneck" ? "Bottlenecks of the war on cancer" : (meta.title ?? cap(meta.plural)); // Rows with glossary marks, in the table's default order (src/lib/tables/kinds.ts). A paged kind carries its first // KIND_PAGE rows and the whole-table facet counts; the rest is /api/v1/tables/kind-.json, fetched on demand. const built = kindBrowser(k); const { facets, columns, hideStatus, hideTldr, defaultSort } = built; const paged = pageKindRows(k, built.rows); const total = built.rows.length; const right = k === "cancer" ?
Whole map →Pick mine →
: k === "drug" ? Rank by cancer type → : k === "target" ?
Specificity classes →Cancer genes by role →
: k === "institution" ? University output → : k === "bottleneck" ? All ideas → : k === "idea" ? → : k === "trial" ? : undefined; return ( <> } title={} seed={title} lede={meta.blurb} right={right} /> {k === "section" && } {k === "bottleneck" && } {k === "cancer" && } {k === "term" && } {isPagedKind(k) && ( // The whole set for crawlers and agents without the table's fetch: every record also has its own page, listed in the sitemap.

The table above loads {Math.min(KIND_PAGE, total)} of {total.toLocaleString("en-GB")} {meta.plural} first and fetches the rest as you scroll, search or filter.{" "} All {total.toLocaleString("en-GB")} {meta.plural} as JSON {" · "} as CSV {" · "} API

)} {k === "institution" && (

Score = Newsweek points (60 − Newsweek/Statista 2026 Oncology rank, 0 if unranked) + NCI designation points (Comprehensive 15, Clinical or Basic 8) + 2 × distinct OnCo objects linked to the institution. The last term measures presence in this evidence base and grows with the corpus. A starting point for argument, not a verdict.

)}
); } /** One clickable card per glossary category, each with its animated schematic; clicking filters the table below. */ function GlossaryCategories() { const g = graph(); const counts = new Map(); for (const t of g.kind("term")) counts.set(t.category, (counts.get(t.category) ?? 0) + 1); const cats = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); return (
Browse by category

Twenty-one kinds of term, each with a short animation of what that kind is about. Click one to filter the glossary; every term page opens with its own picture.

    {cats.map(([c, n]) => (
  • {c}{n}
  • ))}
); } /** * Every cancer as an icon tile, grouped, so a newcomer can find theirs by organ rather than by name. The organ * drawings are one hidden symbol sheet referenced by every tile: 328 inline drawings were 370 KB of the page. */ function CancersGrid() { const g = graph(); const items = g.kind("cancer"); const groups = [...new Set(items.map((c) => c.group))]; return (
c.id)} /> {groups.map((grp) => (
{grp}
{items.filter((c) => c.group === grp).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ( {c.name} ))}
))}
); } function BottlenecksPipeline() { const g = graph(); const items = g.kind("bottleneck").map((b) => ({ id: b.id, name: b.name, tldr: b.tldr, route: routeFor(b), stage: b.stage, severity: b.severity, ideas: (g.incoming(b.id).get("idea") ?? []).length })); return ; } function FrontsGrid() { const g = graph(); const items = g.kind("section").sort((a, b) => a.order - b.order); return (
{items.map((s) => (
{s.name}
))}
); } /** Map dots for the institutions explorer; `type` uses the same label as the table's "type" facet so the legend can drive both. */ function institutionPoints() { return rankInstitutions().map((r) => { const i = r.institution; return { id: i.id, name: i.name, city: i.city, country: i.country, type: cap(i.institutionType.replace("-", " ")), lat: i.lat, lon: i.lng, route: routeFor(i), logo: logoSrc(i.id, i.website), avatar: "org", links: r.links }; }); }