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 } from '@/components/ui/section';4import { EmptyState } from '@/components/ui/empty-state';5import { Badge } from '@/components/ui/badge';6import { searchEntities } from '@/lib/queries/search';7import { str, type SP } from '@/lib/search-params';8import { fmtInt } from '@/lib/format';910export const metadata: Metadata = { title: 'Search', robots: { index: false } };11export const dynamic = 'force-dynamic';1213const TYPE_LABEL: Record<string, string> = { cancer: 'Cancers', gene: 'Genes', variant: 'Variants', drug: 'Drugs', trial: 'Clinical trials', publication: 'Publications', source: 'Sources' };14const TYPE_ORDER = ['cancer', 'gene', 'variant', 'drug', 'trial', 'publication', 'source'];1516export default async function SearchPage({ searchParams }: { searchParams: Promise<SP> }) {17 const sp = await searchParams;18 const q = str(sp, 'q').slice(0, 200);19 const hits = q.length >= 2 ? await searchEntities(q, 50) : [];20 const groups = new Map<string, typeof hits>();21 for (const h of hits) groups.set(h.type, [...(groups.get(h.type) ?? []), h]);22 const types = [...groups.keys()].sort((a, b) => TYPE_ORDER.indexOf(a) - TYPE_ORDER.indexOf(b));2324 return (25 <div>26 <PageHeader kicker="Search" title={q ? <>Results for <q className="italic">{q}</q></> : 'Search'} lede="Typed results across cancers, genes, variants, drugs, trials, publications and sources. Ordering: exact name, then alias or identifier, then prefix, then fuzzy match." />27 <form method="get" action="/search" className="flex max-w-2xl gap-2">28 <input name="q" defaultValue={q} placeholder="glioblastoma, EGFR, L858R, osimertinib, NCT04487080, 31562796…" className="flex-1 border border-rule-strong bg-white px-3 py-2 text-[15px] outline-none focus:border-accent" aria-label="Search query" autoFocus={!q} />29 <button type="submit" className="border border-ink bg-ink px-4 py-2 text-[14px] text-paper hover:bg-ink-2">30 Search31 </button>32 </form>3334 {q.length > 0 && q.length < 2 ? <p className="mt-4 text-[13.5px] text-ink-3">Type at least two characters.</p> : null}35 {q.length >= 2 && hits.length === 0 ? (36 <div className="mt-6">37 <EmptyState title="No entity matches" knows={[{ label: 'Browse cancers', href: '/cancers' }, { label: 'Taxonomy', href: '/taxonomy' }, { label: 'Genes', href: '/genes' }, { label: 'Drugs', href: '/drugs' }]}>38 No cancer, gene, variant, drug, trial or publication matched <q>{q}</q>. Only indexed entities are searchable; some entity types are not yet ingested on this environment.39 </EmptyState>40 </div>41 ) : null}42 {hits.length ? (43 <div className="mt-6 space-y-8">44 <p className="text-[13px] text-ink-3">45 <span className="ci-num font-medium text-ink">{fmtInt(hits.length)}</span> results in {types.length} {types.length === 1 ? 'type' : 'types'}46 </p>47 {types.map((t) => (48 <section key={t} aria-labelledby={`type-${t}`}>49 <h2 id={`type-${t}`} className="mb-2 text-xl">50 {TYPE_LABEL[t] ?? t} <span className="ci-num text-[13px] text-ink-3">{groups.get(t)!.length}</span>51 </h2>52 <ul className="divide-y divide-rule">53 {groups.get(t)!.map((h) => (54 <li key={`${h.type}-${h.id}`} className="flex flex-wrap items-baseline gap-x-3 py-2 text-[14px]">55 <Link href={h.href} className="ci-link font-medium">56 {h.title}57 </Link>58 {h.subtitle ? <span className="text-[12.5px] text-ink-3">{h.subtitle}</span> : null}59 <span className="ml-auto flex items-center gap-1.5">60 <span className="ci-mono text-[10.5px] text-ink-4">{h.id}</span>61 <Badge tone="outline">{h.match}</Badge>62 </span>63 </li>64 ))}65 </ul>66 </section>67 ))}68 </div>69 ) : null}70 </div>71 );72}73