From 786a834610649fc95585f0c459cf9dcbf2dad188 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 18:23:04 +0000 Subject: [PATCH] Map errors in every language, and print buttons that show they are busy (UPG Neighbor, All People Groups, Prayer Walk Planner, Wall) --- .../all-people-groups/all-people-groups.vue | 4 +++- .../prayer-walk-planner/prayer-walk-planner.vue | 16 ++++++++++++++-- plugin-designs/upg-neighbor/src/campaigns.vue | 4 +++- plugin-designs/upg-neighbor/src/mobilize.vue | 4 +++- .../src/components/PeopleGroupsWall.vue | 12 ++++++++---- .../wall-of-people-groups/src/wall.css | 6 ++++++ 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/plugin-designs/all-people-groups/all-people-groups.vue b/plugin-designs/all-people-groups/all-people-groups.vue index 67697631d..cf01d3e27 100644 --- a/plugin-designs/all-people-groups/all-people-groups.vue +++ b/plugin-designs/all-people-groups/all-people-groups.vue @@ -31,6 +31,7 @@ */ import { inject, ref, computed, onMounted, onBeforeUnmount } from 'vue' import { useOptionalI18n } from '@map/composables/useOptionalT.js' +import { tStatic } from '@map/i18n/index.js' import { useMapInstance, engineGlobal } from '@map/composables/useMapInstance.js' import { resolveMapProvider, setProjectionOn } from '@map/composables/useMapProvider.js' import { useMapData } from '@map/composables/useMapData.js' @@ -354,7 +355,8 @@ onMounted(async () => { } // No map: if the pane is still here that is a real failure (token, container) and says so; if it // was unmounted while initialising, useMapInstance returned quietly and so do we. - if (!map.value) { if (mapContainer.value) mapError.value = 'Map failed to initialize — check token and container'; return } + // Shown after tr('loadError'); this detail used to be English in every locale. + if (!map.value) { if (mapContainer.value) mapError.value = tStatic('messages.mapUnavailable'); return } map.value.once('style.load', async () => { try { const rows = await rowsPromise diff --git a/plugin-designs/prayer-walk-planner/prayer-walk-planner.vue b/plugin-designs/prayer-walk-planner/prayer-walk-planner.vue index b9313bd45..b58c201b8 100644 --- a/plugin-designs/prayer-walk-planner/prayer-walk-planner.vue +++ b/plugin-designs/prayer-walk-planner/prayer-walk-planner.vue @@ -574,7 +574,14 @@ function onDeleteSelected() { } function onSelectAll() { polygons.forEach(p => { p.selected = true }); refreshMapSources() } function onClearSelection() { clearSelectionInternal() } -function onPrintClick() { printInternal(null) } +// Printing renders one off-screen map per selected zone and waits for its tiles: seconds, not a +// click. The button shows it is busy and cannot start a second print meanwhile. +const printBusy = ref(false) +async function onPrintClick() { + if (printBusy.value) return + printBusy.value = true + try { await printInternal(null) } finally { printBusy.value = false } +} function startRename(id) { editingTitleId.value = id } function commitRename(id, value) { const p = polygons.find(p => p.id === id) @@ -620,6 +627,10 @@ useShadowStyles( '.pwp-btn.is-active{background:#92b195;border-color:#92b195;color:#fff}' + '.pwp-btn.is-accent{background:#92b195;border-color:#92b195;color:#fff}' + '.pwp-btn.is-accent:hover{opacity:.9}' + + '.pwp-btn.busy{cursor:progress;opacity:.75}' + + ".pwp-btn.busy::after{content:'';display:inline-block;width:.8em;height:.8em;margin-inline-start:.5em;vertical-align:-.1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:pwp-spin .8s linear infinite}" + + '@keyframes pwp-spin{to{transform:rotate(360deg)}}' + + '@media (prefers-reduced-motion:reduce){.pwp-btn.busy::after{animation:none}}' + '.pwp-btn:disabled{opacity:.45;cursor:not-allowed}' + '.pwp-btn.is-danger{color:#8a3b3b;border-color:#e0c4c4}' + '.pwp-spacer{flex:1}' + @@ -692,7 +703,8 @@ useShadowStyles(
{{ tr('countSelected', { n: selectedCount, total: polygons.length }) }} - diff --git a/plugin-designs/upg-neighbor/src/campaigns.vue b/plugin-designs/upg-neighbor/src/campaigns.vue index 634386eb5..310adc160 100644 --- a/plugin-designs/upg-neighbor/src/campaigns.vue +++ b/plugin-designs/upg-neighbor/src/campaigns.vue @@ -14,6 +14,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { setProjectionOn } from '@map/composables/useMapProvider.js' import { whenStyleReady } from './whenStyleReady.js' import { useNeighborT } from './composables/useNeighborT.js' +import { tStatic } from '@map/i18n/index.js' import { loadNearData, E_COLORS, UNREACHED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js' const nt = useNeighborT() @@ -124,7 +125,8 @@ onMounted(async () => { .catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) }) await initializeMap() const m = map.value - if (!m) { mapError.value = nt('messages.mapUnavailable'); return } + // A shared key, so tStatic: nt() would prefix it with neighbor. and never resolve. + if (!m) { mapError.value = tStatic('messages.mapUnavailable'); return } whenStyleReady(m, () => { setProjectionOn(m, 'mercator'); repaint(); frame() m.on('click', 'cp-u', (ev) => { diff --git a/plugin-designs/upg-neighbor/src/mobilize.vue b/plugin-designs/upg-neighbor/src/mobilize.vue index 20b180a87..5c880ccc8 100644 --- a/plugin-designs/upg-neighbor/src/mobilize.vue +++ b/plugin-designs/upg-neighbor/src/mobilize.vue @@ -13,6 +13,7 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { setProjectionOn } from '@map/composables/useMapProvider.js' import { whenStyleReady } from './whenStyleReady.js' import { useNeighborT } from './composables/useNeighborT.js' +import { tStatic } from '@map/i18n/index.js' import { loadNearData, E_COLORS, UNREACHED, REACHED, SELECTED, NAVY, fold, fmtNum, downloadCsv } from './nearData.js' const nt = useNeighborT() @@ -216,7 +217,8 @@ onMounted(async () => { .catch((e) => { loadError.value = nt('neighbor.mob.loadError', { message: String(e.message || e) }) }) await initializeMap() const m = map.value - if (!m) { mapError.value = nt('messages.mapUnavailable'); return } + // A shared key, so tStatic: nt() would prefix it with neighbor. and never resolve. + if (!m) { mapError.value = tStatic('messages.mapUnavailable'); return } whenStyleReady(m, () => { setProjectionOn(m, 'mercator') repaint(); frame() diff --git a/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue b/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue index 356f440d5..6f5787aa4 100644 --- a/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue +++ b/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue @@ -7,7 +7,9 @@ {{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }} {{ isLandscape ? $t('wall.currentLandscape') : $t('wall.currentPortrait') }} - + + {{ $t('wall.groupsLoaded', { n: totalGroups }) }} @@ -42,6 +44,7 @@ export default { inject: { profileConfig: { default: null } }, data() { return { + printing: false, // the HD poster print is loading photos (drives the button's busy state) svg: null, zoom: null, cardWidth: 50, @@ -504,7 +507,7 @@ export default { // Reconcile which cards hold their HD photo with what the view needs now (wallLod.js). updateLod() { - if (!this._lodGrid || !this.svg || !this._imgNodes || this._printing) return; + if (!this._lodGrid || !this.svg || !this._imgNodes || this.printing) return; const el = this.svg.node(); const t = this._lodT || wallD3().zoomTransform(el); const r = el.getBoundingClientRect(); @@ -524,7 +527,8 @@ export default { // THE HD POSTER PATH: load every photo, wait for them, print, then go back to level of detail. async printWall() { const nodes = (this._imgNodes || []).filter(Boolean); - this._printing = true; + if (this.printing) return; + this.printing = true; await Promise.all(nodes.map((n) => new Promise((res) => { if (n.getAttribute('href')) return res(); n.addEventListener('load', res, { once: true }); @@ -533,7 +537,7 @@ export default { n.setAttribute('href', n.getAttribute('data-hd')); }))); try { window.print(); } finally { - this._printing = false; + this.printing = false; for (const n of nodes) n.removeAttribute('href'); this._hd = new Set(); this.updateLod(); diff --git a/plugin-designs/wall-of-people-groups/src/wall.css b/plugin-designs/wall-of-people-groups/src/wall.css index 14e9384c3..55a652acb 100644 --- a/plugin-designs/wall-of-people-groups/src/wall.css +++ b/plugin-designs/wall-of-people-groups/src/wall.css @@ -79,6 +79,12 @@ body { transition: all 0.2s; } +.control-btn.busy { opacity: .7; cursor: progress; } +.control-btn.busy::after { content: ''; display: inline-block; width: .8em; height: .8em; margin-inline-start: .5em; + vertical-align: -.1em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; + animation: wall-spin .8s linear infinite; } +@keyframes wall-spin { to { transform: rotate(360deg); } } +@media (prefers-reduced-motion: reduce) { .control-btn.busy::after { animation: none; } } .control-btn:hover { background: #0081d6; transform: translateY(-1px);