spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { ClaimBadge } from '@/components/ui/badge';6import type { MapCountryDatum } from '@/components/charts/world-map';7import { countryCounts } from '@/lib/queries/trial-sites';8import { fmtInt } from '@/lib/format';910/**11 * Home module: compact choropleth of RECRUITING trial sites per country, all cancers, any phase12 * (precomputed by `pnpm cix intel`). Links to the full map with filters and its data table. The13 * country list under the map keeps the module readable without colour or hover.14 */15export async function TrialMapModule({ topN = 8 }: { topN?: number }) {16 const rows = await countryCounts({ cancerId: null, phase: null, recruitingOnly: true });17 const totalSites = rows.reduce((s, r) => s + r.sites, 0);18 const data: MapCountryDatum[] = rows.map((r) => ({ country: r.country, iso3: r.iso3, sites: r.sites, trials: r.trials, href: `/trials?country=${encodeURIComponent(r.country)}&status=RECRUITING` }));19 return (20 <Section21 id="trial-map"22 kicker="Where trials recruit"23 title="Recruiting trial sites by country"24 description="Study locations with status RECRUITING (or in a recruiting study when the site has no status), all cancers, any phase. A site is one registrant-entered location; multi-site studies weigh by their number of sites."25 actions={26 <Link href="/trials/map?recruiting=1" className="ci-link">27 Full choropleth map and filters →28 </Link>29 }30 >31 {rows.length === 0 ? (32 <EmptyState title="Trial map not yet computed" compact>33 Country aggregates appear after <code className="ci-mono">pnpm cix intel</code> has run on ingested ClinicalTrials.gov locations.34 </EmptyState>35 ) : (36 <div>37 <div className="min-w-0">38 <div className="ci-table-wrap">39 <table className="ci-table" id="trial-map-top">40 <thead>41 <tr>42 <th className="num">#</th>43 <th>Country</th>44 <th className="num">Recruiting sites</th>45 <th className="num">Trials</th>46 </tr>47 </thead>48 <tbody>49 {rows.slice(0, topN).map((r, i) => (50 <tr key={r.country}>51 <td className="num">{i + 1}</td>52 <td>53 <Link href={data[i]!.href} className="ci-link">54 {r.country}55 </Link>56 </td>57 <td className="num">{fmtInt(r.sites)}</td>58 <td className="num">{fmtInt(r.trials)}</td>59 </tr>60 ))}61 </tbody>62 </table>63 </div>64 <p className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">65 <ClaimBadge kind="computed" />66 <span>67 {fmtInt(totalSites)} recruiting sites · {fmtInt(rows.length)} countries · <span className="ci-mono">{rows[0]?.formula_version}</span> · source: clinicaltrials68 </span>69 </p>70 <Freshness dataUpdatedAt={rows[0]?.computed_at ?? null} extra="registrant-entered locations" />71 </div>72 </div>73 )}74 </Section>75 );76}77