Ban Yaro Blues — Hymne in der WELT-Welt

Eigener Song (KI-Demo via Suno) als Marken-Hymne. Dezente Player-Karte unter
dem Tageszitat; preload=none → 6 MB MP3 lädt erst bei Play, der SW cacht sie
danach für offline. Der Banner ist einmalige Einladung und verschwindet nach
erstem Hören (durchgehört oder >30s + Pause); danach dezenter runder Play-Button
unten links als Gegenspieler zum FAB, nur in WELT. Audio-Element zentral in
index.html → übersteht Welt-Wechsel & Re-Renders.

„Gehört" wird hybrid gemerkt: localStorage (sofort/offline) + DB-Flag
anthem_heard am User (neue Spalte, über /auth/me, gesetzt via
POST /api/profile/anthem-heard) — geräte- und deploy-übergreifend, damit der
Banner nicht erneut nervt.
This commit is contained in:
rene 2026-06-14 21:33:23 +02:00
parent f7370028da
commit d0a76e1b54
11 changed files with 149 additions and 18 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1292'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1295'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -220,6 +220,7 @@ window.Worlds = (() => {
track.style.transform = `translateX(${-_cur * (100 / 3)}%)`;
_updateDots();
_updateFab();
_anthem.updateButton();
// Karte neu rendern nachdem Transition abgeschlossen
if (_cur === 2 && _map) {
setTimeout(() => _map.invalidateSize(), animated ? 380 : 50);
@ -1933,6 +1934,83 @@ window.Worlds = (() => {
{ t:"Ein Hund braucht keinen Kalender, er weiß genau, wann du nach Hause kommst.", a:"Unbekannt" },
];
// ── HYMNE (Ban Yaro Blues) ───────────────────────────────────
// Banner in der WELT-Welt lädt zum ersten Hören ein und verschwindet danach.
// Ab dann: dezenter runder Play-Button unten links (Gegenspieler zum FAB).
// Audio-Element lebt zentral in index.html → übersteht Re-Renders & Welt-Wechsel.
const _anthem = (() => {
const KEY = 'by_anthem_heard';
let _bound = false;
const _audio = () => document.getElementById('anthem-audio');
// Gehört? Server-Flag (geräteübergreifend, deploy-fest) ODER lokal (sofort/offline).
const heard = () => {
if (_state?.user?.anthem_heard) return true;
try { return localStorage.getItem(KEY) === '1'; } catch (_) { return false; }
};
function _markHeard() {
try { localStorage.setItem(KEY, '1'); } catch (_) {}
if (!_state?.user?.anthem_heard) { // server-seitig genau einmal merken
if (_state?.user) _state.user.anthem_heard = 1;
try { API.post('/profile/anthem-heard', {}).catch(() => {}); } catch (_) {}
}
document.getElementById('ww-anthem-card')?.classList.add('hidden'); // Banner live ausblenden
updateButton();
}
function _sync() {
const a = _audio();
const playing = !!(a && !a.paused && !a.ended);
document.getElementById('ww-anthem-icon')?.querySelector('use')
?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
const sub = document.getElementById('ww-anthem-sub');
if (sub) sub.textContent = playing ? 'läuft … (tippen zum Pausieren)' : 'unsere Hymne · anhören';
const btn = document.getElementById('worlds-anthem');
if (btn) {
btn.classList.toggle('playing', playing);
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
}
}
function _bindAudio() {
if (_bound) return;
const a = _audio();
if (!a) return;
_bound = true;
a.addEventListener('play', _sync);
a.addEventListener('pause', () => { if (a.currentTime >= 30) _markHeard(); _sync(); });
a.addEventListener('ended', () => { _markHeard(); _sync(); });
}
function toggle() {
const a = _audio();
if (!a) return;
if (a.paused) a.play().catch(() => {});
else a.pause();
}
function updateButton() {
document.getElementById('worlds-anthem')?.classList.toggle('hidden', !(_cur === 2 && heard()));
}
// Bei jedem WELT-Render aufrufen: Audio-Listener sichern, Klicks binden, Status setzen.
function initWelt() {
_bindAudio();
const card = document.getElementById('ww-anthem-card');
if (card && !card._anthemBound) {
card._anthemBound = true;
card.addEventListener('click', toggle);
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
}
const btn = document.getElementById('worlds-anthem');
if (btn && !btn._anthemBound) { btn._anthemBound = true; btn.addEventListener('click', toggle); }
_sync();
updateButton();
}
return { heard, toggle, updateButton, initWelt };
})();
function _renderWelt() {
const el = document.getElementById('ww-content');
if (!el) return;
@ -1959,6 +2037,19 @@ window.Worlds = (() => {
${_esc(quote.a)}
</div>
</div>
${_anthem.heard() ? '' : `
<div class="world-info-card" id="ww-anthem-card" role="button" tabindex="0"
style="margin-top:10px;display:flex;align-items:center;gap:14px;cursor:pointer">
<div style="width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,0.12);
display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ph-icon" id="ww-anthem-icon" style="width:20px;height:20px;color:#fff" aria-hidden="true"><use href="/icons/phosphor.svg#play"></use></svg>
</div>
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-sm);font-weight:600;color:#fff">Ban Yaro Blues</div>
<div id="ww-anthem-sub" style="font-size:11px;color:rgba(255,255,255,0.45);margin-top:2px">unsere Hymne · anhören</div>
</div>
<svg class="ph-icon" style="width:15px;height:15px;color:rgba(255,255,255,0.3);flex-shrink:0" aria-hidden="true"><use href="/icons/phosphor.svg#music-notes"></use></svg>
</div>`}
</div>
<div class="world-bottom">
<div class="world-chips-grid">
@ -1972,6 +2063,8 @@ window.Worlds = (() => {
</div>
`;
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
_anthem.initWelt();
}
// ── HELPERS ──────────────────────────────────────────────────