spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import type { Metadata } from 'next';2import Link from 'next/link';3import type { ReactNode } from 'react';4import { t, tOpt } from '@/i18n';5import { isNotBuilt } from '@/lib/api';6import { apiExplore } from '@/lib/api-explore';7import { formatDate, grouped } from '@/lib/format';8import { routes } from '@/lib/site';9import type { MethodologyResponse } from '@/lib/types-explore';10import { StatusBadge } from '@/components/data/freshness-badge';11import { PageHeader } from '@/components/explore/page-header';12import { Toc } from '@/components/methodology/toc';1314export const revalidate = 3600;1516export const metadata: Metadata = {17 title: t('method.title'),18 description: t('method.sub'),19 alternates: { canonical: routes.methodology() },20};2122const SECTIONS = ['principles', 'countries', 'indicators', 'sources', 'validation', 'freshness', 'rankings', 'changes', 'similarity', 'insights', 'dna', 'limitations', 'versioning', 'licence'] as const;2324function Prose({ id, title, children }: { id: string; title: string; children: ReactNode }) {25 return (26 <section id={id} className="scroll-mt-28 border-t border-rule py-8 first:border-t-0 md:py-10" aria-labelledby={`${id}-h`}>27 <h2 id={`${id}-h`} className="display text-2xl text-ink md:text-3xl">28 <a href={`#${id}`} className="link-quiet">29 {title}30 </a>31 </h2>32 <div className="mt-3 max-w-prose space-y-3 text-base leading-relaxed text-ink-2 [&_p]:text-pretty">{children}</div>33 </section>34 );35}3637function Kv({ rows }: { rows: Array<[string, ReactNode]> }) {38 return (39 <dl className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-6 gap-y-1 text-sm">40 {rows.map(([k, v]) => (41 <div key={k} className="contents">42 <dt className="text-ink-2">{k}</dt>43 <dd className="tnum text-right text-ink">{v}</dd>44 </div>45 ))}46 </dl>47 );48}4950export default async function MethodologyPage() {51 let m: MethodologyResponse | null = null;52 try {53 m = await apiExplore.methodology();54 } catch (e) {55 if (!isNotBuilt(e)) throw e;56 }57 const toc = SECTIONS.map((id) => ({ id, label: t(`method.${id}` as 'method.principles') }));58 const stale = m?.validation.stale_after_days ?? { A: 800, Q: 200, M: 75 };5960 return (61 <>62 <PageHeader title={t('method.title')} lede={t('method.sub')} meta={m?.meta.built_at ? `${t('site.footer.refreshed', { date: formatDate(m.meta.built_at) })}${m.meta.run_id ? ` · ${t('site.footer.build', { run: m.meta.run_id })}` : ''}` : undefined} />63 <div className="lg:grid lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-12">64 <div className="lg:self-start">65 <Toc items={toc} />66 </div>67 <article className="min-w-0">68 <Prose id="principles" title={t('method.principles')}>69 <ol className="list-decimal space-y-2 pl-5">70 {(m?.principles ?? []).map((p) => (71 <li key={p}>{p}</li>72 ))}73 </ol>74 </Prose>7576 <Prose id="countries" title={t('method.countries')}>77 <p>{t('method.countries.text')}</p>78 <p className="text-sm">79 <Link href={routes.regions()} className="text-accent hover:underline">80 {t('regions.title')} →81 </Link>82 </p>83 </Prose>8485 <Prose id="indicators" title={t('method.indicators')}>86 <p>{t('method.indicators.text', { n: grouped(m?.indicators.n ?? 260) })}</p>87 {m ? (88 <div className="grid gap-6 pt-2 sm:grid-cols-2">89 <div>90 <h3 className="mb-1.5 text-sm font-semibold text-ink">{t('method.indicators.formats')}</h3>91 <Kv rows={m.indicators.formats.map((f) => [f.format, grouped(f.n)] as [string, ReactNode])} />92 </div>93 <div>94 <h3 className="mb-1.5 text-sm font-semibold text-ink">{t('method.indicators.units')}</h3>95 <Kv rows={m.indicators.units.slice(0, 10).map((u) => [u.unit, grouped(u.n)] as [string, ReactNode])} />96 <h3 className="mb-1.5 mt-5 text-sm font-semibold text-ink">{t('method.indicators.frequencies')}</h3>97 <Kv rows={Object.entries(m.indicators.frequencies).map(([k, v]) => [tOpt(`indicator.frequency.${k}`, k), grouped(v)] as [string, ReactNode])} />98 <h3 className="mb-1.5 mt-5 text-sm font-semibold text-ink">{t('method.indicators.aggregations')}</h3>99 <Kv rows={Object.entries(m.indicators.aggregations).map(([k, v]) => [k, grouped(v)] as [string, ReactNode])} />100 </div>101 </div>102 ) : null}103 <p className="text-sm">104 <Link href={routes.indicators()} className="text-accent hover:underline">105 {t('indicators.title')} →106 </Link>107 </p>108 </Prose>109110 <Prose id="sources" title={t('method.sources')}>111 <p>{m?.sources.priority_rule}</p>112 <p>{t('method.sources.text2')}</p>113 {m ? (114 <div className="grid gap-6 pt-2 sm:grid-cols-2">115 <div>116 <h3 className="mb-1.5 text-sm font-semibold text-ink">{t('method.sources.mapped')}</h3>117 <Kv118 rows={Object.entries(m.sources.series_mapped_per_connector)119 .sort((a, b) => b[1] - a[1])120 .map(([k, v]) => [k, grouped(v)] as [string, ReactNode])}121 />122 </div>123 <div>124 <h3 className="mb-1.5 text-sm font-semibold text-ink">{t('method.sources.urls')}</h3>125 <ul className="space-y-1 text-xs">126 {Object.entries(m.sources.urls).map(([k, v]) => (127 <li key={k} className="grid grid-cols-[5rem_minmax(0,1fr)] gap-2">128 <span className="text-ink-2">{k}</span>129 <code className="break-all font-mono text-ink">{v}</code>130 </li>131 ))}132 </ul>133 </div>134 </div>135 ) : null}136 <p className="text-sm">137 <Link href={routes.sources()} className="text-accent hover:underline">138 {t('sources.title')} →139 </Link>140 </p>141 </Prose>142143 <Prose id="validation" title={t('method.validation')}>144 <p>{t('method.validation.text')}</p>145 {m ? (146 <table className="w-full border-collapse text-sm">147 <thead>148 <tr className="border-b border-rule text-left text-xs text-ink-3">149 <th scope="col" className="py-1.5 pr-3 font-medium">{t('method.rule.code')}</th>150 <th scope="col" className="py-1.5 pr-3 font-medium">{t('method.rule.severity')}</th>151 <th scope="col" className="py-1.5 font-medium">{t('method.rule.effect')}</th>152 </tr>153 </thead>154 <tbody className="divide-y divide-rule">155 {m.validation.rules.map((r) => (156 <tr key={r.code}>157 <td className="py-2 pr-3 align-top font-mono text-xs text-ink">{r.code}</td>158 <td className="py-2 pr-3 align-top text-xs text-ink-2">{r.severity}</td>159 <td className="py-2 align-top text-ink-2">{r.text}</td>160 </tr>161 ))}162 </tbody>163 </table>164 ) : null}165 <h3 className="pt-2 text-sm font-semibold text-ink">{t('method.validation.statuses')}</h3>166 <ul className="space-y-2">167 {(['verified', 'imported', 'warning', 'stale', 'quarantined'] as const).map((s) => (168 <li key={s} className="flex items-start gap-3">169 <span className="mt-0.5 shrink-0">170 <StatusBadge status={s} />171 </span>172 <span className="text-sm">{t(`method.validation.status.${s}` as 'method.validation.status.verified')}</span>173 </li>174 ))}175 </ul>176 <h3 className="pt-2 text-sm font-semibold text-ink">{t('method.validation.stale')}</h3>177 <p className="text-sm">{t('method.validation.staleText', { a: stale.A ?? 800, q: stale.Q ?? 200, m: stale.M ?? 75 })}</p>178 </Prose>179180 <Prose id="freshness" title={t('method.freshness')}>181 <p>{t('method.freshness.text')}</p>182 {m ? (183 <>184 <h3 className="pt-2 text-sm font-semibold text-ink">{t('method.periods')}</h3>185 <Kv rows={Object.entries(m.periods).map(([k, v]) => [k, <code key={k} className="font-mono text-xs">{v}</code>] as [string, ReactNode])} />186 </>187 ) : null}188 </Prose>189190 <Prose id="rankings" title={t('method.rankings')}>191 <p>{t('method.rankings.text')}</p>192 {m ? <p className="text-sm text-ink-3">{m.derived.rankings}</p> : null}193 <p className="text-sm">194 <Link href={routes.rankings()} className="text-accent hover:underline">195 {t('nav.rankings')} →196 </Link>197 </p>198 </Prose>199200 <Prose id="changes" title={t('method.changes')}>201 <p>{t('method.changes.text')}</p>202 {m ? <p className="text-sm text-ink-3">{m.derived.changes}</p> : null}203 <p className="text-sm">204 <Link href={routes.changes()} className="text-accent hover:underline">205 {t('changes.title')} →206 </Link>207 </p>208 </Prose>209210 <Prose id="similarity" title={t('method.similarity')}>211 <p>{t('method.similarity.text')}</p>212 {m ? (213 <>214 <h3 className="pt-2 text-sm font-semibold text-ink">{t('method.similarity.modes')}</h3>215 <dl className="space-y-1.5 text-sm">216 {Object.entries(m.derived.similarity.modes).map(([k, v]) => (217 <div key={k} className="grid grid-cols-[7rem_minmax(0,1fr)] gap-2">218 <dt className="font-medium text-ink">{tOpt(`country.similar.mode.${k}`, k)}</dt>219 <dd className="text-ink-2">{v}</dd>220 </div>221 ))}222 </dl>223 <p className="text-sm text-ink-3">{m.derived.similarity.method}</p>224 </>225 ) : null}226 </Prose>227228 <Prose id="insights" title={t('method.insights')}>229 <p>{t('method.insights.text')}</p>230 {m ? <p className="text-sm text-ink-3">{m.derived.insights}</p> : null}231 </Prose>232233 <Prose id="dna" title={t('method.dna')}>234 <p>{t('method.dna.text')}</p>235 {m ? (236 <ul className="grid gap-x-6 gap-y-1.5 text-sm sm:grid-cols-2">237 {m.derived.country_dna.dimensions.map((d) => (238 <li key={d.id} className="grid grid-cols-[8rem_minmax(0,1fr)] gap-2">239 <span className="font-medium text-ink">{d.label}</span>240 <span className="min-w-0">241 {d.indicators.map((s, i) => (242 <span key={s}>243 {i > 0 ? ', ' : ''}244 <Link href={routes.indicator(s)} className="text-accent hover:underline">245 {s}246 </Link>247 </span>248 ))}249 </span>250 </li>251 ))}252 </ul>253 ) : null}254 {m ? <p className="text-sm text-ink-3">{m.derived.world_aggregates}</p> : null}255 </Prose>256257 <Prose id="limitations" title={t('method.limitations')}>258 <p>{t('method.limitations.text')}</p>259 </Prose>260261 <Prose id="versioning" title={t('method.versioning')}>262 <p>{t('method.versioning.text')}</p>263 </Prose>264265 <Prose id="licence" title={t('method.licence')}>266 <p>{t('method.licence.text')}</p>267 {m ? (268 <>269 <h3 className="pt-2 text-sm font-semibold text-ink">{t('method.formatting')}</h3>270 <Kv rows={Object.entries(m.formatting).map(([k, v]) => [k, v] as [string, ReactNode])} />271 </>272 ) : null}273 <p className="text-sm">274 <Link href={routes.data()} className="text-accent hover:underline">275 {t('data.title')} →276 </Link>277 </p>278 </Prose>279 </article>280 </div>281 </>282 );283}284