/* ============================================================ 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 _lastUserId = undefined; let _dogs = []; // gecachte Hundesliste let _dogIdx = 0; // aktuell angezeigter Hund // 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; _cur = 1; // immer HUND als Start _setupSwipe(); _setupButtons(); _goTo(_cur, false); show(); // Welten parallel rendern _renderJetzt(); _renderHund(); } 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(); } // Nach Login/Logout neu rendern const currentUserId = _state?.user?.id ?? null; if (currentUserId !== _lastUserId) { _lastUserId = currentUserId; _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(); _t.moved = dx; const base = -_cur * (100 / 3); track.style.transform = `translateX(calc(${base}% + ${dx}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(); } }); } 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(); // 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)); } function _updateFab() { const fab = document.getElementById('worlds-fab'); if (!fab) return; const icons = ['note-pencil', 'paw-print', 'warning']; const titles = ['Schnelleintrag', 'Hund-Eintrag', 'Alarm melden']; fab.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${icons[_cur]}`); fab.title = titles[_cur]; } function _setupButtons() { document.getElementById('worlds-fab')?.addEventListener('click', _openFab); document.getElementById('worlds-settings')?.addEventListener('click', () => navigateTo('settings')); document.getElementById('worlds-back')?.addEventListener('click', () => show()); document.querySelectorAll('.wdot').forEach((dot, i) => { dot.style.pointerEvents = 'auto'; dot.addEventListener('click', () => { _goTo(i, true); if (i === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }); }); } function _openFab() { const isWelt = _cur === 2; const dogName = _state?.user ? null : null; // falls mehrere Hunde: erweiterbar const options = isWelt ? [ { icon:'skull', color:'#EF4444', label:'Giftköder melden', sub:'Warnung für andere Hundebesitzer', page:'poison' }, { icon:'dog', color:'#3B82F6', label:'Verlorenen Hund melden', sub:'Hilf beim Wiederfinden', page:'lost' }, { icon:'map-pin', color:'#10B981', label:'Ort vorschlagen', sub:'Neuen POI auf der Karte', page:'map' }, ] : [ { icon:'book-open', color:'#8B5CF6', label:'Tagebucheintrag', sub:'Erlebnis, Foto oder Notiz', page:'diary', action:'openNew' }, { icon:'target', color:'#F59E0B', label:'Training aufzeichnen',sub:'Übung absolviert', page:'uebungen' }, { 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' }, ]; // Overlay erstellen const ov = document.createElement('div'); ov.id = 'fab-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:300;display:flex;flex-direction:column;justify-content:flex-end'; ov.innerHTML = `
${isWelt ? 'Was möchtest du melden?' : 'Was möchtest du eintragen?'}
${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.querySelectorAll('.fab-option').forEach(btn => { btn.addEventListener('click', () => { _close(); const page = btn.dataset.page; const action = btn.dataset.action; navigateTo(page); if (action === 'openNew') { setTimeout(() => window.App?.callModule?.(page, 'openNew'), 400); } }); }); } // ── CHIP-KONFIGURATION ────────────────────────────────────── // Alle verfügbaren Chips mit Metadaten const _ALL_CHIPS = [ { icon:'note-pencil', label:'Notizblock', page:'notes' }, { icon:'currency-eur', label:'Ausgaben', page:'expenses' }, { icon:'first-aid', label:'Erste Hilfe', page:'erste-hilfe' }, { icon:'handshake', label:'Playdate', page:'playdate' }, { icon:'chat-circle-dots', label:'Nachrichten', page:'chat' }, { icon:'sun', label:'Wetter', page:'wetter' }, { icon:'gear', label:'Profil', page:'settings', pinned:true }, { icon:'book-open', label:'Tagebuch', page:'diary' }, { icon:'heartbeat', label:'Gesundheit', page:'health' }, { icon:'target', label:'Übungen', page:'uebungen' }, { icon:'list-checks', label:'Trainings-\npläne',page:'trainingsplaene'}, { icon:'heart', label:'Adoption', page:'adoption' }, { icon:'house-line', label:'Sitting', page:'sitting' }, { icon:'books', label:'Wiki', page:'wiki' }, { icon:'scales', label:'Wurfbörse', page:'wurfboerse' }, { icon:'map-trifold', label:'Karte', page:'map' }, { icon:'push-pin', label:'Forum', page:'forum' }, { icon:'users', label:'Freunde', page:'friends' }, { icon:'paw-print', label:'Gassi', page:'walks' }, { icon:'skull', label:'Giftköder', page:'poison' }, { icon:'warning-circle', label:'Rückrufe', page:'recalls' }, { icon:'dog', label:'Verlorene', page:'lost' }, { icon:'path', label:'Routen', page:'routes' }, { icon:'calendar-dots', label:'Events', page:'events' }, { icon:'sparkle', label:'Jobs', page:'jobs' }, { icon:'book-open', label:'Knigge', page:'knigge' }, { icon:'film-slate', label:'Filme', page:'movies' }, { icon:'tree-structure', label:'Zucht-\nkartei', page:'zuchthunde', role:'breeder' }, { icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder' }, { icon:'sparkle', label:'Social', page:'social', role:'social' }, { icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' }, { icon:'gear', label:'Admin', page:'admin', role:'admin' }, ]; const _DEFAULT_CONFIG = { jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','settings'], hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse'], welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events','jobs','knigge','movies'], }; function _getConfig() { try { return JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG; } catch { return _DEFAULT_CONFIG; } } function _saveConfig(cfg) { try { localStorage.setItem('world_chips', JSON.stringify(cfg)); } catch {} } function _chipMeta(page) { return _ALL_CHIPS.find(c => c.page === page) || null; } function _chipAllowed(chip) { const u = _state?.user; if (!chip?.role) return true; if (chip.role === 'breeder') 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 === 'admin') return u?.rolle === 'admin'; return true; } function _chipsForWorld(world) { const pages = _getConfig()[world] || _DEFAULT_CONFIG[world]; return pages.map(_chipMeta).filter(c => c && _chipAllowed(c)); } // ── KONFIGURATIONS-MODAL ───────────────────────────────────── function _openConfigModal() { let cfg = JSON.parse(JSON.stringify(_getConfig())); // deep copy let _drag = null; // { page, fromWorld, ghost } 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 ov = document.createElement('div'); ov.id = 'wc-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:500;display:flex;flex-direction:column;justify-content:flex-end'; document.body.appendChild(ov); const _removeDragListeners = () => { document.removeEventListener('touchmove', _onDragMove); document.removeEventListener('touchend', _onDragEnd); document.removeEventListener('touchcancel', _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() { ov.innerHTML = `
Welten einrichten
Lang drücken & ziehen zum Verschieben. ✕ zum Entfernen.
${['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 ? ` ` : `
`} ${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); _render(); }); }); // Touch-Drag ov.querySelectorAll('.wc-chip').forEach(chip => { chip.addEventListener('touchstart', e => _onDragStart(e, chip), { passive: true }); }); document.addEventListener('touchmove', _onDragMove, { passive: false }); document.addEventListener('touchend', _onDragEnd); } function _onDragStart(e, chipEl) { if (_drag) _cancelDrag(); const touch = e.touches[0]; // Drag erst nach Bewegungs-Threshold aktivieren (verhindert Scroll-Konflikte) _drag = { page: chipEl.dataset.page, zone: chipEl.dataset.zone, chipEl, ghost: null, dropZone: null, active: false, startX: touch.clientX, startY: touch.clientY, ox: 0, oy: 0, }; document.addEventListener('touchmove', _onDragMove, { passive: false }); document.addEventListener('touchend', _onDragEnd); document.addEventListener('touchcancel', _onDragEnd); } function _activateDrag(touch) { 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 = (touch.clientX - _drag.ox) + 'px'; ghost.style.top = (touch.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; const touch = e.touches[0]; if (!_drag.active) { const dx = Math.abs(touch.clientX - _drag.startX); const dy = Math.abs(touch.clientY - _drag.startY); if (dx < 8 && dy < 8) return; // Threshold noch nicht erreicht _activateDrag(touch); } e.preventDefault(); // Scroll erst NACH Threshold blockieren _drag.ghost.style.left = (touch.clientX - _drag.ox) + 'px'; _drag.ghost.style.top = (touch.clientY - _drag.oy) + 'px'; let foundZone = null; ov.querySelectorAll('.wc-zone').forEach(z => { const r = z.getBoundingClientRect(); const over = touch.clientX >= r.left && touch.clientX <= r.right && touch.clientY >= r.top && touch.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 todayKey = 'bg_' + new Date().toISOString().slice(0, 10); const cached = _wLoad(todayKey); if (cached?.data) return cached.data; try { const r = await _cachedGet(`diary_${dog.id}`, `/dogs/${dog.id}/diary?limit=30`); const entries = r.data?.entries || r.data || []; const withPhotos = entries.filter(e => (e.foto_urls?.length || e.foto_url)); if (!withPhotos.length) { const u = dog.foto_url || null; if(u) _wSave(todayKey, u); return u; } const day = Math.floor(Date.now() / 86400000); const entry = withPhotos[day % withPhotos.length]; const url = (entry.foto_urls?.[0] || entry.foto_url); _wSave(todayKey, url); return url; } catch { return dog.foto_url || null; } } function _applyBgImage(url) { const track = document.getElementById('worlds-track'); if (!track) return; if (url) { const img = new Image(); img.onload = () => { track.style.backgroundImage = `url('${url}')`; track.style.backgroundSize = '100% auto'; track.style.backgroundPosition = '0 40%'; track.style.backgroundRepeat = 'no-repeat'; }; img.onerror = () => _applyBgImage(null); img.src = url; } else { track.style.backgroundImage = 'linear-gradient(160deg,#1a1f35 0%,#16213e 33%,#1a2535 67%,#0f1921 100%)'; track.style.backgroundSize = '100% 100%'; } } // ── CHIP-HELPER ────────────────────────────────────────────── function _chip(icon, label, page) { return `
${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] = await Promise.allSettled([ _getCachedWeather(), user ? _cachedGet('dogs', '/dogs') : Promise.resolve({ data: [], fromCache: false, ageMin: 0 }), user ? _getNearbyAlerts() : Promise.resolve([]), ]); 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 isOffline = weatherObj.fromCache && dogsObj.fromCache; const staleMin = Math.max(weatherObj.ageMin || 0, dogsObj.ageMin || 0); // Panorama-Bild setzen (nur wenn noch kein Bild vorhanden) const track = document.getElementById('worlds-track'); if (dog && !track?.style.backgroundImage?.startsWith('url')) { _loadDailyImage(dog).then(_applyBgImage); } else if (!dog) { _applyBgImage(null); } const hour = new Date().getHours(); const greet = hour < 5 ? 'Gute Nacht' : hour < 12 ? 'Guten Morgen' : hour < 18 ? 'Hallo' : 'Guten Abend'; const firstName = user?.name?.split(' ')[0] || ''; const dayStr = new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' }); const stale = isOffline && staleMin > 5 ? `· Offline` : ''; const weatherLine = w ? `${Math.round(w.temp_c)}° ${_esc(w.desc?.split(' ')[0] || '')} · ${Math.round(w.wind_kmh || 0)} km/h · ${w.precip_prob || 0}% Regen` : ''; // Streak-Reminder let streakHtml = ''; if (user && dog) { try { const sr = await _cachedGet(`streak_${dog.id}`, `/streak/${dog.id}`); const s = sr.data; const streak = s?.current_streak || 0; const trainedToday = s?.last_training_date === new Date().toISOString().slice(0,10); const col = trainedToday ? 'rgba(16,185,129,0.8)' : (streak > 0 ? 'rgba(245,158,11,0.8)' : 'rgba(255,255,255,0.4)'); const label = streak > 0 ? (trainedToday ? `✓ ${streak} Tage Streak` : `🔥 ${streak} Tage — heute noch trainieren!`) : (trainedToday ? '✓ Heute trainiert' : 'Noch kein Training heute'); streakHtml = `
${label}
`; } catch {} } // 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?.foto_url; const uInitial = firstName ? firstName.charAt(0).toUpperCase() : '?'; const userAvatarHtml = `
${uFoto ? `` : `
${uInitial}
`}
`; el.innerHTML = `
${_esc(greet)}${firstName ? `, ${_esc(firstName)}` : ''}${stale}
${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}
${user ? userAvatarHtml : ''}
${alertHtml} ${streakHtml}
Deine Bereiche
${features.map(f => _chip(f.icon, f.label, f.page)).join('')}
`; el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); } // ── 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
`; return; } el.innerHTML = _skeleton(4); const dogsRes = await _cachedGet('dogs', '/dogs'); const dogs = dogsRes.data || []; if (!dogs.length) { el.innerHTML = `
🐶
Noch kein Hund angelegt
Erstelle das Profil deines Hundes
`; return; } // Hunde global cachen für schnelles Cycling _dogs = dogs; if (_dogIdx >= _dogs.length) _dogIdx = 0; const dog = _dogs[_dogIdx]; 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}
Alles über ${_esc(dog.name)}
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
`; // Avatar → Hundeprofil el.querySelector('#wh-avatar')?.addEventListener('click', () => 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(); } }); }); } // ── 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' }, ]; 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)}
Die Welt da draußen
${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
`; el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav))); } // ── 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 }); const [p, l] = await Promise.allSettled([ API.get(`/poison/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).catch(() => []), API.get(`/lost/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).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 _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,'"'); } return { init, show, hide, navigateTo, openConfig: _openConfigModal, get _visible() { return _visible; } }; })();