banyaro/backend/static/js/app.js
rene 472e0dd63f Hunde-Profil + Login/Register + Auth-Redirect
dog-profile.js: Profil anlegen, anzeigen, bearbeiten, Foto-Upload,
  Alter-Berechnung, Löschen (mit Confirm).
settings.js: Login/Register-Tabs, Logout, Push-Subscription,
  nach Login → Tagebuch oder Profil anlegen.
app.js: _onLoggedOut() leitet direkt zur Settings-Seite weiter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 17:57:51 +02:00

257 lines
8.3 KiB
JavaScript

/* ============================================================
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
document.getElementById('header-title').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;
}
const container = document.querySelector(`#page-${pageId} .page-body`);
if (!container) 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 = {};
}
}
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;
}
// + Button
if (e.target.closest('#nav-add')) {
_showQuickAdd();
}
});
// Browser Back/Forward
window.addEventListener('popstate', e => {
const page = e.state?.page || 'diary';
navigate(page, false);
});
// Initial: URL-Hash auslesen
const hash = location.hash.replace('#', '');
if (hash && pages[hash]) {
navigate(hash, false);
}
}
// ----------------------------------------------------------
// SCHNELL-HINZUFÜGEN (+ Button)
// ----------------------------------------------------------
function _showQuickAdd() {
UI.modal.open({
title: 'Was möchtest du hinzufügen?',
body: `
<div class="flex flex-col gap-3">
<button class="btn btn-secondary w-full" data-quick="diary">
📖 Tagebuch-Eintrag
</button>
<button class="btn btn-secondary w-full" data-quick="health">
💉 Gesundheits-Eintrag
</button>
<button class="btn btn-danger w-full" data-quick="poison">
⚠️ Giftköder melden
</button>
<button class="btn btn-secondary w-full" data-quick="place">
📍 Ort hinzufügen
</button>
<button class="btn btn-nature w-full" data-quick="walk">
🦮 Gassi-Treffen erstellen
</button>
</div>
`,
});
// Quick-Add Aktionen
document.querySelector('#modal-container').addEventListener('click', e => {
const btn = e.target.closest('[data-quick]');
if (!btn) return;
UI.modal.close();
const action = btn.dataset.quick;
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?.(); }
}, { once: true });
}
// ----------------------------------------------------------
// AUTH
// ----------------------------------------------------------
async function _checkAuth() {
try {
const user = await API.auth.me();
state.user = user;
_onLoggedIn();
} catch {
_onLoggedOut();
}
}
function _onLoggedIn() {
document.getElementById('sidebar-username').textContent = state.user.name;
_loadDogs();
}
function _onLoggedOut() {
state.user = null;
navigate('settings', false);
}
async function _loadDogs() {
try {
state.dogs = await API.dogs.list();
if (state.dogs.length > 0) {
state.activeDog = state.dogs[0];
}
// Seitenmodule über neuen Hund informieren
_notifyDogChange();
} catch { /* kein Hund vorhanden */ }
}
function _notifyDogChange() {
// Alle geladenen Seiten-Module über Hundwechsel informieren
Object.values(pages).forEach(p => p.module?.onDogChange?.(state.activeDog));
}
// ----------------------------------------------------------
// INITIALISIERUNG
// ----------------------------------------------------------
async function init() {
_bindNavigation();
await _checkAuth();
// Erste Seite laden (Hash oder Standard: diary)
const startPage = location.hash.replace('#', '') || 'diary';
navigate(pages[startPage] ? startPage : 'diary', false);
}
// ----------------------------------------------------------
// ÖFFENTLICHE API
// (andere Module können App.state, App.navigate etc. nutzen)
// ----------------------------------------------------------
return { init, navigate, state };
})();
// App starten
document.addEventListener('DOMContentLoaded', () => App.init());