Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run

# Conflicts:
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js
#	public/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html
#	public/doxa-maps/regions-dashboard/region-dashboards/index.html
#	public/doxa-maps/regions-dashboard/regions-dashboard.js
#	public/doxa-maps/regions-dashboard/regions-list/index.html
#	public/doxa-maps/regions-dashboard/regions-mfe/index.html
This commit is contained in:
doxa-maps-88655c70 2026-09-24 01:15:03 +00:00
commit 3bafe7cd55
17 changed files with 554 additions and 168 deletions

View file

@ -0,0 +1,47 @@
/**
* printInPlace.js — SHARED (@map/composables/printInPlace.js).
*
* ★ THE ONE print mechanism, graduated VERBATIM from my-upg-100-list/poster-wall.vue
* openPrintWindow() (commit e957cbe "Print the poster in place, on a portrait sheet") on the
* Driver's ruling that killed the pop-up path: print must happen IN PLACE — no new browser
* window, no pop-up blocker, the dialog rises over the page the user is looking at.
*
* WHY IT GRADUATES (library high bar: reusable + parameterized, 2+ consumers): the
* prayer-walk-planner's print still used window.open(), and that is not just the rejected UX --
* it is AGENT-HOSTILE. window.open() without a user gesture is pop-up-blocked, so
* PrayerWalkPlanner.print() called from an agent (the Driver: "the agent can also create
* polygons and trigger the print function") silently did nothing. An iframe cannot be blocked
* and needs no gesture, which is what makes the same call work for a human click AND an agent.
*
* Mechanics preserved from the poster-wall original, each for a measured reason:
* - OFF-SCREEN 1x1, not display:none — a display:none iframe does not lay out in every
* engine, and a zero-height layout can print a blank first page.
* - autoprint appends the print bootstrap AFTER the caller's markup so it runs once the
* document is parsed (matches <img onload> timing).
* - afterprint + a 60s floor reclaim the frame, so repeated prints never leak nodes.
*
* Returns the frame's contentWindow, or null when the host is sandboxed/detached (caller
* keeps its own fallback).
*/
export function openPrintFrame(bodyHtml, { autoprint = false } = {}) {
const frame = document.createElement('iframe')
frame.setAttribute('aria-hidden', 'true')
frame.style.cssText = 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0'
document.body.appendChild(frame)
const doc = frame.contentDocument
if (!doc) { frame.remove(); return null }
doc.open()
doc.write(bodyHtml)
if (autoprint) {
doc.write(
'<script>window.addEventListener("load",function(){setTimeout(function(){window.focus();window.print()},150)})</' + 'script>')
}
doc.close()
const drop = () => { if (frame.parentNode) frame.remove() }
try { frame.contentWindow.addEventListener('afterprint', () => setTimeout(drop, 0)) } catch { /* cross-doc guard */ }
setTimeout(drop, 60000)
return frame.contentWindow
}

View file

@ -7,34 +7,21 @@
<link rel="icon" href="data:,">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: #f3f3f1; color: #3b463d; min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 20px; }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.sub { color: #6d766e; font-size: 13px; margin: 6px 0 24px; line-height: 1.6; }
.sub code { background: rgba(59, 70, 61, 0.08); padding: 1px 6px; border-radius: 4px; }
.frame {
height: 640px; border: 1px solid #d1d4cd; border-radius: 8px; overflow: hidden;
background: #fff;
}
prayer-walk-planner { display: block; width: 100%; height: 100%; }
/* ★ FULL-VIEWPORT MOUNT (BUG-PRAYER-WALK-PLANNER-MAP-DOES-NOT-FILL-THE-PAGE). This page used
to stage the element inside a centred 1100px card with a fixed 640px .frame — measured on
the dev server, the map painted a 781x584 window at y=206 in a 1440x900 viewport, which is
exactly the Driver's report: "the map is showing up but it's not filling the full page."
The BUILT leaf already mounts full-viewport; the dev harness now matches it (two-host rule:
what the Driver sees on the dev server must be what the leaf ships). The client-side API
note lives in the component header now — a planner is a full-page tool, not a demo card:
document.querySelector('prayer-walk-planner').addPolygon(coords, title) /
window.PrayerWalkPlanner.first(). */
html, body { height: 100%; background: #f3f3f1; }
prayer-walk-planner { position: fixed; inset: 0; display: block; }
</style>
</head>
<body>
<div class="wrap">
<h1>prayer-walk-planner · staging</h1>
<p class="sub">
Draw a polygon, batch-select zones, click Print to open an in-browser print dialog.
Persisted in <code>localStorage</code> only — no backend. Client-side API:
<code>document.querySelector('prayer-walk-planner').addPolygon(coords, title)</code> or
<code>window.PrayerWalkPlanner.first()</code> — see the component header for the full surface.
</p>
<div class="frame">
<prayer-walk-planner instance-id="staging"></prayer-walk-planner>
</div>
</div>
<prayer-walk-planner instance-id="staging"></prayer-walk-planner>
<!-- Vite multi-page entry. In dev, served transformed; in build, Vite emits
../../../public/doxa-maps/prayer-walk-planner/prayer-walk-planner.js and rewrites this script tag. -->

View file

@ -49,6 +49,7 @@
import { ref, shallowRef, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { openPrintFrame } from '@map/composables/printInPlace.js'
// ─── Props ────────────────────────────────────────────────────────────────────
const props = defineProps({
@ -301,7 +302,7 @@ function staticSnapshotUrl(coords) {
return null
}
function printInternal(ids) {
async function printInternal(ids) {
const targets = ids && ids.length
? polygons.filter(p => ids.includes(p.id))
: polygons.filter(p => p.selected)
@ -310,59 +311,92 @@ function printInternal(ids) {
return { opened: false, count: 0 }
}
const w = window.open('', '_blank', 'width=900,height=1000')
if (!w) {
statusMsg.value = 'Pop-up blocked — allow pop-ups to print.'
return { opened: false, count: 0 }
}
const esc = (s) => String(s == null ? '' : s).replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]))
// One thumbnail per zone, cropped from the LIVE map canvas so it needs no network
// request and no key — a snapshot of whatever basemap is currently painted.
const thumbs = {}
try {
if (map.value) {
const canvas = map.value.getCanvas()
thumbs.shared = canvas.toDataURL('image/png')
// ── PER-ZONE POSTER SNAPSHOT (card BUILD-PWP-POSTERS-FROM-THE-SHARED-POSTER-LIBRARY) ──
// Driver: "use the same poster generation software that the UPG 100 list used ... for
// generating prayer walking maps." The UPG generator (library/scripts/posters/generate.mjs)
// is BUILD-TIME over 28 fixed zones; walk zones are drawn at RUNTIME, so this is its runtime
// sibling: same design language (portrait sheet, title band, framed map, honest credit --
// the 839b0c4 poster rulings), map image sourced from the live keyless MapLibre canvas.
// For EACH zone the camera fits that zone's bounds and waits for idle, so the snapshot is a
// real WALKING-SCALE street map with the zone polygon painted on it -- not the whole-world
// viewport the old single shared snapshot cropped from. The camera is restored afterwards.
const shots = {}
if (map.value) {
const m = map.value
const prevCenter = m.getCenter(), prevZoom = m.getZoom()
const idle = () => new Promise((res) => { m.once('idle', res); setTimeout(res, 4000) })
for (const p of targets) {
try {
let minLng = Infinity, minLat = Infinity, maxLng = -Infinity, maxLat = -Infinity
for (const [lng, lat] of p.coords) {
if (lng < minLng) minLng = lng; if (lng > maxLng) maxLng = lng
if (lat < minLat) minLat = lat; if (lat > maxLat) maxLat = lat
}
m.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding: 72, duration: 0, maxZoom: 17 })
await idle()
// ★ CAPTURE INSIDE A RENDER FRAME. MapLibre does not preserve the WebGL drawing buffer
// (preserveDrawingBuffer:false), so toDataURL() outside a frame returns a BLANK white
// image that still reads as a valid data URL -- measured: the first poster shipped a
// white rectangle while hasSnapshot said true. triggerRepaint + once('render') reads
// the pixels while the buffer is live; the standard MapLibre snapshot pattern.
shots[p.id] = await new Promise((res) => {
m.once('render', () => { try { res(m.getCanvas().toDataURL('image/png')) } catch (_) { res(null) } })
m.triggerRepaint()
})
if (!shots[p.id]) delete shots[p.id]
} catch (_) { /* tainted/unavailable canvas -- the sheet still prints without the map */ }
}
} catch (_) { /* canvas may be tainted or unavailable — pages still print without it */ }
try { m.jumpTo({ center: prevCenter, zoom: prevZoom }) } catch (_) { /* restore is best-effort */ }
}
const pages = targets.map((p) => {
const [clng, clat] = polygonCentroid(p.coords)
const pointList = p.coords.slice(0, -1).map(([lng, lat]) => formatCoord(lng, lat)).join(' &nbsp;·&nbsp; ')
const pointList = p.coords.slice(0, -1).map(([lng, lat]) => formatCoord(lng, lat)).join(' &nbsp;&middot;&nbsp; ')
const img = shots[p.id]
return (
'<section class="zone-page">' +
'<h1>' + esc(p.title) + '</h1>' +
'<p class="centroid">Center: ' + formatCoord(clng, clat) + '</p>' +
(thumbs.shared ? '<img class="snapshot" src="' + thumbs.shared + '" alt="Map snapshot">' : '') +
'<h2>Coordinates</h2>' +
'<header class="band"><h1>' + esc(p.title) + '</h1>' +
'<p class="kicker">Prayer walking map &middot; centre ' + formatCoord(clng, clat) + '</p></header>' +
(img ? '<figure class="mapframe"><img class="snapshot" src="' + img + '" alt="Walking map of ' + esc(p.title) + '"></figure>' : '') +
'<p class="charge">Walk the streets of this zone and pray for the people who live, work and worship along them.</p>' +
'<h2>Zone corners</h2>' +
'<p class="points">' + pointList + '</p>' +
'<p class="footer">Prayer Walk Planner &middot; generated ' + esc(new Date().toLocaleDateString()) + '</p>' +
'<p class="footer">Prayer Walk Planner &middot; generated ' + esc(new Date().toLocaleDateString()) +
' &middot; Map &copy; OpenFreeMap &middot; Data &copy; OpenStreetMap contributors</p>' +
'</section>'
)
}).join('')
const title = 'Prayer Walking Plan — ' + targets.length + (targets.length === 1 ? ' zone' : ' zones')
w.document.open()
w.document.write(
const w = openPrintFrame(
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title>' +
'<style>' +
'@page{size:auto;margin:14mm}' +
// letter portrait explicitly -- size:auto lets the browser pick landscape (the same
// defect the poster path fixed in 839b0c4); a walking sheet is a portrait document.
'@page{size:letter portrait;margin:12mm}' +
'html,body{margin:0;background:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#3b463d}' +
'.zone-page{page-break-after:always;padding:16px 8px;max-width:720px;margin:0 auto}' +
'.zone-page{page-break-after:always;max-width:720px;margin:0 auto;padding:10px 8px}' +
'.zone-page:last-child{page-break-after:auto}' +
'h1{font-size:26px;margin:0 0 6px;border-bottom:3px solid #92b195;padding-bottom:8px}' +
'h2{font-size:14px;text-transform:uppercase;letter-spacing:.06em;color:#6d766e;margin:18px 0 6px}' +
'.centroid{font-size:14px;color:#6d766e;margin:0 0 14px}' +
'.snapshot{width:100%;max-height:360px;object-fit:cover;border:1px solid #d1d4cd;border-radius:4px}' +
'.points{font-size:13px;line-height:1.7;word-spacing:2px}' +
'.footer{margin-top:22px;font-size:10px;color:#6d766e}' +
'</style></head><body>' + pages +
'<script>window.onload=function(){setTimeout(function(){window.focus();window.print()},200)}<\/script>' +
'</body></html>'
// The poster design language: a dark-green title band like the UPG poster header,
// the map in a framed figure, quiet uppercase kickers -- material light palette only.
'.band{background:#3b463d;color:#fff;border-radius:8px;padding:14px 18px;margin:0 0 12px}' +
'h1{font-size:26px;margin:0;letter-spacing:-0.01em}' +
'.kicker{margin:4px 0 0;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#d1d4cd}' +
'.mapframe{margin:0;border:1px solid #d1d4cd;border-radius:6px;overflow:hidden}' +
'.snapshot{display:block;width:100%;max-height:520px;object-fit:cover}' +
'.charge{font-size:14px;line-height:1.6;color:#3b463d;margin:12px 0 0}' +
'h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6d766e;margin:16px 0 4px}' +
'.points{font-size:12px;line-height:1.7;word-spacing:2px;color:#6d766e}' +
'.footer{margin-top:18px;font-size:10px;color:#6d766e;border-top:1px solid #d1d4cd;padding-top:8px}' +
'</style></head><body>' + pages + '</body></html>',
{ autoprint: true }
)
w.document.close()
if (!w) {
statusMsg.value = 'Print frame unavailable in this host.'
return { opened: false, count: 0 }
}
return { opened: true, count: targets.length }
}
@ -373,7 +407,7 @@ function selectPolygon(id, selected = true) { return togglePolygonSelection(id,
function clearSelection() { clearSelectionInternal() }
function listPolygons() { return polygons.map(p => ({ ...p })) }
function getPolygon(id) { const p = polygons.find(p => p.id === id); return p ? { ...p } : null }
function print(ids = null) { return printInternal(ids) }
function print(ids = null) { return printInternal(ids) } // async: resolves once the dialog is raised
defineExpose({
addPolygon, deletePolygon, selectPolygon, clearSelection,

View file

@ -0,0 +1,8 @@
{
"type": "single",
"label": "Regions — region + country dashboard",
"config": {
"pinHint": false
},
"note": "pinHint:false because this page is an EXACT CLONE of DX-life's /regions/<slug> (RegionDetail.vue for a region, CountryDetail.vue for a country) and their page carries no coach-mark over the map. The shared PinHint's 'Tap a coloured dot' overlay is our own chrome; leaving it on would be a visible difference from the page being cloned. Opt-out is the documented channel (library/components/hints/PinHint.vue). Siblings doing the same for their own reason: cities-internet/connectivity-map.meta.json, gospel-resources/resources-map.meta.json."
}

View file

@ -24,8 +24,8 @@
*/
import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import BarChart from '@map/components/charts/BarChart.vue'
import DataTable from '@map/components/tables/DataTable.vue'
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
import UupgsList from './src/UupgsList.vue'
import RegionCountryMap from './src/RegionCountryMap.vue'
const props = defineProps({
@ -217,28 +217,10 @@ const tiles = computed(() => {
]
})
// The comparison bars: a region's countries against each other, or all regions against each
// other when nothing is selected. A country selection keeps its region's bars so the country
// stays legible IN CONTEXT rather than as one lone bar.
const chartRows = computed(() => {
const r = selectedRegion.value
const rows = r ? r.countries : regions.value
return rows
.map((x) => ({
label: x.name || x.label || 'Other',
value: x.count,
// Highlight the selected country inside its region's chart.
color: selectedCountry.value && x.slug === selectedCountry.value.slug ? '#3b463d' : '#92b195',
}))
.sort((a, b) => b.value - a.value)
.slice(0, 24)
})
const chartTitle = computed(() =>
selectedRegion.value
? `People groups by country — ${selectedRegion.value.label || 'Other'}`
: 'People groups by region'
)
// ★ NO BAR CHART. It was ours, not theirs, and the rule is that a SELECTED region or country
// must look IDENTICAL to its DX-life page — RegionDetail.vue has four stat cards, the map, three
// goal cards and the country table, and nothing else. The one deliberate difference in this
// element is the NO-SELECTION state below, which their site has no route for at all.
// ── DX-life's RegionDetail, field for field ──────────────────────────────────
// Ported from root-dx-life/doxa-life/app/components/public/RegionDetail.vue (read-only clone,
@ -329,6 +311,31 @@ const summaryLine = computed(() => {
// regions so the page still compares like with like. The prompt below then says how to narrow it.
const isAll = computed(() => subject.value.kind === 'all')
// ── THE ONE DELIBERATE DIFFERENCE: the no-selection chooser ─────────────────
// DX-life has no route for a dashboard without a subject (/regions is the list,
// /regions/<slug> always has one, [slug].vue 404s otherwise). Opening tab 2 first is a state
// only this element can reach, so it gets the one thing their site cannot give: a GENERIC map
// of everything, plus three independent ways to choose a subject — a region dropdown, a country
// dropdown, and the all-regions table, whose rows are already clickable. Once a subject is
// chosen the page becomes the faithful clone and this chooser disappears.
const regionOptions = computed(() =>
regions.value.map((r) => ({ value: r.slug, label: r.label || 'Other', count: r.count }))
)
const countryOptions = computed(() =>
regions.value
.flatMap((r) => r.countries.map((c) => ({ value: c.slug, label: c.name, count: c.count })))
.sort((a, b) => a.label.localeCompare(b.label))
)
function onPickRegion(e) {
sel.value = { regionSlug: e?.value || '', countrySlug: '' }
}
function onPickCountry(e) {
const slug = e?.value || ''
const owner = regions.value.find((r) => r.countries.some((c) => c.slug === slug))
sel.value = { regionSlug: owner?.slug || '', countrySlug: slug }
}
// ── the map's subject: which countries to highlight, and how to frame them ──
// DX-life gets centre/zoom/bounds from config/countries-meta.ts, a build-time table we do not
// have. Deriving the frame from the FOCUS POINTS themselves is better, not a workaround: their
@ -380,33 +387,9 @@ const mapNote = computed(() => {
.replace('%countries%', formatNumber(countries))
})
// ── the country page's people-group list ────────────────────────────────────
// CountryDetail.vue:63-69 nests UupgsList locked to the country. That component is 662 lines of
// search + eight filter dropdowns + photo cards, and this repo ALREADY ships that element as the
// people-groups-grid bundle -- re-implementing it here would be a third copy. So the country page
// gets the same INFORMATION through our own table pattern (@map/components/tables/DataTable.vue,
// which carries the mobile column-collapse and row-detail behaviour the Driver asked for once
// already). Nesting the real people-groups-grid element is the right end state and is a cap-4
// composition question, not a styling one.
const countryTable = computed(() => {
const c = selectedCountry.value
if (!c) return null
const rows = points.value
.filter((p) => p.code === c.code)
.sort((a, b) => (b.population - a.population) || a.name.localeCompare(b.name))
.map((p) => ({ name: p.name, population: p.population, committed: p.committed }))
return {
meta: { title: c.name + ' people groups' },
columns: [
{ field: 'name', title: 'People Group' },
{ field: 'population', title: 'Population', hozAlign: 'right', formatter: 'money',
formatterParams: { precision: 0, thousand: ',' } },
{ field: 'committed', title: 'People Praying', hozAlign: 'right', formatter: 'money',
formatterParams: { precision: 0, thousand: ',' } },
],
rows,
}
})
// The country page's list is DX-life's own UupgsList, ported at src/UupgsList.vue — not a table
// of ours. It fetches and filters the people groups itself, exactly as theirs does, so there is
// no countryTable computed here any more.
// ── in-dashboard navigation ─────────────────────────────────────────────────
// Their back-link (RegionDetail.vue:48) goes to /regions. We have no router, so it drops the
@ -551,6 +534,10 @@ useShadowStyles(
'.rd-chart{ padding:23.5264px clamp(17.689px, 4cqw, 31.2901px); }' +
// RegionDetail wraps the note and the map in a .stack--sm (13.3px). The note is their
// .text-center .color-primary .font-weight-bold -- --color-brand-primary at weight 700.
// The chooser's two controls, side by side and wrapping on a phone. Its own block so the
// clone below is untouched by it.
'.rd-chooser{ display:flex; flex-wrap:wrap; gap:13.3px; align-items:flex-end;' +
' justify-content:center; }' +
'.rd-mapblock{ display:flex; flex-direction:column; gap:13.3px; }' +
'.rd-mapnote{ margin:0; text-align:center; color:#92b195; font-weight:700; font-size:19.5312px; }' +
// .rounded-xlg on their <CountryMap>; the child takes border-radius:inherit from here.
@ -676,6 +663,28 @@ useShadowStyles(
</p>
</div>
<!-- THE CHOOSER — the no-selection state, and the only thing here DX-life does not have -->
<div v-if="isAll" class="rd-chooser">
<FilterDropdown
label="Region"
name="region"
:options="regionOptions"
value=""
placeholder="Type a region…"
@filter-change="onPickRegion"
@filter-clear="clearSelection"
/>
<FilterDropdown
label="Country"
name="country"
:options="countryOptions"
value=""
placeholder="Type a country…"
@filter-change="onPickCountry"
@filter-clear="clearSelection"
/>
</div>
<!-- RegionDetail.vue:53-70 — four stat cards. CountryDetail has none. -->
<div v-if="subject.kind !== 'country'" class="rd-stats">
<div v-for="st in stats" :key="st.label" class="rd-card rd-card--small rd-stat">
@ -701,13 +710,8 @@ useShadowStyles(
</div>
</div>
<!-- Ours, not theirs, and kept: the one view that RANKS the subject's parts. -->
<div v-if="subject.kind !== 'country'" class="rd-card rd-chart">
<BarChart :series="chartRows" :title="chartTitle" unit="people groups" color="#92b195" />
</div>
<!-- RegionDetail.vue:88-141 — the three DOXA goal cards in their .switcher -->
<div v-if="subject.kind !== 'country'" class="rd-switcher">
<div v-if="subject.kind === 'region'" class="rd-switcher">
<div class="rd-card rd-card--secondary rd-goal-card">
<h2 class="rd-h2">Prayer Coverage</h2>
<div>
@ -777,11 +781,15 @@ useShadowStyles(
</div>
</div>
<!-- CountryDetail.vue:63-69 — UupgsList locked to the country, through our table pattern. -->
<div v-if="subject.kind === 'country' && countryTable" class="rd-section">
<h2 class="rd-h2 rd-h2--start">People groups in {{ subject.title }}</h2>
<DataTable :table="countryTable" :assets-base="assetsBase" />
</div>
<!-- CountryDetail.vue:63-69 — <UupgsList :lock-country-code hide-see-all-link />, and
nothing around it: their page puts no heading above this block. -->
<UupgsList
v-if="subject.kind === 'country' && selectedCountry"
:lock-country-code="selectedCountry.code"
:country-name="selectedCountry.name"
:pray-base-url="prayBaseUrl"
:language-code="languageCode"
/>
</template>
</div>
</div>

View file

@ -0,0 +1,8 @@
{
"type": "single",
"label": "Regions — the list page",
"config": {
"pinHint": false
},
"note": "pinHint:false: this profile draws NO map at all (it is the clone of DX-life's /regions index), but it now sits in a bundle that owns a map, so generate-tree classifies it isMap and would bake the coach-mark onto a page with no dots. Same shape as cities-internet/connectivity-map.meta.json."
}

View file

@ -1,6 +1,12 @@
{
"type": "nested-elements-mfe",
"label": "Regions — List + Dashboards",
"composes": ["regions-dashboard/regions-list", "regions-dashboard/region-dashboards"],
"note": "The Driver's 2-tab Regions element. Composite MFE on the pane-wrapper layout, imitating my-upg-100-list/upg-100-mfe: a SELECT VIEW switcher (library/components/ViewSelect.vue) over Regions List at [0,0] and Region Dashboards at [1,0]. Clicking a region or country on tab 1 forwards the selection on 'doxa-regions-dashboard:select' and swaps to tab 2."
"composes": [
"regions-dashboard/regions-list",
"regions-dashboard/region-dashboards"
],
"note": "The Driver's 2-tab Regions element. Composite MFE on the pane-wrapper layout, imitating my-upg-100-list/upg-100-mfe: a SELECT VIEW switcher (library/components/ViewSelect.vue) over Regions List at [0,0] and Region Dashboards at [1,0]. Clicking a region or country on tab 1 forwards the selection on 'doxa-regions-dashboard:select' and swaps to tab 2. pinHint:false: the cloned pages carry no coach-mark, and the composite must declare it because a pane's config comes from here, not from the child's own meta.",
"config": {
"pinHint": false
}
}

View file

@ -42,7 +42,12 @@ const instanceId = computed(() => cfg.value?.instanceId || 'regions-mfe')
const spec = computed(() => ({
id: 'regions-mfe',
tag: TAG,
config: { profile: 'regions-list', instanceId: instanceId.value },
// pinHint:false reaches the PANES from here, not from their own meta.json. A child profile's
// meta only configures its own LEAF page; nested in this composite each pane is handed the
// config built below, and PinHint.vue reads pinHint off the INJECTED profileConfig. Without
// this the coach-mark reappeared over the cloned map inside the composite while the leaf
// pages were correctly clean — the same cap-4 inheritance gap assetsBase hit.
config: { profile: 'regions-list', instanceId: instanceId.value, pinHint: false },
panes: [
{ id: 'list', label: 'Regions List', at: [0, 0], config: { profile: 'regions-list' } },
{ id: 'dashboard', label: 'Region Dashboards', at: [1, 0], config: { profile: 'region-dashboards' } },
@ -63,7 +68,7 @@ let offScroll = null
// own default. Forwarded by KEY LIST, never by spreading the whole host config, so a pane's own
// `profile` / `instanceId` cannot be overwritten by the host's — that is what makes a pane a
// distinct view at all (pane.config is spread last and always wins).
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa']
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint']
function paneElement(pane) {
const el = document.createElement(TAG)

View file

@ -0,0 +1,282 @@
<script setup>
/**
* UupgsList.vue — DX-life's people-group list, ported for the country page.
*
* @description CountryDetail.vue:63-69 nests <UupgsList :lock-country-code="country.code"
* hide-see-all-link />. That component IS the bottom half of the page you land on when you
* click a country on DX-life, so cloning that page means cloning this. Ported from
* root-dx-life/doxa-life/app/components/public/UupgsList.vue (read-only clone, standing law),
* which is itself a Lit -> Vue port of marketing-theme/js/components/src/uupgs-list.ts and
* whose own header says it "preserves class names, markup structure, fetch endpoints, and
* filter logic verbatim so the source SCSS applies unchanged".
*
* WHAT IS CLONED. The markup and the class names, so the ported CSS below is theirs rather than
* a lookalike: .filters / .filters__toggle / .filters__panel / .filters__active / .filter-chip,
* the .search-box, the .grid[data-width-lg] card grid, and the .uupg__card grid-template-areas
* with its 576px reflow. Their endpoint and field list too (UupgsList.vue:388).
*
* SCOPED TO WHAT THE COUNTRY PAGE ACTUALLY RENDERS, and that is a deliberate narrowing rather
* than an omission. Their component carries four call-sites' worth of options — useSelectCard,
* useHighlightedUUPGs, randomizeList, dontShowListOnLoad, a select-card variant with prayer
* counters, the /highlighted endpoint, the see-all link. CountryDetail passes NONE of them: it
* passes a locked country and hide-see-all-link, which selects the DEFAULT full card and hides
* the see-all. Porting the branches the page cannot reach would be code no consumer runs.
*
* NOT CLONED, and it is visible: their cards show a photo per people group from image_url, and
* the /api/people-groups/list response we consume carries has_photo + image_url, so the img is
* wired. Where a group has no photo the card reflows without one, as theirs does.
*/
import { ref, computed, onMounted, watch } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
const props = defineProps({
// ISO-3. Their `lockCountryCode`: the country filter is pre-applied and its dropdown hidden.
lockCountryCode: { type: String, default: '' },
countryName: { type: String, default: '' },
prayBaseUrl: { type: String, default: 'https://pray.doxa.life' },
languageCode: { type: String, default: '' },
researchUrl: { type: String, default: 'https://doxa.life/research/' },
// Their perPage / morePerPage defaults (UupgsList.vue props block).
perPage: { type: Number, default: 24 },
morePerPage: { type: Number, default: 0 },
})
// Their field list, verbatim (UupgsList.vue:388).
const FIELDS = 'name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,location_description'
const rows = ref([])
const loading = ref(true)
const failed = ref(false)
const searchTerm = ref('')
const filtersExpanded = ref(false)
const shown = ref(props.perPage)
// Their filter set, minus `country` — a locked country hides that one (their own rule).
const active = ref({ wagf_region: null, wagf_block: null, rop1: null, religion: null })
const label = (f) => (f && typeof f === 'object' ? f.label : f) || ''
const value = (f) => (f && typeof f === 'object' ? f.value : f) || ''
async function load() {
loading.value = true
failed.value = false
try {
const r = await fetch(`${props.prayBaseUrl}/api/people-groups/list?fields=${FIELDS}&lang=${props.languageCode}`)
if (!r.ok) throw new Error(`HTTP ${r.status}`)
const d = await r.json()
const all = Array.isArray(d.posts) ? d.posts : []
rows.value = props.lockCountryCode
? all.filter((p) => value(p.country_code) === props.lockCountryCode)
: all
} catch (e) {
console.error('[UupgsList] fetch error:', e)
failed.value = true
rows.value = []
} finally {
loading.value = false
}
}
onMounted(load)
watch(() => props.lockCountryCode, load)
// Distinct values per filter key, with counts — the shape FilterDropdown wants.
function optionsFor(key) {
const m = new Map()
for (const p of rows.value) {
const v = value(p[key]); const l = label(p[key])
if (!v) continue
const e = m.get(v) || { value: v, label: l || v, count: 0 }
e.count++; m.set(v, e)
}
return [...m.values()].sort((a, b) => a.label.localeCompare(b.label))
}
const filterDefs = computed(() => [
{ name: 'wagf_region', label: 'WAGF Region' },
{ name: 'wagf_block', label: 'WAGF Block' },
{ name: 'rop1', label: 'Affinity Bloc' },
{ name: 'religion', label: 'Religion' },
].map((f) => ({ ...f, options: optionsFor(f.name) })))
const activeChips = computed(() =>
Object.entries(active.value).filter(([, v]) => v).map(([name, v]) => ({ name, ...v }))
)
// Their filter+search pipeline: every active filter ANDs, then the term matches a name or an
// alternate name. Substring, not fuzzy — the fuzzy ladder is the /research page's own concern.
const filtered = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
return rows.value.filter((p) => {
for (const [k, v] of Object.entries(active.value)) {
if (v && value(p[k]) !== v.value) return false
}
if (!term) return true
const alt = p.imb_alternate_name || ''
return String(p.name || '').toLowerCase().includes(term) || String(alt).toLowerCase().includes(term)
})
})
const displayed = computed(() => filtered.value.slice(0, shown.value))
const showMore = computed(() => filtered.value.length > shown.value)
function loadMore() { shown.value += (props.morePerPage || props.perPage) }
watch([searchTerm, active], () => { shown.value = props.perPage }, { deep: true })
function onFilterChange(e) { if (e && e.name) active.value = { ...active.value, [e.name]: { value: e.value, label: e.label } } }
function onFilterClear(e) { if (e && e.name) active.value = { ...active.value, [e.name]: null } }
function clearAll() { active.value = { wagf_region: null, wagf_block: null, rop1: null, religion: null } }
function profileUrl(p) { return `${props.researchUrl}${p.slug || ''}` }
// Every value resolved against DX-life's 10px rem root, from the same token files as the rest of
// this bundle. Sources: blocks/_filters.scss (.filters*, .filter-chip*), blocks/_form.scss
// (.search-box, input[type=search]), blocks/_cards.scss:143-236 (.uupg__*), blocks/_buttons.scss
// (.button, .button.compact), composition/_grid.scss (.grid + data-width-lg).
// NO BACKTICKS in this string, including comments (house style: one terminates the literal).
useShadowStyles(
':host{ display:block; }' +
'.ul-root{ display:flex; flex-direction:column; gap:17.689px;' +
' --dx-text:#2a332a; --dx-muted:#6d766e; --dx-white:#fff; color:var(--dx-text);' +
' font:19.5312px/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }' +
// .filters — a .stack--md column
'.filters{ display:flex; flex-direction:column; gap:17.689px; }' +
// .search-box + input[type=search]: 100% wide, pill, text indented past the icon
'.search-box{ position:relative; width:100%; max-width:480px; margin-inline:auto; }' +
'.search-box input{ box-sizing:border-box; width:100%; border:1px solid #666; border-radius:100px;' +
' background:var(--dx-white); padding:10px 23.5264px; text-indent:1.5em; font:inherit;' +
' font-size:19.5312px; color:var(--dx-text); }' +
'.search-box input::placeholder{ color:gray; }' +
'.search-icon{ position:absolute; left:16px; top:50%; transform:translateY(-50%); width:18px;' +
' height:18px; fill:none; stroke:var(--dx-muted); stroke-width:2; pointer-events:none; }' +
// .filters__toggle — centred, chevron rotates when expanded
'.filters__toggle{ background:none; border:0; cursor:pointer; font:inherit; font-size:15.625px;' +
' display:inline-flex; align-items:center; gap:7.5188px; margin-inline:auto; color:var(--dx-text); }' +
'.filters__toggle svg{ transition:transform 200ms ease; }' +
'.filters__toggle[aria-expanded="true"] svg{ transform:rotate(180deg); }' +
'.filters__panel{ display:flex; flex-wrap:wrap; gap:13.3px; justify-content:center;' +
' align-items:flex-start; }' +
'.filters__active{ display:flex; flex-wrap:wrap; gap:7.5188px; align-items:center;' +
' justify-content:center; }' +
// .filter-chip — surface-brand-lighter on white, fully round
'.filter-chip{ display:inline-flex; gap:7.5188px; background:#6d766e; color:#fff;' +
' border-radius:100px; padding:5.6532px 13.3px; font-size:15.625px; text-align:center; }' +
'.filter-chip__remove{ background:none; border:0; cursor:pointer; font-size:1.1em; line-height:1;' +
' color:inherit; padding:0; display:inline-flex; align-items:center; }' +
'.filter-chip__remove:hover{ opacity:0.7; }' +
'.filters__clear-all{ background:none; border:0; cursor:pointer; font:inherit; font-size:15.625px;' +
' color:var(--dx-text); text-decoration:underline; text-underline-offset:4px;' +
' text-decoration-thickness:2px; }' +
'.ul-count{ text-align:center; font-size:15.625px; color:var(--dx-muted); }' +
// .grid[data-width-lg] — auto-fit, 400px min track, --spacing-2xl gap
'.grid{ display:grid; grid-gap:41.6158px;' +
' grid-template-columns:repeat(auto-fit, minmax(min(400px,100%), 1fr)); }' +
// .card + .uupg__card: their grid-template-areas, radius-xlg, --spacing-md padding + gap
'.uupg__card{ box-sizing:border-box; background:var(--dx-white); color:var(--dx-text);' +
' border-radius:35px; display:grid;' +
' grid-template-areas:"image header adopted" "image content content" "footer footer footer-right";' +
' grid-template-columns:auto 1fr auto; grid-template-rows:auto auto auto;' +
' gap:17.689px; padding:17.689px; }' +
'.uupg__image{ width:100px; height:100px; object-fit:cover; grid-area:image; border-radius:7.5px;' +
' background:#f3f3f1; }' +
'.uupg__header{ grid-area:header; }' +
'.uupg__name{ margin:0; font-family:inherit; font-size:24.4141px; font-weight:500;' +
' line-height:1.25; }' +
'.uupg__country{ margin:0; font-size:19.5312px; }' +
'.uupg__country-link{ color:#4e594f; text-decoration:none; }' +
'.uupg__country-link:hover{ text-decoration:underline; }' +
// theirs is display:none — the location line is fetched and deliberately not shown
'.uupg__content{ display:none; grid-area:content; }' +
// .button.compact, grid-area footer, bottom-aligned
'.uupg__more-button{ grid-area:footer; align-self:flex-end; display:block; width:fit-content;' +
' box-sizing:border-box; padding:10px 41.6158px; color:#f3f3f1; background:#92b195;' +
' border-radius:100px; border:1px solid transparent; line-height:1; text-decoration:none;' +
' text-transform:uppercase; font-size:19.5312px;' +
' font-family:"Brandon Grotesque",Poppins,-apple-system,"Segoe UI",sans-serif;' +
' transition:background-color .3s ease, color .3s ease; cursor:pointer; }' +
'.uupg__more-button:hover{ background:#2a332a; color:#f3f3f1; }' +
// their 576px reflow: the photo and the header stop sharing a row
'@media (max-width:576px){ .uupg__card{' +
' grid-template-areas:"image adopted" "header header" "content content" "footer footer";' +
' grid-template-columns:auto 1fr; grid-template-rows:auto auto auto auto; } }' +
'.loading, .ul-status{ text-align:center; color:var(--dx-muted); font-size:19.5312px; padding:17.689px; }' +
'.ul-status.ul-failed{ font-weight:600; }' +
// .center | .button compact .stack-spacing-2xl
'.ul-more{ margin-inline:auto; margin-top:41.6158px; display:block; width:fit-content;' +
' padding:10px 41.6158px; color:#f3f3f1; background:#92b195; border-radius:100px;' +
' border:1px solid transparent; line-height:1; text-transform:uppercase; font-size:19.5312px;' +
' font-family:"Brandon Grotesque",Poppins,-apple-system,"Segoe UI",sans-serif; cursor:pointer; }' +
'.ul-more:hover{ background:#2a332a; }',
'uupgs-list'
)
</script>
<template>
<div class="ul-root">
<div class="filters">
<div class="search-box">
<svg class="search-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" stroke-linecap="round" />
</svg>
<label class="sr-only" :for="'ul-search'">Search</label>
<input id="ul-search" v-model="searchTerm" type="search" placeholder="Search" aria-label="Search people groups">
</div>
<button
type="button"
class="filters__toggle"
:aria-expanded="filtersExpanded ? 'true' : 'false'"
@click="filtersExpanded = !filtersExpanded"
>
{{ filtersExpanded ? 'Hide Filters' : 'Show Filters' }}
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div v-if="filtersExpanded" class="filters__panel">
<FilterDropdown
v-for="f in filterDefs"
:key="f.name"
:label="f.label"
:name="f.name"
:options="f.options"
:value="active[f.name]?.value || ''"
placeholder="Type to search…"
@filter-change="onFilterChange"
@filter-clear="onFilterClear"
/>
</div>
<div v-if="activeChips.length" class="filters__active">
<span v-for="c in activeChips" :key="c.name" class="filter-chip">
{{ c.label }}
<button type="button" class="filter-chip__remove" :aria-label="'Remove ' + c.label" @click="onFilterClear({ name: c.name })">×</button>
</span>
<button type="button" class="filters__clear-all" @click="clearAll">Clear all</button>
</div>
</div>
<p v-if="!loading && !failed" class="ul-count">
{{ filtered.length.toLocaleString('en-US') }} people groups<span v-if="countryName"> in {{ countryName }}</span>
</p>
<div v-if="failed" class="ul-status ul-failed" role="status">
People groups could not be loaded. This is a connection problem, not a filter.
</div>
<div id="results" class="grid" data-width-lg>
<div v-for="p in displayed" :key="p.slug" class="uupg__card">
<img v-if="p.image_url" class="uupg__image" :src="p.image_url" :alt="p.name" loading="lazy">
<div class="uupg__header">
<h3 class="uupg__name">{{ p.name }}</h3>
<p class="uupg__country">
<a class="uupg__country-link" :href="profileUrl(p)">{{ label(p.country_code) }}</a>
<span v-if="label(p.rop1)"> ({{ label(p.rop1) }})</span>
</p>
</div>
<p v-if="p.location_description" class="uupg__content">{{ p.location_description }}</p>
<a class="uupg__more-button" :href="profileUrl(p)">Full profile</a>
</div>
<div v-if="loading" class="loading">Loading…</div>
</div>
<button v-if="showMore" type="button" class="ul-more" @click="loadMore">Load more</button>
</div>
</template>

File diff suppressed because one or more lines are too long

View file

@ -813,7 +813,7 @@
"bundle": "prayer-walk-planner",
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"output": "doxa-maps/prayer-walk-planner/prayer-walk-planner.js",
"integrity": "sha384-l3nBNqhx1fG3fiLlkCtbIf5R66cNHhZfKs1nIsfznYgpcBsSfG+vUqjAzloqqEwk",
"integrity": "sha384-OTTFfQpIOOhwH3QUT2Mg2GN32VxfytqbJz9E4jwV48oMXe2iDp+x9EAMr7CDEr2C",
"elements": [
"prayer-walk-planner"
],
@ -829,7 +829,7 @@
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
"embed": {
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-l3nBNqhx1fG3fiLlkCtbIf5R66cNHhZfKs1nIsfznYgpcBsSfG+vUqjAzloqqEwk\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-OTTFfQpIOOhwH3QUT2Mg2GN32VxfytqbJz9E4jwV48oMXe2iDp+x9EAMr7CDEr2C\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
}
}
]
@ -838,7 +838,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR",
"integrity": "sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9",
"elements": [
"regions-dashboard"
],
@ -858,7 +858,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -872,7 +872,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -886,7 +886,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <prayer-walk-planner> custom element. -->
<script src="../prayer-walk-planner.js" integrity="sha384-l3nBNqhx1fG3fiLlkCtbIf5R66cNHhZfKs1nIsfznYgpcBsSfG+vUqjAzloqqEwk" crossorigin="anonymous"></script>
<script src="../prayer-walk-planner.js" integrity="sha384-OTTFfQpIOOhwH3QUT2Mg2GN32VxfytqbJz9E4jwV48oMXe2iDp+x9EAMr7CDEr2C" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"region-dashboards","dataSource":"pray-tools","instanceId":"regions-dashboard-region-dashboards","assetsBase":"../../assets"};
var BASE_CONFIG = {"pinHint":false,"profile":"region-dashboards","dataSource":"pray-tools","instanceId":"regions-dashboard-region-dashboards","assetsBase":"../../assets"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
var TOKEN_FREE = false;

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"regions-list","dataSource":"pray-tools","instanceId":"regions-dashboard-regions-list","assetsBase":"../../assets"};
var BASE_CONFIG = {"pinHint":false,"profile":"regions-list","dataSource":"pray-tools","instanceId":"regions-dashboard-regions-list","assetsBase":"../../assets"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
var TOKEN_FREE = false;

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-WvWmE3YlemzQpGOlRSTFZurwYkIbPGz5GoXBMeHtmXgm4P1H/n+2wdGzjgXFQtxR" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-TsByiuoI9fzzdRROm6GocsQuBAYAJld4MuhMZlSLw0Y6Crnzqyoy8I+V15j8RTB9" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -56,7 +56,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"regions-mfe","dataSource":"pray-tools","instanceId":"regions-dashboard-regions-mfe","assetsBase":"../../assets"};
var BASE_CONFIG = {"pinHint":false,"profile":"regions-mfe","dataSource":"pray-tools","instanceId":"regions-dashboard-regions-mfe","assetsBase":"../../assets"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
var TOKEN_FREE = false;