SPB Git

spb/cancerindex

Public
16.7 KB · 333 lines tsx
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
Files · 602dc111f3dd7d7dec082e01832909f07dca7bd3
Menu

Theme