From 4bde3d7c907d6e9f354bb6e3037a292f71277ca9 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 10:02:30 +0000 Subject: [PATCH] Regions list: each region's View country posters toggle embeds the shared poster section, and one Show on map handler serves the list and the Country Posters view --- .../regions-dashboard/regions-list.vue | 166 ++-------- .../regions-dashboard/regions-mfe.vue | 17 +- public/dx-maps/doxa-maps-spa/manifest.json | 14 +- public/dx-maps/manifest.json | 14 +- .../country-poster-sections/index.html | 2 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 308 +++++++++--------- .../regions-dashboard/regions-list/index.html | 2 +- .../regions-dashboard/regions-mfe/index.html | 2 +- 9 files changed, 206 insertions(+), 321 deletions(-) diff --git a/plugin-designs/regions-dashboard/regions-list.vue b/plugin-designs/regions-dashboard/regions-list.vue index 11b5c104e..61dbdaba0 100644 --- a/plugin-designs/regions-dashboard/regions-list.vue +++ b/plugin-designs/regions-dashboard/regions-list.vue @@ -44,7 +44,6 @@ import { applyPeopleGroupList } from '@map/lists/peopleGroupList.js' import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js' import { splitLastWord } from './src/useLastWordHighlight.js' import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js' -import { countryName } from '@map/utils/countryName.js' import { useOptionalT } from '@map/composables/useOptionalT.js' // ar renders right-to-left. RTL_LOCALES is the shared set and :dir on our own root is the // house pattern (doxa-simple-map.vue:18, :246, :1346) — an EMBEDDED element must not reach for @@ -60,20 +59,14 @@ import { DXLIFE_TOKENS } from './dxlife-tokens.js' // and the no-selection chooser. The requirement: a toggle at the bottom of each region section // that opens that region's country posters. // -// NOTHING HERE IS REBUILT. PosterWall is already the shared, fully parameterised grid that -// my-upg-100-list/poster-wall.vue and country-posters/country-poster-wall.vue both render, and -// it already owns the card layout, the "Show on map" header button, the Print Options footer -// and the client-side raster. This file supplies only data and a toggle — the same division of -// labour country-poster-wall.vue states in its own header ("this profile supplies only its -// data and taxonomy"). -import PosterWall from '@map/components/poster/PosterWall.vue' +// NOTHING HERE IS REBUILT. Each region's posters are the library's +// (library/elements/posterSectionElement.js), the same section, cards and print options as the Country Posters +// wall and the Regions dashboard's third view, so the list is composed from the shared element rather than +// drawing a wall of its own. This file supplies only the toggle and the region. import RegionSectionCard from '@map/components/regions/RegionSectionCard.vue' -import posterData from '@map/data/country-posters.json' -import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js' -import { makeCountrySheet, largeSheet, sheetWithheld } from '@map/packet/countrySheet.js' -import { isPhonePrint } from '@map/composables/printInPlace.js' -import { keyedError } from '@map/i18n/keyedError.js' import { resolveAssetsBase } from '@map/utils/embedBase.js' +import { registerPosterSectionElement } from '@map/elements/posterSectionElement.js' +import { regionKey } from '@map/components/poster/types/countryPosterType.js' import { onBeforeUnmount } from 'vue' import { cyrillicHeading } from '@map/styles/portedGlobals.js' import { hostElement } from '@map/utils/hostElement.js' @@ -276,44 +269,29 @@ async function loadRegions() { } } -// ─── COUNTRY POSTERS, per region ───────────────────────────────────────────────────────── -// One wall per region, opened by the footer toggle. instanceId is suffixed with the region -// slug because PosterWall scopes its CustomEvent by instanceId so several embeds on one page -// never cross-talk (PosterWall.vue:40) — and here there genuinely are several. -const POSTER_EVENT = 'doxa-regions:show-country-on-map' +// ─── COUNTRY POSTERS, per region ───────────────────────────────────────────── +// One poster section per region, opened by the footer toggle: the library's . +// Its scope is the region's key through the country type's own regionKey, so the list and the element can never +// spell a region two ways. It is not collapsible (the toggle already opens and shuts it) and shows two cards a row. +// ⚠ The element files countries under the poster data's region (library/data/country-posters.json), so this list +// shows exactly that data's membership for each region. +registerPosterSectionElement() +const SECTION_EVENT = 'doxa-country-posters:show-on-map' // Read off the injected config the same way every other value in this file is (see the note // above cfg) — a bare default here would make two embeds on one page answer each other's // poster events. const instanceId = computed(() => cfg.value.instanceId || 'regions-list') const posterAssetsBase = computed(() => resolveAssetsBase(cfg.value)) - -// ★ THE PRINT ROWS, AND THE SHEET DRAWN FROM THEM. Supplying `records` is what makes the Prayer -// List, Card Deck and Prayer Packet buttons appear on each country (PosterWall hides them when a -// wall cannot supply rows, rather than failing at click time). One fetch per language, cached in -// the provider and shared by every country in every region, so opening five regions is still one -// request. The same provider feeds the sheet, so the printed map and the printed list can never -// disagree about which groups are in a country. -const countryRecords = createPeopleGroupRecords({ - apiBase: () => prayBaseUrl.value, - lang: () => languageCode.value, - profileConfig: injectedCfg, +const sectionAttrs = (region) => ({ + type: 'country', + scope: regionKey(region.value || region.label), + locale: i18nLocale.value || languageCode.value || 'en', + collapsible: 'false', + columns: '2', + 'assets-base': posterAssetsBase.value, + // The same people-groups service this list reads its regions from, so a card's list and deck match it. + 'api-base-url': prayBaseUrl.value, }) -// Page 1 of a country packet, and the country poster, drawn at the click. Nothing is stored: no -// country sheet ships, and the packet's default lift would ask for posters/country-.html and -// 404 on all 175. -const sheetForRecord = makeCountrySheet({ - records: countryRecords, - assetsBase: () => posterAssetsBase.value, - // The same list the Country Posters wall computes its series from, so this tab's printed - // "map N of M" agrees with the wall's print and with the country's thumbnail. - countries: posterData.countries || [], -}) -// ★ THE POSTER IS ENGLISH BY RECORDED DECISION (series.json english_only_note), so the generator gets -// the DATA's own record, never the card: the card's label is localised (localisedPoster below), and -// handing it over printed "Pray for السودان" beside an English "Countries: Sudan" in Arabic. The -// Country Posters wall resolves the record by id the same way (country-poster-wall.vue, byId). -const POSTER_BY_ID = new Map((posterData.countries || []).map(c => [c.id, c])) -const countrySheet = (card, format) => sheetForRecord(POSTER_BY_ID.get(card && card.id) || card, format) const openPosters = ref(new Set()) function togglePosters(region) { @@ -323,74 +301,17 @@ function togglePosters(region) { } const postersOpen = (region) => openPosters.value.has(region.slug) -// The poster dataset is keyed by ISO-3 (`iso3`); the regions rollup keys by `code`. Same -// codes, so the join is direct. -const POSTER_BY_ISO3 = new Map((posterData.countries || []).map(c => [String(c.iso3 || '').toUpperCase(), c])) - -// Every country in the region gets a card, in PosterWall's own shape. A country with no -// generated poster is NOT dropped — it is passed with a reason, which is what the wall's -// `unsupported` slot is for, so the grid never silently loses a country. -// ★ EACH CARD NAMED IN THE READER'S LANGUAGE. item.label is the card's accessible name, the print -// HUD's title, and the title on the printed list, deck and packet — and country-posters.json carries -// it in English only, so every card was English in every locale. Same fix as the country-posters -// wall, and the same two caveats travel with it: -// · ENGLISH KEEPS THE DATA'S OWN NAMES. Intl's English differs from them for 32 of these 175 -// countries, and countryName.js records that swap as a product decision rather than a refactor. -// · THE SHEET GENERATOR STILL GETS THE ORIGINAL RECORD. The poster itself is English by recorded -// decision, so only the card's label is localised — postersFor's result feeds the wall, while -// countrySheet (above) swaps the card back for the json's own record, by id, before generating. -// The i18n locale REF is read, not a snapshot, so a runtime language switch renames the cards. -function localisedPoster(rec) { - const locale = i18nLocale.value || 'en' - if (/^en\b/i.test(locale)) return rec - const name = countryName(rec.iso3, locale) - return name && name !== rec.iso3 ? { ...rec, label: name } : rec -} - -function postersFor(region) { - return (region.countries || []).map((c) => { - const rec = POSTER_BY_ISO3.get(String(c.code || '').toUpperCase()) - if (rec) return localisedPoster(rec) - return { - id: `country-${String(c.code || '').toLowerCase()}`, - iso3: c.code, - label: c.name, - count: c.count, - unsupportedReason: t('No poster has been generated for this country yet.'), - } - }) -} - -// ★ THE REASON IS TRANSLATED, THE PRESENCE OF ONE IS STILL THE DATA'S. The record carries its own -// English note ("India has 855 people groups — too many…"), and returning it put one untranslated -// sentence on the wall in every locale. The record still DECIDES whether a country is unsupported; -// only the wording comes from the catalogue now. messages.countryPosterTooMany exists in all -// eleven, and needs no number in it: the card already shows the count directly above. -// India is the only country in the dataset carrying one (855 people groups, no sheet); China is -// fine at 3x4. My own not-yet reason, for a country with no record at all, is already through t(). -// ★ A COUNTRY TOO LARGE FOR PAPER (countrySheet.js's LARGE_SHEETS; India) prints as one large sheet through the same -// makeCountrySheet, so its record's "too many" note no longer makes it unsupported here. It is DESKTOP-ONLY: on a phone -// its card says so and offers no rows, rather than failing (the Country Posters wall applies the same rule, same table). -const ON_PHONE = isPhonePrint() -const phoneBlocked = (c) => ON_PHONE && Boolean(largeSheet(c)?.desktopOnly) -const posterUnsupported = (c) => (sheetWithheld(c) ? tKey('messages.countryPosterTooMany') : phoneBlocked(c) ? tKey('poster.phoneLimitAll') - : c.unsupportedReason && !largeSheet(c) ? tKey('messages.countryPosterTooMany') : null) -// A blocked card's rows REJECT with the notice, never [] ("no people groups" on a card that has hundreds). -// A withheld whole-country poster (countrySheet.js WITHHELD: India) withholds its list, packet and cards too. -const posterRows = (item) => (sheetWithheld(item) ? Promise.reject(keyedError('messages.countryPosterTooMany', 'Too many people groups to fit on one poster legibly — a solution is in progress.')) : phoneBlocked(item) ? Promise.reject(keyedError('poster.phoneLimitAll', 'This needs a desktop or laptop computer.')) : countryRecords({ type: 'country', values: [item.iso3] })) - -// One region's wall is already inside a region section, so it needs no second level of -// collapsing and no grouping: everything in it is that one region. -const posterGroupBy = () => 'countries' - -// PosterWall's "Show on map" dispatches a window CustomEvent carrying listId (the poster id, +// A poster card's "Show on map" dispatches a window CustomEvent carrying listId (the poster id, // e.g. "country-chn") or null when the user toggles the same card off. Mapping that id back to // the country record and reusing selectCountry() is what makes the button open the dashboard // tab for that country: the requirement is that the view-on-map button opens the country // dashboard page for that country. +// The library's poster sections name themselves 'poster-section-country-' (PosterSectionView.vue). In the +// Regions composite this list is always mounted, so it also answers the third view's sections (Country Posters): +// ONE handler for both, which keeps the region context in the selection. function onPosterShowOnMap(ev) { const d = ev?.detail || {} - if (!String(d.instanceId || '').startsWith(instanceId.value)) return + if (!String(d.instanceId || '').startsWith('poster-section-country-')) return if (!d.listId) return // a deselect is not a navigation const iso3 = String(d.listId).replace(/^country-/, '').toUpperCase() for (const region of regions.value) { @@ -407,8 +328,8 @@ function onPosterShowOnMap(ev) { // already had this watcher (line 541); this tab was simply missed. watch(languageCode, () => { loadRegions() }) -onMounted(() => { if (typeof window !== 'undefined') window.addEventListener(POSTER_EVENT, onPosterShowOnMap) }) -onBeforeUnmount(() => { if (typeof window !== 'undefined') window.removeEventListener(POSTER_EVENT, onPosterShowOnMap) }) +onMounted(() => { if (typeof window !== 'undefined') window.addEventListener(SECTION_EVENT, onPosterShowOnMap) }) +onBeforeUnmount(() => { if (typeof window !== 'undefined') window.removeEventListener(SECTION_EVENT, onPosterShowOnMap) }) onMounted(loadRegions) @@ -583,32 +504,7 @@ useShadowStyles( @select-country="(c) => selectCountry(c, region)" > diff --git a/plugin-designs/regions-dashboard/regions-mfe.vue b/plugin-designs/regions-dashboard/regions-mfe.vue index 09c5ee279..34de2472b 100644 --- a/plugin-designs/regions-dashboard/regions-mfe.vue +++ b/plugin-designs/regions-dashboard/regions-mfe.vue @@ -25,7 +25,6 @@ * wall's 'show-on-map', and it keeps each pane under its own ProfileLoader/Pinia. */ import { ref, computed, inject, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' -import { countrySlug } from '@map/utils/countries-meta.js' import { usePaneConfig } from '@map/composables/usePaneConfig.js' import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js' @@ -182,17 +181,9 @@ function onCountrySelected(e) { forwardSelection({ regionSlug: d.region?.slug || '', countrySlug: d.slug || '' }) } -// A card's "Show on map" in the third view (Country Posters) opens that country's dashboard, the same move the -// Regions list makes for its own posters (regions-list.vue onPosterShowOnMap). The library's poster sections name -// themselves 'poster-section-country-' (PosterSectionView.vue), and this view is the only place in the -// Regions element that places them; a deselect (listId null) is not a navigation. -const SECTION_EVENT = 'doxa-country-posters:show-on-map' -function onSectionShowOnMap(e) { - const d = e?.detail || {} - if (!String(d.instanceId || '').startsWith('poster-section-country-') || !d.listId) return - const iso3 = String(d.listId).replace(/^country-/, '').toUpperCase() - if (iso3) forwardSelection({ regionSlug: '', countrySlug: countrySlug(iso3) }) -} +// A card's "Show on map" in the third view (Country Posters) opens that country's dashboard: the Regions list pane +// (always mounted here) answers every poster section's event with its own selectCountry(), which arrives above as +// 'country-selected'. One handler for both views (regions-list.vue onPosterShowOnMap). onMounted(() => { if (!stage.value) return @@ -214,13 +205,11 @@ onMounted(() => { // rather than on window keeps two Regions embeds on one page from hearing each other. stage.value.addEventListener('region-selected', onRegionSelected) stage.value.addEventListener('country-selected', onCountrySelected) - window.addEventListener(SECTION_EVENT, onSectionShowOnMap) }) onBeforeUnmount(() => { stage.value?.removeEventListener('region-selected', onRegionSelected) stage.value?.removeEventListener('country-selected', onCountrySelected) - window.removeEventListener(SECTION_EVENT, onSectionShowOnMap) offScroll?.() column?.destroy() column = null diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index 78c94c077..22f761ba5 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -1292,7 +1292,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-7vvaXTReLkqv07Q3hQoNHKRlUGDK9qiHlkCOtXdq8+tDgb6gMsE5vvlAQd0df0U0", + "integrity": "sha384-lZ8Vn+9kmj9kjEriFPQpxan3twIaZUZgEamL0UEpH2S4gmgm7TPN83Sw3NmYz/ru", "elements": [ "regions-dashboard" ], @@ -1315,7 +1315,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1331,8 +1331,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -1348,7 +1348,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1364,8 +1364,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index 78c94c077..22f761ba5 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -1292,7 +1292,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-7vvaXTReLkqv07Q3hQoNHKRlUGDK9qiHlkCOtXdq8+tDgb6gMsE5vvlAQd0df0U0", + "integrity": "sha384-lZ8Vn+9kmj9kjEriFPQpxan3twIaZUZgEamL0UEpH2S4gmgm7TPN83Sw3NmYz/ru", "elements": [ "regions-dashboard" ], @@ -1315,7 +1315,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1331,8 +1331,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -1348,7 +1348,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1364,8 +1364,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/regions-dashboard/country-poster-sections/index.html b/public/dx-maps/regions-dashboard/country-poster-sections/index.html index 4edd89df4..02780fdf6 100644 --- a/public/dx-maps/regions-dashboard/country-poster-sections/index.html +++ b/public/dx-maps/regions-dashboard/country-poster-sections/index.html @@ -67,7 +67,7 @@ - + - + - + - +