summarizeRegions() was duplicated in regions-list.vue and
region-dashboards.vue — two copies of the same rollup, so two chances
for the region page and the dashboard to disagree about one number.
Both now import a port of DX-life's config/countries-meta.ts, placed in
library/ because it has two named consumers.
Ported verbatim: slugify's replace chain, FULL_PRAYER_COVERAGE_COUNT,
addToStats's conditions, fitView's constants, lngExtent's largest-gap
walk, WORLD_VIEW, both sort orders. Changed only: TypeScript to JS
(interfaces to JSDoc), and their `import { COUNTRIES } from
'../app/utils/countries'` becomes library/data/countries.json — their
same 242-entry {value,label} array, in their order. toNumber and
emptyStats are exported beyond their surface, and only those two: their
pages need no cross-region total because DX-life has no "all regions"
view, and ours does for the no-selection chooser.
Two things the copies had wrong, both fixed by porting rather than
patching:
- They slugged a country from the API's LOCALIZED label. Theirs slugs
from the canonical English name, deliberately. The live site settled
it: /regions/the-united-states is 200 and /regions/united-states is
404; /regions/the-democratic-republic-of-the-congo is 200 and
/regions/dr-congo is 404. Since the dashboard writes the slug into
the URL hash, our deep links pointed at pages that do not exist
there.
- They dropped centre/zoom/bounds entirely, so our maps derived their
own frame. Theirs computes a fit view per country and per region,
including the antimeridian: Oceania's bounds come out east 187.9,
where a naive max() says 178 and spans the globe backwards.
Also fixed regions-list.vue's formatNumber, still hardcoded en-US after
the same fix landed in region-dashboards.vue, and its region sort, which
used (a.label || 'Other') where theirs compares a.label directly.
Proof: region-vs-live 73/73 and country-vs-live 28/28 UNCHANGED — the
rollup is equivalent. New _probe-countries-meta.mjs is 20/20 and
includes a negative test showing the antimeridian assertion really
discriminates. oceania-and-list, regions-swap, standalone-app,
dashboard, total and search all green. 159 lines of duplication gone.
No Vue, no Pinia, no Mapbox. Anything reactive is a composable; anything stateful is a store.
File
Purpose
apiBaseUrl.js
Resolves the API base URL at runtime (window.MAP_APP_API_URL / VITE_API_BASE_URL)
embedBase.js
Share/embed URL ladder — the built bundle derives its tree root from its own script url (profileConfig.shareUrl / embedBaseUrl / window.MAP_EMBED_BASE_URL override) so tree-relative <bundle>/<profile>/index.html links work on any host
geoUtils.js
Centroid, bounds, validation, GeoJSON helpers
ClusterHelpers.js
Shared helpers for the two clustering utilities below
MSTClusteringUtil.js
Minimum-spanning-tree clustering for affinity blocks
NetworkClusteringUtil.js
Network-line clustering — connect nearby pins with edges
No import { ref } from 'vue' and no import mapboxgl in this folder — operate on plain
[lng, lat] arrays.
Named exports (for tree-shaking), JSDoc on every export.
Writing reactive logic or needing Mapbox APIs? That's a composable — move it to ../composables/.