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, Section, Note } from '@/components/ui/section';4import { EmptyState } from '@/components/ui/empty-state';5import { Freshness } from '@/components/ui/freshness';6import { Badge } from '@/components/ui/badge';7import { SourceBadge } from '@/components/ui/source-badge';8import { listGeographiesWithObservations, topCancersFor, allSitesObservations, WHO_REGION_LABEL } from '@/lib/queries/geography';9import { listSources } from '@/lib/queries/sources';10import { fmtInt, fmtValue, humanize, toDate, unitLabel } from '@/lib/format';1112export const metadata: Metadata = {13 title: 'Countries',14 description: 'Geographies with cancer epidemiology observations in CancerIndex: years covered, sources, and the latest-year figures per country.',15 alternates: { canonical: '/countries' },16};17export const revalidate = 3600;1819/**20 * /countries (§47): every geography that carries at least one epidemiology observation, with its year span,21 * the sources behind it and either the all-sites totals (when the source publishes one) or the top cancer by22 * deaths / new cases for the latest year. Geographies without data are not listed — nothing is invented.23 */24export default async function CountriesPage() {25 const [geos, sources] = await Promise.all([listGeographiesWithObservations(), listSources()]);26 const epiSources = sources.filter((s) => ['cdc-uscs', 'cdc-wonder', 'seer', 'seer-explorer', 'iarc-globocan'].includes(s.slug));2728 const details = await Promise.all(29 geos.map(async (g) => {30 const [allSites, deaths, cases] = await Promise.all([allSitesObservations(g.id, g.max_year, 'all'), topCancersFor(g, 'mortality_count', g.max_year, 'all', 1), topCancersFor(g, 'incidence_count', g.max_year, 'all', 1)]);31 return { g, allSites, deaths, cases };32 }),33 );34 const freshest = geos.map((g) => toDate(g.last_updated)).filter((d): d is Date => !!d).sort((a, b) => b.getTime() - a.getTime())[0] ?? null;3536 return (37 <div>38 <PageHeader kicker="Geographies" title="Countries with epidemiology data" lede="Population statistics are attached per geography, year and sex, exactly as published by a registry or statistical agency. This page lists only geographies for which at least one observation has been ingested.">39 <p className="mt-3 flex flex-wrap items-center gap-1.5 text-[12.5px] text-ink-3">40 Epidemiology sources:41 {epiSources.map((s) => (42 <span key={s.slug} className="inline-flex items-center gap-1">43 <SourceBadge p={{ sourceSlug: s.slug, sourceName: s.name, license: s.license, layer: 'raw' }} compact />44 <Badge tone={s.status === 'active' ? 'ok' : s.status === 'review' || s.status === 'awaiting_credentials' ? 'warn' : 'neutral'}>{humanize(s.status)}</Badge>45 </span>46 ))}47 </p>48 </PageHeader>4950 <Section id="list" kicker="Coverage" title={`${fmtInt(geos.length)} ${geos.length === 1 ? 'geography' : 'geographies'} with observations`} description="Years covered, distinct cancer entities with at least one observation, and the latest-year figures. Totals across all cancer sites are shown only when the source itself publishes an all-sites row; sites are never summed by CancerIndex.">51 {geos.length === 0 ? (52 <EmptyState title="No geography has observations yet">53 Country pages appear as soon as a licensed registry connector ingests observations. IARC / GLOBOCAN stays under license review and SEER awaits credentials, so global coverage is not available yet.54 </EmptyState>55 ) : (56 <>57 <div className="ci-table-wrap">58 <table className="ci-table">59 <thead>60 <tr>61 <th>Geography</th>62 <th>ISO3</th>63 <th>WHO region</th>64 <th className="num">Years</th>65 <th className="num">Cancers</th>66 <th className="num">Observations</th>67 <th>Sources</th>68 <th>Latest year</th>69 <th>Highest annual deaths</th>70 <th>Highest annual new cases</th>71 </tr>72 </thead>73 <tbody>74 {details.map(({ g, allSites, deaths, cases }) => {75 const total = (metric: string) => allSites.find((a) => a.metric === metric);76 const td = total('mortality_count');77 const tc = total('incidence_count');78 const d0 = deaths.rows[0];79 const c0 = cases.rows[0];80 return (81 <tr key={g.id}>82 <td>83 <Link className="ci-link font-medium" href={`/country/${g.slug}`}>84 {g.name}85 </Link>86 <span className="ml-1.5 text-[11.5px] text-ink-3">{humanize(g.kind)}</span>87 </td>88 <td className="ci-mono">{g.iso3 ?? '—'}</td>89 <td className="text-[12.5px] text-ink-2">{g.who_region ? WHO_REGION_LABEL[g.who_region] ?? g.who_region : '—'}</td>90 <td className="num">91 {g.min_year}–{g.max_year}92 </td>93 <td className="num">{fmtInt(g.n_cancers)}</td>94 <td className="num">{fmtInt(g.n_obs)}</td>95 <td>96 <span className="inline-flex flex-wrap gap-1">97 {(g.sources ?? []).map((s) => (98 <SourceBadge key={s.slug} p={{ sourceSlug: s.slug, sourceName: s.name }} compact />99 ))}100 </span>101 </td>102 <td className="ci-num">{g.max_year}</td>103 <td className="text-[12.5px]">104 {td ? (105 <span>106 All sites: <span className="ci-num font-medium">{fmtValue(td.value, td.unit)}</span> {unitLabel(td.unit)} ({td.year})107 </span>108 ) : d0 ? (109 <span>110 <Link className="ci-link" href={`/cancer/${d0.slug}`}>111 {d0.canonical_name}112 </Link>{' '}113 <span className="ci-num font-medium">{fmtValue(d0.value, d0.unit)}</span> <span className="text-ink-3">({deaths.year})</span>114 </span>115 ) : (116 <span className="text-ink-4">—</span>117 )}118 </td>119 <td className="text-[12.5px]">120 {tc ? (121 <span>122 All sites: <span className="ci-num font-medium">{fmtValue(tc.value, tc.unit)}</span> {unitLabel(tc.unit)} ({tc.year})123 </span>124 ) : c0 ? (125 <span>126 <Link className="ci-link" href={`/cancer/${c0.slug}`}>127 {c0.canonical_name}128 </Link>{' '}129 <span className="ci-num font-medium">{fmtValue(c0.value, c0.unit)}</span> <span className="text-ink-3">({cases.year})</span>130 </span>131 ) : (132 <span className="text-ink-4">—</span>133 )}134 </td>135 </tr>136 );137 })}138 </tbody>139 </table>140 </div>141 <p className="mt-2 text-[12px] text-ink-3">"Highest annual deaths / new cases" is the top single site group for the latest year of that metric, both sexes, all ages — not a national total. Where a source publishes an all-sites aggregate it is shown instead and labelled "All sites".</p>142 <Freshness dataUpdatedAt={freshest} extra={`${fmtInt(geos.reduce((n, g) => n + g.n_obs, 0))} observations in total`} />143 </>144 )}145 </Section>146147 <Section id="status" kicker="Coverage status" title="Why only these geographies?" level={3}>148 <Note>149 CancerIndex publishes population statistics only from sources whose license has been reviewed (CLAUDE.md §10.4). U.S. Cancer Statistics (CDC, NPCR/SEER incidence and NVSS mortality) is approved and ingested. The IARC Global Cancer Observatory (GLOBOCAN), which would provide estimates for 185 countries, is under license review; the SEER API awaits credentials. Until then no country outside the United States has observations, and no global ranking of countries is shown. Estimates are never substituted for registry counts.150 </Note>151 <p className="mt-2 text-[13px]">152 <Link className="ci-link" href="/sources">153 Source registry and license status →154 </Link>{' '}155 ·{' '}156 <Link className="ci-link" href="/methodology#country-scopes">157 Methodology: country scopes →158 </Link>159 </p>160 </Section>161 </div>162 );163}164