spb/cancerindex
Public
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 { CODE_SYSTEM_LABEL, codeUrl } from '@/lib/site';7import { humanize, fmtInt } from '@/lib/format';8import { TABS, type CancerBundle, type TabKey } from './load';910const PRIMARY_SYSTEMS = ['ncit', 'oncotree', 'icd10', 'icd10cm', 'doid', 'umls', 'mesh', 'mondo'];1112export function CancerHeader({ b, tab }: { b: CancerBundle; tab: TabKey }) {13 const { cancer: c, counters, codes, aliases, path } = b;14 const abbreviations = aliases.filter((a) => a.alias_type === 'abbreviation').map((a) => a.alias);15 const shown = codes.filter((k) => PRIMARY_SYSTEMS.includes(k.system)).sort((a, b2) => PRIMARY_SYSTEMS.indexOf(a.system) - PRIMARY_SYSTEMS.indexOf(b2.system));16 const otherCodes = codes.length - shown.length;1718 const badges: Array<{ label: string; tone?: 'neutral' | 'accent' | 'warn' | 'outline'; title: string }> = [];19 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)' });20 if (c.pediatric_relevant) badges.push({ label: 'Pediatric', title: 'Pediatric-relevant entity (NCIt childhood neoplasm branch)' });21 if (c.hematologic) badges.push({ label: 'Hematologic', title: 'Hematologic malignancy' });22 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' });23 if (c.top_level) badges.push({ label: 'Top-level', tone: 'outline', title: 'Member of the mutually exclusive global ranking set' });24 if (counters) {25 if (counters.active_trial_count > 0) badges.push({ label: `${fmtInt(counters.active_trial_count)} active trials`, tone: 'outline', title: 'From ClinicalTrials.gov, over descendants' });26 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' });27 if (counters.evidence_count > 0) badges.push({ label: `${fmtInt(counters.evidence_count)} evidence items`, tone: 'outline', title: 'Accepted CIViC evidence items' });28 }2930 const crumbs = [{ label: 'Cancers', href: '/cancers' }, ...path.slice(0, -1).map((p) => ({ label: p.canonical_name, href: `/cancer/${p.slug}` })), { label: c.canonical_name }];3132 return (33 <header className="pt-5">34 <Breadcrumbs items={crumbs} />35 {b.redirectedFrom ? (36 <p className="mt-2 border-l-2 border-warn pl-3 text-[12.5px] text-warn">37 <span className="ci-mono">{b.redirectedFrom.id}</span> ({b.redirectedFrom.canonical_name}) was merged into this entity.38 </p>39 ) : null}40 {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}41 <div className="mt-3 flex flex-wrap items-start justify-between gap-4">42 <div className="min-w-0">43 <p className="ci-kicker mb-1">{humanize(c.entity_type)}</p>44 <h1 className="text-3xl leading-tight sm:text-[40px]">{c.canonical_name}</h1>45 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-3">46 <span className="ci-mono text-ink-2">{c.id}</span>47 {c.short_name && c.short_name !== c.canonical_name ? <span>{c.short_name}</span> : null}48 {abbreviations.length ? <span>{abbreviations.slice(0, 6).join(' · ')}</span> : null}49 </p>50 {badges.length ? (51 <div className="mt-2 flex flex-wrap gap-1.5">52 {badges.map((bd) => (53 <Badge key={bd.label} tone={bd.tone ?? 'neutral'} title={bd.title}>54 {bd.label}55 </Badge>56 ))}57 </div>58 ) : null}59 </div>60 <dl className="grid shrink-0 grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[12.5px]">61 {shown.map((k) => {62 const url = codeUrl(k.system, k.code);63 return (64 <div key={`${k.system}-${k.code}`} className="contents">65 <dt className="text-ink-3">{CODE_SYSTEM_LABEL[k.system] ?? k.system}</dt>66 <dd>67 {url ? (68 <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`}>69 {k.code} <ExternalLink className="h-3 w-3" aria-hidden />70 </a>71 ) : (72 <span className="ci-mono border border-rule px-1.5 py-[1px]">{k.code}</span>73 )}74 </dd>75 </div>76 );77 })}78 {otherCodes > 0 ? (79 <div className="contents">80 <dt className="text-ink-3">Other</dt>81 <dd>82 <Link href={`/cancer/${c.slug}/sources`} className="ci-link">83 {otherCodes} more {otherCodes === 1 ? 'code' : 'codes'}84 </Link>85 </dd>86 </div>87 ) : null}88 {shown.length === 0 && otherCodes === 0 ? <dd className="col-span-2 text-ink-4">No cross-reference codes yet</dd> : null}89 </dl>90 </div>91 <div className="mt-4">92 <Tabs93 ariaLabel="Entity sections"94 current={tab}95 tabs={TABS.map((t) => ({96 key: t.key,97 label: t.label,98 href: t.key === 'overview' ? `/cancer/${c.slug}` : `/cancer/${c.slug}/${t.key}`,99 count:100 t.key === 'trials'101 ? counters?.trial_count102 : t.key === 'evidence'103 ? counters?.evidence_count104 : t.key === 'drugs'105 ? counters?.drug_count106 : t.key === 'statistics'107 ? counters?.epidemiology_obs_count108 : t.key === 'survival'109 ? counters?.survival_obs_count110 : t.key === 'genomics'111 ? counters?.cohort_count112 : null,113 }))}114 />115 </div>116 </header>117 );118}119