Wall of People Groups: Bless mode by default, a blessing verse under every people group, and Social mode one click away

This commit is contained in:
Doxa Maps 2026-09-28 19:20:55 +00:00
parent 988092d363
commit f4c3767784
3 changed files with 114 additions and 2 deletions

View file

@ -0,0 +1,73 @@
/**
* blessings.js — the verses the Wall's BLESS mode prints under each people group, and the poster's
* banner / sub-banner / footer text.
*
* SOURCE: the World English Bible, PUBLIC DOMAIN ("The World English Bible is in the Public Domain. You
* may copy and share it freely." — ebible.org/eng-web). Every text below was extracted mechanically
* from ebible.org/eng-web chapter pages (2026-09-28), never retyped or paraphrased. A card quotes a
* whole verse, or one whole sentence of it (marked "b" in the reference), always with its reference.
*
* THE DIVINE NAME IS ONE SWITCH. The WEB prints "Yahweh"; the WEB British Edition (also public domain)
* prints "the LORD" / "The LORD" in the same places. Texts carry {NAME} (mid-sentence) and {Name}
* (sentence start); change DIVINE_NAME below to switch every verse at once. (If switching, re-verify the
* handful of verses against the British Edition text: it also uses British spelling.)
*/
export const DIVINE_NAME = { mid: 'Yahweh', start: 'Yahweh' } // British Edition: { mid: 'the LORD', start: 'The LORD' }
const withName = (s) => s.replace(/\{Name\}/g, DIVINE_NAME.start).replace(/\{NAME\}/g, DIVINE_NAME.mid)
// Blessings spoken over a people, then the nations' praise. Assigned by group index, so a group keeps
// its verse and neighbours alternate.
const VERSES = [
{ ref: 'Numbers 6:24', text: '{Name} bless you, and keep you.' },
{ ref: 'Numbers 6:25', text: '{Name} make his face to shine on you, and be gracious to you.' },
{ ref: 'Numbers 6:26', text: '{Name} lift up his face toward you, and give you peace.' },
{ ref: 'Psalm 134:3', text: 'May {NAME} bless you from Zion, even he who made heaven and earth.' },
{ ref: 'Psalm 115:15', text: 'Blessed are you by {NAME}, who made heaven and earth.' },
{ ref: 'Psalm 29:11', text: '{Name} will give strength to his people. {Name} will bless his people with peace.' },
{ ref: 'Psalm 121:7', text: '{Name} will keep you from all evil. He will keep your soul.' },
{ ref: 'Psalm 121:8', text: '{Name} will keep your going out and your coming in, from this time forward, and forever more.' },
{ ref: 'Psalm 67:1', text: 'May God be merciful to us, bless us, and cause his face to shine on us.' },
{ ref: 'Psalm 67:7', text: 'God will bless us. All the ends of the earth shall fear him.' },
{ ref: 'Psalm 22:27', text: 'All the ends of the earth shall remember and turn to {NAME}. All the relatives of the nations shall worship before you.' },
{ ref: 'Psalm 86:9', text: 'All nations you have made will come and worship before you, Lord. They shall glorify your name.' },
{ ref: 'Psalm 117:1', text: 'Praise {NAME}, all you nations! Extol him, all you peoples!' },
{ ref: 'Psalm 72:17b', text: 'All nations will call him blessed.' },
{ ref: 'Genesis 12:3b', text: 'All the families of the earth will be blessed through you.' },
]
/** The verse for the group at index i: { text, ref }. */
export function blessingFor(i) {
const v = VERSES[((i % VERSES.length) + VERSES.length) % VERSES.length]
return { text: withName(v.text), ref: v.ref }
}
export const BLESSING_COUNT = VERSES.length
// The poster. The banner is the Driver's own title; the footer verse is his DECIDE (sheet row 99):
// Proverbs 11:11 is recommended (the blessing verse; it says "city"), and Proverbs 14:34 is the
// alternative (the "nation" verse, about righteousness).
export const POSTER = {
banner: 'Bless the nations in Jesus’ name',
subBanner: { ref: 'Proverbs 18:21', text: 'Death and life are in the power of the tongue; those who love it will eat its fruit.' },
footer: { ref: 'Proverbs 11:11', text: 'By the blessing of the upright, the city is exalted, but it is overthrown by the mouth of the wicked.' },
footerAlt: { ref: 'Proverbs 14:34', text: 'Righteousness exalts a nation, but sin is a disgrace to any people.' },
}
/** Greedy word wrap to at most maxLines lines of ~perLine characters (for SVG/canvas text). */
export function wrapText(text, perLine, maxLines = 4) {
const words = String(text).split(/\s+/)
const lines = []
let cur = ''
for (const w of words) {
if (!cur) cur = w
else if ((cur + ' ' + w).length <= perLine) cur += ' ' + w
else { lines.push(cur); cur = w }
}
if (cur) lines.push(cur)
if (lines.length > maxLines) {
const kept = lines.slice(0, maxLines)
kept[maxLines - 1] = kept[maxLines - 1].replace(/\s*\S*$/, '') + '…'
return kept
}
return lines
}

View file

@ -1,7 +1,7 @@
<template>
<div class="wall-container">
<div class="wall-header">
<h1>{{ $t('wall.title') }}</h1>
<h1>{{ mode === 'bless' ? $t('wall.blessBanner') : $t('wall.title') }}</h1>
<div class="wall-controls">
<button @click="toggleOrientation" class="control-btn orientation-btn">
<span class="btn-label">{{ isLandscape ? $t('wall.toPortrait') : $t('wall.toLandscape') }}</span>
@ -11,6 +11,12 @@
start a second load while the first is still running. -->
<button @click="printWall" class="control-btn" :class="{ busy: printing }" :disabled="printing" :aria-busy="printing ? 'true' : 'false'">{{ $t('wall.printPoster') }}</button>
<button @click="resetZoom" class="control-btn">{{ $t('wall.resetZoom') }}</button>
<!-- BLESS (default): name, photo and a blessing verse under every group, made to print.
SOCIAL: the interactive card (pray, comments, share). -->
<div class="mode-toggle" role="group" :aria-label="$t('wall.modeLabel')">
<button type="button" class="control-btn" :class="{ on: mode === 'bless' }" :aria-pressed="mode === 'bless' ? 'true' : 'false'" @click="setMode('bless')">{{ $t('wall.modeBless') }}</button>
<button type="button" class="control-btn" :class="{ on: mode === 'social' }" :aria-pressed="mode === 'social' ? 'true' : 'false'" @click="setMode('social')">{{ $t('wall.modeSocial') }}</button>
</div>
<span class="group-count">{{ $t('wall.groupsLoaded', { n: totalGroups }) }}</span>
</div>
</div>
@ -37,6 +43,7 @@
<script>
import { lodPlan } from '../wallLod.js'
import { wallD3 } from '../wallD3.js'
import { blessingFor, wrapText } from '../blessings.js'
// Wall of People Groups - D3 Zoomable Whiteboard
export default {
name: 'PeopleGroupsWall',
@ -44,6 +51,7 @@ export default {
inject: { profileConfig: { default: null } },
data() {
return {
mode: 'bless', // 'bless' (default) | 'social'
printing: false, // the HD poster print is loading photos (drives the button's busy state)
svg: null,
zoom: null,
@ -296,7 +304,7 @@ export default {
});
// Add status (moved down to accommodate 2-line names)
cardGroups
if (this.mode !== 'bless') cardGroups
.append('text')
.attr('x', cardWidth / 2)
.attr('y', this.cardHeight * 0.14)
@ -363,6 +371,26 @@ export default {
this.$nextTick(() => { this.applyStartView(); this.updateLod(); });
// Add action buttons row with FontAwesome icons - Instagram style
// BLESS: a verse under the photo instead of the buttons and counters (blessings.js, WEB, verbatim).
if (this.mode === 'bless') {
const vFont = Math.max(3, cardWidth * 0.052);
const refFont = vFont * 0.82;
const perLine = Math.max(10, Math.floor(cardWidth / (vFont * 0.5)));
const vy = imageY + imageHeight + vFont * 1.5;
cardGroups.each(function(d, i) {
const b = blessingFor(i);
const lines = wrapText(b.text, perLine, 4);
const g = wallD3().select(this);
const t = g.append('text').attr('class', 'bless-verse').attr('text-anchor', 'middle')
.attr('font-size', vFont).attr('fill', '#262626').attr('font-style', 'italic').style('user-select', 'text');
lines.forEach((ln, k) => t.append('tspan').attr('x', cardWidth / 2).attr('y', vy + k * vFont * 1.25).text(ln));
g.append('text').attr('class', 'bless-ref').attr('x', cardWidth / 2)
.attr('y', vy + lines.length * vFont * 1.25 + refFont * 0.5).attr('text-anchor', 'middle')
.attr('font-size', refFont).attr('fill', '#8e8e8e').text(b.ref);
});
return;
}
const buttonY = this.cardHeight * 0.72;
const buttonSize = iconSize * 0.7; // Make buttons smaller (was iconSize)
const buttonSpacing = buttonSize * 1.6; // Tighter spacing
@ -444,6 +472,12 @@ export default {
},
setMode(m) {
if (this.mode === m) return;
this.mode = m;
// Redraw the cards in the new mode; zoom, the level-of-detail state and the photos come back as before.
if (this.svg) { this.svg.select('.cards-container').selectAll('*').remove(); this.renderCards(); }
},
handlePrayer(group) {
// This method is deprecated - now using openPrayerDialog
this.openPrayerDialog(group);

View file

@ -85,6 +85,11 @@ body {
animation: wall-spin .8s linear infinite; }
@keyframes wall-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .control-btn.busy::after { animation: none; } }
.mode-toggle { display: inline-flex; gap: 0; }
.mode-toggle .control-btn { border-radius: 0; }
.mode-toggle .control-btn:first-child { border-start-start-radius: 6px; border-end-start-radius: 6px; }
.mode-toggle .control-btn:last-child { border-start-end-radius: 6px; border-end-end-radius: 6px; }
.mode-toggle .control-btn.on { background: #262626; color: #fff; }
.control-btn:hover {
background: #0081d6;
transform: translateY(-1px);