spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { ExternalLink } from 'lucide-react';3import { Badge } from '@/components/ui/badge';4import { Breadcrumbs } from '@/components/layout/breadcrumbs';5import { Tabs } from '@/components/ui/tabs';6import { GraphLink } from '@/components/graph/graph-link';7import { CODE_SYSTEM_LABEL, codeUrl } from '@/lib/site';8import { humanize, fmtInt } from '@/lib/format';9import { TABS, type CancerBundle, type TabKey } from './load';1011const PRIMARY_SYSTEMS = ['ncit', 'oncotree', 'icd10', 'icd10cm', 'doid', 'umls', 'mesh', 'mondo'];1213export function CancerHeader({ b, tab }: { b: CancerBundle; tab: TabKey }) {14 const { cancer: c, counters, codes, aliases, path } = b;15 const abbreviations = aliases.filter((a) => a.alias_type === 'abbreviation').map((a) => a.alias);16 const shown = codes.filter((k) => PRIMARY_SYSTEMS.includes(k.system)).sort((a, b2) => PRIMARY_SYSTEMS.indexOf(a.system) - PRIMARY_SYSTEMS.indexOf(b2.system));17 const otherCodes = codes.length - shown.length;1819 const badges: Array<{ label: string; tone?: 'neutral' | 'accent' | 'warn' | 'outline'; title: string }> = [];20 if (c.rare_cancer === true) badges.push({ label: 'Rare', tone: 'accent', title: 'Flagged rare from incidence data (rule: age-standardized incidence < 6 per 100,000)' });21 if (c.pediatric_relevant) badges.push({ label: 'Pediatric', title: 'Pediatric-relevant entity (NCIt childhood neoplasm branch)' });22 if (c.hematologic) badges.push({ label: 'Hematologic', title: 'Hematologic malignancy' });23 if (!c.malignant) badges.push({ label: c.entity_type === 'precursor_condition' ? 'Precursor' : 'Non-malignant', tone: 'outline', title: 'Not a malignant neoplasm; kept for taxonomy completeness' });24 if (c.top_level) badges.push({ label: 'Top-level', tone: 'outline', title: 'Member of the mutually exclusive global ranking set' });25 if (counters) {26 if (counters.active_trial_count > 0) badges.push({ label: `${fmtInt(counters.active_trial_count)} active trials`, tone: 'outline', title: 'From ClinicalTrials.gov, over descendants' });27 if (counters.approved_drug_count > 0) badges.push({ label: `${fmtInt(counters.approved_drug_count)} approved drugs`, tone: 'outline', title: 'Drugs with at least one approval in any jurisdiction' });28 if (counters.evidence_count > 0) badges.push({ label: `${fmtInt(counters.evidence_count)} evidence items`, tone: 'outline', title: 'Accepted CIViC evidence items' });29 }3031 const crumbs = [{ label: 'Cancers', href: '/cancers' }, ...path.slice(0, -1).map((p) => ({ label: p.canonical_name, href: `/cancer/${p.slug}` })), { label: c.canonical_name }];3233 return (34 <header className="pt-5">35 <Breadcrumbs items={crumbs} />36 {b.redirectedFrom ? (37 <p className="mt-2 border-l-2 border-warn pl-3 text-[12.5px] text-warn">38 <span className="ci-mono">{b.redirectedFrom.id}</span> ({b.redirectedFrom.canonical_name}) was merged into this entity.39 </p>40 ) : null}41 {c.status === 'deprecated' ? <p className="mt-2 border-l-2 border-danger pl-3 text-[12.5px] text-danger">Deprecated entity{c.deprecated_reason ? `: ${c.deprecated_reason}` : ''}. Kept for identifier stability.</p> : null}42 <div className="mt-3 flex flex-wrap items-start justify-between gap-4">43 <div className="min-w-0">44 <p className="ci-kicker mb-1">{humanize(c.entity_type)}</p>45 <h1 className="text-3xl leading-tight sm:text-[40px]">{c.canonical_name}</h1>46 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-3">47 <span className="ci-mono text-ink-2">{c.id}</span>48 {c.short_name && c.short_name !== c.canonical_name ? <span>{c.short_name}</span> : null}49 {abbreviations.length ? <span>{abbreviations.slice(0, 6).join(' · ')}</span> : null}50 <GraphLink type="cancer" entityRef={c.slug} />51 </p>52 {badges.length ? (53 <div className="mt-2 flex flex-wrap gap-1.5">54 {badges.map((bd) => (55 <Badge key={bd.label} tone={bd.tone ?? 'neutral'} title={bd.title}>56 {bd.label}57 </Badge>58 ))}59 </div>60 ) : null}61 </div>62 <dl className="grid shrink-0 grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[12.5px]">63 {shown.map((k) => {64 const url = codeUrl(k.system, k.code);65 return (66 <div key={`${k.system}-${k.code}`} className="contents">67 <dt className="text-ink-3">{CODE_SYSTEM_LABEL[k.system] ?? k.system}</dt>68 <dd>69 {url ? (70 <a href={url} target="_blank" rel="noopener noreferrer" className="ci-mono inline-flex items-center gap-1 border border-rule px-1.5 py-[1px] text-ink no-underline hover:border-accent hover:text-accent" title={`Open ${k.code} in the ${CODE_SYSTEM_LABEL[k.system] ?? k.system} browser`}>71 {k.code} <ExternalLink className="h-3 w-3" aria-hidden />72 </a>73 ) : (74 <span className="ci-mono border border-rule px-1.5 py-[1px]">{k.code}</span>75 )}76 </dd>77 </div>78 );79 })}80 {otherCodes > 0 ? (81 <div className="contents">82 <dt className="text-ink-3">Other</dt>83 <dd>84 <Link href={`/cancer/${c.slug}/sources`} className="ci-link">85 {otherCodes} more {otherCodes === 1 ? 'code' : 'codes'}86 </Link>87 </dd>88 </div>89 ) : null}90 {shown.length === 0 && otherCodes === 0 ? <dd className="col-span-2 text-ink-4">No cross-reference codes yet</dd> : null}91 </dl>92 </div>93 <div className="mt-4">94 <Tabs95 ariaLabel="Entity sections"96 current={tab}97 tabs={TABS.map((t) => ({98 key: t.key,99 label: t.label,100 href: t.key === 'overview' ? `/cancer/${c.slug}` : `/cancer/${c.slug}/${t.key}`,101 count:102 t.key === 'trials'103 ? counters?.trial_count104 : t.key === 'evidence'105 ? counters?.evidence_count106 : t.key === 'drugs'107 ? counters?.drug_count108 : t.key === 'statistics'109 ? counters?.epidemiology_obs_count110 : t.key === 'survival'111 ? counters?.survival_obs_count112 : t.key === 'genomics'113 ? counters?.cohort_count114 : null,115 }))}116 />117 </div>118 </header>119 );120}121