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, ClaimBadge } from '@/components/ui/badge';5import { EmptyState } from '@/components/ui/empty-state';6import { Freshness } from '@/components/ui/freshness';7import { Pager } from '@/components/ui/pager';8import { SourceBadge } from '@/components/ui/source-badge';9import { PIPELINE_FUNNEL, PIPELINE_PAGE_SIZE, PIPELINE_STAGES, countPipelineRows, listPipelineRows, pipelineSourceSlugs, pipelineSummary, pipelineTopDrugsPerStage, topLevelCancerOptions, type PipelineRow, type PipelineStage } from '@/lib/queries/approvals';10import { fmtDate, fmtInt, phaseLabel } from '@/lib/format';11import { pageInfo } from '@/lib/pagination';12import { str, int, withParams, type SP } from '@/lib/search-params';1314export const metadata: Metadata = {15 title: 'Drug development pipeline',16 description: 'Development stage of oncology drugs per top-level cancer, derived from registered interventional trials and jurisdiction-aware approvals.',17 alternates: { canonical: '/pipeline' },18};19export const revalidate = 600;2021const STAGE_LABEL: Record<PipelineStage, string> = {22 phase_not_stated: 'Phase not stated',23 phase1: 'Phase 1',24 phase2: 'Phase 2',25 phase3: 'Phase 3',26 phase4: 'Phase 4',27 approved: 'Approved',28 withdrawn: 'Withdrawn',29};3031function StageBadge({ stage }: { stage: PipelineStage }) {32 const tone = stage === 'approved' ? 'ok' : stage === 'withdrawn' ? 'danger' : stage === 'phase_not_stated' ? 'outline' : 'neutral';33 return <Badge tone={tone}>{STAGE_LABEL[stage] ?? stage}</Badge>;34}3536/**37 * Funnel: one server-rendered SVG bar per stage (phase 1 → approved), width ∝ number of drugs,38 * with the representative drugs (most active trials) listed beside each bar.39 */40function Funnel({ stages, top, hrefFor }: { stages: Array<{ stage: PipelineStage; drugs: number; active_trials: number }>; top: Map<PipelineStage, PipelineRow[]>; hrefFor: (stage: PipelineStage) => string }) {41 const funnel = PIPELINE_FUNNEL.map((s) => stages.find((x) => x.stage === s) ?? { stage: s, drugs: 0, active_trials: 0 });42 const max = Math.max(...funnel.map((s) => s.drugs), 1);43 return (44 <ol className="m-0 list-none space-y-3 p-0">45 {funnel.map((s) => {46 const w = Math.max(s.drugs > 0 ? 1 : 0, Math.round((s.drugs / max) * 100));47 const reps = top.get(s.stage) ?? [];48 return (49 <li key={s.stage} className="grid gap-x-4 gap-y-1 sm:grid-cols-[140px_1fr]">50 <div className="text-[13.5px]">51 <Link className="ci-link font-medium" href={hrefFor(s.stage)}>52 {STAGE_LABEL[s.stage]}53 </Link>54 <p className="m-0 text-[12px] text-ink-3">55 <span className="ci-num">{fmtInt(s.drugs)}</span> drug{s.drugs === 1 ? '' : 's'} · <span className="ci-num">{fmtInt(s.active_trials)}</span> active trials56 </p>57 </div>58 <div className="min-w-0">59 <svg viewBox="0 0 100 10" preserveAspectRatio="none" width="100%" height="14" role="img" aria-label={`${STAGE_LABEL[s.stage]}: ${s.drugs} drugs`} className="block">60 <rect x="0" y="0" width="100" height="10" fill="var(--color-paper-2)" />61 <rect x="0" y="0" width={w} height="10" fill={s.stage === 'approved' ? 'var(--color-ink-2)' : 'var(--color-accent)'} />62 </svg>63 <p className="m-0 mt-1 text-[12.5px] text-ink-2">64 {reps.length ? (65 reps.map((r, i) => (66 <span key={r.id}>67 {i > 0 ? ', ' : ''}68 <Link className="ci-link" href={`/drug/${r.drug_slug}`} title={`${fmtInt(r.active_trials)} active trials`}>69 {r.drug_name}70 </Link>71 </span>72 ))73 ) : (74 <span className="text-ink-4">no drug at this stage</span>75 )}76 </p>77 </div>78 </li>79 );80 })}81 </ol>82 );83}8485export default async function PipelinePage({ searchParams }: { searchParams: Promise<SP> }) {86 const sp = await searchParams;87 const cancerSlug = str(sp, 'cancer');88 const stage = (PIPELINE_STAGES as readonly string[]).includes(str(sp, 'stage')) ? (str(sp, 'stage') as PipelineStage) : '';89 const requestedPage = int(sp, 'page', 1, 1, 100_000);90 const options = await topLevelCancerOptions();91 const cancer = options.find((c) => c.slug === cancerSlug) ?? null;92 const scopeId = cancer?.id ?? null;93 const [summary, top, total, sources] = await Promise.all([pipelineSummary(scopeId), pipelineTopDrugsPerStage(scopeId, 5), countPipelineRows(scopeId, stage), pipelineSourceSlugs()]);94 const info = pageInfo(requestedPage, PIPELINE_PAGE_SIZE, total);95 const rows = total ? await listPipelineRows(scopeId, stage, info.page) : [];96 const current = { cancer: cancer?.slug ?? '', stage, page: info.page > 1 ? info.page : '' };97 const href = (o: Record<string, string | number | null | undefined>) => `/pipeline${withParams(current, o)}`;98 const side = summary.stages.filter((s) => s.stage === 'withdrawn' || s.stage === 'phase_not_stated');99100 return (101 <div>102 <PageHeader kicker="Derived" title={cancer ? `Development pipeline · ${cancer.canonical_name}` : 'Drug development pipeline'} lede="For each drug, the most advanced stage supported by registered interventional trials and jurisdiction-aware approval records. Approval in any ingested jurisdiction outranks trial phase; a drug is counted once, at its highest stage.">103 <p className="mt-2 flex flex-wrap items-center gap-2 text-[12.5px]">104 <ClaimBadge kind="computed" />105 {summary.formulaVersion ? <span className="ci-mono text-ink-3">{summary.formulaVersion}</span> : null}106 <Link className="ci-link" href="/methodology/pipeline">107 Stage rules108 </Link>109 <Link className="ci-link" href="/approvals">110 Approvals feed →111 </Link>112 </p>113 </PageHeader>114115 <form method="get" action="/pipeline" className="flex flex-wrap items-end gap-2 border-y border-rule py-3 text-[13.5px]">116 <label className="flex flex-col gap-1">117 <span className="ci-kicker">Top-level cancer</span>118 <select name="cancer" defaultValue={cancer?.slug ?? ''} className="max-w-[360px] border border-rule-strong bg-white px-2 py-1.5">119 <option value="">All cancers (one row per drug)</option>120 {options.map((c) => (121 <option key={c.id} value={c.slug}>122 {c.canonical_name} ({fmtInt(c.drugs)})123 </option>124 ))}125 </select>126 </label>127 <label className="flex flex-col gap-1">128 <span className="ci-kicker">Stage</span>129 <select name="stage" defaultValue={stage} className="border border-rule-strong bg-white px-2 py-1.5">130 <option value="">Any</option>131 {PIPELINE_STAGES.map((s) => (132 <option key={s} value={s}>133 {STAGE_LABEL[s]}134 </option>135 ))}136 </select>137 </label>138 <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-paper hover:bg-ink-2">139 Apply140 </button>141 {cancer || stage ? (142 <Link className="ci-link text-[12.5px]" href="/pipeline">143 Clear144 </Link>145 ) : null}146 </form>147148 {summary.drugs === 0 ? (149 <div className="mt-4">150 <EmptyState knows={[{ label: 'Approvals feed', href: '/approvals' }, { label: 'Drugs', href: '/drugs' }]}>{cancer ? `No drug is linked to ${cancer.canonical_name} through a registered interventional trial or an approval record yet.` : 'The pipeline has not been computed on this environment yet (pnpm cix intel).'}</EmptyState>151 </div>152 ) : (153 <>154 <Section id="funnel" kicker="Funnel" title={`${fmtInt(summary.drugs)} drugs by highest stage`} description={cancer ? `Drugs reach ${cancer.canonical_name} through trial conditions or approval indications mapped to it or to one of its descendant entities.` : 'Across all cancers: a drug is placed once, at the highest stage it reaches anywhere. Representative drugs are those with the most active trials at that stage.'}>155 <Funnel stages={summary.stages} top={top} hrefFor={(s) => href({ stage: s, page: '' })} />156 {side.some((s) => s.drugs > 0) ? (157 <p className="mt-3 text-[12.5px] text-ink-3">158 Outside the funnel:{' '}159 {side160 .filter((s) => s.drugs > 0)161 .map((s, i) => (162 <span key={s.stage}>163 {i > 0 ? ' · ' : ''}164 <Link className="ci-link" href={href({ stage: s.stage, page: '' })}>165 {STAGE_LABEL[s.stage]}166 </Link>{' '}167 <span className="ci-num">{fmtInt(s.drugs)}</span>168 </span>169 ))}170 . "Withdrawn" = every approval record for the scope is withdrawn or superseded; "phase not stated" = trials exist but none states a phase.171 </p>172 ) : null}173 <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[12px] text-ink-3">174 <ClaimBadge kind="computed" />175 {sources.map((s) => (176 <SourceBadge key={s} p={{ sourceSlug: s, layer: 'derived', note: 'Inputs: trial_interventions × trial_conditions × clinical_trials (interventional) and drug_approvals.' }} />177 ))}178 {summary.formulaVersion ? <span className="ci-mono">{summary.formulaVersion}</span> : null}179 </div>180 <Freshness dataUpdatedAt={summary.computedAt} extra="recomputed with the intelligence layer" />181 </Section>182183 <Section id="drugs" kicker="Drugs" title={`${fmtInt(total)} ${stage ? STAGE_LABEL[stage].toLowerCase() : ''} drug${total === 1 ? '' : 's'}${cancer ? ` · ${cancer.canonical_name}` : ''}`} description="Most advanced stage first, then most active trials. Trial counts are interventional studies linking the drug (and, when a cancer is selected, the cancer).">184 {rows.length === 0 ? (185 <EmptyState compact>No drug at this stage for the selected scope.</EmptyState>186 ) : (187 <>188 <div className="ci-table-wrap">189 <table className="ci-table">190 <thead>191 <tr>192 <th scope="col">Drug</th>193 <th scope="col">Stage</th>194 <th scope="col">Max phase</th>195 <th scope="col" className="num">Active</th>196 <th scope="col" className="num">Recruiting</th>197 <th scope="col" className="num">Phase 3</th>198 <th scope="col" className="num">Total trials</th>199 <th scope="col" className="num">Approvals</th>200 <th scope="col">Jurisdictions</th>201 <th scope="col">First approval</th>202 <th scope="col">First trial</th>203 </tr>204 </thead>205 <tbody>206 {rows.map((r) => (207 <tr key={r.id}>208 <td className="min-w-[160px]">209 <Link className="ci-link font-medium" href={`/drug/${r.drug_slug}#pipeline`}>210 {r.drug_name}211 </Link>212 </td>213 <td>214 <StageBadge stage={r.stage} />215 </td>216 <td className="text-[12.5px] text-ink-2">{r.max_phase ? phaseLabel(r.max_phase) : '—'}</td>217 <td className="num">{fmtInt(r.active_trials)}</td>218 <td className="num">{fmtInt(r.recruiting_trials)}</td>219 <td className="num">{fmtInt(r.phase3_trials)}</td>220 <td className="num">{fmtInt(r.total_trials)}</td>221 <td className="num">{fmtInt(r.approvals)}</td>222 <td className="ci-mono text-[12px]">{r.jurisdictions.length ? r.jurisdictions.join(', ') : '—'}</td>223 <td className="whitespace-nowrap">{fmtDate(r.first_approval_date)}</td>224 <td className="whitespace-nowrap">{r.first_trial_date ? (r.first_trial_date.length >= 10 ? fmtDate(r.first_trial_date) : r.first_trial_date) : '—'}</td>225 </tr>226 ))}227 </tbody>228 </table>229 </div>230 <Pager total={total} pageSize={PIPELINE_PAGE_SIZE} page={info.page} hrefFor={(p) => href({ page: p > 1 ? p : '' })} label="Pipeline pages" noun="drugs" />231 </>232 )}233 <div className="mt-4">234 <Note tone="warn">Stages describe registered activity and ingested regulatory records, not efficacy. Only ingested jurisdictions (currently US and Canada) can place a drug at "approved"; trial phases are as registered by sponsors. Nothing here is a treatment recommendation.</Note>235 </div>236 </Section>237 </>238 )}239 </div>240 );241}242