SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
6.6 KB · 121 lines tsx
Raw Blame History
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