Gospel page audio: narration script, ElevenLabs generator, player on /salvation, admin Gospel Page setting; v2.40.0
Publish Container / build (push) Successful in 17m6s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 08:06:32 -04:00
1 parent 330c354d54
commit ad1c3e35b1
8 files changed
+293 -3

No files matched your search

+63
View File
@@ -628,6 +628,21 @@
@keyframes plea-glow { 0%, 100% { box-shadow: 0 0 0 4px rgba(184,134,11,0.3), 0 10px 30px rgba(0,0,0,0.5), 0 0 30px rgba(240,194,74,0.25); } 50% { box-shadow: 0 0 0 8px rgba(184,134,11,0.2), 0 10px 30px rgba(0,0,0,0.5), 0 0 55px rgba(240,194,74,0.55); } }
@media (prefers-reduced-motion: reduce) { .plea-big { animation: none; } }
@media (max-width: 600px) { .plea-big { display: block; padding: 1.15rem 0.6rem; font-size: 1.12rem; letter-spacing: 0.05em; white-space: nowrap; } .plea-big span { margin-left: 0.3rem; } }
.audio-card { margin: 1.5rem 0 0; padding: 1.1rem 1.3rem; background: linear-gradient(145deg, rgba(139,26,26,0.3), rgba(26,9,5,0.85)); border: 1px solid var(--gold); border-radius: 4px; color: var(--parchment); }
.audio-card[hidden] { display: none; }
.audio-top { display: flex; align-items: center; gap: 0.9rem; }
.audio-icon { font-size: 1.8rem; }
.audio-title { font-family: 'Cinzel', serif; font-weight: 700; font-size: 1.05rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--gold-light); }
.audio-sub { font-size: 1rem; opacity: 0.9; font-style: italic; }
.audio-controls { display: flex; align-items: center; gap: 0.7rem; margin-top: 0.9rem; }
.audio-btn { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.8rem; letter-spacing: 0.05em; color: var(--parchment); background: transparent; border: 1px solid rgba(184,134,11,0.6); border-radius: 2px; padding: 0.55rem 0.7rem; cursor: pointer; white-space: nowrap; }
.audio-btn:hover { background: rgba(184,134,11,0.18); }
.audio-play { width: 2.9rem; height: 2.9rem; border-radius: 50%; padding: 0; font-size: 1rem; color: #1a0f05; background: var(--gold); border-color: var(--gold); }
.audio-play:hover { background: var(--gold-light); }
#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; } }
</style>
</head>
<body>
@@ -668,6 +683,24 @@
</div>
<div class="panel" id="panel-case" role="tabpanel" aria-labelledby="tab-case">
<div class="audio-card" id="audio-card" hidden>
<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>
</div>
</div>
<audio id="audio-el" preload="none"></audio>
<div class="audio-controls">
<button type="button" class="audio-btn audio-play" id="audio-play" aria-label="Play">▶</button>
<button type="button" class="audio-btn" id="audio-back" aria-label="Back 15 seconds">↺ 15</button>
<input type="range" id="audio-seek" min="0" max="100" value="0" step="0.1" aria-label="Seek">
<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>
<section class="gospel-message" aria-labelledby="good-news">
<div class="docket" aria-hidden="true">
<span class="docket-scales">⚖</span>
@@ -1616,6 +1649,36 @@ 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) {
if (!d || !d.url) return;
const card = document.getElementById('audio-card');
const el = document.getElementById('audio-el');
const playBtn = document.getElementById('audio-play');
const seek = document.getElementById('audio-seek');
const timeEl = document.getElementById('audio-time');
const speedBtn = document.getElementById('audio-speed');
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; };
const paint = function () {
seek.value = el.duration ? (el.currentTime / el.duration) * 100 : 0;
timeEl.textContent = fmt(el.currentTime) + (el.duration ? ' / ' + fmt(el.duration) : '');
};
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('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); });
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] + '×'; });
}).catch(function () {});
// ── The plea ──
const select = document.getElementById('plea-select');
const guilty = document.getElementById('plea-guilty');