Gospel page audio: choose-your-path narration (case, then Guilty or Not guilty tracks), branching player, three admin audio fields, generator --sample/--only; v2.41.0
Publish Container / build (push) Successful in 16m4s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 08:24:33 -04:00
1 parent ad1c3e35b1
commit ae0016cfee
9 files changed
+228 -55

No files matched your search

+97 -9
View File
@@ -643,6 +643,22 @@
#audio-seek { flex: 1; min-width: 0; accent-color: var(--gold); height: 1.6rem; cursor: pointer; }
.audio-time { font-family: 'Cinzel', serif; font-size: 0.8rem; min-width: 4.6rem; text-align: right; color: var(--parchment-dark); white-space: nowrap; }
@media (max-width: 600px) { .audio-controls { flex-wrap: wrap; } #audio-seek { flex-basis: 100%; order: 5; } .audio-time { margin-left: auto; } }
.audio-actions { margin-top: 1rem; padding-top: 1rem; border-top: 1px dashed rgba(184,134,11,0.5); }
.audio-actions[hidden] { display: none; }
.audio-q { font-family: 'Cinzel', serif; font-weight: 900; font-size: 1.25rem; letter-spacing: 0.06em; text-transform: uppercase; color: #fff; margin-bottom: 0.7rem; text-align: center; }
.audio-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; }
.audio-choice { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 1rem 0.8rem; font-family: 'Cinzel', serif; font-weight: 900; font-size: 1.1rem; letter-spacing: 0.08em; text-transform: uppercase; color: #1a0f05; background: linear-gradient(135deg, #f0c24a, var(--gold) 55%, #9a6f0a); border: 2px solid #f6d77a; border-radius: 3px; cursor: pointer; box-shadow: 0 0 22px rgba(240,194,74,0.3); transition: transform .15s, filter .15s; }
.audio-choice small { font-family: 'EB Garamond', serif; font-weight: 500; font-size: 0.95rem; letter-spacing: 0; text-transform: none; }
.audio-choice:hover { transform: translateY(-2px); filter: brightness(1.08); }
.audio-choice--alt { background: transparent; color: var(--parchment); border-color: var(--gold); box-shadow: none; }
.audio-choice--alt:hover { background: rgba(184,134,11,0.18); filter: none; }
.audio-actions-row { display: flex; flex-wrap: wrap; gap: 0.7rem; justify-content: center; }
.audio-actions-row .audio-choice { flex: 1 1 14rem; }
.audio-skip { margin-top: 0.8rem; text-align: center; }
.audio-skip[hidden] { display: none; }
.audio-link { background: none; border: 0; color: var(--parchment-dark); font-family: 'EB Garamond', serif; font-size: 1rem; font-style: italic; text-decoration: underline; cursor: pointer; }
@media (max-width: 600px) { .audio-choices { grid-template-columns: 1fr; } }
</style>
</head>
<body>
@@ -687,8 +703,8 @@
<div class="audio-top">
<span class="audio-icon" aria-hidden="true">🎧</span>
<div>
<p class="audio-title">Listen to the case</p>
<p class="audio-sub">The full case read aloud, about 10 minutes.</p>
<p class="audio-title" id="audio-title">Listen to the case</p>
<p class="audio-sub" id="audio-sub">The case read aloud, about 10 minutes. At the end you choose your plea.</p>
</div>
</div>
<audio id="audio-el" preload="none"></audio>
@@ -699,6 +715,8 @@
<span class="audio-time" id="audio-time">0:00</span>
<button type="button" class="audio-btn audio-speed" id="audio-speed" aria-label="Playback speed">1×</button>
</div>
<div class="audio-actions" id="audio-actions" hidden></div>
<p class="audio-skip" id="audio-skip-wrap"><button type="button" class="audio-link" id="audio-skip">Skip to the plea</button></p>
</div>
<section class="gospel-message" aria-labelledby="good-news">
@@ -1650,8 +1668,9 @@ buildTimeline();
});
// ── Audio narration (shown only when a URL is set in Admin → Gospel Page) ──
fetch('/api/salvation-audio').then(function (r) { return r.ok ? r.json() : { url: '' }; }).then(function (d) {
// ── Audio narration (shown only when URLs are set in Admin → Gospel Page) ──
// The listener hears the case, then chooses Guilty or Not guilty and hears that branch.
fetch('/api/salvation-audio').then(function (r) { return r.ok ? r.json() : {}; }).then(function (d) {
if (!d || !d.url) return;
const card = document.getElementById('audio-card');
const el = document.getElementById('audio-el');
@@ -1659,6 +1678,12 @@ buildTimeline();
const seek = document.getElementById('audio-seek');
const timeEl = document.getElementById('audio-time');
const speedBtn = document.getElementById('audio-speed');
const titleEl = document.getElementById('audio-title');
const subEl = document.getElementById('audio-sub');
const actions = document.getElementById('audio-actions');
const skipWrap = document.getElementById('audio-skip-wrap');
let part = 'case';
el.src = d.url;
card.hidden = false;
const fmt = function (t) { if (!isFinite(t)) return '0:00'; const m = Math.floor(t / 60); const s = Math.floor(t % 60); return m + ':' + (s < 10 ? '0' : '') + s; };
@@ -1666,14 +1691,75 @@ buildTimeline();
seek.value = el.duration ? (el.currentTime / el.duration) * 100 : 0;
timeEl.textContent = fmt(el.currentTime) + (el.duration ? ' / ' + fmt(el.duration) : '');
};
const setPlayIcon = function (playing) { playBtn.textContent = playing ? '❚❚' : '▶'; playBtn.setAttribute('aria-label', playing ? 'Pause' : 'Play'); };
const button = function (cls, html, onClick) {
const b = document.createElement('button');
b.type = 'button'; b.className = cls; b.innerHTML = html; b.addEventListener('click', onClick);
return b;
};
const show = function (nodes) { actions.innerHTML = ''; nodes.forEach(function (n) { actions.appendChild(n); }); actions.hidden = false; };
const hideActions = function () { actions.hidden = true; actions.innerHTML = ''; };
const reveal = function () { card.scrollIntoView({ behavior: 'smooth', block: 'center' }); };
function showChoice() {
skipWrap.hidden = true;
const wrap = document.createElement('div');
const q = document.createElement('p'); q.className = 'audio-q'; q.textContent = 'How do you plead?';
const row = document.createElement('div'); row.className = 'audio-choices';
row.appendChild(button('audio-choice', 'Guilty<small>I accept the pardon</small>', function () { plead('guilty'); }));
row.appendChild(button('audio-choice audio-choice--alt', 'Not guilty<small>I stand on my own record</small>', function () { plead('not-guilty'); }));
wrap.appendChild(q); wrap.appendChild(row);
show([wrap]);
}
function playPart(name, url, title, sub) {
part = name;
hideActions();
titleEl.textContent = title; subEl.textContent = sub;
el.src = url; el.playbackRate = el.playbackRate || 1;
el.play().catch(function () {});
}
function plead(value) {
skipWrap.hidden = true;
// Keep the page in step with the audio: select the plea and open its panel without scrolling away.
choose(value, { scroll: false, setSelect: true });
const url = value === 'guilty' ? d.guiltyUrl : d.notGuiltyUrl;
if (!url) { choose(value, { scroll: true, setSelect: true }); return; }
playPart(value === 'guilty' ? 'guilty' : 'not-guilty', url,
value === 'guilty' ? 'You pleaded guilty' : 'You pleaded not guilty',
value === 'guilty' ? 'Here is what happens next.' : 'Please be sure you understand what this means.');
reveal();
}
function onEnded() {
setPlayIcon(false);
if (part === 'case') { showChoice(); reveal(); return; }
const row = document.createElement('div'); row.className = 'audio-actions-row';
if (part === 'guilty') {
row.appendChild(button('audio-choice', 'Let us know', function () {
choose('guilty', { scroll: false, setSelect: true });
const pray = document.getElementById('btn-pray'); if (pray && pray.style.display !== 'none') pray.click();
document.getElementById('plea-guilty').scrollIntoView({ behavior: 'smooth', block: 'start' });
}));
row.appendChild(button('audio-choice audio-choice--alt', 'Read the record', function () { location.hash = '#record'; }));
} else {
row.appendChild(button('audio-choice', 'Change my plea to guilty', function () { plead('guilty'); }));
row.appendChild(button('audio-choice audio-choice--alt', 'Read the record', function () { location.hash = '#record'; }));
row.appendChild(button('audio-choice audio-choice--alt', 'Ask a question', function () { location.href = '/contact?source=salvation'; }));
}
show([row]); reveal();
}
playBtn.addEventListener('click', function () { if (el.paused) el.play(); else el.pause(); });
el.addEventListener('play', function () { playBtn.textContent = '❚❚'; playBtn.setAttribute('aria-label', 'Pause'); });
el.addEventListener('pause', function () { playBtn.textContent = '▶'; playBtn.setAttribute('aria-label', 'Play'); });
el.addEventListener('ended', function () { playBtn.textContent = '▶'; playBtn.setAttribute('aria-label', 'Play'); });
el.addEventListener('play', function () { setPlayIcon(true); });
el.addEventListener('pause', function () { setPlayIcon(false); });
el.addEventListener('ended', onEnded);
el.addEventListener('timeupdate', paint);
el.addEventListener('loadedmetadata', paint);
seek.addEventListener('input', function () { if (el.duration) el.currentTime = (seek.value / 100) * el.duration; });
document.getElementById('audio-back').addEventListener('click', function () { el.currentTime = Math.max(0, el.currentTime - 15); });
document.getElementById('audio-skip').addEventListener('click', function () { el.pause(); showChoice(); });
const speeds = [1, 1.25, 1.5, 0.85];
let si = 0;
speedBtn.addEventListener('click', function () { si = (si + 1) % speeds.length; el.playbackRate = speeds[si]; speedBtn.textContent = speeds[si] + '×'; });
@@ -1685,7 +1771,9 @@ buildTimeline();
const notGuilty = document.getElementById('plea-notguilty');
const cardG = document.getElementById('card-guilty');
const cardN = document.getElementById('card-notguilty');
function choose(value) {
function choose(value, opts) {
const o = opts || {};
if (o.setSelect) select.value = value;
guilty.hidden = value !== 'guilty';
notGuilty.hidden = value !== 'not-guilty';
cardG.classList.toggle('is-selected', value === 'guilty');
@@ -1693,7 +1781,7 @@ buildTimeline();
cardG.classList.toggle('is-dim', value === 'not-guilty');
cardN.classList.toggle('is-dim', value === 'guilty');
const panel = value === 'guilty' ? guilty : value === 'not-guilty' ? notGuilty : null;
if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (panel && o.scroll !== false) panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
select.addEventListener('change', function () { choose(select.value); });
document.getElementById('btn-change-plea').addEventListener('click', function () { select.value = 'guilty'; choose('guilty'); });