spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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 { 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';17import { TrialIntelModule } from '@/components/home/trial-intel-module';18import { TrialMapModule } from '@/components/home/trial-map-module';19import { GraphModule } from '@/components/home/graph-module';20import { ExplorerModule } from '@/components/home/explorer-module';21import { ApprovalsModule } from '@/components/home/approvals-module';2223export const revalidate = 900;2425export default async function HomePage() {26 const [counts, spotlight, preview, curated, sources] = await Promise.all([27 getSiteCounts(),28 getRareSpotlight(),29 previewSnapshot(['mortality_count', 'incidence_count', 'as_mortality_rate', 'active_trials', 'publications_5y', 'curated_evidence_items']),30 mostCuratedEvidence(8),31 listSources(),32 ]);33 const topLevel = await resolveTopLevel(TOP_LEVEL_CANCERS.map((t) => t.ncit));34 const topMatched = TOP_LEVEL_CANCERS.filter((t) => topLevel.has(t.ncit)).length;3536 const ticker: Array<{ label: string; value: number; note: string; href: string }> = [37 { label: 'Cancers indexed', value: counts.cancers, note: 'active malignant disease entities (NCIt)', href: '/cancers?malignant=1' },38 { label: 'Active oncology trials', value: counts.activeTrials, note: 'interventional, recruiting or active (ClinicalTrials.gov)', href: '/trials?status=active' },39 { label: 'Recruiting Phase III', value: counts.recruitingPhase3, note: 'interventional Phase III studies recruiting now', href: '/trials?status=RECRUITING&phase=PHASE3' },40 { label: 'Approved oncology drugs', value: counts.approvedDrugs, note: `drugs with ≥ 1 approval record (${fmtInt(counts.approvals)} records, all authorities)`, href: '/approvals' },41 { label: 'Genes', value: counts.genes, note: 'HGNC gene records', href: '/genes' },42 { label: 'Publications', value: counts.publications, note: 'PubMed records linked', href: '/search' },43 { label: 'Sources', value: counts.sources, note: `${counts.activeSources} active connector${counts.activeSources === 1 ? '' : 's'}`, href: '/sources' },44 ];4546 return (47 <div className="pb-8">48 {/* Hero */}49 <section className="pt-10 sm:pt-16" aria-labelledby="hero-title">50 <p className="ci-kicker mb-3">Global cancer intelligence</p>51 <h1 id="hero-title" className="max-w-4xl text-4xl leading-[1.05] sm:text-6xl">52 Understand cancer <span className="italic text-ink-2">through data.</span>53 </h1>54 <p className="mt-4 max-w-2xl text-[16px] leading-relaxed text-ink-2">55 CancerIndex connects epidemiology, survival, genomics, treatments, clinical trials, research and regulatory decisions across every recognized cancer entity — with a source, a date and a formula version on every number.56 </p>57 <div className="mt-6 max-w-2xl">58 <CommandPalette variant="hero" />59 <p className="mt-2 text-[12.5px] text-ink-3">60 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 · or open the <Link className="ci-link" href="/explore">Data explorer</Link>, the <Link className="ci-link" href="/trials/intelligence">trial intelligence</Link> table, the <Link className="ci-link" href="/graph">knowledge graph</Link> or <Link className="ci-link" href="/pulse">what changed this month</Link>.61 </p>62 </div>63 </section>6465 {/* Ticker */}66 <section aria-label="Index counts" className="ci-rule mt-10 pt-4">67 <ul className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4 lg:grid-cols-7">68 {ticker.map((t) => (69 <li key={t.label}>70 <Link href={t.href} className="block no-underline">71 <span className="ci-num block font-display text-2xl text-ink sm:text-3xl">{fmtInt(t.value)}</span>72 <span className="block text-[12.5px] font-medium text-ink-2">{t.label}</span>73 <span className="block text-[11.5px] text-ink-3">{t.note}</span>74 </Link>75 </li>76 ))}77 </ul>78 <Freshness dataUpdatedAt={counts.lastIngestAt} extra="Counts are live database counts, not estimates" />79 </section>8081 <div className="mt-10 grid gap-10 lg:grid-cols-[1.35fr_1fr]">82 <div className="space-y-10">83 {/* Cancer burden (US, latest year) — §105 "Cancer burden today" */}84 <BurdenModule slug="united-states" />8586 {/* Data explorer default chart (SPEC §110) */}87 <ExplorerModule />8889 {/* Clinical trial intelligence (SPEC §10) */}90 <TrialIntelModule limit={8} />9192 {/* New oncology approvals (SPEC §14) */}93 <ApprovalsModule limit={8} />9495 {/* Rankings preview */}96 <Section97 id="rankings"98 kicker={preview && parseScopeKey(preview.snapshot.scope_key).geo !== 'WORLD' ? `Rankings · ${parseScopeKey(preview.snapshot.scope_key).geo}` : 'Global rankings'}99 title={preview ? preview.metric.name : 'Global rankings'}100 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}101 actions={102 <Link href="/rankings" className="ci-link">103 All rankings →104 </Link>105 }106 >107 {preview ? (108 <>109 {!preview.metric.slug.match(/incidence|mortality|survival/) ? (110 <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>111 ) : null}112 <div className="ci-table-wrap">113 <table className="ci-table">114 <thead>115 <tr>116 <th className="num">#</th>117 <th>Cancer</th>118 <th className="num">119 {preview.metric.name} ({unitLabel(preview.metric.unit)})120 </th>121 <th>Confidence</th>122 </tr>123 </thead>124 <tbody>125 {preview.rows.map((r) => (126 <tr key={r.id}>127 <td className="num">{r.rank}</td>128 <td>129 <Link className="ci-link" href={`/cancer/${r.slug}`}>130 {r.canonical_name}131 </Link>132 </td>133 <td className="num">{fmtValue(r.value, r.unit)}</td>134 <td>135 <ConfidenceBadge level={r.confidence} />136 </td>137 </tr>138 ))}139 </tbody>140 </table>141 </div>142 <div className="mt-2 flex items-center gap-2">143 <ClaimBadge kind="computed" />144 <Link href={`/rankings/${preview.metric.slug}?scope=${encodeURIComponent(preview.snapshot.scope_key)}`} className="ci-link text-[13px]">145 Full table, methodology and "Why this rank?" →146 </Link>147 </div>148 <Freshness dataUpdatedAt={preview.snapshot.generated_at} extra={`sources: ${preview.snapshot.source_ids.join(', ') || 'see methodology'}`} />149 </>150 ) : (151 <EmptyState title="Rankings not yet computed">152 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.153 <div className="mt-1">154 <Link href="/methodology" className="ci-link">155 Read how rankings are computed156 </Link>157 </div>158 </EmptyState>159 )}160 </Section>161162 {/* Fastest-rising incidence (derived on the fly, §105/§328) */}163 <RisingModule slug="united-states" />164165 {/* Largest trial / research gaps (§105, §265-266) */}166 <GapsModule geo="USA" />167168 {/* Curated evidence */}169 <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.">170 {curated.length ? (171 <>172 <div className="ci-table-wrap">173 <table className="ci-table">174 <thead>175 <tr>176 <th>Cancer</th>177 <th className="num">Evidence items (count)</th>178 <th className="num">Genes (count)</th>179 </tr>180 </thead>181 <tbody>182 {curated.map((r) => (183 <tr key={r.slug}>184 <td>185 <Link className="ci-link" href={`/cancer/${r.slug}/evidence`}>186 {r.canonical_name}187 </Link>188 </td>189 <td className="num">{fmtInt(r.evidence_count)}</td>190 <td className="num">{fmtInt(r.gene_count)}</td>191 </tr>192 ))}193 </tbody>194 </table>195 </div>196 <Freshness dataUpdatedAt={curated[0]?.computed_at} extra="source: civic" />197 </>198 ) : (199 <EmptyState>No curated evidence items have been ingested yet. CIViC evidence will appear here grouped by cancer, with native evidence levels and directions.</EmptyState>200 )}201 </Section>202 </div>203204 <aside className="space-y-10">205 {/* Where trials recruit (SPEC §11) */}206 <TrialMapModule topN={8} />207208 {/* Knowledge graph teaser (SPEC §18) */}209 <GraphModule limit={3} />210211 {/* Taxonomy at a glance */}212 <Section213 id="taxonomy"214 kicker="Taxonomy at a glance"215 title="Top-level cancers"216 description={`${topMatched} of ${TOP_LEVEL_CANCERS.length} registry site groups are currently resolved to an indexed entity through their NCIt anchor code.`}217 actions={218 <Link href="/taxonomy" className="ci-link">219 Tree browser →220 </Link>221 }222 >223 <ul className="columns-1 gap-6 text-[13.5px] sm:columns-2">224 {TOP_LEVEL_CANCERS.map((t) => {225 const m = topLevel.get(t.ncit);226 return (227 <li key={t.key} className="flex items-baseline justify-between gap-2 border-b border-rule py-1">228 {m ? (229 <Link href={`/cancer/${m.slug}`} className="ci-link truncate">230 {t.name}231 </Link>232 ) : (233 <span className="truncate text-ink-3" title={`NCIt ${t.ncit} not yet indexed`}>234 {t.name}235 </span>236 )}237 <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>238 </li>239 );240 })}241 </ul>242 <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>243 </Section>244245 {/* Rare spotlight */}246 <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.">247 {spotlight ? (248 <div className="text-[13.5px]">249 <div className="flex flex-wrap items-center gap-1.5">250 <Badge>{humanize(spotlight.entityType)}</Badge>251 {spotlight.primaryOncotreeCode ? <Badge mono>OncoTree {spotlight.primaryOncotreeCode}</Badge> : null}252 {spotlight.rareCancer === true ? <Badge tone="accent">Rare</Badge> : null}253 </div>254 {spotlight.parentName ? (255 <p className="mt-2 text-ink-2">256 Classified under{' '}257 <Link className="ci-link" href={`/cancer/${spotlight.parentSlug}`}>258 {spotlight.parentName}259 </Link>260 .261 </p>262 ) : null}263 <p className="mt-1 text-ink-3">{spotlight.rareCancer == null ? 'Rarity status unknown: no incidence observation is attached yet.' : null}</p>264 <Link href={`/cancer/${spotlight.slug}`} className="ci-link mt-2 inline-block">265 Open entity →266 </Link>267 </div>268 ) : (269 <EmptyState compact>No malignant entity indexed yet.</EmptyState>270 )}271 </Section>272273 {/* Sources */}274 <Section275 id="sources"276 kicker="Sources"277 title="Source registry"278 actions={279 <Link href="/sources" className="ci-link">280 Coverage matrix →281 </Link>282 }283 >284 {sources.length ? (285 <ul className="divide-y divide-rule text-[13.5px]">286 {sources.map((s) => (287 <li key={s.slug} className="flex flex-wrap items-center justify-between gap-2 py-1.5">288 <Link className="ci-link" href={`/source/${s.slug}`}>289 {s.name}290 </Link>291 <span className="flex items-center gap-1.5">292 <StatusBadge status={s.status} />293 <Badge tone={s.license_status === 'approved' ? 'ok' : s.license_status === 'blocked' ? 'danger' : 'warn'} title="License review status">294 license: {s.license_status}295 </Badge>296 </span>297 </li>298 ))}299 </ul>300 ) : (301 <EmptyState compact>No source registered.</EmptyState>302 )}303 <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" />304 </Section>305 </aside>306 </div>307 </div>308 );309}310