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 { Pagination } from '@/components/ui/pagination';7import { listGenes } from '@/lib/queries/genomics';8import { fmtInt } from '@/lib/format';9import { str, int, bool, withParams, type SP } from '@/lib/search-params';1011export const metadata: Metadata = { title: 'Genes', description: 'HGNC genes with curated cancer evidence, variants and cohort frequencies.' };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 GenesPage({ searchParams }: { searchParams: Promise<SP> }) {17 const sp = await searchParams;18 const q = str(sp, 'q');19 const cancerOnly = bool(sp, 'cancer') ?? false;20 const page = int(sp, 'page', 1, 1, 100_000);21 const { rows, total } = await listGenes({ q, cancerOnly, page, pageSize: PAGE_SIZE });22 const href = (o: Record<string, string | number | null | undefined>) => `/genes${withParams({ q, cancer: cancerOnly ? '1' : '' }, o)}`;23 return (24 <div>25 <PageHeader kicker="Genes" title="Genes" lede="Gene symbols and names follow HGNC. 'Cancer gene' means the gene has at least one curated cancer edge — a derived flag, not a biological verdict." />26 <form method="get" action="/genes" 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">Symbol, alias or name</span>29 <input name="q" defaultValue={q} placeholder="e.g. KRAS, HER2, tumor protein" className="border border-rule-strong bg-white px-2 py-1.5 outline-none focus:border-accent" />30 </label>31 <label className="inline-flex items-center gap-1 pb-2">32 <input type="checkbox" name="cancer" value="1" defaultChecked={cancerOnly} /> Cancer genes only33 </label>34 <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-paper hover:bg-ink-2">35 Apply36 </button>37 </form>38 <p className="mt-3 text-[13px] text-ink-2">39 <span className="ci-num font-medium text-ink">{fmtInt(total)}</span> genes40 </p>41 {rows.length === 0 ? (42 <div className="mt-3">43 <EmptyState title={total === 0 && !q ? 'Genes not yet available' : 'No gene matches'} knows={[{ label: 'Cancers explorer', href: '/cancers' }, { label: 'Sources', href: '/sources' }]}>44 {total === 0 && !q ? 'The HGNC connector has not run on this environment. Genes appear here once ingested, with CIViC evidence and GDC cohort frequencies attached.' : 'Try a different symbol or alias.'}45 </EmptyState>46 </div>47 ) : (48 <>49 <div className="ci-table-wrap mt-3">50 <table className="ci-table">51 <thead>52 <tr>53 <th>Symbol</th>54 <th>Name</th>55 <th>Location</th>56 <th>Locus type</th>57 <th className="num">Evidence items</th>58 <th className="num">Variants</th>59 <th>Flags</th>60 </tr>61 </thead>62 <tbody>63 {rows.map((g) => (64 <tr key={g.id}>65 <td>66 <Link className="ci-mono ci-link font-medium" href={`/gene/${g.symbol}`}>67 {g.symbol}68 </Link>69 </td>70 <td className="min-w-[220px]">{g.name ?? '—'}</td>71 <td className="ci-mono text-[12px]">{g.location ?? '—'}</td>72 <td className="text-[12.5px]">{g.locus_type ?? '—'}</td>73 <td className="num">{fmtInt(g.evidence_count ?? 0)}</td>74 <td className="num">{fmtInt(g.variant_count ?? 0)}</td>75 <td>{g.is_cancer_gene ? <Badge tone="accent">Cancer gene</Badge> : null}</td>76 </tr>77 ))}78 </tbody>79 </table>80 </div>81 <Pagination page={page} pageSize={PAGE_SIZE} total={total} hrefFor={(p) => href({ page: p === 1 ? '' : p })} />82 </>83 )}84 </div>85 );86}87