/* ============================================================ BAN YARO — App Core Router, State-Management, Navigation, Initialisierung. ============================================================ */ const App = (() => { // ---------------------------------------------------------- // STATE — zentraler App-Zustand // ---------------------------------------------------------- const state = { user: null, // eingeloggter User (oder null) dogs: [], // Hunde des Users activeDog: null, // aktuell gewählter Hund page: 'diary', // aktive Seite }; // ---------------------------------------------------------- // SEITENDEFINITIONEN // Jede Seite: { id, title, load() } // load() wird beim ersten Aufruf einmalig ausgeführt // ---------------------------------------------------------- const pages = { diary: { title: 'Tagebuch', module: null }, health: { title: 'Gesundheit', module: null }, 'dog-profile': { title: 'Mein Hund', module: null }, map: { title: 'Karte', module: null }, routes: { title: 'Routen', module: null }, places: { title: 'Orte', module: null }, events: { title: 'Events', module: null }, poison: { title: 'Giftköder-Alarm', module: null }, walks: { title: 'Gassi-Treffen', module: null }, sitting: { title: 'Sitting', module: null }, forum: { title: 'Forum', module: null }, wiki: { title: 'Wiki', module: null }, knigge: { title: 'Knigge', module: null }, movies: { title: 'Filme', module: null }, settings: { title: 'Einstellungen', module: null }, }; // ---------------------------------------------------------- // ROUTER // ---------------------------------------------------------- function navigate(pageId, pushHistory = true) { if (!pages[pageId]) return; // Aktive Seite ausblenden document.querySelector('.page.active')?.classList.remove('active'); document.querySelectorAll('.nav-item.active, .sidebar-item.active') .forEach(el => el.classList.remove('active')); // Neue Seite einblenden document.getElementById(`page-${pageId}`)?.classList.add('active'); // Navigation markieren document.querySelectorAll(`[data-page="${pageId}"]`) .forEach(el => el.classList.add('active')); // Header-Titel setzen (nur wenn kein Dog-Switcher aktiv ist) const titleEl = document.getElementById('header-title'); if (titleEl) titleEl.textContent = pages[pageId].title; // History if (pushHistory) { history.pushState({ page: pageId }, '', `#${pageId}`); } state.page = pageId; UI.scrollTop(); // Seiten-Modul lazy laden (einmalig) _loadPage(pageId); } async function _loadPage(pageId) { const page = pages[pageId]; if (page.module) { // Bereits geladen → nur refresh aufrufen wenn vorhanden page.module.refresh?.(); return; } // Guard: verhindert doppelten Load bei gleichzeitigen navigate()-Aufrufen if (page._loading) return; page._loading = true; const container = document.querySelector(`#page-${pageId} .page-body`); if (!container) { page._loading = false; return; } // Skeleton während Laden container.innerHTML = UI.skeleton(4); try { // Seiten-Script dynamisch laden await _loadScript(`/js/pages/${pageId}.js`); const mod = window[`Page_${pageId.replace(/-/g, '_')}`]; if (mod?.init) { await mod.init(container, state); page.module = mod; } else { // Platzhalter wenn Seite noch nicht gebaut container.innerHTML = UI.emptyState({ icon: '🚧', title: pages[pageId].title, text: 'Diese Seite ist noch in Entwicklung.', }); page.module = {}; // verhindert erneutes Laden } } catch { container.innerHTML = UI.emptyState({ icon: '🚧', title: pages[pageId].title, text: 'Diese Seite ist noch in Entwicklung.', }); page.module = {}; } finally { page._loading = false; } } function _loadScript(src) { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } // ---------------------------------------------------------- // NAVIGATION EVENTS // ---------------------------------------------------------- function _bindNavigation() { // Bottom Nav + Sidebar Klicks document.addEventListener('click', e => { const item = e.target.closest('[data-page]'); if (item) { navigate(item.dataset.page); return; } // Sidebar-User (kein data-page, damit keine Aktiv-Markierung) if (e.target.closest('#sidebar-user')) { navigate('settings'); return; } // + Button (Mobile Bottom-Nav + Desktop Sidebar) if (e.target.closest('#nav-add') || e.target.closest('#sidebar-add')) { _showQuickAdd(); return; } // Hamburger-Menü (Mobile) if (e.target.closest('#header-menu-btn')) { _toggleSidebar(); return; } // Backdrop → Sidebar schließen if (e.target.closest('#sidebar-backdrop')) { _closeSidebar(); return; } // Sidebar-Item auf Mobile → schließen nach Navigation if (e.target.closest('#sidebar .sidebar-item')) { _closeSidebar(); } }); // Browser Back/Forward window.addEventListener('popstate', e => { _closeSidebar(); const page = e.state?.page || 'diary'; navigate(page, false); }); // Hash-Navigation wird in init() nach _checkAuth() behandelt (nicht hier), // damit kein doppelter _loadPage()-Aufruf entsteht. } // ---------------------------------------------------------- // MOBILE SIDEBAR DRAWER // ---------------------------------------------------------- function _toggleSidebar() { const sidebar = document.getElementById('sidebar'); const backdrop = document.getElementById('sidebar-backdrop'); const isOpen = sidebar?.classList.contains('open'); isOpen ? _closeSidebar() : _openSidebar(); } function _openSidebar() { const sidebar = document.getElementById('sidebar'); if (!sidebar) return; // Inline-Styles: unabhängig vom CSS-Cache-Stand // Inline-Styles: unabhängig vom CSS-Cache; Farben als Fallback hardcoded const bg = getComputedStyle(document.documentElement) .getPropertyValue('--c-surface').trim() || '#ffffff'; sidebar.style.cssText = [ 'display:flex', 'position:fixed', 'top:0', 'left:0', 'bottom:0', 'width:240px', 'z-index:2000', `background:${bg}`, 'flex-direction:column', 'overflow:hidden', 'box-shadow:4px 0 24px rgba(0,0,0,0.22)', 'border-right:1px solid #e5e7eb', ].join(';'); document.getElementById('sidebar-backdrop')?.classList.add('visible'); } function _closeSidebar() { const sidebar = document.getElementById('sidebar'); if (!sidebar) return; sidebar.style.cssText = ''; // alle Inline-Styles weg → CSS übernimmt wieder document.getElementById('sidebar-backdrop')?.classList.remove('visible'); } // ---------------------------------------------------------- // SCHNELL-HINZUFÜGEN (+ Button) // ---------------------------------------------------------- function _showQuickAdd() { UI.modal.open({ title: 'Was möchtest du hinzufügen?', body: `
`, }); // Quick-Add Aktionen document.querySelector('#modal-container').addEventListener('click', e => { const btn = e.target.closest('[data-quick]'); if (!btn) return; const action = btn.dataset.quick; UI.modal.close(); // Kurzes Delay wegen iOS Ghost-Click: nach modal.close() feuert iOS // ~300ms später ein synthetisches Click-Event an derselben Position. // Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort. setTimeout(() => { if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); } if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); } if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); } if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); } if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); } }, 350); }, { once: true }); } // ---------------------------------------------------------- // AUTH // ---------------------------------------------------------- async function _checkAuth() { try { const user = await API.auth.me(); state.user = user; await _onLoggedIn(); } catch { _onLoggedOut(); } } async function _onLoggedIn() { document.getElementById('sidebar-username').textContent = state.user.name; await _loadDogs(); } function _onLoggedOut() { state.user = null; state.dogs = []; state.activeDog = null; _renderDogSwitcher(); navigate('settings', false); } async function _loadDogs() { try { state.dogs = await API.dogs.list(); if (state.dogs.length > 0) { // Zuletzt aktiven Hund aus localStorage wiederherstellen const savedId = parseInt(localStorage.getItem('by_active_dog') || '0'); state.activeDog = state.dogs.find(d => d.id === savedId) || state.dogs[0]; } _renderDogSwitcher(); _notifyDogChange(); } catch { /* kein Hund vorhanden */ } } function _notifyDogChange() { Object.values(pages).forEach(p => p.module?.onDogChange?.(state.activeDog)); } // ---------------------------------------------------------- // HUNDE-SWITCHER (Header Mobile + Sidebar-Logo Desktop) // ---------------------------------------------------------- function _renderDogSwitcher() { _renderSwitcherInto(document.getElementById('header-dog-switcher'), 'hdr'); _renderSwitcherInto(document.getElementById('sidebar-dog-switcher'), 'sb'); } function _renderSwitcherInto(el, ctxId) { if (!el) return; const dog = state.activeDog; const others = state.dogs.filter(d => d.id !== dog?.id); // Fallback: kein User oder kein Hund → Standardlogo if (!state.user || !dog) { if (ctxId === 'sb') { el.innerHTML = ` Ban Yaro Ban Yaro`; } else { el.innerHTML = `Ban Yaro`; } return; } const avHtml = d => d.foto_url ? `${_esc(d.name)}` : `🐕`; // Inaktive Hunde rechts let othersHtml = ''; if (others.length === 1) { othersHtml = `
${avHtml(others[0])}
`; } else if (others.length >= 2) { const visible = others.slice(0, 3); const extraCount = others.length - 3; othersHtml = `
${visible.map((d, i) => `
${avHtml(d)}
`).join('')} ${extraCount > 0 ? `
+${extraCount}
` : ''}
`; } const titleClass = ctxId === 'sb' ? 'sidebar-logo-text' : 'header-title'; el.innerHTML = `
${avHtml(dog)}
Ban Yaro ${othersHtml}`; // Klick aktiver Avatar → Hund-Profil el.querySelector(`#dog-sw-active-${ctxId}`)?.addEventListener('click', () => { navigate('dog-profile'); }); // 1 anderer Hund → direkter Tausch if (others.length === 1) { el.querySelector('.dog-sw-other')?.addEventListener('click', () => { setActiveDog(others[0].id); }); } // 2+ andere Hunde → Stack klick öffnet Quickpicker if (others.length >= 2) { const stack = el.querySelector(`#dog-sw-stack-${ctxId}`); const qp = el.querySelector(`#dog-qp-${ctxId}`); stack?.addEventListener('click', e => { e.stopPropagation(); // Alle anderen Quickpicker schließen document.querySelectorAll('.dog-quickpick').forEach(q => { if (q !== qp) q.classList.add('hidden'); }); qp?.classList.toggle('hidden'); }); el.querySelectorAll('.dog-qp-item').forEach(item => { item.addEventListener('click', e => { e.stopPropagation(); setActiveDog(parseInt(item.dataset.dogId)); qp?.classList.add('hidden'); }); }); } } // Quickpicker schließen bei Klick außerhalb document.addEventListener('click', () => { document.querySelectorAll('.dog-quickpick').forEach(q => q.classList.add('hidden')); }); function setActiveDog(dogId) { const dog = state.dogs.find(d => d.id === dogId); if (!dog || dog.id === state.activeDog?.id) return; state.activeDog = dog; localStorage.setItem('by_active_dog', String(dogId)); _renderDogSwitcher(); _notifyDogChange(); } function _esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ---------------------------------------------------------- // INITIALISIERUNG // ---------------------------------------------------------- async function init() { _bindNavigation(); await _checkAuth(); // Erste Seite laden: Hash aus URL oder Standard 'diary'. // Bewusst NACH _checkAuth(), damit _loadPage() nur einmal aufgerufen wird // (vorher war Hash-Navigation auch in _bindNavigation() → doppelter Aufruf). const hash = location.hash.replace('#', ''); const startPage = (hash && pages[hash]) ? hash : 'diary'; navigate(startPage, false); } // ---------------------------------------------------------- // ÖFFENTLICHE API // (andere Module können App.state, App.navigate etc. nutzen) // ---------------------------------------------------------- return { init, navigate, state, setActiveDog, renderDogSwitcher: _renderDogSwitcher }; })(); // App starten document.addEventListener('DOMContentLoaded', () => App.init());