spb/cancerindex
Public
1import Link from 'next/link';2import { TOP_LEVEL_CANCERS } from '@cancerindex/ontology';3import { CommandPalette } from '@/components/layout/command-palette';4import { EmptyState } from '@/components/ui/empty-state';5import { Freshness } from '@/components/ui/freshness';6import { Section } from '@/components/ui/section';7import { Badge, ClaimBadge, ConfidenceBadge, StatusBadge } from '@/components/ui/badge';8import { getSiteCounts, getRareSpotlight } from '@/lib/queries/stats';9import { previewSnapshot } from '@/lib/queries/rankings';10import { mostActiveResearch, mostCuratedEvidence } from '@/lib/queries/trials';11import { resolveTopLevel } from '@/lib/queries/cancers';12import { listSources } from '@/lib/queries/sources';13import { fmtInt, fmtValue, fmtDate, scopeLabel, unitLabel, humanize, parseScopeKey } from '@/lib/format';14import { BurdenModule } from '@/components/home/burden-module';15import { RisingModule } from '@/components/home/rising-module';16import { GapsModule } from '@/components/home/gaps-module';1718export const revalidate = 900;1920export default async function HomePage() {21 const [counts, spotlight, preview, active, curated, sources] = await Promise.all([22 getSiteCounts(),23 getRareSpotlight(),24 previewSnapshot(['mortality_count', 'incidence_count', 'as_mortality_rate', 'active_trials', 'publications_5y', 'curated_evidence_items']),25 mostActiveResearch(8),26 mostCuratedEvidence(8),27 listSources(),28 ]);29 const topLevel = await resolveTopLevel(TOP_LEVEL_CANCERS.map((t) => t.ncit));30 const topMatched = TOP_LEVEL_CANCERS.filter((t) => topLevel.has(t.ncit)).length;3132 const ticker: Array<{ label: string; value: number; note: string; href: string }> = [33 { label: 'Cancers indexed', value: counts.cancers, note: 'active malignant disease entities', href: '/cancers?malignant=1' },34 { label: 'Subtypes', value: counts.subtypes, note: 'histologic and molecular subtypes', href: '/cancers?entity_type=subtype' },35 { label: 'Genes', value: counts.genes, note: 'HGNC gene records', href: '/genes' },36 { label: 'Variants', value: counts.variants, note: 'distinct variant entities', href: '/genes' },37 { label: 'Clinical trials', value: counts.trials, note: 'ClinicalTrials.gov studies ingested', href: '/trials' },38 { label: 'Publications', value: counts.publications, note: 'PubMed records linked', href: '/search' },39 { label: 'Sources', value: counts.sources, note: `${counts.activeSources} active connector${counts.activeSources === 1 ? '' : 's'}`, href: '/sources' },40 ];4142 return (43 <div className="pb-8">44 {/* Hero */}45 <section className="pt-10 sm:pt-16" aria-labelledby="hero-title">46 <p className="ci-kicker mb-3">Provenance-first oncology intelligence</p>47 <h1 id="hero-title" className="max-w-4xl text-4xl leading-[1.05] sm:text-6xl">48 CancerIndex — <span className="italic text-ink-2">The global index of cancer.</span>49 </h1>50 <p className="mt-4 max-w-2xl text-[16px] leading-relaxed text-ink-2">51 Every recognized malignant disease entity — taxonomy, epidemiology, genomics, variants, therapies, trials, literature and transparent rankings — with a source, a date and a formula version on every number.52 </p>53 <div className="mt-6 max-w-2xl">54 <CommandPalette variant="hero" />55 <p className="mt-2 text-[12.5px] text-ink-3">56 Try <Link className="ci-link" href="/search?q=glioma">glioma</Link>, <Link className="ci-link" href="/search?q=KRAS">KRAS</Link>, <Link className="ci-link" href="/search?q=osimertinib">osimertinib</Link> or an NCT number.57 </p>58 </div>59 </section>6061 {/* Ticker */}62 <section aria-label="Index counts" className="ci-rule mt-10 pt-4">63 <ul className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4 lg:grid-cols-7">64 {ticker.map((t) => (65 <li key={t.label}>66 <Link href={t.href} className="block no-underline">67 <span className="ci-num block font-display text-2xl text-ink sm:text-3xl">{fmtInt(t.value)}</span>68 <span className="block text-[12.5px] font-medium text-ink-2">{t.label}</span>69 <span className="block text-[11.5px] text-ink-3">{t.note}</span>70 </Link>71 </li>72 ))}73 </ul>74 <Freshness dataUpdatedAt={counts.lastIngestAt} extra="Counts are live database counts, not estimates" />75 </section>7677 <div className="mt-10 grid gap-10 lg:grid-cols-[1.35fr_1fr]">78 <div className="space-y-10">79 {/* Cancer burden (US, latest year) — §105 "Cancer burden today" */}80 <BurdenModule slug="united-states" />8182 {/* Rankings preview */}83 <Section84 id="rankings"85 kicker={preview && parseScopeKey(preview.snapshot.scope_key).geo !== 'WORLD' ? `Rankings · ${parseScopeKey(preview.snapshot.scope_key).geo}` : 'Global rankings'}86 title={preview ? preview.metric.name : 'Global rankings'}87 description={preview ? `${scopeLabel(preview.snapshot.scope_key)} · formula ${preview.snapshot.formula_version} · generated ${fmtDate(preview.snapshot.generated_at)} · ${fmtInt(preview.snapshot.eligible_entities)} eligible entities` : undefined}88 actions={89 <Link href="/rankings" className="ci-link">90 All rankings →91 </Link>92 }93 >94 {preview ? (95 <>96 {!preview.metric.slug.match(/incidence|mortality|survival/) ? (97 <p className="mb-2 text-[12.5px] text-ink-3">No epidemiology-based ranking has been computed yet (awaiting licensed burden data). Showing the research-activity ranking instead — it measures registered activity, not disease burden.</p>98 ) : null}99 <div className="ci-table-wrap">100 <table className="ci-table">101 <thead>102 <tr>103 <th className="num">#</th>104 <th>Cancer</th>105 <th className="num">106 {preview.metric.name} ({unitLabel(preview.metric.unit)})107 </th>108 <th>Confidence</th>109 </tr>110 </thead>111 <tbody>112 {preview.rows.map((r) => (113 <tr key={r.id}>114 <td className="num">{r.rank}</td>115 <td>116 <Link className="ci-link" href={`/cancer/${r.slug}`}>117 {r.canonical_name}118 </Link>119 </td>120 <td className="num">{fmtValue(r.value, r.unit)}</td>121 <td>122 <ConfidenceBadge level={r.confidence} />123 </td>124 </tr>125 ))}126 </tbody>127 </table>128 </div>129 <div className="mt-2 flex items-center gap-2">130 <ClaimBadge kind="computed" />131 <Link href={`/rankings/${preview.metric.slug}?scope=${encodeURIComponent(preview.snapshot.scope_key)}`} className="ci-link text-[13px]">132 Full table, methodology and "Why this rank?" →133 </Link>134 </div>135 <Freshness dataUpdatedAt={preview.snapshot.generated_at} extra={`sources: ${preview.snapshot.source_ids.join(', ') || 'see methodology'}`} />136 </>137 ) : (138 <EmptyState title="Rankings not yet computed">139 Ranking snapshots are produced by the ranking engine once counters and licensed epidemiology observations are available. Nothing is shown until a snapshot exists with a scope, a formula version and an inputs hash.140 <div className="mt-1">141 <Link href="/methodology" className="ci-link">142 Read how rankings are computed143 </Link>144 </div>145 </EmptyState>146 )}147 </Section>148149 {/* Fastest-rising incidence (derived on the fly, §105/§328) */}150 <RisingModule slug="united-states" />151152 {/* Largest trial / research gaps (§105, §265-266) */}153 <GapsModule geo="USA" />154155 {/* Active research */}156 <Section id="active-research" kicker="Clinical research" title="Most active clinical research" description="Cancers with the most interventional trials in an active status (recruiting, not yet recruiting, enrolling by invitation, active not recruiting), counted over the entity and its descendants.">157 {active.length ? (158 <>159 <div className="ci-table-wrap">160 <table className="ci-table">161 <thead>162 <tr>163 <th>Cancer</th>164 <th className="num">Active trials (count)</th>165 <th className="num">Recruiting (count)</th>166 </tr>167 </thead>168 <tbody>169 {active.map((r) => (170 <tr key={r.slug}>171 <td>172 <Link className="ci-link" href={`/cancer/${r.slug}/trials`}>173 {r.canonical_name}174 </Link>175 </td>176 <td className="num">{fmtInt(r.active_trial_count)}</td>177 <td className="num">{fmtInt(r.recruiting_trial_count)}</td>178 </tr>179 ))}180 </tbody>181 </table>182 </div>183 <Freshness dataUpdatedAt={active[0]?.computed_at} extra="source: clinicaltrials · counters refreshed deterministically" />184 </>185 ) : (186 <EmptyState>187 No clinical trials have been ingested yet; trial counters are computed from ClinicalTrials.gov conditions mapped to the taxonomy.188 <div className="mt-1">189 <Link href="/trials" className="ci-link">190 Trials explorer191 </Link>192 </div>193 </EmptyState>194 )}195 </Section>196197 {/* Curated evidence */}198 <Section id="curated" kicker="Molecular knowledge" title="Most curated molecular evidence" description="Accepted CIViC evidence items whose disease maps to the cancer or its descendants.">199 {curated.length ? (200 <>201 <div className="ci-table-wrap">202 <table className="ci-table">203 <thead>204 <tr>205 <th>Cancer</th>206 <th className="num">Evidence items (count)</th>207 <th className="num">Genes (count)</th>208 </tr>209 </thead>210 <tbody>211 {curated.map((r) => (212 <tr key={r.slug}>213 <td>214 <Link className="ci-link" href={`/cancer/${r.slug}/evidence`}>215 {r.canonical_name}216 </Link>217 </td>218 <td className="num">{fmtInt(r.evidence_count)}</td>219 <td className="num">{fmtInt(r.gene_count)}</td>220 </tr>221 ))}222 </tbody>223 </table>224 </div>225 <Freshness dataUpdatedAt={curated[0]?.computed_at} extra="source: civic" />226 </>227 ) : (228 <EmptyState>No curated evidence items have been ingested yet. CIViC evidence will appear here grouped by cancer, with native evidence levels and directions.</EmptyState>229 )}230 </Section>231 </div>232233 <aside className="space-y-10">234 {/* Taxonomy at a glance */}235 <Section236 id="taxonomy"237 kicker="Taxonomy at a glance"238 title="Top-level cancers"239 description={`${topMatched} of ${TOP_LEVEL_CANCERS.length} registry site groups are currently resolved to an indexed entity through their NCIt anchor code.`}240 actions={241 <Link href="/taxonomy" className="ci-link">242 Tree browser →243 </Link>244 }245 >246 <ul className="columns-1 gap-6 text-[13.5px] sm:columns-2">247 {TOP_LEVEL_CANCERS.map((t) => {248 const m = topLevel.get(t.ncit);249 return (250 <li key={t.key} className="flex items-baseline justify-between gap-2 border-b border-rule py-1">251 {m ? (252 <Link href={`/cancer/${m.slug}`} className="ci-link truncate">253 {t.name}254 </Link>255 ) : (256 <span className="truncate text-ink-3" title={`NCIt ${t.ncit} not yet indexed`}>257 {t.name}258 </span>259 )}260 <span className="ci-num shrink-0 text-[11.5px] text-ink-3">{m ? (m.descendant_count != null ? `${fmtInt(m.descendant_count)} desc.` : <span className="ci-mono">{t.ncit}</span>) : 'pending'}</span>261 </li>262 );263 })}264 </ul>265 <p className="mt-2 text-[11.5px] text-ink-3">Site groups follow GLOBOCAN / ICD-10 ranges so burden estimates attach without double counting. "pending" = NCIt concept not yet ingested.</p>266 </Section>267268 {/* Rare spotlight */}269 <Section id="spotlight" kicker="Rare cancer spotlight" title={spotlight ? spotlight.canonicalName : 'Spotlight'} description="A deterministic daily pick among indexed malignant entities, chosen to surface the long tail of the taxonomy rather than the thirty most common cancers.">270 {spotlight ? (271 <div className="text-[13.5px]">272 <div className="flex flex-wrap items-center gap-1.5">273 <Badge>{humanize(spotlight.entityType)}</Badge>274 {spotlight.primaryOncotreeCode ? <Badge mono>OncoTree {spotlight.primaryOncotreeCode}</Badge> : null}275 {spotlight.rareCancer === true ? <Badge tone="accent">Rare</Badge> : null}276 </div>277 {spotlight.parentName ? (278 <p className="mt-2 text-ink-2">279 Classified under{' '}280 <Link className="ci-link" href={`/cancer/${spotlight.parentSlug}`}>281 {spotlight.parentName}282 </Link>283 .284 </p>285 ) : null}286 <p className="mt-1 text-ink-3">{spotlight.rareCancer == null ? 'Rarity status unknown: no incidence observation is attached yet.' : null}</p>287 <Link href={`/cancer/${spotlight.slug}`} className="ci-link mt-2 inline-block">288 Open entity →289 </Link>290 </div>291 ) : (292 <EmptyState compact>No malignant entity indexed yet.</EmptyState>293 )}294 </Section>295296 {/* Sources */}297 <Section298 id="sources"299 kicker="Sources"300 title="Source registry"301 actions={302 <Link href="/sources" className="ci-link">303 Coverage matrix →304 </Link>305 }306 >307 {sources.length ? (308 <ul className="divide-y divide-rule text-[13.5px]">309 {sources.map((s) => (310 <li key={s.slug} className="flex flex-wrap items-center justify-between gap-2 py-1.5">311 <Link className="ci-link" href={`/source/${s.slug}`}>312 {s.name}313 </Link>314 <span className="flex items-center gap-1.5">315 <StatusBadge status={s.status} />316 <Badge tone={s.license_status === 'approved' ? 'ok' : s.license_status === 'blocked' ? 'danger' : 'warn'} title="License review status">317 license: {s.license_status}318 </Badge>319 </span>320 </li>321 ))}322 </ul>323 ) : (324 <EmptyState compact>No source registered.</EmptyState>325 )}326 <Freshness dataUpdatedAt={sources.reduce<Date | null>((m, s) => (s.last_run_finished_at && (!m || s.last_run_finished_at > m) ? s.last_run_finished_at : m), null)} extra="License status is reviewed before any connector goes live" />327 </Section>328 </aside>329 </div>330 </div>331 );332}333