/* ============================================================ BAN YARO — Drei Welten Navigation JETZT | HUND | WELT — horizontales Swipe-System ============================================================ */ window.Worlds = (() => { let _state = null; let _cur = 1; // 0=JETZT 1=HUND 2=WELT let _visible = false; let _map = null; let _weltInited = false; let _refreshPending = false; // gesetzt wenn refresh() während !_visible aufgerufen wird let _lastUserId = undefined; let _dogs = []; // gecachte Hundesliste let _dogIdx = 0; // aktuell angezeigter Hund let _hasBgPhoto = false; // Hintergrund-Foto vorhanden? let _setupDone = false; // Swipe/Button-Listener nur einmal registrieren // Touch-Tracking const _t = { x:0, y:0, active:false, vert:null, moved:0 }; // ── OFFLINE-CACHE (localStorage) ──────────────────────────── // Letzten bekannten Zustand speichern → sofort zeigen, dann updaten function _wSave(key, data) { try { localStorage.setItem('w3_' + key, JSON.stringify({ ts: Date.now(), data })); } catch {} } function _wLoad(key) { try { const p = JSON.parse(localStorage.getItem('w3_' + key) || 'null'); return p ? { data: p.data, ageMin: Math.round((Date.now() - p.ts) / 60000) } : null; } catch { return null; } } // API-Aufruf mit Cache-Fallback: sofort Cache, dann Netz async function _cachedGet(cacheKey, path) { const cached = _wLoad(cacheKey); let fresh = null; try { fresh = await API.get(path); _wSave(cacheKey, fresh); } catch {} return { data: fresh ?? cached?.data ?? null, fromCache: !fresh, ageMin: fresh ? 0 : (cached?.ageMin ?? null) }; } // ── PUBLIC ────────────────────────────────────────────────── async function init(appState) { _state = appState; _lastUserId = undefined; // Neurender erzwingen _cur = 1; if (!_setupDone) { _setupDone = true; _setupSwipe(); _setupButtons(); _showSwipeHints(); } _goTo(_cur, false); show(); } function _showSwipeHints() { if (localStorage.getItem('worlds_swipe_seen')) return; localStorage.setItem('worlds_swipe_seen', '1'); const ov = document.getElementById('worlds-overlay'); if (!ov) return; const hint = document.createElement('div'); hint.style.cssText = [ 'position:absolute;inset:0;pointer-events:none;z-index:55', 'display:flex;align-items:center;justify-content:space-between', 'padding:0 8px;transition:opacity 1s ease', ].join(';'); const arrowStyle = ` display:flex;flex-direction:column;align-items:center;gap:4px; background:rgba(0,0,0,0.42);backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.18);border-radius:14px; padding:10px 10px;animation:worlds-pulse 1.2s ease infinite alternate; `; hint.innerHTML = `
JETZT
WELT
`; ov.appendChild(hint); setTimeout(() => { hint.style.opacity = '0'; }, 2800); setTimeout(() => hint.remove(), 3900); } function show(worldIdx) { const ov = document.getElementById('worlds-overlay'); if (!ov) return; ov.style.display = 'block'; requestAnimationFrame(() => ov.classList.add('worlds-visible')); _visible = true; document.getElementById('app-header')?.classList.add('worlds-hidden'); document.getElementById('bottom-nav')?.classList.add('worlds-hidden'); document.getElementById('worlds-back')?.classList.remove('worlds-back-visible'); if (worldIdx != null) _goTo(worldIdx, false); if (_cur === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } // Ausstehender Refresh (z.B. nach Foto-Upload während Worlds unsichtbar) if (_refreshPending) { _refreshPending = false; _renderJetzt(); _renderHund(); return; } // Nach Login/Logout: Config aus DB laden, dann rendern const currentUserId = _state?.user?.id ?? null; if (currentUserId !== _lastUserId) { _lastUserId = currentUserId; if (currentUserId) { _loadConfigFromServer().then(() => { _renderJetzt(); _renderHund(); }); } else { _cfgCache = null; _renderJetzt(); _renderHund(); } } } function hide() { const ov = document.getElementById('worlds-overlay'); if (!ov) return; ov.classList.remove('worlds-visible'); ov.style.display = 'none'; _visible = false; document.getElementById('app-header')?.classList.remove('worlds-hidden'); document.getElementById('bottom-nav')?.classList.remove('worlds-hidden'); document.getElementById('worlds-back')?.classList.add('worlds-back-visible'); } function navigateTo(pageId) { hide(); if (window.App?.navigate) window.App.navigate(pageId); } // ── SWIPE ──────────────────────────────────────────────────── function _setupSwipe() { const track = document.getElementById('worlds-track'); if (!track) return; track.addEventListener('touchstart', e => { _t.x = e.touches[0].clientX; _t.y = e.touches[0].clientY; _t.active = true; _t.vert = null; _t.moved = 0; track.style.transition = 'none'; }, { passive: true }); track.addEventListener('touchmove', e => { if (!_t.active) return; const dx = e.touches[0].clientX - _t.x; const dy = e.touches[0].clientY - _t.y; if (_t.vert === null) _t.vert = Math.abs(dy) > Math.abs(dx) + 4; if (_t.vert) return; e.preventDefault(); // Nicht über erste/letzte Seite hinausziehen const cdx = _cur === 0 ? Math.min(0, dx) : _cur === 2 ? Math.max(0, dx) : dx; _t.moved = cdx; const base = -_cur * (100 / 3); track.style.transform = `translateX(calc(${base}% + ${cdx}px))`; }, { passive: false }); track.addEventListener('touchend', () => { if (!_t.active || _t.vert) { _t.active = false; return; } _t.active = false; let next = _cur; if (_t.moved < -55 && _cur < 2) next = _cur + 1; else if (_t.moved > 55 && _cur > 0) next = _cur - 1; _goTo(next, true); if (next === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }); // Mausrad-Navigation (Desktop) let _wheelCooldown = false; track.addEventListener('wheel', e => { e.preventDefault(); if (_wheelCooldown) return; const next = e.deltaX > 30 || e.deltaY > 30 ? Math.min(2, _cur + 1) : e.deltaX < -30 || e.deltaY < -30 ? Math.max(0, _cur - 1) : _cur; if (next === _cur) return; _wheelCooldown = true; setTimeout(() => { _wheelCooldown = false; }, 500); _goTo(next, true); if (next === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }, { passive: false }); } function _goTo(idx, animated) { _cur = Math.max(0, Math.min(2, idx)); const track = document.getElementById('worlds-track'); if (!track) return; track.style.transition = animated ? 'transform 0.32s cubic-bezier(0.25, 0.46, 0.45, 0.94)' : 'none'; 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); } } function _updateDots() { document.querySelectorAll('.wdot').forEach((d, i) => d.classList.toggle('active', i === _cur)); document.querySelectorAll('.wlabel').forEach((l, i) => l.classList.toggle('active', i === _cur)); } function _fabOptions() { const worldNames = ['jetzt', 'hund', 'welt']; const chips = _chipsForWorld(worldNames[_cur]); const opts = []; for (const chip of chips) { if (chip.fab) for (const o of chip.fab) { if (opts.length < 6) opts.push(o); } } return opts; } function _updateFab() { const fab = document.getElementById('worlds-fab'); if (!fab) return; const opts = _fabOptions(); if (!opts.length) { fab.style.display = 'none'; return; } fab.style.display = ''; fab.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#paw-print`); fab.title = 'Schnellaktion'; } function _setupButtons() { document.getElementById('worlds-fab')?.addEventListener('click', _openFab); document.getElementById('worlds-back')?.addEventListener('click', () => { if (_state?.user) show(); else if (window.App) window.App.navigate('welcome'); }); document.querySelectorAll('.wdot').forEach((dot, i) => { dot.style.pointerEvents = 'auto'; dot.addEventListener('click', () => { _goTo(i, true); if (i === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }); }); document.querySelectorAll('.wlabel').forEach((lbl, i) => { lbl.style.pointerEvents = 'auto'; lbl.style.cursor = 'pointer'; lbl.addEventListener('click', () => { _goTo(i, true); if (i === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }); }); } function _openFab() { const options = _fabOptions(); const meldenPages = new Set(['poison','lost','recalls','map']); const meldenCount = options.filter(o => meldenPages.has(o.page)).length; const title = meldenCount > options.length / 2 ? 'Was möchtest du melden?' : 'Was möchtest du eintragen?'; const ov = document.createElement('div'); ov.id = 'fab-overlay'; ov.className = 'w3-sheet-overlay'; ov.innerHTML = `
${options.length ? title : 'Schnellzugriff'}
${options.map(o => ` `).join('')}
`; document.body.appendChild(ov); const _close = () => ov.remove(); ov.querySelector('#fab-backdrop').addEventListener('click', _close); ov.querySelector('#fab-close').addEventListener('click', _close); ov.querySelector('#fab-all-btn').addEventListener('click', () => { _close(); _openAllChips(); }); ov.querySelectorAll('.fab-option').forEach(btn => { btn.addEventListener('click', () => { _close(); const page = btn.dataset.page; const action = btn.dataset.action; if (action === 'quickGassi') { _openQuickGassi(); return; } navigateTo(page); if (action === 'openNew') { setTimeout(() => window.App?.callModule?.(page, 'openNew'), 400); } }); }); } function _openAllChips() { const worldNames = ['jetzt', 'hund', 'welt']; const worldLabels = { jetzt: 'JETZT', hund: 'HUND', welt: 'WELT' }; // Alle Seiten die aktuell in irgendeiner Welt konfiguriert sind const cfg = _getConfig(); const configured = new Set(worldNames.flatMap(w => cfg[w] || [])); const sections = worldNames.map(w => { const chips = (_DEFAULT_CONFIG[w] || []).map(_chipMeta) .filter(c => c && _chipAllowed(c) && !configured.has(c.page)); if (!chips.length) return ''; return `
${worldLabels[w]}
${chips.map(c => ` `).join('')}
`; }).join(''); const ov = document.createElement('div'); ov.id = 'fab-overlay'; ov.className = 'w3-sheet-overlay'; ov.innerHTML = `
Ausgeblendete Funktionen
${sections || `
Alle Funktionen sind bereits in deinen Welten sichtbar.
`}
Einzelne Funktionen ausblenden oder zwischen den Welten verschieben: in deinem Profil.
`; document.body.appendChild(ov); const _close = () => ov.remove(); ov.querySelector('#fab-backdrop').addEventListener('click', _close); ov.querySelector('#fab-close').addEventListener('click', _close); ov.querySelectorAll('.all-chip-btn').forEach(btn => { btn.addEventListener('click', () => { _close(); navigateTo(btn.dataset.page); }); }); ov.querySelector('#fab-all-goto-worlds')?.addEventListener('click', () => { _close(); _openConfigModal(); }); } // ── SCHNELL-GASSI ───────────────────────────────────────────── async function _openQuickGassi() { const dog = _dogs[_dogIdx] || null; if (!dog) { UI.toast?.error('Kein Hund gefunden. Bitte zuerst ein Profil anlegen.'); navigateTo('dog-profile'); return; } // Wetter aus Cache holen (kein Wait nötig) let weatherData = null; try { const wc = _wLoad('weather'); if (wc?.data) weatherData = wc.data; } catch {} let selectedMin = 30; const durations = [15, 30, 45, 60]; const ov = document.createElement('div'); ov.id = 'quick-gassi-overlay'; ov.className = 'w3-sheet-overlay'; ov.style.zIndex = '400'; const weatherLine = weatherData ? `
🌡 ${Math.round(weatherData.temp_c)}° · ${_esc(weatherData.desc?.split(' ')[0] || '')}
` : ''; ov.innerHTML = `
🐾 Schnell-Gassi
${_esc(dog.name)} · ohne GPS
${weatherLine}
Dauer
${durations.map(d => ` `).join('')}
`; document.body.appendChild(ov); const _close = () => ov.remove(); ov.querySelector('#qg-backdrop').addEventListener('click', _close); ov.querySelector('#qg-close').addEventListener('click', _close); // Dauer-Toggle ov.querySelectorAll('.qg-dur').forEach(btn => { btn.addEventListener('click', () => { selectedMin = parseInt(btn.dataset.min); ov.querySelectorAll('.qg-dur').forEach(b => { b.classList.toggle('active', parseInt(b.dataset.min) === selectedMin); }); }); }); // Eintragen ov.querySelector('#qg-submit').addEventListener('click', async () => { const submitBtn = ov.querySelector('#qg-submit'); submitBtn.disabled = true; submitBtn.textContent = 'Wird eingetragen…'; try { // Kein Tagebucheintrag — nur Streak pingen await API.post(`/streak/${dog.id}/ping`); _close(); UI.toast?.success(`Gassi eingetragen! ${selectedMin} min 🐾`); // Streak-Cache invalidieren try { localStorage.removeItem('w3_streak_' + dog.id); } catch {} // JETZT-Welt neu rendern für aktuellen Streak setTimeout(() => _renderJetzt(), 300); } catch (err) { submitBtn.disabled = false; submitBtn.innerHTML = ' Eintragen'; UI.toast?.error('Fehler beim Eintragen. Bitte erneut versuchen.'); } }); } // ── CHIP-KONFIGURATION ────────────────────────────────────── // Alle verfügbaren Chips mit Metadaten const _ALL_CHIPS = [ { icon:'note-pencil', label:'Notizblock', page:'notes', pro: true, fab:[{ icon:'note-pencil', color:'#10B981', label:'Neue Notiz', sub:'Schnellnotiz erstellen', page:'notes', action:'openNew' }] }, { icon:'currency-eur', label:'Ausgaben', page:'expenses', fab:[{ icon:'currency-eur', color:'#3B82F6', label:'Ausgabe eintragen', sub:'Einmalig oder Dauerauftrag', page:'expenses', action:'openNew' }] }, { icon:'first-aid', label:'Erste Hilfe', page:'erste-hilfe' }, { icon:'handshake', label:'Playdate', page:'playdate', pro: true, fab:[{ icon:'handshake', color:'#F59E0B', label:'Playdate anfragen', sub:'Treffen mit anderen Hunden', page:'playdate', action:'openNew' }] }, { icon:'chat-circle-dots', label:'Nachrichten', page:'chat', pro: true }, { icon:'sun', label:'Wetter', page:'wetter' }, { icon:'book-open', label:'Tagebuch', page:'diary', fab:[{ icon:'book-open', color:'#8B5CF6', label:'Tagebucheintrag', sub:'Erlebnis, Foto oder Notiz', page:'diary', action:'openNew' }] }, { icon:'heartbeat', label:'Gesundheit', page:'health', fab:[{ icon:'heartbeat', color:'#EF4444', label:'Tierarztbesuch', sub:'Befund oder Impfung eintragen', page:'health' }, { icon:'wave-sine', color:'#06B6D4', label:'Gewicht messen', sub:'Aktuelles Gewicht eintragen', page:'health' }] }, { icon:'target', label:'Übungen', page:'uebungen', fab:[{ icon:'target', color:'#F59E0B', label:'Training aufzeichnen', sub:'Übung absolviert', page:'uebungen' }] }, { icon:'list-checks', label:'Trainingspläne', page:'trainingsplaene', fab:[{ icon:'list-checks', color:'#10B981', label:'Plan erstellen', sub:'Neuen Trainingsplan anlegen', page:'trainingsplaene', action:'openNew' }] }, { icon:'heart', label:'Adoption', page:'adoption', fab:[{ icon:'heart', color:'#EF4444', label:'Hund anbieten', sub:'Zur Adoption freigeben', page:'adoption', action:'openNew' }] }, { icon:'house-line', label:'Sitting', page:'sitting', fab:[{ icon:'house-line', color:'#8B5CF6', label:'Sitter anfragen', sub:'Betreuung buchen', page:'sitting', action:'openNew' }] }, { icon:'books', label:'Wiki', page:'wiki' }, { icon:'scales', label:'Wurfbörse', page:'wurfboerse' }, { icon:'map-trifold', label:'Karte', page:'map', fab:[{ icon:'map-pin', color:'#10B981', label:'Ort vorschlagen', sub:'Neuen POI auf der Karte', page:'map' }] }, { icon:'push-pin', label:'Forum', page:'forum', fab:[{ icon:'push-pin', color:'#8B5CF6', label:'Forum-Beitrag', sub:'Thema oder Frage erstellen', page:'forum', action:'openNew' }] }, { icon:'users', label:'Freunde', page:'friends', fab:[{ icon:'users', color:'#3B82F6', label:'Freund einladen', sub:'Per Link einladen', page:'friends', action:'openNew' }] }, { icon:'paw-print', label:'Gassi', page:'walks', pro: true, fab:[{ icon:'paw-print', color:'#F59E0B', label:'Gassirunde', sub:'Neue Runde starten', page:'walks', action:'openNew' }, { icon:'paw-print', color:'#10B981', label:'Schnell-Gassi', sub:'Kurze Runde ohne GPS eintragen', page:'walks', action:'quickGassi' }] }, { icon:'skull', label:'Giftköder', page:'poison', fab:[{ icon:'skull', color:'#EF4444', label:'Giftköder melden', sub:'Warnung für andere Hundebesitzer', page:'poison' }] }, { icon:'warning-circle', label:'Rückrufe', page:'recalls', fab:[{ icon:'warning-circle', color:'#EF4444', label:'Rückruf melden', sub:'Produkt oder Futter', page:'recalls', action:'openNew' }] }, { icon:'dog', label:'Verlorene', page:'lost', fab:[{ icon:'dog', color:'#3B82F6', label:'Verlorenen melden', sub:'Hilf beim Wiederfinden', page:'lost' }] }, { icon:'path', label:'Routen', page:'routes', fab:[{ icon:'path', color:'#10B981', label:'Route aufzeichnen', sub:'GPS-Tracking starten', page:'routes', action:'openNew' }] }, { icon:'calendar-dots', label:'Events', page:'events', fab:[{ icon:'calendar-dots', color:'#06B6D4', label:'Event erstellen', sub:'Veranstaltung ankündigen', page:'events', action:'openNew' }] }, { icon:'sparkle', label:'Jobs', page:'jobs' }, { icon:'book-open', label:'Knigge', page:'knigge' }, { icon:'film-slate', label:'Filme', page:'movies' }, { icon:'certificate', label:'Züchter', page:'breeder-dashboard', role:'breeder', fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }, { icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] }, { icon:'sparkle', label:'Social', page:'social', role:'social', fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] }, { icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' }, { icon:'handshake', label:'Partner', page:'partner-dashboard', role:'partner' }, { icon:'gear', label:'Admin', page:'admin', role:'admin' }, // ── NEUE FEATURES ──────────────────────────────────────────── { icon:'fork-knife', label:'Ernährung', page:'ernaehrung', pro: true, fab:[{ icon:'fork-knife', color:'#F97316', label:'Futter-Tagebuch', sub:'Mahlzeit oder Futtercheck', page:'ernaehrung' }] }, { icon:'airplane', label:'Reise', page:'reise', pro: true }, { icon:'smiley', label:'Persönlichkeit', page:'personality' }, ]; const _DEFAULT_CONFIG = { jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','partner-dashboard','admin'], hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse', 'breeder-dashboard','ernaehrung','personality'], welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events', 'jobs','knigge','movies','reise'], }; // _cfgCache: wird beim Init aus DB geladen, Fallback localStorage → Default let _cfgCache = null; function _mergeDefaults(cfg) { const result = JSON.parse(JSON.stringify(cfg)); const hidden = new Set(result.hidden || []); // Chips die bereits einer Welt zugewiesen sind, nicht nochmal einfügen const allAssigned = new Set([ ...(result.jetzt || []), ...(result.hund || []), ...(result.welt || []), ]); for (const world of ['jetzt', 'hund', 'welt']) { const def = _DEFAULT_CONFIG[world] || []; const saved = result[world] || []; for (const page of def) { // Nur echte Neu-Chips anhängen: nicht zugewiesen UND nicht bewusst ausgeblendet if (!allAssigned.has(page) && !hidden.has(page)) { saved.push(page); allAssigned.add(page); } } result[world] = saved; } return result; } async function _loadConfigFromServer() { try { const res = await API.get('/profile/world-config'); if (res?.config) { _cfgCache = _mergeDefaults(res.config); try { localStorage.setItem('world_chips', JSON.stringify(_cfgCache)); } catch {} return; } // Noch nichts in DB: lokale Config hochladen (einmalige Migration) const local = (() => { try { return JSON.parse(localStorage.getItem('world_chips') || 'null'); } catch { return null; } })(); if (local) { _cfgCache = _mergeDefaults(local); API.put('/profile/world-config', { config: _cfgCache }).catch(() => {}); return; } } catch {} // Fallback: localStorage → Default try { _cfgCache = _mergeDefaults(JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG); } catch { _cfgCache = _DEFAULT_CONFIG; } } function _getConfig() { return _cfgCache || _DEFAULT_CONFIG; } function _saveConfig(cfg) { // Bewusst ausgeblendete Chips tracken: Default-Chips die keiner Welt zugewiesen sind const allAssigned = new Set([...(cfg.jetzt||[]), ...(cfg.hund||[]), ...(cfg.welt||[])]); const allDefault = [..._DEFAULT_CONFIG.jetzt, ..._DEFAULT_CONFIG.hund, ..._DEFAULT_CONFIG.welt]; cfg.hidden = allDefault.filter(p => !allAssigned.has(p)); _cfgCache = cfg; try { localStorage.setItem('world_chips', JSON.stringify(cfg)); } catch {} if (_state?.user) { API.put('/profile/world-config', { config: cfg }).catch(() => {}); } } function _chipMeta(page) { return _ALL_CHIPS.find(c => c.page === page) || null; } function _chipAllowed(chip) { const u = _state?.user; const tier = u?.subscription_tier || 'standard'; const isTest = tier.endsWith('_test'); // Pro-Chips: komplett ausblenden wenn kein Zugriff if (chip.pro) { if (!u) return false; if (isTest) return ['pro_test','breeder_test'].includes(tier); if (u.rolle === 'admin' || u.rolle === 'moderator') return true; if (u.is_moderator || u.is_social_media) return true; return ['pro','breeder'].includes(tier); } // Role-Checks (hart — komplett ausblenden) if (!chip?.role) return true; if (chip.role === 'breeder') { if (isTest) return tier === 'breeder_test'; return u?.rolle === 'breeder' || u?.rolle === 'admin'; } if (chip.role === 'social') return u?.is_social_media || u?.rolle === 'admin'; if (chip.role === 'mod') return u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator; if (chip.role === 'partner') return !!u?.is_partner || u?.rolle === 'admin'; if (chip.role === 'admin') return u?.rolle === 'admin'; return true; } // Gibt true zurück wenn User vollen Pro-Zugriff hat function _hasProAccess() { const u = _state?.user; if (!u) return false; const tier = u.subscription_tier || 'standard'; if (tier.endsWith('_test')) return ['pro_test','breeder_test'].includes(tier); if (u.rolle === 'admin' || u.rolle === 'moderator') return true; if (u.is_moderator || u.is_social_media) return true; return ['pro','breeder'].includes(tier); } function _chipsForWorld(world) { const cfg = _getConfig(); const pages = cfg[world] || _DEFAULT_CONFIG[world]; // Deduplizieren + filtern — _chipAllowed entscheidet ob angezeigt const seen = new Set(); const chips = pages .filter(p => { if (seen.has(p)) return false; seen.add(p); return true; }) .map(_chipMeta) .filter(c => c && _chipAllowed(c)); return chips; } // ── KONFIGURATIONS-MODAL ───────────────────────────────────── function _openConfigModal() { let cfg = JSON.parse(JSON.stringify(_getConfig())); // deep copy let _drag = null; // { page, fromWorld, ghost } let _removeHintShown = false; // „ausblenden ≠ löschen"-Toast nur einmal pro Session const isAdmin = _state?.user?.rolle === 'admin'; const worldColors = { jetzt:'rgba(196,132,58,0.6)', hund:'rgba(196,132,58,0.8)', welt:'rgba(99,130,220,0.6)' }; const worldLabels = { jetzt:'JETZT', hund:'HUND', welt:'WELT', pool:'Nicht verwendet' }; const allAssigned = () => new Set([...cfg.jetzt, ...cfg.hund, ...cfg.welt]); const poolChips = () => _ALL_CHIPS.filter(c => _chipAllowed(c) && !allAssigned().has(c.page) && !c.pinned); const bottomNav = document.getElementById('bottom-nav'); if (bottomNav) bottomNav.style.display = 'none'; const _isDesktop = window.innerWidth >= 768; const ov = document.createElement('div'); ov.id = 'wc-overlay'; ov.style.cssText = _isDesktop ? 'position:fixed;inset:0;z-index:500;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px)' : 'position:fixed;inset:0;z-index:500;display:flex;flex-direction:column;justify-content:flex-end'; document.body.appendChild(ov); const _removeDragListeners = () => { document.removeEventListener('pointermove', _onDragMove); document.removeEventListener('pointerup', _onDragEnd); document.removeEventListener('pointercancel', _onDragEnd); }; const _cancelDrag = () => { if (!_drag) return; _removeDragListeners(); _drag.ghost?.remove(); if (_drag.chipEl) _drag.chipEl.style.opacity = ''; ov.querySelectorAll('.wc-zone').forEach(z => z.style.borderColor = 'transparent'); _drag = null; }; const _closeModal = () => { _cancelDrag(); // laufenden Drag abbrechen ov.remove(); if (bottomNav) bottomNav.style.removeProperty('display'); }; function _render() { const _sheetStyle = _isDesktop ? 'position:relative;z-index:1;background:rgba(18,22,32,0.97);border-radius:20px;width:90%;max-width:1100px;max-height:88vh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 0 20px' : 'position:relative;z-index:1;background:rgba(18,22,32,0.97);border-radius:24px 24px 0 0;max-height:92vh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 0 calc(env(safe-area-inset-bottom,16px)+16px)'; const _gridCols = _isDesktop ? 'repeat(auto-fill,minmax(120px,1fr))' : 'repeat(4,1fr)'; const _chipH = _isDesktop ? '64px' : '80px'; ov.innerHTML = ` ${!_isDesktop ? '
' : ''}
Welten einrichten
Lang drücken & ziehen zum Verschieben. ✕ blendet aus (löscht nicht) — ausgeblendete Funktionen bleiben über „Weitere Funktionen" abrufbar.
${['jetzt','hund','welt','pool'].map(w => { const chips = w === 'pool' ? poolChips() : (cfg[w] || []).map(_chipMeta).filter(c => c && _chipAllowed(c)); const col = worldColors[w] || 'var(--c-border)'; return `
${worldLabels[w]} ${w !== 'pool' ? `(${chips.length})` : ''}
${chips.map(c => `
${!c.pinned ? ` ` : `
`} ${isAdmin && c.pro ? `P` : ''} ${isAdmin && c.role === 'breeder' ? `Z` : ''} ${c.label.replace('\n','
')}
`).join('')} ${chips.length === 0 ? `
${w==='pool'?'Alle Chips sind zugeordnet':'Leer — ziehe Chips hierher'}
` : ''}
`; }).join('')}
`; _bindEvents(); } function _bindEvents() { ov.querySelector('#wc-bg')?.addEventListener('click', _closeModal); ov.querySelector('#wc-cancel')?.addEventListener('click', _closeModal); ov.querySelector('#wc-reset')?.addEventListener('click', () => { cfg = JSON.parse(JSON.stringify(_DEFAULT_CONFIG)); _render(); }); ov.querySelector('#wc-save')?.addEventListener('click', () => { _saveConfig(cfg); _closeModal(); _renderJetzt(); _renderHund(); _renderWelt(); }); // Remove-Buttons ov.querySelectorAll('.wc-remove').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const page = btn.dataset.page, zone = btn.dataset.zone; const meta = _chipMeta(page); if (meta?.pinned) return; // gepinnte Chips können nicht entfernt werden if (zone !== 'pool') { cfg[zone] = cfg[zone].filter(p => p !== page); // Klarstellen: ausblenden ≠ löschen (einmal pro Session) if (!_removeHintShown) { _removeHintShown = true; UI.toast?.info('Ausgeblendet, nicht gelöscht — über „Weitere Funktionen" jederzeit wieder einblendbar.'); } } _render(); }); }); // Pointer-Drag (funktioniert auf Mouse + Touch) ov.querySelectorAll('.wc-chip').forEach(chip => { chip.addEventListener('pointerdown', e => _onDragStart(e, chip)); }); } function _onDragStart(e, chipEl) { if (e.button !== undefined && e.button !== 0) return; // nur linke Maustaste if (_drag) _cancelDrag(); chipEl.setPointerCapture(e.pointerId); _drag = { page: chipEl.dataset.page, zone: chipEl.dataset.zone, chipEl, ghost: null, dropZone: null, active: false, startX: e.clientX, startY: e.clientY, ox: 0, oy: 0, }; document.addEventListener('pointermove', _onDragMove); document.addEventListener('pointerup', _onDragEnd); document.addEventListener('pointercancel', _onDragEnd); } function _activateDrag(e) { const rect = _drag.chipEl.getBoundingClientRect(); _drag.ox = _drag.startX - rect.left; _drag.oy = _drag.startY - rect.top; _drag.active = true; const ghost = _drag.chipEl.cloneNode(true); ghost.querySelectorAll('button').forEach(b => b.style.display = 'none'); ghost.style.position = 'fixed'; ghost.style.zIndex = '9999'; ghost.style.opacity = '0.9'; ghost.style.pointerEvents= 'none'; ghost.style.transform = 'scale(1.08) rotate(-2deg)'; ghost.style.width = rect.width + 'px'; ghost.style.height = rect.height + 'px'; ghost.style.left = (e.clientX - _drag.ox) + 'px'; ghost.style.top = (e.clientY - _drag.oy) + 'px'; ghost.style.transition = 'none'; ghost.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)'; document.body.appendChild(ghost); _drag.ghost = ghost; _drag.chipEl.style.opacity = '0.2'; } function _onDragMove(e) { if (!_drag) return; if (!_drag.active) { const dx = Math.abs(e.clientX - _drag.startX); const dy = Math.abs(e.clientY - _drag.startY); if (dx < 8 && dy < 8) return; _activateDrag(e); } _drag.ghost.style.left = (e.clientX - _drag.ox) + 'px'; _drag.ghost.style.top = (e.clientY - _drag.oy) + 'px'; let foundZone = null; ov.querySelectorAll('.wc-zone').forEach(z => { const r = z.getBoundingClientRect(); const over = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom; z.style.borderColor = over ? (worldColors[z.dataset.zone] || 'rgba(196,132,58,0.8)') : 'transparent'; if (over) foundZone = z.dataset.zone; }); _drag.dropZone = foundZone; } function _onDragEnd() { if (!_drag) return; _removeDragListeners(); const { ghost, chipEl, dropZone, page, zone: fromZone, active } = _drag; _drag = null; ov.querySelectorAll('.wc-zone').forEach(z => z.style.borderColor = 'transparent'); ghost?.remove(); if (chipEl) chipEl.style.opacity = ''; if (!active) return; // nur Tap, kein Drag — nichts verschieben const meta = _chipMeta(page); if (dropZone && dropZone !== fromZone && !(meta?.pinned && dropZone === 'pool')) { if (fromZone !== 'pool') cfg[fromZone] = cfg[fromZone].filter(p => p !== page); if (dropZone !== 'pool' && !cfg[dropZone].includes(page)) cfg[dropZone].push(page); _render(); } } _render(); } // ── PANORAMA HINTERGRUNDBILD ───────────────────────────────── async function _loadDailyImage(dog) { if (!dog) return null; const userId = _state?.user?.id || 'anon'; const todayKey = `bg3_${userId}_` + new Date().toISOString().slice(0, 10); const cached = _wLoad(todayKey); if (cached?.data) return cached.data; try { const dash = await API.dogs.welcomeDashboard(dog.id); const url = dash?.random_photo?.url || dog.foto_url || null; if (url) _wSave(todayKey, url); return url; } catch { return dog.foto_url || null; } } let _bgUrl = null; // aktuell gesetztes Hintergrundbild let _bgBrightness = null; // { top, bottom } Roh-Helligkeit (0–255) je Bildhälfte für adaptive Abdunklung function _isDarkMode() { const t = document.documentElement.getAttribute('data-theme'); if (t === 'dark') return true; if (t === 'light') return false; return window.matchMedia('(prefers-color-scheme: dark)').matches; } function _bgWithOverlay(url) { return _isDarkMode() ? `linear-gradient(rgba(0,0,0,0.45),rgba(0,0,0,0.45)), url('${url}')` : `url('${url}')`; } function _applyBgOrientation() { const ov = document.getElementById('worlds-overlay'); const track = document.getElementById('worlds-track'); if (!ov || !track || !_bgUrl) return; const portrait = window.matchMedia('(orientation: portrait)').matches; if (portrait) { // Panorama: Bild über alle drei Welten, scrollt mit dem Swipe ov.style.backgroundImage = ''; track.style.backgroundImage = _bgWithOverlay(_bgUrl); track.style.backgroundSize = 'cover'; track.style.backgroundPosition = 'center 40%'; track.style.backgroundRepeat = 'no-repeat'; } else { // Vollbild pro Welt (Landscape / Desktop) track.style.backgroundImage = ''; ov.style.backgroundImage = _bgWithOverlay(_bgUrl); ov.style.backgroundSize = 'cover'; ov.style.backgroundPosition = 'center 40%'; ov.style.backgroundRepeat = 'no-repeat'; } } // Orientierungswechsel → Bild neu setzen window.matchMedia('(orientation: portrait)').addEventListener('change', _applyBgOrientation); // Theme-Wechsel → Overlay-Intensität + adaptive Abdunklung neu anwenden new MutationObserver(() => { _applyBgOrientation(); _applyAdaptiveDim(); }) .observe(document.documentElement, { attributeFilter: ['data-theme'] }); // Helligkeit (0–255) der oberen und unteren Bildhälfte via Mini-Canvas. // Oben = Begrüßungs-Banner + JETZT-Chip-Reihe, unten = Feature-Chips. // Gibt null zurück bei CORS-Tainting oder Fehler (→ Fallback-Abdunklung). function _measureBrightnessRegions(img) { try { const c = document.createElement('canvas'); const w = c.width = 32, h = c.height = 32; const ctx = c.getContext('2d', { willReadFrequently: true }); ctx.drawImage(img, 0, 0, w, h); const data = ctx.getImageData(0, 0, w, h).data; const half = h / 2; let sumTop = 0, sumBot = 0, nTop = 0, nBot = 0; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; const lum = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; if (y < half) { sumTop += lum; nTop++; } else { sumBot += lum; nBot++; } } } return { top: sumTop / nTop, bottom: sumBot / nBot }; } catch { return null; } } // Helligkeit (0–255) → Abdunklungs-Alpha. Im Dark Mode liegt zusätzlich // ein 0.45-Overlay über dem Bild, daher wirkt es dort dunkler. function _dimForBrightness(b) { if (_isDarkMode()) b *= 0.55; if (b <= 90) return 0.14; if (b >= 190) return 0.48; return 0.14 + (b - 90) / 100 * (0.48 - 0.14); } // Setzt --wbg-dim-top/-bottom adaptiv pro Bildbereich: heller Bereich → // mehr Abdunklung (Lesbarkeit), dunkler → wenig. function _applyAdaptiveDim() { const r = _bgBrightness || { top: 110, bottom: 110 }; const dimTop = _dimForBrightness(r.top).toFixed(2); const dimBot = _dimForBrightness(r.bottom).toFixed(2); ['wp-jetzt', 'wp-hund', 'wp-welt'].forEach(id => { const el = document.getElementById(id); if (!el) return; el.style.setProperty('--wbg-dim-top', dimTop); el.style.setProperty('--wbg-dim-bottom', dimBot); }); } function _applyBgImage(url) { const ov = document.getElementById('worlds-overlay'); const track = document.getElementById('worlds-track'); if (!ov || !track) return; if (url) { const toLoad = new Image(); toLoad.onload = () => { _hasBgPhoto = true; _bgUrl = url; _bgBrightness = _measureBrightnessRegions(toLoad); _applyAdaptiveDim(); _applyBgOrientation(); const hint = document.getElementById('wh-photo-hint'); if (hint) { const seen = parseInt(localStorage.getItem('banyaro_wh_hint_seen') || '0'); if (seen < 2) { localStorage.setItem('banyaro_wh_hint_seen', String(seen + 1)); setTimeout(() => { hint.style.transition = 'opacity 0.6s'; hint.style.opacity = '0'; setTimeout(() => hint.remove(), 650); }, 4000); } else { hint.remove(); } } }; toLoad.onerror = () => _applyBgImage(null); toLoad.src = url; } else { _hasBgPhoto = false; _bgUrl = null; _bgBrightness = { top: 20, bottom: 20 }; // dunkler Fallback-Gradient → minimale Abdunklung _applyAdaptiveDim(); track.style.backgroundImage = ''; ov.style.backgroundImage = 'linear-gradient(160deg,#1a1f35 0%,#16213e 33%,#1a2535 67%,#0f1921 100%)'; ov.style.backgroundSize = '100% 100%'; } } // ── CHIP-HELPER ────────────────────────────────────────────── function _isRoleBasedPro() { const u = _state?.user; if (!u) return false; const t = u.subscription_tier || 'standard'; if (t.endsWith('_test') || ['pro','breeder'].includes(t)) return false; return u.rolle === 'admin' || u.rolle === 'moderator' || u.is_moderator || u.is_social_media; } function _chip(icon, label, page, locked = false, proBadge = false, breederBadge = false) { const style = locked ? 'opacity:0.25;cursor:default;' : ''; const badge = proBadge ? `P` : breederBadge ? `Z` : ''; return `
${badge} ${label}
`; } // ── JETZT WORLD ────────────────────────────────────────────── async function _renderJetzt() { const el = document.getElementById('wj-content'); if (!el) return; const user = _state?.user; el.innerHTML = _skeleton(3); const [weatherRes, dogsRes, alertsRes, achRes] = await Promise.allSettled([ _getCachedWeather(), user ? _cachedGet('dogs', '/dogs') : Promise.resolve({ data: [], fromCache: false, ageMin: 0 }), user ? _getNearbyAlerts() : Promise.resolve([]), user ? _cachedGet('achievements_me', '/achievements/me') : Promise.resolve({ data: null }), ]); const weatherObj = weatherRes.value || { data: null, fromCache: false, ageMin: 0 }; const dogsObj = dogsRes.value || { data: [], fromCache: false, ageMin: 0 }; const w = weatherObj.data; const dogList = dogsObj.data || []; const dog = dogList[0] || null; const alertList = alertsRes.value || []; const totalKm = achRes.value?.data?.stats?.total_km ?? null; const isOffline = weatherObj.fromCache && dogsObj.fromCache; const staleMin = Math.max(weatherObj.ageMin || 0, dogsObj.ageMin || 0); // Panorama-Bild setzen (nur wenn noch kein Bild vorhanden) if (dog && !_bgUrl) { _loadDailyImage(dog).then(_applyBgImage); } else if (!dog) { _applyBgImage(null); } const hour = new Date().getHours(); const firstName = user?.name?.split(' ')[0] || ''; const dayStr = new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' }); // User-Geburtstag heute? const _todayDdMm = (() => { const d = new Date(); return String(d.getDate()).padStart(2,'0')+'.'+String(d.getMonth()+1).padStart(2,'0'); })(); const userBdayToday = user?.geburtstag && user.geburtstag === _todayDdMm; const greet = userBdayToday ? `Herzlichen Glückwunsch` : (hour < 5 ? 'Gute Nacht' : hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Hallo' : 'Guten Abend'); const stale = isOffline && staleMin > 5 ? `· Offline` : ''; // Gassi-Score aus Wetterdaten berechnen function _calcGassiScore(wd) { if (!wd) return null; let s = 10; const t = wd.temp_c ?? 20, p = wd.precip_prob ?? 0, wind = wd.wind_kmh ?? 0; if (t > 30) s -= 3; else if (t > 25) s -= 1; else if (t < 0) s -= 3; else if (t < 5) s -= 1; if (p > 70) s -= 3; else if (p > 40) s -= 2; else if (p > 20) s -= 1; if (wind > 60) s -= 2; else if (wind > 40) s -= 1; if (wd.thunderstorm) s -= 3; return Math.max(1, Math.min(10, s)); } const gassiScore = _calcGassiScore(w); const gassiColor = gassiScore >= 8 ? '#10B981' : gassiScore >= 5 ? '#F59E0B' : '#EF4444'; const weatherEmoji = !w ? ['🌤️'] : w.thunderstorm ? ['⛈️'] : (w.precip_prob ?? 0) > 70 ? ['🌧️'] : (w.precip_prob ?? 0) > 30 ? ['🌦️'] : (w.temp_c ?? 20) > 28 ? ['☀️', '🔥'] : (w.temp_c ?? 20) < 2 ? ['🌨️'] : ['☀️']; // User-Geburtstag Reminder const userBdayHtml = userBdayToday ? `
Alles Gute zum Geburtstag, ${_esc(firstName)}!
Wir wünschen dir und deinem Hund einen wunderschönen Tag 🐾
` : ''; // Alert-Reminder const alertHtml = alertList.slice(0,1).map(a => `
${_esc(a.title)}
`).join(''); // Feature Chips aus Config const features = user ? _chipsForWorld('jetzt') : [ { icon:'sun', label:'Wetter', page:'wetter' }, { icon:'first-aid',label:'Erste Hilfe', page:'erste-hilfe' }, { icon:'gear', label:'Anmelden', page:'settings' }, ]; // User-Avatar für JETZT Info-Card const uFoto = _state?.user?.avatar_url; const uInitial = firstName ? firstName.charAt(0).toUpperCase() : '?'; const userAvatarHtml = `
${uFoto ? `` : `
${uInitial}
`}
`; el.innerHTML = `
${_esc(greet)}${firstName ? `, ${_esc(firstName)}` : ''}${stale}
${_esc(dayStr)}
${user ? userAvatarHtml : ''}
${userBdayHtml} ${alertHtml} ${user && dog ? `
${weatherEmoji.map(e => `${e}`).join('')}
Gassi-Score
${gassiScore ?? '—'} ${gassiScore ? `/10` : ''}
${w ? `
${w.location_name ? `
${w.location_name}
` : ''}
${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen
${w.rain_warning_time ? `
⚠ Umschwung ab ${w.rain_warning_time}
` : w.next_rain_time ? `
ab ${w.next_rain_time} Uhr
` : ''}
` : ''}
Gassirunde ${totalKm != null ? `∑ ${totalKm} km` : ''}
Übung
` : ''}
${features.map(f => _chip(f.icon, f.label, f.page, false, false, false)).join('')}
`; el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); if (user && dog) { _loadJetztExercise(dog); _loadJetztRoute(); } } async function _loadJetztExercise(dog) { const valEl = document.getElementById('wj-exercise-val'); if (!valEl) return; try { const res = await _cachedGet(`dash_${dog.id}`, `/dogs/${dog.id}/welcome-dashboard`); const ex = res.data?.daily_exercise; valEl.textContent = ex?.name || 'Stand erfassen →'; const chip = document.getElementById('wj-exercise-chip'); if (chip) { chip.style.cursor = 'pointer'; chip.onclick = () => { hide(); if (window.App) window.App.navigate('uebungen', true, ex ? { exercise_id: ex.exercise_id || '', name: ex.name || '' } : {} ); }; } } catch { valEl.textContent = 'Stand erfassen →'; } } async function _loadJetztRoute() { const valEl = document.getElementById('wj-route-val'); if (!valEl) return; // Tages-Cache (gleicher Key wie welcome.js) const today = new Date().toISOString().slice(0, 10); const cacheKey = 'by_daily_route_' + today; const cached = localStorage.getItem(cacheKey); if (cached) { try { _applyJetztRoute(valEl, JSON.parse(cached)); return; } catch {} } let loc; try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); } catch { valEl.textContent = 'Standort nötig'; return; } const dayIdx = Math.floor(Date.now() / 86400000); const km = [2, 4, 6][dayIdx % 3]; const seed = dayIdx % 5; try { const result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed }); if (!result?.gps_track?.length) { valEl.textContent = 'Keine Route gefunden'; return; } localStorage.setItem(cacheKey, JSON.stringify(result)); // alte Einträge aufräumen Object.keys(localStorage) .filter(k => k.startsWith('by_daily_route_') && k !== cacheKey) .forEach(k => localStorage.removeItem(k)); _applyJetztRoute(valEl, result); } catch { valEl.textContent = 'Route nicht verfügbar'; } } function _applyJetztRoute(valEl, result) { const durStr = result.dauer_min < 60 ? `${result.dauer_min} min` : `${Math.floor(result.dauer_min / 60)}h ${result.dauer_min % 60 > 0 ? ' ' + (result.dauer_min % 60) + 'min' : ''}`; valEl.textContent = `${result.distanz_km} km · ${durStr}`; } // ── HUND WORLD ─────────────────────────────────────────────── async function _renderHund() { const el = document.getElementById('wh-content'); if (!el) return; const user = _state?.user; if (!user) { el.innerHTML = `
🐾
Dein Hund wartet
Melde dich an um loszulegen
`; el.querySelector('#world-login-btn')?.addEventListener('click', () => navigateTo('settings')); return; } el.innerHTML = _skeleton(4); const dogsRes = await _cachedGet('dogs', '/dogs'); const dogs = dogsRes.data || []; if (!dogs.length) { const features = [ { icon:'book-open', color:'#8B5CF6', title:'Tagebuch', sub:'Fotos & Erlebnisse' }, { icon:'heartbeat', color:'#EF4444', title:'Gesundheit', sub:'Impfungen & Gewicht' }, { icon:'target', color:'#F59E0B', title:'Training', sub:'104 Übungen' }, { icon:'books', color:'#10B981', title:'Wiki', sub:'Alle Rassen' }, { icon:'scales', color:'#3B82F6', title:'Wurfbörse', sub:'Welpen finden' }, { icon:'currency-eur', color:'#06B6D4', title:'Ausgaben', sub:'Budget im Blick' }, ]; el.innerHTML = `
🐶
Dein Hund wartet!
Lege ein Profil an und schalte alle Features frei
Was dich erwartet
${features.map(f => `
${f.title}
`).join('')}
`; el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); el.querySelector('#welcome-add-dog-btn')?.addEventListener('click', () => navigateTo('dog-profile')); return; } // Hunde global cachen für schnelles Cycling _dogs = dogs; if (_dogIdx >= _dogs.length) _dogIdx = 0; const dog = _dogs[_dogIdx]; // Geburtstag prüfen (heute oder morgen → Feature sichtbar) function _birthdayState(geb) { if (!geb) return null; const today = new Date(); const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1); const mm = String(today.getMonth() + 1).padStart(2, '0'); const dd = String(today.getDate()).padStart(2, '0'); const mt = String(tomorrow.getMonth() + 1).padStart(2, '0'); const dt = String(tomorrow.getDate()).padStart(2, '0'); const mmdd = geb.slice(5); // 'MM-DD' if (mmdd === `${mm}-${dd}`) return 'today'; if (mmdd === `${mt}-${dt}`) return 'tomorrow'; return null; } const bdayDog = _dogs.find(d => _birthdayState(d.geburtstag)) || null; // Großes Banner nur wenn der AKTIVE Hund Geburtstag hat const bday = (bdayDog && bdayDog.id === dog.id) ? _birthdayState(dog.geburtstag) : null; const bdayYear = bday && dog.geburtstag ? new Date().getFullYear() - parseInt(dog.geburtstag.slice(0, 4)) : null; // Hinweis in Info-Karte wenn ein ANDERER Hund Geburtstag hat const otherBdayDog = (bdayDog && bdayDog.id !== dog.id) ? bdayDog : null; const [streakRes, diaryRes] = await Promise.allSettled([ _cachedGet(`streak_${dog.id}`, `/streak/${dog.id}`), _cachedGet(`diary_${dog.id}`, `/dogs/${dog.id}/diary?limit=1`), ]); const streak = streakRes.value?.data ?? streakRes.value; const diaryData = diaryRes.value?.data ?? diaryRes.value; const lastEntry = diaryData?.entries?.[0] || diaryData?.[0] || null; const ageStr = dog.alter_jahre ? _fmtAlter(dog.alter_jahre) : ''; const stats = [dog.rasse, ageStr, dog.gewicht_kg ? dog.gewicht_kg + ' kg' : null].filter(Boolean).join(' · '); const otherDogs = _dogs.filter((_, i) => i !== _dogIdx); const chips = _chipsForWorld('hund'); // Avatar des aktuellen Hundes (links, → Hundeprofil) const dogAvatarHtml = dog.foto_url ? `` : `
🐶
`; // Andere Hunde-Avatare (rechts, überlagernd, → Hund wechseln) const otherAvatarsHtml = otherDogs.length > 0 ? `
${otherDogs.slice(0, 4).map((d, i) => { const targetIdx = _dogs.indexOf(d); return `
${d.foto_url ? `` : `
${_esc(d.name?.charAt(0)?.toUpperCase() || '?')}
`}
`; }).join('')}
` : ''; el.innerHTML = `
${dogAvatarHtml}
${_esc(dog.name)}
${stats ? `
${_esc(stats)}
` : ''}
${otherAvatarsHtml}
${otherBdayDog ? `
${_esc(otherBdayDog.name)} hat ${_birthdayState(otherBdayDog.geburtstag) === 'today' ? 'heute' : 'morgen'} Geburtstag!
` : ''}
${bday ? `
${bday === 'today' ? `Alles Gute zum ${bdayYear}. Geburtstag, ${_esc(bdayDog.name)}!` : `Morgen hat ${_esc(bdayDog.name)} Geburtstag!`}
${bdayYear ? `
${bday === 'today' ? `${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} gemeinsam` : `Wird ${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} alt`}
` : ''}
${bday === 'today' ? `Was hat sich ${_esc(bdayDog.name)} gewünscht?` : 'KI-Überraschungsideen'}
${bday === 'today' && new Date().getHours() >= 18 ? `
Halte den besonderen Tag im Tagebuch fest 🐾
` : ''}` : ''}
${!_hasBgPhoto ? `
Hintergrund-Foto hinzufügen
Tagebuchfotos im Querformat erscheinen hier als Panorama
` : ''}
${chips.map(c => _chip(c.icon, c.label, c.page, false, false, false)).join('')}
`; // Avatar → Hundeprofil (aktiven Hund auf den angezeigten setzen) el.querySelector('#wh-avatar')?.addEventListener('click', () => { const shown = _dogs[_dogIdx]; if (shown && shown.id !== _state?.activeDog?.id) App.setActiveDog(shown.id); navigateTo('dog-profile'); }); // Chips el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); // Name → nächster Hund if (_dogs.length > 1) { el.querySelector('#wh-cycle-btn')?.addEventListener('click', () => { _dogIdx = (_dogIdx + 1) % _dogs.length; _renderHund(); }); } // Andere Hund-Avatare → zu diesem Hund wechseln el.querySelectorAll('.wh-other-dog').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); if (!isNaN(idx) && idx !== _dogIdx) { _dogIdx = idx; _renderHund(); } }); }); // Geburtstag-Hinweis → zum Geburtstagshund wechseln if (otherBdayDog) { if (!document.getElementById('by-bday-anim-style')) { const s = document.createElement('style'); s.id = 'by-bday-anim-style'; s.textContent = '@keyframes by-bday-bounce{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-5px) scale(1.3)}}'; document.head.appendChild(s); } el.querySelector('#wh-other-bday-hint')?.addEventListener('click', () => { const idx = _dogs.indexOf(otherBdayDog); if (idx >= 0) { _dogIdx = idx; _renderHund(); } }); } // Geburtstags-Banner → KI el.querySelector('#wh-bday-banner')?.addEventListener('click', () => _openBdayKI(dog, bday)); // Abend-Banner: nach 18 Uhr am echten Geburtstag → Tagebucheintrag anregen if (bday === 'today' && new Date().getHours() >= 18) { el.querySelector('#wh-bday-evening')?.addEventListener('click', () => { navigateTo('diary'); setTimeout(() => window.App?.callModule?.('diary', 'openNew'), 400); }); } } async function _openBdayKI(dog, bdayMode) { const isToday = bdayMode === 'today'; const title = isToday ? `🎂 ${_esc(dog.name)}s Geburtstagstraum` : `🎁 Überraschungen für ${_esc(dog.name)}`; const ov = document.createElement('div'); ov.className = 'w3-sheet-overlay'; ov.innerHTML = `
${title}
KI denkt nach…
${isToday ? ` ` : ''}
`; document.body.appendChild(ov); const _close = () => ov.remove(); ov.querySelector('.w3-backdrop').addEventListener('click', _close); ov.querySelector('#bday-ki-close').addEventListener('click', _close); ov.querySelector('#bday-diary-btn')?.addEventListener('click', () => { _close(); navigateTo('diary'); setTimeout(() => window.App?.callModule?.('diary', 'openNew'), 400); }); try { const res = await API.post('/ki/geburtstag', { dog_id: bdayDog.id, name: bdayDog.name, rasse: bdayDog.rasse || null, alter: bdayDog.alter_jahre ? Math.round(bdayDog.alter_jahre) : null, mode: bdayMode, }); const body = ov.querySelector('#bday-ki-body'); if (body) { const html = _esc(res.answer || '') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\n/g, '
'); body.innerHTML = `
${html}
`; } } catch (err) { const body = ov.querySelector('#bday-ki-body'); if (body) { const msg = err?.status === 429 ? 'Heute bereits abgerufen — morgen gibt es neue Ideen 🐾' : 'KI momentan nicht verfügbar. Versuch es später nochmal 🐾'; body.innerHTML = `
${msg}
`; } } } // ── WELT WORLD ─────────────────────────────────────────────── const _QUOTES = [ { t:'Ein Hund ist die einzige Kreatur, die dich mehr liebt als sich selbst.', a:'Josh Billings' }, { t:'Hunde haben Besitzer. Katzen haben Personal.', a:'Unbekannt' }, { t:'Bis man einen Hund geliebt hat, ist ein Teil der Seele unerwacht.', a:'Anatole France' }, { t:'Hunde sind nicht unser ganzes Leben — aber sie machen unser Leben ganz.', a:'Roger Caras' }, { t:'Der Hund hat nur einen Fehler: Er vertraut dem Menschen zu sehr.', a:'Unbekannt' }, { t:'Ein treuer Hund ist besser als ein unzuverlässiger Mensch.', a:'Deutsches Sprichwort' }, { t:'Wer einen Hund hat, gibt nie allein zu Tisch.', a:'Unbekannt' }, { t:'Die reinste Form der Freude ist die Freude eines Hundes.', a:'Milan Kundera' }, { t:'Hunde lachen nicht mit dem Mund, sondern mit dem Schwanz.', a:'Max Eastman' }, { t:'Ein Haus ohne Hund ist ein leeres Haus.', a:'Unbekannt' }, { t:'Wenn du verstehen willst, was Liebe ist, beobachte einen Hund.', a:'Unbekannt' }, { t:'Der Hund fragt nicht warum. Er fragt nur: wann gehen wir?', a:'Unbekannt' }, { t:'Hunde riechen nicht schlecht. Sie riechen nur anders als wir denken.', a:'Unbekannt' }, { t:'In einer Welt voller Unsicherheiten ist der Hund das Verlässlichste.', a:'Unbekannt' }, { t:'Ein Spaziergang mit einem Hund ist nie wirklich ein Umweg.', a:'Unbekannt' }, { t:'Hunde bringen das Beste im Menschen hervor.', a:'Unbekannt' }, { t:'Wer mit Hunden liegt, steht mit Flöhen auf — und mit einem Lächeln.', a:'Unbekannt' }, { t:'Ein Hund weiß, wann du traurig bist. Er weiß nicht warum. Aber er ist da.', a:'Unbekannt' }, { t:'Das Geheimnis eines Hundes: Er beurteilt dich nie.', a:'Unbekannt' }, { t:'Der Hund ist Gattung: Mensch.', a:'Charles M. Schulz' }, { t:'Hunde haben viel zu sagen. Wir haben verlernt zuzuhören.', a:'Unbekannt' }, { t:'Ein guter Hund macht aus einem schlechten Tag einen erträglichen.', a:'Unbekannt' }, { t:'Der Hund ist der philosophischste aller Haustiere.', a:'George Graham Vest' }, { t:'Wer einen Hund hat, braucht keinen Therapeuten.', a:'Unbekannt' }, { t:'Hunde lieben bedingungslos. Das ist ihr größtes Geschenk.', a:'Unbekannt' }, { t:'Der schönste Empfang ist der eines Hundes an der Tür.', a:'Unbekannt' }, { t:'Ein Hund ändert dein Leben. Meistens zum Besseren.', a:'Unbekannt' }, { t:'Hunde altern in Würde. Menschen könnten von ihnen lernen.', a:'Unbekannt' }, { t:'Kein Psychiater der Welt kann so gut zuhören wie ein Hund.', a:'Unbekannt' }, { t:'Wo Hunde sind, da ist das Zuhause.', a:'Unbekannt' }, { t:'Der Hund hat keinen Begriff von Vergangenheit oder Zukunft. Er lebt.', a:'Milan Kundera' }, { t:"Wenn du einen verhungernden Hund aufnimmst und es ihm gutgehen lässt, wird er dich nicht beißen. Das ist der wesentliche Unterschied zwischen Hund und Mensch.", a:"Mark Twain" }, { t:"Der Hund ist ein Gentleman. Ich hoffe, in seinen Himmel zu kommen, nicht in den der Menschen.", a:"Mark Twain" }, { t:"Je mehr ich über die Menschen lerne, desto mehr liebe ich meinen Hund.", a:"Mark Twain" }, { t:"Der Himmel vergibt nach Gunst. Ginge es nach Verdienst, bliebest du draußen und dein Hund käme hinein.", a:"Mark Twain" }, { t:"Die Treue eines Hundes ist ein kostbares Geschenk, das nicht weniger bindende Pflichten auferlegt als die Freundschaft eines Menschen.", a:"Konrad Lorenz" }, { t:"Es gibt keine Treue, die nicht schon einmal gebrochen worden wäre, außer der eines wahrhaft treuen Hundes.", a:"Konrad Lorenz" }, { t:"Die Bindung an einen echten Hund ist so beständig, wie es Bande auf dieser Erde nur sein können.", a:"Konrad Lorenz" }, { t:"Der Wunsch, einen Hund zu halten, entspringt der uralten Sehnsucht des zivilisierten Menschen nach dem verlorenen Paradies.", a:"Konrad Lorenz" }, { t:"Wenn ich daran denke, dass mein Hund mich mehr liebt, als ich ihn, dann beschämt mich das.", a:"Konrad Lorenz" }, { t:"Der Hund ist, mit Recht, das Sinnbild der Treue.", a:"Arthur Schopenhauer" }, { t:"Woran sollte man sich von der Falschheit der Menschen erholen, wenn die Hunde nicht wären, in deren ehrliches Gesicht man ohne Misstrauen blicken kann.", a:"Arthur Schopenhauer" }, { t:"Der Anblick jedes Tieres erfreut mich unmittelbar und mir geht dabei das Herz auf, am meisten bei dem der Hunde.", a:"Arthur Schopenhauer" }, { t:"Hunde lieben ihre Freunde und beißen ihre Feinde, ganz anders als Menschen, die niemals rein lieben, sondern stets Liebe und Hass vermengen.", a:"Sigmund Freud" }, { t:"Hunde schenken Zuneigung ohne Zwiespalt und die Schönheit eines Daseins, das ganz in sich ruht.", a:"Sigmund Freud" }, { t:"Gibt es im Himmel keine Hunde, dann will ich, wenn ich sterbe, dorthin gehen, wo sie hingekommen sind.", a:"Will Rogers" }, { t:"Für seinen Hund ist jeder Mensch ein Napoleon, daher die anhaltende Beliebtheit der Hunde.", a:"Aldous Huxley" }, { t:"Bevor du einen Hund hast, kannst du dir kaum vorstellen, wie das Leben mit ihm wäre. Danach kannst du dir kein anderes Leben mehr vorstellen.", a:"Caroline Knapp" }, { t:"Wer einmal einen wundervollen Hund hatte, dessen Leben ist ohne einen ärmer.", a:"Dean Koontz" }, { t:"Einen Hund zu streicheln und zu kraulen kann den Geist so beruhigen wie tiefe Meditation und ist fast so gut für die Seele wie ein Gebet.", a:"Dean Koontz" }, { t:"Tausendmal hat er mir gesagt, dass ich sein Grund zu leben bin, durch die Art, wie er sich an mein Bein lehnt.", a:"Gene Hill" }, { t:"Hunde sind unser Bindeglied zum Paradies. Sie kennen weder Bosheit noch Neid noch Unzufriedenheit.", a:"Milan Kundera" }, { t:"Mit einem Hund an einem schönen Nachmittag auf einem Hügel zu sitzen ist wie eine Rückkehr nach Eden.", a:"Milan Kundera" }, { t:"Hunde sprechen sehr wohl, doch nur zu denen, die zu lauschen verstehen.", a:"Orhan Pamuk" }, { t:"Tiere sind so angenehme Freunde, sie stellen keine Fragen und üben keine Kritik.", a:"George Eliot" }, { t:"Das größte Vergnügen mit einem Hund ist, dass man sich vor ihm zum Narren machen kann und er nicht nur nicht tadelt, sondern selbst mitmacht.", a:"Samuel Butler" }, { t:"Bedenke, dass der Allmächtige, der uns den Hund zum Gefährten gab, ihm ein edles Wesen verlieh, das des Betrugs unfähig ist.", a:"Sir Walter Scott" }, { t:"Ich habe oft über den Grund nachgedacht, warum Hunde so kurz leben, und bin überzeugt, es geschieht aus Mitleid mit dem Menschen.", a:"Sir Walter Scott" }, { t:"Hunde beißen mich nie. Nur Menschen.", a:"Marilyn Monroe" }, { t:"Wer hält dich für so großartig wie dein Hund.", a:"Audrey Hepburn" }, { t:"Ich gehe mit meinen Hunden, das hält mich fit. Ich rede mit meinen Hunden, das hält mich gesund.", a:"Audrey Hepburn" }, { t:"Sobald er seinen Herrn erblickte, ließ Argos die Ohren sinken und wedelte mit dem Schwanz, doch zu ihm hin zu kommen vermochte er nicht mehr.", a:"Homer" }, { t:"Hunde und Philosophen tun das meiste Gute und erhalten den geringsten Lohn.", a:"Diogenes" }, { t:"Hunde sind besser als Menschen, denn sie wissen, doch sie verraten es nicht.", a:"Emily Dickinson" }, { t:"Bei Schlachten, die das Schicksal von Völkern entschieden, blieb ich ungerührt. Hier aber, beim Kummer eines einzigen Hundes, war ich zu Tränen gerührt.", a:"Napoleon Bonaparte" }, { t:"Wenn Hunde in den Himmel kommen, brauchen sie keine Flügel, denn Gott weiß, dass Hunde das Laufen am meisten lieben.", a:"Cynthia Rylant" }, { t:"Meine ganze Bestimmung war es, ihn zu lieben, bei ihm zu sein und ihn glücklich zu machen.", a:"W. Bruce Cameron" }, { t:"Meine Hundefreunde scheinen meine Grenzen zu verstehen und bleiben immer dicht an meiner Seite.", a:"Helen Keller" }, { t:"Gib einem Hund dein Herz, das er zerreißen kann.", a:"Rudyard Kipling" }, { t:"Ein Hund lehrt einen Jungen Treue, Beharrlichkeit und sich dreimal zu drehen, bevor man sich hinlegt.", a:"Robert Benchley" }, { t:"Du kannst zu einem Hund den größten Unsinn sagen, und er schaut dich an, als wollte er sagen: Donnerwetter, du hast recht, darauf wäre ich nie gekommen.", a:"Dave Barry" }, { t:"Wenn ich an die Unsterblichkeit glaube, dann daran, dass gewisse Hunde in den Himmel kommen, und nur sehr, sehr wenige Menschen.", a:"James Thurber" }, { t:"Eine Tür ist das, auf deren falscher Seite ein Hund sich ständig befindet.", a:"Ogden Nash" }, { t:"Natürlich kann man ohne Hund leben, es lohnt sich nur nicht.", a:"Heinz Rühmann" }, { t:"Ein Leben ohne Mops ist möglich, aber sinnlos.", a:"Loriot" }, { t:"Die Welt wäre ein schönerer Ort, wenn jeder so bedingungslos lieben könnte wie ein Hund.", a:"M.K. Clinton" }, { t:"Der durchschnittliche Hund ist ein netterer Mensch als der durchschnittliche Mensch.", a:"Andy Rooney" }, { t:"Niemand schätzt das ganz besondere Genie deiner Unterhaltung so sehr wie dein Hund.", a:"Christopher Morley" }, { t:"Ich habe meinem Schmerz einen Namen gegeben und nenne ihn Hund, denn er ist ebenso treu und klug wie jeder andere Hund.", a:"Friedrich Nietzsche" }, { t:"Dem Hunde, wenn er gut gezogen, wird selbst ein weiser Mann gewogen.", a:"Johann Wolfgang von Goethe" }, { t:"Verstößt ein Herr seinen Hund, weil er ihm das Brot nicht mehr verdienen kann, so zeigt das stets eine sehr kleine Seele des Herrn an.", a:"Immanuel Kant" }, { t:"Wenn du keinen Hund besitzt, ist nicht unbedingt etwas mit dir verkehrt, aber vielleicht stimmt etwas mit deinem Leben nicht.", a:"Roger Caras" }, { t:"Alte Hunde sind wie alte Schuhe, bequem. Sie sind vielleicht etwas aus der Form, aber sie passen einfach gut.", a:"Bonnie Wilcox" }, { t:"Kommt ein Hund nicht zu dir, nachdem er dir ins Gesicht gesehen hat, so solltest du heimgehen und dein Gewissen prüfen.", a:"Woodrow Wilson" }, { t:"Springt ein Hund auf deinen Schoß, dann weil er dich mag. Tut eine Katze dasselbe, ist es nur, weil dein Schoß wärmer ist.", a:"Alfred North Whitehead" }, { t:"Geld kann dir einen feinen Hund kaufen, aber nur Liebe bringt ihn dazu, mit dem Schwanz zu wedeln.", a:"Kinky Friedman" }, { t:"Wenn ich für eine Reise den Koffer hervorhole, weiß er es lange vorher und gerät in einen Zustand milder Aufregung.", a:"John Steinbeck" }, { t:"Ein Hund ist ein Band zwischen Fremden.", a:"John Steinbeck" }, { t:"Mein kleiner Hund, ein Herzschlag zu meinen Füßen.", a:"Edith Wharton" }, { t:"Geschaffen wurde der Hund eigens für die Kinder. Er ist der Gott des Übermuts.", a:"Henry Ward Beecher" }, { t:"Hunde sind klug. Sie kriechen in eine stille Ecke und lecken ihre Wunden und kehren erst in die Welt zurück, wenn sie wieder heil sind.", a:"Agatha Christie" }, { t:"Schönheit ohne Eitelkeit, Stärke ohne Übermut, Mut ohne Wildheit und alle Tugenden des Menschen ohne seine Laster.", a:"Lord Byron" }, { t:"Gott dreht Wolken um und um, um den Hunden im Hundehimmel flauschige Betten zu bereiten.", a:"Cynthia Rylant" }, { t:"Hunde besitzen eine Eigenschaft, die unter Menschen selten ist, nämlich zu erkennen, wer Hilfe braucht, und sie zu geben.", a:"Caroline Knapp" }, { t:"In manchen Dingen ist mein Hund klüger als ich, in anderen ist er bodenlos unwissend.", a:"John Steinbeck" }, { t:"Ein Hund zur Hand ist besser als ein Bruder in der Ferne.", a:"Persisches Sprichwort" }, { t:"Wenn du bei jedem bellenden Hund stehen bleibst, beendest du deine Reise nie.", a:"Arabisches Sprichwort" }, { t:"Es ist schwer, einen so treuen Gefährten zu finden wie einen Hund.", a:"Mongolisches Sprichwort" }, { t:"Ein Hund ohne Schwanz kann nicht zeigen, dass er sich freut.", a:"Albanisches Sprichwort" }, { t:"Ein Hund, der mit dem Schwanz wedelt, bezieht keine Prügel.", a:"Japanisches Sprichwort" }, { t:"Der hungrige Hund fürchtet den Stock nicht.", a:"Japanisches Sprichwort" }, { t:"Treffen sich im Paradies eine Menschenseele und eine Hundeseele, verneigt sich der Mensch vor dem Hund.", a:"Sibirisches Sprichwort" }, { t:"Solange der Mensch denkt, Tiere fühlten nicht, fühlen Tiere, dass der Mensch nicht denkt.", a:"Indianische Weisheit" }, { t:"Mit Hunden zu leben tut dem Menschen gut.", a:"Tibetisches Sprichwort" }, { t:"Ein Hund ist ein Herz auf vier Beinen.", a:"Irisches Sprichwort" }, { t:"Der Hund vergisst den einen Bissen nicht, und wirfst du ihm auch hundert Steine nach.", a:"Chinesisches Sprichwort" }, { t:"Sei der Freund meines Hundes, dann bist du auch der meine.", a:"Indianische Weisheit" }, { t:"Hüte dich vor dem Menschen, der nicht spricht, und vor dem Hund, der nicht bellt.", a:"Indianische Weisheit" }, { t:"Ein kluger Hund bellt nicht ohne Grund.", a:"Französisches Sprichwort" }, { t:"In seiner eigenen Hütte ist jeder Hund ein Löwe.", a:"Französisches Sprichwort" }, { t:"Hunde, die sich beißen, halten gegen den Wolf zusammen.", a:"Armenisches Sprichwort" }, { t:"Der Hund bellt, doch die Karawane zieht weiter.", a:"Türkisches Sprichwort" }, { t:"Hat ein Armer den Hund großgezogen, folgt er keinem Reichen mehr.", a:"Mongolisches Sprichwort" }, { t:"Hat der Hund zu viele Herren, schläft er hungrig ein.", a:"Afrikanisches Sprichwort" }, { t:"Ich hoffe, einmal der Mensch zu werden, für den mein Hund mich hält.", a:"Ungarisches Sprichwort" }, { t:"Eines Hundes Treue währt ein ganzes Leben lang.", a:"Spanisches Sprichwort" }, { t:"Faule Schäfer haben die besten Hunde.", a:"Deutsches Sprichwort" }, { t:"Hunde, die viel bellen, beißen selten.", a:"Italienisches Sprichwort" }, { t:"Wer einen guten Hund hat, braucht keinen Wächter.", a:"Italienisches Sprichwort" }, { t:"Wo der Hund frei laufen darf, ist das Glück nicht weit.", a:"Unbekannt" }, { t:"Ein Hund schaut nicht auf deinen Stand, nur auf dein Herz.", a:"Unbekannt" }, { t:"Dem Hund ist gleich, ob du reich bist; ihm reicht, dass du heimkommst.", a:"Unbekannt" }, { t:"Ein Hund braucht keine Worte, um dich zu trösten.", a:"Unbekannt" }, { t:"Der beste Platz der Welt ist neben einem Hund.", a:"Unbekannt" }, { t:"Ein Tag mit Hund ist nie ganz verloren.", a:"Unbekannt" }, { t:"Ein Hund füllt die Stille im Haus mit leisem Glück.", a:"Unbekannt" }, { t:"Hunde messen die Zeit nicht in Stunden, sondern in Spaziergängen.", a:"Unbekannt" }, { t:"Ein Hund findet zum Glück immer den kürzesten Weg.", a:"Unbekannt" }, { t:"Mit einem Hund an der Seite läuft man nie allein.", a:"Unbekannt" }, { t:"Ein Hund vergibt schneller, als wir uns entschuldigen können.", a:"Unbekannt" }, { t:"Wer die Sprache der Hunde lernt, hört auf zu reden und beginnt zu fühlen.", a:"Unbekannt" }, { t:"Ein Hund kennt deinen Namen nicht, aber er kennt dein Herz.", a:"Unbekannt" }, { t:"Hunde sind die Pünktlichsten, wenn es ums Glücklichsein geht.", a:"Unbekannt" }, { t:"Ein Hund wartet nicht auf morgen, um dich heute zu lieben.", a:"Unbekannt" }, { t:"Manche Engel haben Fell und kalte Pfoten.", a:"Unbekannt" }, { t:"Ein Hund nimmt dich, wie du bist, und macht dich trotzdem besser.", a:"Unbekannt" }, { t:"Was ein Hund über Freundschaft weiß, lernt der Mensch ein Leben lang.", a:"Unbekannt" }, { t:"Hunde haben kurze Leben, weil sie das Lieben so gut können, dass sie keine Zeit verschwenden.", a:"Unbekannt" }, { t:"Glück hat vier Pfoten und einen wedelnden Schwanz.", a:"Unbekannt" }, { t:"Ein Hund teilt dein Schweigen, ohne es zu füllen.", a:"Unbekannt" }, { t:"Der treueste Blick der Welt kommt von unten und wedelt dabei.", a:"Unbekannt" }, { t:"Ein Hund braucht keinen Sonntag, jeder Tag mit dir ist ihm Feiertag.", a:"Unbekannt" }, { t:"Wo ein Hund die Stiefel bringt, fehlt es nie an Liebe.", a:"Unbekannt" }, { t:"Hunde rechnen nicht nach, wie viel du gibst; sie geben einfach alles zurück.", a:"Unbekannt" }, { t:"Ein Hund sieht nicht, wie du aussiehst, sondern wer du bist.", a:"Unbekannt" }, { t:"Ein Hund macht aus einem Spaziergang ein kleines Abenteuer.", a:"Unbekannt" }, { t:"Wer einem Hund ins Auge sieht, schaut der Ehrlichkeit beim Atmen zu.", a:"Unbekannt" }, { t:"Ein Hund spürt deinen schlechten Tag und bleibt trotzdem.", a:"Unbekannt" }, { t:"Das Schwierigste am Hundeleben ist, dass es zu kurz für so viel Liebe ist.", a:"Unbekannt" }, { t:"Ein Hund braucht wenig und schenkt davon das Meiste.", a:"Unbekannt" }, { t:"Hunde sind der Beweis, dass Treue keine Worte braucht.", a:"Unbekannt" }, { t:"Ein wedelnder Schwanz hat schon manchen Tag gerettet.", a:"Unbekannt" }, { t:"Wer einen Hund versteht, braucht den Menschen weniger zu erklären.", a:"Unbekannt" }, { t:"Ein Hund spart seine Liebe nicht auf, er verschenkt sie sofort und vollständig.", a:"Unbekannt" }, { t:"Die kürzeste Verbindung zwischen zwei Menschen ist manchmal eine Hundeleine.", a:"Unbekannt" }, { t:"Ein Hund kennt keinen Stolz, nur Wiedersehensfreude.", a:"Unbekannt" }, { t:"Wer mit einem Hund alt wird, lernt das Glück im Kleinen.", a:"Unbekannt" }, { t:"Ein Hund hält dir keine Reden, er hält dir die Treue.", a:"Unbekannt" }, { t:"Vor einem Hund muss man nichts vorgeben; er liebt das Echte.", a:"Unbekannt" }, { t:"Ein Hund schreibt keine Briefe, doch sein Schwanz erzählt alles.", a:"Unbekannt" }, { t:"Hunde nehmen die kleinen Dinge ernst, darum sind sie so groß im Lieben.", a:"Unbekannt" }, { t:"Ein Hund verzeiht dir den Regen, solange du mit ihm hinausgehst.", a:"Unbekannt" }, { t:"Wer das Vertrauen eines Hundes gewinnt, hat etwas Selteneres als Gold.", a:"Unbekannt" }, { t:"Ein Hund macht stille Tage warm und laute Tage leiser.", a:"Unbekannt" }, { t:"Die treueste Uhr im Haus ist der Hund vor dem Futternapf.", a:"Unbekannt" }, { t:"Ein Hund fragt nicht, wie der Tag war; er macht ihn einfach besser.", a:"Unbekannt" }, { t:"Wer einen Hund an der Seite hat, ist nirgends ganz fremd.", a:"Unbekannt" }, { t:"Ein Hund kennt nur ein Tempo bei der Liebe: sofort.", a:"Unbekannt" }, { t:"Ein Hund am Feuer wärmt mehr als das Feuer selbst.", a:"Unbekannt" }, { t:"Wer den Hund gut behandelt, dem öffnet sich das Herz von selbst.", a:"Unbekannt" }, { t:"Ein Hund braucht keinen Kalender, er weiß genau, wann du nach Hause kommst.", a:"Unbekannt" }, ]; // ── ALBUM (Ban Yaro — eigene Songs) ────────────────────────── // Banner in der WELT-Welt lädt zum Entdecken ein und verschwindet nach erstem // Öffnen. Ab dann: dezenter runder Button unten links (Gegenspieler zum FAB), // der das Album-Modal öffnet (Liste mit Play je Titel). Audio-Element lebt // zentral in index.html → übersteht Re-Renders & Welt-Wechsel. const _anthem = (() => { const KEY = 'by_anthem_heard'; const LANG_KEY = 'by_album_lang'; // EN-Album erst sichtbar, wenn die 7 *-en.mp3 in static/sounds/ liegen. // Aktivieren: Dateien ablegen → EN_READY = true → make bump → deploy. const EN_READY = true; // 2026-06-16: 7 *-en.mp3 liegen in static/sounds/ const SONGS_DE = [ { title: 'Ban Yaro Blues', sub: 'Die Hymne', file: '/sounds/ban-yaro-blues.mp3?v=2' }, { title: 'Ban Yaro Mobil', sub: 'Erste Fahrt im Anhänger', file: '/sounds/ban-yaro-mobil.mp3' }, { title: 'Amy', sub: 'Eine Liebesromanze', file: '/sounds/amy.mp3' }, { title: 'Beim Friseur', sub: 'Halbes Fell, Energie pur', file: '/sounds/beim-friseur.mp3' }, { title: 'Leckerli-Paradies', sub: 'Voller Napf, volles Glück', file: '/sounds/leckerli-paradies.mp3' }, { title: 'Platsch!', sub: 'Ab ins kühle Nass', file: '/sounds/platsch.mp3' }, { title: 'Bester Freund', sub: 'Du und ich', file: '/sounds/bester-freund.mp3' }, ]; const SONGS_EN = [ { title: 'Ban Yaro Blues', sub: 'The anthem', file: '/sounds/ban-yaro-blues-en.mp3' }, { title: 'Ban Yaro Mobile', sub: 'First ride in the trailer', file: '/sounds/ban-yaro-mobil-en.mp3' }, { title: 'Amy', sub: 'A love duet', file: '/sounds/amy-en.mp3' }, { title: "At the Groomer's", sub: 'Half the fur, all the energy', file: '/sounds/at-the-groomers-en.mp3' }, { title: 'Treat Paradise', sub: 'Full bowl, full heart', file: '/sounds/treat-paradise-en.mp3' }, { title: 'Splash!', sub: 'Into the cool water', file: '/sounds/splash-en.mp3' }, { title: 'Best Friend', sub: 'You and me', file: '/sounds/best-friend-en.mp3' }, ]; // „neo" = Neon Edition: dieselben EN-Texte, anderer Musikstil (Electro + Garage-Rock). const SONGS_EN_ELECTRO = [ { title: 'Ban Yaro Blues', sub: 'Garage-rock anthem', file: '/sounds/ban-yaro-blues-en-electro.mp3' }, { title: 'Ban Yaro Mobile', sub: 'Motorik road groove', file: '/sounds/ban-yaro-mobil-en-electro.mp3' }, { title: 'Amy', sub: 'Vocoder love duet', file: '/sounds/amy-en-electro.mp3' }, { title: "At the Groomer's", sub: 'Disco-funk salon', file: '/sounds/at-the-groomers-en-electro.mp3' }, { title: 'Treat Paradise', sub: 'Euphoric filter-house', file: '/sounds/treat-paradise-en-electro.mp3' }, { title: 'Splash!', sub: 'Funk-house groove', file: '/sounds/splash-en-electro.mp3' }, { title: 'Best Friend', sub: 'Acoustic-soul to synth', file: '/sounds/best-friend-en-electro.mp3' }, ]; const ALBUMS = { de: SONGS_DE, en: SONGS_EN, neo: SONGS_EN_ELECTRO }; let _lang = (() => { try { const v = EN_READY ? localStorage.getItem(LANG_KEY) : null; return ALBUMS[v] ? v : 'de'; } catch (_) { return 'de'; } })(); const _songs = () => (EN_READY && ALBUMS[_lang]) ? ALBUMS[_lang] : SONGS_DE; let _bound = false, _curIdx = -1; const _audio = () => document.getElementById('anthem-audio'); // Entdeckt? 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(); } // Runder Button (orange wenn etwas läuft) + Modal-Songzeilen an Wiedergabe angleichen. function _sync() { const a = _audio(); const playing = !!(a && !a.paused && !a.ended); document.getElementById('worlds-anthem')?.classList.toggle('playing', playing); document.querySelectorAll('#album-modal .album-song').forEach((row, i) => { const active = (i === _curIdx) && playing; row.classList.toggle('album-song--active', active); row.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${active ? 'pause' : 'play'}`); }); } function _bindAudio() { if (_bound) return; const a = _audio(); if (!a) return; _bound = true; a.addEventListener('play', _sync); a.addEventListener('pause', _sync); a.addEventListener('ended', () => { // automatisch zum nächsten Song if (_curIdx >= 0 && _curIdx < _songs().length - 1) _play(_curIdx + 1); else { _curIdx = -1; _sync(); } }); } function _play(i) { const a = _audio(); const songs = _songs(); if (!a || !songs[i]) return; if (i === _curIdx && !a.paused) { a.pause(); return; } // aktiven Song pausieren _curIdx = i; a.src = songs[i].file; a.play().catch(() => {}); _markHeard(); _sync(); } function _closeAlbum() { document.getElementById('album-modal')?.remove(); } // Sprache wechseln: aktuelle Wiedergabe stoppen (andere Datei) und Liste neu zeichnen. function _setLang(l) { if (l === _lang || !EN_READY) return; _lang = l; try { localStorage.setItem(LANG_KEY, l); } catch (_) {} const a = _audio(); if (a) a.pause(); _curIdx = -1; _fillAlbum(); _sync(); } // Inhalt des Sheets (neu) rendern + innere Controls binden — auch bei Sprachwechsel. function _fillAlbum() { const sheet = document.querySelector('#album-modal .album-sheet'); if (!sheet) return; const songs = _songs(); const de = _lang === 'de'; const subtitle = de ? 'Songs · selbst gemacht 🎸' : _lang === 'neo' ? 'songs · neon edition 🎛️' : 'songs · homemade 🎸'; sheet.innerHTML = `
${de ? 'Ban Yaro — das Album' : 'Ban Yaro — The Album'}
${songs.length} ${subtitle}
${EN_READY ? `
` : ''}
${songs.map((s, i) => `
${_esc(s.title)} ${_esc(s.sub)}
`).join('')}
`; sheet.querySelector('.album-close').addEventListener('click', _closeAlbum); sheet.querySelectorAll('.album-lang-btn').forEach(b => b.addEventListener('click', () => _setLang(b.dataset.lang))); sheet.querySelectorAll('.album-song').forEach(row => { const i = parseInt(row.dataset.i, 10); row.addEventListener('click', () => _play(i)); row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _play(i); } }); }); } function openAlbum() { _markHeard(); if (document.getElementById('album-modal')) return; const ov = document.createElement('div'); ov.id = 'album-modal'; ov.innerHTML = `
`; document.body.appendChild(ov); ov.addEventListener('click', e => { if (e.target === ov) _closeAlbum(); }); _fillAlbum(); _sync(); } function updateButton() { document.getElementById('worlds-anthem')?.classList.toggle('hidden', !(_cur === 2 && heard())); } // Bei jedem WELT-Render: 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', openAlbum); card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openAlbum(); } }); } const btn = document.getElementById('worlds-anthem'); if (btn && !btn._anthemBound) { btn._anthemBound = true; btn.addEventListener('click', openAlbum); } _sync(); updateButton(); } return { heard, toggle: openAlbum, updateButton, initWelt, count: SONGS_DE.length }; })(); function _renderWelt() { const el = document.getElementById('ww-content'); if (!el) return; const user = _state?.user; const isMod = user?.rolle === 'admin' || user?.rolle === 'moderator' || user?.is_moderator; const isAdmin = user?.rolle === 'admin'; const isSocial = user?.is_social_media || isAdmin; // Tagesbasierter Spruch const day = Math.floor(Date.now() / 86400000); const quote = _QUOTES[day % _QUOTES.length]; const chips = _chipsForWorld('welt'); el.innerHTML = `
Gedanke des Tages
»${_esc(quote.t)}«
— ${_esc(quote.a)}
${_anthem.heard() ? '' : `
Ban Yaro — das Album
${_anthem.count} Songs · zum Anhören
`}
${chips.map(c => _chip(c.icon, c.label, c.page, false, false, false)).join('')}
`; el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); _anthem.initWelt(); } // ── HELPERS ────────────────────────────────────────────────── async function _getCachedWeather() { const cached = _wLoad('weather'); let fresh = null, pos = null; try { pos = await API.getLocation({ timeout: 5000, maximumAge: 600_000 }); fresh = await API.weather.get(pos.lat, pos.lon); _wSave('weather', fresh); } catch {} const data = fresh ?? cached?.data ?? null; return { data, fromCache: !fresh, ageMin: fresh ? 0 : (cached?.ageMin ?? null) }; } async function _getWeather() { const r = await _getCachedWeather(); return r.data; } async function _getNearbyAlerts() { const out = []; try { const pos = await API.getLocation({ timeout: 4000, maximumAge: 600_000 }); // /poison + /lost filtern per lat/lon serverseitig (poison: Radius in METERN, // lost: radius_km). Die früheren /nearby-Pfade existierten nie (459cd42-Verlust) // — das doppelte catch hat den 404 jahrelang verschluckt. const [p, l] = await Promise.allSettled([ API.get(`/poison?lat=${pos.lat}&lon=${pos.lon}&radius=5000`).catch(() => []), API.get(`/lost?lat=${pos.lat}&lon=${pos.lon}&radius_km=20`).catch(() => []), ]); if (p.value?.length) out.push({ icon:'skull', color:'#EF4444', title:'Giftköder in der Nähe', sub:`${p.value.length} Meldung${p.value.length>1?'en':''}`, page:'poison' }); if (l.value?.length) out.push({ icon:'dog', color:'#3B82F6', title:'Verlorener Hund', sub:`${l.value.length} Meldung${l.value.length>1?'en':''}`, page:'lost' }); } catch {} return out; } function _skeleton(n) { return Array.from({length: n}, (_, i) => `
`).join(''); } function _updateBdayTabIndicator(bdayDog) { if (bdayDog && !document.getElementById('by-bday-tab-style')) { const s = document.createElement('style'); s.id = 'by-bday-tab-style'; s.textContent = '@keyframes by-bday-bounce{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-5px) scale(1.25)}}' + '.wlabel-bday-ic{display:inline-block;animation:by-bday-bounce 1.2s ease-in-out infinite;margin-left:3px;font-size:.85em}'; document.head.appendChild(s); } const hundTab = document.querySelectorAll('#world-labels .wlabel')[1]; if (!hundTab) return; hundTab.querySelector('.wlabel-bday-ic')?.remove(); if (bdayDog) { const ic = document.createElement('span'); ic.className = 'wlabel-bday-ic'; ic.textContent = '🎂'; ic.title = `${bdayDog.name} hat ${_birthdayState(bdayDog.geburtstag) === 'today' ? 'heute' : 'morgen'} Geburtstag!`; hundTab.appendChild(ic); } } function _fmtDate(d) { if (!d) return ''; try { return new Date(d).toLocaleDateString('de-DE', { day:'numeric', month:'short' }); } catch { return d; } } function _fmtAlter(j) { if (!j) return ''; if (j < 1) return `${Math.round(j * 12)} Monate`; return j < 2 ? '1 Jahr' : `${Math.round(j)} Jahre`; } function _esc(s) { if (s == null) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function refresh(appState) { if (appState) _state = appState; localStorage.removeItem('w3_dogs'); _bgUrl = null; if (_visible) { if (_cur === 0) _renderJetzt(); else if (_cur === 1) _renderHund(); else _renderWelt(); } else { _refreshPending = true; } } return { init, show, hide, navigateTo, refresh, openConfig: _openConfigModal, get _visible() { return _visible; } }; })();