spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { Badge, ClaimBadge, StatusBadge } from '@/components/ui/badge';4import { SourceBadge, TableProvenance, type ProvenanceInfo } from '@/components/ui/source-badge';5import type { TrialListRow } from '@/lib/queries/trials';6import { fmtDate, fmtInt, phaseLabel } from '@/lib/format';78const CT: ProvenanceInfo = { sourceSlug: 'clinicaltrials', sourceName: 'ClinicalTrials.gov', dataset: 'ClinicalTrials.gov API v2 studies', layer: 'normalized' };910/**11 * Dense trial table. All rows come from one registry, so the provenance popover is rendered once in12 * the caption (with the latest retrieval date of the page) and each row carries a compact badge.13 */14export function TrialTable({ rows, summary }: { rows: TrialListRow[]; summary?: ReactNode }) {15 const latest = rows.reduce<Date | string | null>((m, t) => (m == null || String(t.updated_at) > String(m) ? t.updated_at : m), null);16 return (17 <div>18 <TableProvenance p={{ ...CT, retrievedAt: latest }} claim={<ClaimBadge kind="published" />}>19 {summary ?? `${fmtInt(rows.length)} stud${rows.length === 1 ? 'y' : 'ies'}`} · status and phase as posted by the registrant; each row links to the study record.20 </TableProvenance>21 <div className="ci-table-wrap">22 <table className="ci-table">23 <thead>24 <tr>25 <th scope="col">NCT</th>26 <th scope="col">Title</th>27 <th scope="col">Status</th>28 <th scope="col">Phase</th>29 <th scope="col" className="num">30 Enrollment (n)31 </th>32 <th scope="col">Sponsor</th>33 <th scope="col" className="num">34 Countries35 </th>36 <th scope="col">Last update</th>37 <th scope="col">Source</th>38 </tr>39 </thead>40 <tbody>41 {rows.map((t) => (42 <tr key={t.id}>43 <td>44 <Link href={`/trial/${t.nct_id}`} className="ci-mono ci-link">45 {t.nct_id}46 </Link>47 </td>48 <td className="min-w-[280px] max-w-[520px]">49 <Link href={`/trial/${t.nct_id}`} className="text-ink no-underline hover:text-accent">50 {t.brief_title}51 </Link>52 {t.acronym ? <span className="ml-1 text-[12px] text-ink-3">({t.acronym})</span> : null}53 </td>54 <td>55 <StatusBadge status={t.overall_status} />56 </td>57 <td className="whitespace-nowrap">{t.phases.length ? t.phases.map(phaseLabel).join(' / ') : <span className="text-ink-4">—</span>}</td>58 <td className="num">{t.enrollment_count == null ? '—' : fmtInt(t.enrollment_count)}</td>59 <td className="max-w-[220px] truncate text-[12.5px]" title={t.lead_sponsor ?? ''}>60 {t.lead_sponsor ?? '—'}61 {t.lead_sponsor_class ? (62 <Badge tone="outline" className="ml-1">63 {t.lead_sponsor_class}64 </Badge>65 ) : null}66 </td>67 <td className="num">{t.countries.length}</td>68 <td className="whitespace-nowrap text-[12.5px]">{fmtDate(t.last_update_posted_date)}</td>69 <td>70 <SourceBadge compact title={null} p={CT} />71 </td>72 </tr>73 ))}74 </tbody>75 </table>76 </div>77 </div>78 );79}80