spb/cancerindex
Public
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 { WorldMap, 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 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 className="grid gap-4 lg:grid-cols-[3fr_2fr]">37 <div className="min-w-0">38 <WorldMap data={data} metric="sites" compact ariaLabel={`World map of recruiting clinical trial sites per country, all cancers: ${fmtInt(totalSites)} sites in ${fmtInt(rows.length)} countries. The list beside the map gives the leading countries; the full table is on the trial map page.`} describedBy="trial-map-top" />39 </div>40 <div className="min-w-0">41 <div className="ci-table-wrap">42 <table className="ci-table" id="trial-map-top">43 <thead>44 <tr>45 <th className="num">#</th>46 <th>Country</th>47 <th className="num">Recruiting sites</th>48 <th className="num">Trials</th>49 </tr>50 </thead>51 <tbody>52 {rows.slice(0, topN).map((r, i) => (53 <tr key={r.country}>54 <td className="num">{i + 1}</td>55 <td>56 <Link href={data[i]!.href} className="ci-link">57 {r.country}58 </Link>59 </td>60 <td className="num">{fmtInt(r.sites)}</td>61 <td className="num">{fmtInt(r.trials)}</td>62 </tr>63 ))}64 </tbody>65 </table>66 </div>67 <p className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">68 <ClaimBadge kind="computed" />69 <span>70 {fmtInt(totalSites)} recruiting sites · {fmtInt(rows.length)} countries · <span className="ci-mono">{rows[0]?.formula_version}</span> · source: clinicaltrials71 </span>72 </p>73 <Freshness dataUpdatedAt={rows[0]?.computed_at ?? null} extra="registrant-entered locations" />74 </div>75 </div>76 )}77 </Section>78 );79}80