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.7 KB · 95 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 { Pagination } from '@/components/ui/pagination';7import { listDrugs } from '@/lib/queries/drugs';8import { fmtInt, humanize } from '@/lib/format';9import { str, int, withParams, type SP } from '@/lib/search-params';1011export const metadata: Metadata = { title: 'Drugs', description: 'Oncology drugs with jurisdiction-aware approvals, curated evidence and trials.' };12// Filtered list: rendered per request (searchParams); 600 s is the CDN/ISR hint shared by all list pages.13export const revalidate = 600;14const PAGE_SIZE = 50;1516export default async function DrugsPage({ searchParams }: { searchParams: Promise<SP> }) {17  const sp = await searchParams;18  const q = str(sp, 'q');19  const kind = str(sp, 'kind');20  const page = int(sp, 'page', 1, 1, 100_000);21  const { rows, total, kinds } = await listDrugs({ q, kind, page, pageSize: PAGE_SIZE });22  const href = (o: Record<string, string | number | null | undefined>) => `/drugs${withParams({ q, kind }, o)}`;23  return (24    <div>25      <PageHeader kicker="Drugs" title="Drugs" lede="One entity per molecule (INN preferred); brand names and development codes are aliases. Approvals are always shown with jurisdiction and indication." />26      <form method="get" action="/drugs" className="flex flex-wrap items-end gap-2 border-y border-rule py-3 text-[13.5px]">27        <label className="flex flex-col gap-1">28          <span className="ci-kicker">Name or alias</span>29          <input name="q" defaultValue={q} placeholder="e.g. osimertinib, Keytruda, AZD9291" className="border border-rule-strong bg-white px-2 py-1.5 outline-none focus:border-accent" />30        </label>31        <label className="flex flex-col gap-1">32          <span className="ci-kicker">Kind</span>33          <select name="kind" defaultValue={kind} className="border border-rule-strong bg-white px-2 py-1.5">34            <option value="">Any</option>35            {kinds.map((k) => (36              <option key={k.kind} value={k.kind}>37                {humanize(k.kind)} ({k.n})38              </option>39            ))}40          </select>41        </label>42        <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-paper hover:bg-ink-2">43          Apply44        </button>45      </form>46      <p className="mt-3 text-[13px] text-ink-2">47        <span className="ci-num font-medium text-ink">{fmtInt(total)}</span> drugs48      </p>49      {rows.length === 0 ? (50        <div className="mt-3">51          <EmptyState title={total === 0 && !q && !kind ? 'Drugs not yet available' : 'No drug matches'} knows={[{ label: 'Cancers explorer', href: '/cancers' }, { label: 'Sources', href: '/sources' }]}>52            {total === 0 && !q && !kind ? 'Drug entities are created by the CIViC therapy, NCIt and regulatory connectors; none has run on this environment yet.' : 'Try another name, brand or development code.'}53          </EmptyState>54        </div>55      ) : (56        <>57          <div className="ci-table-wrap mt-3">58            <table className="ci-table">59              <thead>60                <tr>61                  <th>Drug</th>62                  <th>Kind</th>63                  <th>Mechanism</th>64                  <th className="num">Approvals</th>65                  <th className="num">Evidence items</th>66                  <th className="num">Trials</th>67                  <th>Identifiers</th>68                </tr>69              </thead>70              <tbody>71                {rows.map((d) => (72                  <tr key={d.id}>73                    <td className="min-w-[180px]">74                      <Link className="ci-link font-medium" href={`/drug/${d.slug}`}>75                        {d.name}76                      </Link>77                    </td>78                    <td>{d.kind ? <Badge>{humanize(d.kind)}</Badge> : '—'}</td>79                    <td className="max-w-[320px] text-[12.5px] text-ink-2">{d.mechanism ?? '—'}</td>80                    <td className="num">{fmtInt(d.approval_count ?? 0)}</td>81                    <td className="num">{fmtInt(d.evidence_count ?? 0)}</td>82                    <td className="num">{fmtInt(d.trial_count ?? 0)}</td>83                    <td className="ci-mono text-[11px] text-ink-3">{[d.ncit_code, d.chembl_id, d.drugbank_id].filter(Boolean).join(' · ') || '—'}</td>84                  </tr>85                ))}86              </tbody>87            </table>88          </div>89          <Pagination page={page} pageSize={PAGE_SIZE} total={total} hrefFor={(p) => href({ page: p === 1 ? '' : p })} />90        </>91      )}92    </div>93  );94}95