spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Section, Note } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { Badge, ClaimBadge, StatusBadge } from '@/components/ui/badge';6import { SourceBadge } from '@/components/ui/source-badge';7import { countryTrialTotals, countrySitesByCancer, countrySitesByPhase, jurisdictionApprovalSummary, latestJurisdictionApprovals } from '@/lib/queries/country-activity';8import { fmtDate, fmtInt, fmtPct, phaseLabel, truncate } from '@/lib/format';910/** ISO 3166-1 alpha-2 codes that are also `drug_approvals.jurisdiction` codes. */11const JURISDICTIONS: Record<string, string> = { US: 'US', CA: 'CA', GB: 'UK', AU: 'AU', JP: 'JP', CH: 'CH' };1213/**14 * Country research & regulatory activity (SPEC §22): trial sites in the country from ClinicalTrials.gov15 * locations (derived, formula ci-trial-sites-v1) and approval records for the country's regulator.16 * Rendered on every country page, including those without epidemiology observations.17 */18export async function CountryActivity({ iso2, iso3, name, kind }: { iso2: string | null; iso3: string | null; name: string; kind: string }) {19 if (kind !== 'country' || !iso3) return null;20 const jurisdiction = iso2 ? JURISDICTIONS[iso2] ?? null : null;21 const [totals, byCancer, byPhase, approvalSummary, latest] = await Promise.all([countryTrialTotals(iso3), countrySitesByCancer(iso3, 25), countrySitesByPhase(iso3), jurisdiction ? jurisdictionApprovalSummary(jurisdiction) : Promise.resolve([]), jurisdiction ? latestJurisdictionApprovals(jurisdiction, 10) : Promise.resolve([])]);2223 return (24 <>25 <Section id="trial-activity" kicker="Clinical research" title={`Clinical trial activity in ${name}`} description="Registrant-entered study locations on ClinicalTrials.gov mapped to this country (any study type, any phase). A site is one location; a multi-site study weighs by its number of sites. Recruiting = location status RECRUITING, or the study status when the site has none." actions={<Link href={`/trials/map?recruiting=1`} className="ci-link">Trial map →</Link>}>26 {totals ? (27 <>28 <dl className="mb-3 grid grid-cols-2 gap-x-6 gap-y-2 text-[13.5px] sm:grid-cols-4">29 {[30 ['Sites', totals.sites],31 ['Studies with a site here', totals.trials],32 ['Recruiting sites', totals.recruiting_sites],33 ['Recruiting studies', totals.recruiting_trials],34 ].map(([k, v]) => (35 <div key={String(k)}>36 <dt className="ci-kicker">{k}</dt>37 <dd className="ci-num text-left font-display text-2xl">{fmtInt(v as number)}</dd>38 </div>39 ))}40 </dl>41 <div className="grid gap-6 lg:grid-cols-[3fr_2fr]">42 <div className="ci-table-wrap">43 <table className="ci-table">44 <caption className="pb-1 text-[12.5px] text-ink-3">Top-level cancers by sites in {name} (descendants included)</caption>45 <thead>46 <tr>47 <th>Cancer</th>48 <th className="num">Sites</th>49 <th className="num">Studies</th>50 <th className="num">Recruiting studies</th>51 <th className="num">Share of sites</th>52 </tr>53 </thead>54 <tbody>55 {byCancer.map((r) => (56 <tr key={r.cancer_id}>57 <td>58 <Link href={`/cancer/${r.slug}/trials`} className="ci-link">59 {r.canonical_name}60 </Link>61 </td>62 <td className="num">{fmtInt(r.sites)}</td>63 <td className="num">{fmtInt(r.trials)}</td>64 <td className="num">{fmtInt(r.recruiting_trials)}</td>65 <td className="num text-ink-3">{fmtPct(r.sites / totals.sites, 1)}</td>66 </tr>67 ))}68 </tbody>69 </table>70 </div>71 <div className="ci-table-wrap">72 <table className="ci-table">73 <caption className="pb-1 text-[12.5px] text-ink-3">By phase (a Phase II/III study counts in both)</caption>74 <thead>75 <tr>76 <th>Phase</th>77 <th className="num">Sites</th>78 <th className="num">Studies</th>79 <th className="num">Recruiting</th>80 </tr>81 </thead>82 <tbody>83 {byPhase.map((p) => (84 <tr key={p.phase}>85 <td>86 <Link href={`/trials/map?phase=${p.phase}`} className="ci-link">87 {phaseLabel(p.phase)}88 </Link>89 </td>90 <td className="num">{fmtInt(p.sites)}</td>91 <td className="num">{fmtInt(p.trials)}</td>92 <td className="num">{fmtInt(p.recruiting_trials)}</td>93 </tr>94 ))}95 </tbody>96 </table>97 </div>98 </div>99 <p className="mt-2 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">100 <ClaimBadge kind="computed" /> <span>formula ci-trial-sites-v1 · source: clinicaltrials · shares are of sites, not of studies</span>101 </p>102 <Freshness dataUpdatedAt={totals.computed_at} extra="trial_site_country_counts" />103 </>104 ) : (105 <EmptyState compact title="No study location mapped to this country">Country aggregates exist for every country named in ClinicalTrials.gov locations; none names {name} yet, or the aggregate has not been computed.</EmptyState>106 )}107 </Section>108109 {jurisdiction ? (110 <Section id="approvals" kicker="Regulatory" title={`Oncology approval records · ${jurisdiction}`} description="Records of the country's regulator as ingested (one record = one decision for one application or product identifier). Absence of a record here is not evidence of absence." actions={<Link href={`/approvals?jurisdiction=${jurisdiction}`} className="ci-link">All records →</Link>}>111 {approvalSummary.length ? (112 <>113 <p className="mb-2 flex flex-wrap gap-1.5 text-[12px]">114 {approvalSummary.map((s) => (115 <Badge key={`${s.authority}-${s.status}`} tone="outline" title={`${s.drugs} distinct drugs · latest dated ${s.latest ?? '—'}`}>116 {s.authority} · {s.status}: {fmtInt(s.n)} records / {fmtInt(s.drugs)} drugs117 </Badge>118 ))}119 </p>120 <ul className="divide-y divide-rule text-[13.5px]">121 {latest.map((a) => (122 <li key={a.id} className="grid gap-x-3 py-1.5 sm:grid-cols-[92px_1fr]">123 <span className="ci-mono text-[12px] text-ink-3">{a.approval_date ? fmtDate(a.approval_date) : 'undated'}</span>124 <span className="min-w-0">125 <Link href={`/drug/${a.drug_slug}`} className="ci-link font-medium">126 {a.drug_name}127 </Link>{' '}128 <StatusBadge status={a.status} />{' '}129 {a.cancer_slug ? (130 <Link href={`/cancer/${a.cancer_slug}`} className="ci-link">131 {a.cancer_name}132 </Link>133 ) : (134 <span className="text-ink-3">cancer not stated in this record</span>135 )}136 <span className="mt-0.5 block text-[12.5px] text-ink-2" title={a.indication}>137 {truncate(a.indication, 140)}138 </span>139 <SourceBadge p={{ sourceSlug: a.source_slug }} compact />140 </span>141 </li>142 ))}143 </ul>144 <p className="mt-2">145 <ClaimBadge kind="regulatory" />146 </p>147 </>148 ) : (149 <EmptyState compact title="No approval record for this jurisdiction yet">Regulatory connectors are live for the FDA (US), Health Canada (CA) and the EMA (EU). Other regulators are not yet ingested.</EmptyState>150 )}151 </Section>152 ) : (153 <Note>Regulatory records are ingested per jurisdiction (US, CA, EU so far); {name} has no connected regulator yet.</Note>154 )}155 </>156 );157}158