Wall of People Groups, Social: comments say "coming soon", and the heart links to the group's prayer sign-up

- COMMENT (and "View prayers") showed an empty comments board; it now says "Comments are coming soon."
  The unreachable CommentsDialog is removed.
- HEART: "Yes, I'm praying" still stores the prayer in this browser (localStorage, wall-pg:prayers), and the
  dialog now links "Sign up to pray for <group>" to the group's sign-up on pray.doxa.life
  (https://pray.doxa.life/<slug>#signup-section, the page's own sign-up section), in a new tab.
- The prayer dialog scrolls when it is taller than the window: it clipped at 90vh with overflow hidden,
  so on a laptop everything under the photo could not be reached. The photo now gives way (at most 38vh),
  so the question, "Yes, I'm praying" and the sign-up link all show at 1440 x 900.
- SHARE already shares the group's pray.doxa.life page (unchanged).

The two sentences live in a NEW area, wallSocial (11 locales), which only this bundle names (0 other
bundles carry it). Exercised at 1440 in en and ar: the toast and the link (href, new tab, noopener).
This commit is contained in:
Doxa Maps 2026-10-01 05:44:40 +00:00
parent b61cc36492
commit 192c652ad4
20 changed files with 125 additions and 274 deletions

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "سجّل للصلاة من أجل {name}",
"commentsSoon": "التعليقات قادمة قريبًا."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Melden Sie sich an, um für {name} zu beten",
"commentsSoon": "Kommentare folgen in Kürze."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Sign up to pray for {name}",
"commentsSoon": "Comments are coming soon."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Inscríbase para orar por {name}",
"commentsSoon": "Los comentarios llegarán pronto."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Inscrivez-vous pour prier pour {name}",
"commentsSoon": "Les commentaires arrivent bientôt."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "{name} के लिए प्रार्थना करने हेतु पंजीकरण करें",
"commentsSoon": "टिप्पणियाँ जल्द ही आ रही हैं।"
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Iscriviti per pregare per {name}",
"commentsSoon": "I commenti arriveranno presto."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Inscreva-se para orar por {name}",
"commentsSoon": "Os comentários chegarão em breve."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Înscrieți-vă să vă rugați pentru {name}",
"commentsSoon": "Comentariile vor fi disponibile în curând."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "Запишитесь, чтобы молиться за {name}",
"commentsSoon": "Комментарии скоро появятся."
}

View file

@ -0,0 +1,4 @@
{
"signUpToPray": "报名为{name}祷告",
"commentsSoon": "评论功能即将推出。"
}

View file

@ -1,217 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay" @click="closeDialog">
<div class="dialog-container" @click.stop ref="dialogBox" role="dialog" aria-modal="true" :aria-label="$t('wall.chatsTitle')" tabindex="-1">
<!-- Dialog Header -->
<div class="dialog-header">
<h2>{{ $t('wall.chatsTitle') }}</h2>
<button class="close-btn" :aria-label="$t('wall.close')" @click="closeDialog">✕</button>
</div>
<!-- People Group Image (Instagram style) -->
<div class="group-image-container">
<img
:src="getImageUrl()"
:alt="group?.name || $t('wall.peopleGroup')"
class="group-image"
@error="handleImageError"
/>
</div>
<div class="group-name-dialog">{{ group?.name || $t('wall.unknownGroup') }}</div>
<!-- People Group Info -->
<div class="dialog-content">
<div class="group-info">
<div class="info-row">
<strong>{{ $t('wall.status') }}</strong> {{ group?.status || $t('wall.unknown') }}
</div>
<div class="info-row">
<strong>{{ $t('wall.id') }}</strong> {{ group?.ID || $t('wall.na') }}
</div>
<div class="info-row" v-if="group?.lat && group?.lng">
<strong>{{ $t('wall.location') }}</strong> {{ group.lat }}, {{ group.lng }}
</div>
<div class="info-row prayer-stat">
<span class="heart-icon">❤️</span>
{{ $t('wall.havePrayed', { n: prayerCount }, prayerCount) }}
</div>
</div>
<!-- Prayer Button -->
<div class="prayer-action">
<button
class="prayer-btn"
@click="addPrayer"
:class="{ active: justPrayed }"
>
{{ justPrayed ? $t('wall.youPrayed') : $t('wall.prayForGroup') }}
</button>
</div>
<!-- Comments Section -->
<div class="comments-section">
<h3>{{ $t('wall.prayersComments') }}</h3>
<!-- Comment List -->
<div class="comments-list">
<div
v-if="comments.length === 0"
class="no-comments"
>
{{ $t('wall.noComments') }}
</div>
<div
v-for="comment in comments"
:key="comment.id"
class="comment-item"
>
<div class="comment-header">
<span class="comment-icon">💭</span>
<span class="comment-date">{{ formatDate(comment.timestamp) }}</span>
</div>
<div class="comment-text">{{ comment.text }}</div>
</div>
</div>
<!-- Add Comment Form -->
<div class="add-comment">
<textarea
v-model="newComment"
:placeholder="$t('wall.commentPlaceholder')"
rows="3"
maxlength="500"
></textarea>
<div class="comment-actions">
<span class="char-count">{{ newComment.length }}/500</span>
<button
@click="submitComment"
:disabled="!newComment.trim()"
class="submit-btn"
>
{{ $t('wall.postComment') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { trapDialog } from '../trapDialog.js'
// No third-party placeholder service: a missing photo is drawn inline.
const NO_IMAGE = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400'%3E%3Crect width='100%25' height='100%25' fill='%23f3f3f1'/%3E%3Ctext x='50%25' y='50%25' fill='%236d766e' font-family='sans-serif' font-size='22' text-anchor='middle' dominant-baseline='middle'%3ENo image available%3C/text%3E%3C/svg%3E"
// Comments and Prayers Dialog Component
export default {
name: 'CommentsDialog',
props: {
group: {
type: Object,
default: null
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
newComment: '',
justPrayed: false,
imageError: false
};
},
computed: {
prayerCount() {
if (!this.group || !this.$store) return 0;
// Access the prayers state directly
return this.$store.prayers?.[this.group.ID] || 0;
},
comments() {
if (!this.group || !this.$store) return [];
// Access the comments state directly
return this.$store.comments?.[this.group.ID] || [];
}
},
beforeUnmount() { this._releaseTrap?.(); this._releaseTrap = null; },
methods: {
closeDialog() {
this.$emit('close');
this.newComment = '';
this.justPrayed = false;
},
addPrayer() {
if (this.group && this.$store) {
this.$store.addPrayer(this.group.ID);
this.justPrayed = true;
// Reset after 2 seconds
setTimeout(() => {
this.justPrayed = false;
}, 2000);
}
},
submitComment() {
if (this.newComment.trim() && this.group && this.$store) {
this.$store.addComment(
this.group.ID,
this.newComment.trim()
);
this.newComment = '';
}
},
formatDate(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
// The reader's language, from the platform: "just now" / "5 minutes ago" / "hace 5 minutos" …
// (Intl.RelativeTimeFormat), so no hand-built English "m ago" and no catalogue keys to drift.
const locale = String(this.$i18n?.locale || 'en');
let rtf = null;
try { rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); } catch (_) { /* old engine: fall back below */ }
if (rtf) {
if (diffMins < 1) return rtf.format(0, 'second');
if (diffMins < 60) return rtf.format(-diffMins, 'minute');
if (diffHours < 24) return rtf.format(-diffHours, 'hour');
if (diffDays < 7) return rtf.format(-diffDays, 'day');
}
return date.toLocaleDateString(locale);
},
getImageUrl() {
if (this.imageError) {
return NO_IMAGE;
}
// The card's own photo (the live API's image_url); uupg.doxa.life no longer answers.
return this.group?.imageUrl || NO_IMAGE;
},
handleImageError(event) {
this.imageError = true;
event.target.src = NO_IMAGE;
}
},
watch: {
visible(newVal) {
if (newVal) this.$nextTick(() => { this._releaseTrap?.(); this._releaseTrap = trapDialog(this.$refs.dialogBox, () => this.closeDialog()); });
else { this._releaseTrap?.(); this._releaseTrap = null; }
if (newVal) {
// Reset state when dialog opens
this.justPrayed = false;
}
}
}
};
</script>

View file

@ -294,8 +294,9 @@ export default {
this.$emit('open-prayer', group);
},
openComments(group) {
this.$emit('open-comments', group);
// Comments are not built yet: the comment and "view prayers" taps say so instead of opening an empty board.
openComments() {
this.flash(this.$t('wallSocial.commentsSoon'), 3000);
},
truncateText(text, maxLength) {

View file

@ -33,7 +33,10 @@
</button>
</div>
<div v-else class="prayer-confirmed">
<!-- The group's own sign-up on pray.doxa.life (its #signup-section): commit to pray for it there. -->
<a v-if="signUpUrl" :href="signUpUrl" target="_blank" rel="noopener noreferrer" class="signup-link">{{ $t('wallSocial.signUpToPray', { name: group?.name || '' }) }}</a>
<div v-if="hasPrayed" class="prayer-confirmed">
<div class="confirmed-message">
<svg class="heart-icon" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5s-7.2-4.4-9.2-8.8C1.3 8.3 3.2 4.6 6.7 4.6c2 0 3.5 1.1 4.3 2.4.8-1.3 2.3-2.4 4.3-2.4 3.5 0 5.4 3.7 3.9 7.1-2 4.4-9.2 8.8-9.2 8.8z" fill="#ed4956" stroke="#ed4956" stroke-width="1.8" stroke-linejoin="round"/></svg>
<span>{{ $t('wall.thankYou') }}</span>
@ -84,6 +87,13 @@ export default {
imageError: false
};
},
computed: {
// The group's prayer sign-up on pray.doxa.life (the #signup-section of its page); none without a slug.
signUpUrl() {
const slug = this.group && this.group.slug
return slug ? `https://pray.doxa.life/${encodeURIComponent(slug)}#signup-section` : ''
},
},
watch: {
isOpen(newVal) {
if (newVal) this.$nextTick(() => { this._releaseTrap?.(); this._releaseTrap = trapDialog(this.$refs.dialogBox, () => this.closeDialog()); });

View file

@ -275,6 +275,12 @@ body {
}
/* Group Image in Dialog */
/* The prayer dialog SCROLLS when it is taller than the window: .dialog-container clips at 90vh, which hid
everything under the photo on a laptop screen (the sign-up link, and the prayer box after "Yes"). The photo
also gives way, so the prompt and its actions show without scrolling at 900 px. */
.dialog-container.prayer-dialog { overflow-y: auto; overscroll-behavior: contain; }
.prayer-dialog .group-image-container img { max-height: min(400px, 38vh); }
.group-image-container {
width: 100%;
background-color: #fafafa;
@ -513,6 +519,20 @@ body {
font-weight: 600;
}
/* The group's sign-up on pray.doxa.life, under the prayer prompt: a quiet full-width link. */
.signup-link {
display: block;
margin: 12px 0 4px;
padding: 10px 14px;
border: 1px solid #c9bfa4;
border-radius: 8px;
color: #6b4a78;
font-weight: 600;
text-align: center;
text-decoration: none;
}
.signup-link:hover, .signup-link:focus-visible { background: #f7f4ec; }
.prayer-confirm-btn {
padding: 14px 28px;
background: var(--heart-color);

View file

@ -18,29 +18,23 @@
* (src/wallD3.js). Font Awesome is no longer used: the canvas draws its own icons.
*/
import PeopleGroupsWall from './src/components/PeopleGroupsWall.vue'
import CommentsDialog from './src/components/CommentsDialog.vue'
import PrayerDialog from './src/components/PrayerDialog.vue'
import { ensureD3 } from './src/wallD3.js'
export default {
name: 'WallOfPeopleGroups',
components: { PeopleGroupsWall, CommentsDialog, PrayerDialog },
components: { PeopleGroupsWall, PrayerDialog },
data() {
return { ready: false, error: null, commentsDialogVisible: false, prayerDialogVisible: false, selectedGroup: null }
return { ready: false, error: null, prayerDialogVisible: false, selectedGroup: null }
},
mounted() {
ensureD3().then(() => { this.ready = true }, (e) => { this.error = e.message })
},
methods: {
openCommentsDialog(group) { this.selectedGroup = group; this.commentsDialogVisible = true },
closeCommentsDialog() {
this.commentsDialogVisible = false
setTimeout(() => { this.selectedGroup = null }, 300)
},
openPrayerDialog(group) { this.selectedGroup = group; this.prayerDialogVisible = true },
closePrayerDialog() {
this.prayerDialogVisible = false
setTimeout(() => { if (!this.commentsDialogVisible) this.selectedGroup = null }, 300)
setTimeout(() => { this.selectedGroup = null }, 300)
},
handlePrayerConfirmed() { /* PrayerDialog already updated the store */ },
},
@ -49,9 +43,8 @@ export default {
<template>
<div class="app-container">
<people-groups-wall v-if="ready" @open-comments="openCommentsDialog" @open-prayer="openPrayerDialog" />
<people-groups-wall v-if="ready" @open-prayer="openPrayerDialog" />
<div v-else-if="error" class="wall-error">{{ $t('wall.drawLibError') }} {{ error }}</div>
<comments-dialog :visible="commentsDialogVisible" :group="selectedGroup" @close="closeCommentsDialog" />
<prayer-dialog :is-open="prayerDialogVisible" :group="selectedGroup" @close="closePrayerDialog" @prayer-confirmed="handlePrayerConfirmed" />
</div>
</template>

View file

@ -1537,7 +1537,7 @@
"bundle": "wall-of-people-groups",
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
"integrity": "sha384-fa35skhhHsPhVFp2WGniSRI7JhsnghiaoLx1G1+pTYmXcxjD5g0E4aSnbeuFdiQM",
"integrity": "sha384-AXqzXEH2n75QYsrnUifY9pbUEIDPDv/bDdGiWMWjpUaJuxx1y6WfHIsRbKFOrv5W",
"elements": [
"wall-of-people-groups"
],
@ -1557,7 +1557,7 @@
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/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=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-fa35skhhHsPhVFp2WGniSRI7JhsnghiaoLx1G1+pTYmXcxjD5g0E4aSnbeuFdiQM\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-AXqzXEH2n75QYsrnUifY9pbUEIDPDv/bDdGiWMWjpUaJuxx1y6WfHIsRbKFOrv5W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
},
"bundle": "wall-of-people-groups"
}

View file

@ -1537,7 +1537,7 @@
"bundle": "wall-of-people-groups",
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"output": "dx-maps/wall-of-people-groups/wall-of-people-groups.js",
"integrity": "sha384-fa35skhhHsPhVFp2WGniSRI7JhsnghiaoLx1G1+pTYmXcxjD5g0E4aSnbeuFdiQM",
"integrity": "sha384-AXqzXEH2n75QYsrnUifY9pbUEIDPDv/bDdGiWMWjpUaJuxx1y6WfHIsRbKFOrv5W",
"elements": [
"wall-of-people-groups"
],
@ -1557,7 +1557,7 @@
"scriptUrl": "./wall-of-people-groups/wall-of-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./wall-of-people-groups/wall-of-people-groups/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=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-fa35skhhHsPhVFp2WGniSRI7JhsnghiaoLx1G1+pTYmXcxjD5g0E4aSnbeuFdiQM\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./wall-of-people-groups/wall-of-people-groups.js\" integrity=\"sha384-AXqzXEH2n75QYsrnUifY9pbUEIDPDv/bDdGiWMWjpUaJuxx1y6WfHIsRbKFOrv5W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<wall-of-people-groups profile-config='{\"profile\":\"wall-of-people-groups\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></wall-of-people-groups>"
},
"bundle": "wall-of-people-groups"
}

File diff suppressed because one or more lines are too long

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <wall-of-people-groups> custom element. -->
<script src="../wall-of-people-groups.js" integrity="sha384-fa35skhhHsPhVFp2WGniSRI7JhsnghiaoLx1G1+pTYmXcxjD5g0E4aSnbeuFdiQM" crossorigin="anonymous"></script>
<script src="../wall-of-people-groups.js" integrity="sha384-AXqzXEH2n75QYsrnUifY9pbUEIDPDv/bDdGiWMWjpUaJuxx1y6WfHIsRbKFOrv5W" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: