diff --git a/library/packet/countrySheet.js b/library/packet/countrySheet.js new file mode 100644 index 000000000..efb147ebc --- /dev/null +++ b/library/packet/countrySheet.js @@ -0,0 +1,51 @@ +/** + * countrySheet.js — a country's map sheet, GENERATED in the browser at the click. + * + * SHARED (@map/packet/countrySheet.js). Two consumers: the regions dashboard's country-poster wall + * and the standalone element. It was plugin-local while the regions list was its + * only caller; it moved here when the element became the second, rather than being copied. + * + * The print standard is generate, store only previews: no country sheet is shipped, and none should + * be. MEASURED before this existed: data/posters holds 28 .html sheets and all 28 are ZONES, while + * emit-countries.mjs writes only country-posters.json, whose `sheet` field is a PAPER SIZE ('3x3') + * rather than a filename. So a packet's page-1 lift for a country asked for posters/country-.html + * and got a 404 for all 175 of them. This draws it instead. + * + * Same shape as my-upg-100-list's zone sheet (data/zone-sheet-print.js), deliberately, so the two + * surfaces cannot draw the "same" sheet two ways: + * + * const sheet = makeCountrySheet({ records, assetsBase: () => base }) + * usePosterPrints({ …, sheet }) // (item, format) -> html + * + * + * The country case is simpler than the zone case: generateCountrySheet resolves the zone and its + * geometry from the country entry itself, so there is no assignment step to keep in sync. + */ + +/** + * @param {object} o + * records (selector) => Promise the shared provider; this asks it for ONE country + * assetsBase () => string where boundaries/ is served from + * @returns {(item, format) => Promise} the sheet's HTML, or '' when it cannot be drawn + */ +export function makeCountrySheet({ records, assetsBase }) { + return async function countrySheet(item, format) { + const iso3 = item && item.iso3 + if (!iso3) return '' // a selection or a row without a country: no sheet + // Lazy, like the zone sheet: the generator and the boundary loader are only pulled when a + // reader actually prints, so they stay out of every wall's initial download. + const { generateCountrySheet } = await import('@map/packet/posterSheets.js') + const [{ ISO3_NAME }, rows] = await Promise.all([ + import('@map/data/iso3-name.js'), + records({ type: 'country', values: [iso3] }), + ]) + if (!rows || !rows.length) return '' // nothing to pin: fall through rather than draw an empty country + return generateCountrySheet({ + country: { id: item.id, iso3, label: item.label, count: item.count }, + records: rows, + assetsBase: assetsBase(), + ISO3_NAME, + format, + }) + } +} diff --git a/plugin-designs/country-map/country-map.vue b/plugin-designs/country-map/country-map.vue index 52ffbc794..ece51b7ea 100644 --- a/plugin-designs/country-map/country-map.vue +++ b/plugin-designs/country-map/country-map.vue @@ -33,6 +33,26 @@ import { toNumber, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js import { peopleGroupPoints, PEOPLE_GROUP_POINT_FIELDS } from '@map/utils/peopleGroupPoints.js' import { applyPeopleGroupList } from '@map/lists/peopleGroupList.js' import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js' +import { retryableAsyncComponent } from '@map/composables/retryableAsyncComponent.js' +import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js' +import { makeCountrySheet } from '@map/packet/countrySheet.js' +import { resolveAssetsBase } from '@map/utils/embedBase.js' +import posterData from '@map/data/country-posters.json' + +// ★ THE PRINT SURFACE IS OPT-IN AND LAZY, AND BOTH HALVES MATTER. +// +// OPT-IN, because other sites embed this element. A page that does not set showPosters must render +// exactly what it renders today; the surface is additive, never a change to the existing contract. +// +// LAZY, because PosterWall STATICALLY imports usePosterPrints, which pulls the whole printing +// subsystem. A plain `import PosterWall` would therefore land that weight in every embed including +// the ones that never turn the flag on, which is the opposite of opt-in. An async component keeps +// it out until a page asks for it. +// +// retryableAsyncComponent, not raw defineAsyncComponent: Vue CACHES a rejected loader promise, so +// one dropped request would disable the surface for the life of the page. Same reason +// upg-100-list.vue uses it for its detail panel. +const PosterWall = retryableAsyncComponent(() => import('@map/components/poster/PosterWall.vue')) const props = defineProps({ profileConfig: { type: String, default: '' }, @@ -81,6 +101,43 @@ const researchUrl = computed(() => cfg.value.researchUrl || 'https://doxa.life/r const prayBaseUrl = computed(() => cfg.value.prayBaseUrl || 'https://pray.doxa.life') const countryUrl = computed(() => cfg.value.countryUrl || 'https://doxa.life/regions/') const languageCode = computed(() => cfg.value.languageCode || cfg.value.locale || cfg.value.lang || 'en') + +// OFF unless the host says otherwise, and only true for a literal true / 'true': an absent key, a +// typo'd value or a stray string must not switch a public page's UI on by accident. +const showPosters = computed(() => cfg.value.showPosters === true || cfg.value.showPosters === 'true') + +// The wall shows THIS element's country, not a region's list, so its single item is the entry for +// the first country code. An unknown code still yields a row, carrying the reason it cannot print, +// exactly as the regions list does for a country with no poster. +const POSTER_BY_ISO3 = new Map((posterData.countries || []).map((c) => [String(c.iso3 || '').toUpperCase(), c])) +const posterItems = computed(() => { + const iso3 = countryCodes.value[0] + if (!iso3) return [] + const rec = POSTER_BY_ISO3.get(iso3) + if (rec) return [rec] + return [{ + id: `country-${iso3.toLowerCase()}`, + iso3, + label: iso3, + count: 0, + unsupportedReason: t('No poster has been generated for this country yet.'), + }] +}) + +const instanceId = computed(() => cfg.value.instanceId || 'country-map') +const posterAssetsBase = computed(() => resolveAssetsBase(cfg.value)) +// The same shared provider the regions wall uses, not a copy of its fetch: one cached request per +// language, and the same rows feed the sheet, so the printed map and the printed list cannot +// disagree about which groups are in this country. +const countryRecords = createPeopleGroupRecords({ + apiBase: () => prayBaseUrl.value, + lang: () => languageCode.value, + profileConfig: injectedCfg, +}) +const countrySheet = makeCountrySheet({ + records: countryRecords, + assetsBase: () => posterAssetsBase.value, +}) const { t } = useRegionsI18n() // ★ THE ELEMENT FETCHES ITS OWN PINS. prayBaseUrl is in the contract, and it is the API base the @@ -167,4 +224,29 @@ watch(() => [languageCode.value, countryCodes.value.join(',')], loadPoints) "Regionsdaten" all say regions. The people-groups sentence is already in the catalogue and already translated in all eleven, so this costs no new string. -->

{{ t('People groups could not be loaded. This is a connection problem, not a filter.') }}

+ +