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%
4.0 KB · 73 lines tsx
Raw Blame History
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