- Volltext-Suche im Tagebuch (LIKE über Titel/Text/Tags, Debounce 350ms)
- Digitaler Heimtierausweis als druckbare HTML-Seite (/ausweis/{dog_id})
Enthält Impfungen, Medikamente, Allergien, Tierärzte, Chip-Nr.
- Hund teilen: Einladungslink-System (dog_shares-Tabelle, /teilen/{token})
Geteilte Hunde erscheinen in der Hundeliste, Tagebuch/Gesundheit lesbar
- Widget-Seite /#widget: zufälliges Tagebuchbild + nächste Erinnerung
Als PWA-Shortcut im Manifest verankert
- SW-Cache by-v144, APP_VER 117
653 lines
25 KiB
JavaScript
653 lines
25 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — App Core
|
|
Router, State-Management, Navigation, Initialisierung.
|
|
============================================================ */
|
|
|
|
const APP_VER = '117'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
|
|
|
const App = (() => {
|
|
|
|
// ----------------------------------------------------------
|
|
// PWA INSTALL PROMPT — frühzeitig abfangen, bevor es verloren geht
|
|
// ----------------------------------------------------------
|
|
let _installPrompt = null;
|
|
window.addEventListener('beforeinstallprompt', e => {
|
|
e.preventDefault();
|
|
_installPrompt = e;
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// 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 = {
|
|
welcome: { title: 'Willkommen', module: null },
|
|
diary: { title: 'Tagebuch', module: null, requiresAuth: true },
|
|
health: { title: 'Gesundheit', module: null, requiresAuth: true },
|
|
'dog-profile': { title: 'Mein Hund', module: null, requiresAuth: true },
|
|
map: { title: 'Karte', module: null },
|
|
routes: { title: 'Routen', module: null },
|
|
events: { title: 'Events', module: null },
|
|
poison: { title: 'Giftköder-Alarm', module: null },
|
|
walks: { title: 'Gassi-Treffen', module: null, requiresAuth: true },
|
|
sitting: { title: 'Sitting', module: null, requiresAuth: true },
|
|
forum: { title: 'Forum', module: null },
|
|
wiki: { title: 'Wiki', module: null },
|
|
knigge: { title: 'Knigge', module: null },
|
|
movies: { title: 'Filme', module: null },
|
|
trainingsplaene: { title: 'Trainingspläne', module: null },
|
|
uebungen: { title: 'Übungsbibliothek', module: null },
|
|
'erste-hilfe': { title: 'Erste Hilfe', module: null },
|
|
settings: { title: 'Einstellungen', module: null },
|
|
lost: { title: 'Verlorener Hund', module: null },
|
|
friends: { title: 'Freunde', module: null, requiresAuth: true },
|
|
chat: { title: 'Nachrichten', module: null, requiresAuth: true },
|
|
admin: { title: 'Admin', module: null, requiresAuth: true },
|
|
impressum: { title: 'Impressum', module: null },
|
|
datenschutz: { title: 'Datenschutz', module: null },
|
|
widget: { title: 'Widget', module: null, requiresAuth: true },
|
|
};
|
|
|
|
// ----------------------------------------------------------
|
|
// AUTH GUARD — Login-Gate Texte pro Seite
|
|
// ----------------------------------------------------------
|
|
const AUTH_GATE = {
|
|
diary: { icon: 'book-open', text: 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Meilensteine, Fotos.' },
|
|
health: { icon: 'syringe', text: 'Impfungen, Tierarztbesuche, Medikamente und Allergien deines Hundes immer im Blick.' },
|
|
'dog-profile':{ icon: 'paw-print', text: 'Erstelle ein Profil für deinen Hund — mit Foto, Bio und NFC-Tag.' },
|
|
friends: { icon: 'users', text: 'Finde Hundebesitzer in deiner Nähe und baue ein Netzwerk auf.' },
|
|
chat: { icon: 'chat-circle-dots', text: 'Schreibe direkt mit anderen Hundebesitzern.' },
|
|
walks: { icon: 'paw-print', text: 'Organisiere Gassi-Treffen und triff andere Hunde in deiner Gegend.' },
|
|
sitting: { icon: 'house-line', text: 'Finde einen Dogsitter oder biete selbst Sitting an.' },
|
|
};
|
|
|
|
// ----------------------------------------------------------
|
|
// ROUTER
|
|
// ----------------------------------------------------------
|
|
function navigate(pageId, pushHistory = true, params = {}) {
|
|
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, params);
|
|
}
|
|
|
|
async function _loadPage(pageId, params = {}) {
|
|
const page = pages[pageId];
|
|
|
|
// AUTH GUARD — geschützte Seiten für nicht-eingeloggte User
|
|
if (page.requiresAuth && !state.user) {
|
|
const container = document.querySelector(`#page-${pageId} .page-body`);
|
|
if (container) _renderLoginGate(container, pageId);
|
|
return;
|
|
}
|
|
|
|
if (page.module) {
|
|
const hasParams = params && Object.keys(params).length > 0;
|
|
if (hasParams) {
|
|
// Re-init mit neuen Params (z.B. Chat mit bestimmter Konversation)
|
|
const container = document.querySelector(`#page-${pageId} .page-body`);
|
|
page.module.init?.(container, state, params);
|
|
} else {
|
|
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, params);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// LOGIN GATE — wird statt Seiteninhalt angezeigt
|
|
// ----------------------------------------------------------
|
|
function _renderLoginGate(container, pageId) {
|
|
const gate = AUTH_GATE[pageId] || { icon: 'lock', text: 'Dieser Bereich ist nur für angemeldete Nutzer.' };
|
|
const title = pages[pageId]?.title || 'Dieser Bereich';
|
|
|
|
container.innerHTML = `
|
|
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
min-height:60vh;padding:var(--space-8) var(--space-5);text-align:center;gap:var(--space-5)">
|
|
|
|
<!-- Icon -->
|
|
<div style="width:72px;height:72px;border-radius:50%;
|
|
background:var(--c-primary-subtle);
|
|
display:flex;align-items:center;justify-content:center">
|
|
<svg style="width:36px;height:36px;color:var(--c-primary)" aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#${UI.escape(gate.icon)}"></use>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Text -->
|
|
<div style="max-width:300px">
|
|
<h2 style="font-size:var(--text-xl);font-weight:var(--weight-bold);
|
|
color:var(--c-text);margin:0 0 var(--space-2)">
|
|
${UI.escape(title)}
|
|
</h2>
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
|
line-height:1.6;margin:0">
|
|
${UI.escape(gate.text)}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- CTAs -->
|
|
<div style="display:flex;flex-direction:column;gap:var(--space-3);width:100%;max-width:280px">
|
|
<button class="btn btn-primary" id="gate-login-btn">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sign-out"></use></svg>
|
|
Anmelden
|
|
</button>
|
|
<button class="btn btn-secondary" id="gate-register-btn">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user-plus"></use></svg>
|
|
Kostenlos registrieren
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Hinweis was sonst frei ist -->
|
|
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0">
|
|
Karte, Wiki, Übungen, Erste Hilfe und mehr sind ohne Account zugänglich.
|
|
</p>
|
|
|
|
<!-- Install-Hinweis -->
|
|
<button id="gate-install-hint"
|
|
style="background:none;border:none;cursor:pointer;padding:0;
|
|
font-size:var(--text-xs);color:var(--c-primary);
|
|
display:flex;align-items:center;gap:4px;text-decoration:underline">
|
|
<svg style="width:13px;height:13px" aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#download-simple"></use>
|
|
</svg>
|
|
Ban Yaro als App installieren
|
|
</button>
|
|
|
|
</div>
|
|
`;
|
|
|
|
container.querySelector('#gate-login-btn')?.addEventListener('click', () => {
|
|
navigate('settings');
|
|
});
|
|
container.querySelector('#gate-register-btn')?.addEventListener('click', () => {
|
|
navigate('settings');
|
|
});
|
|
container.querySelector('#gate-install-hint')?.addEventListener('click', () => {
|
|
navigate('welcome');
|
|
});
|
|
}
|
|
|
|
function _loadScript(src) {
|
|
// Versionierter URL damit SW/Browser-Cache bei jedem Deploy invalidiert wird
|
|
const versioned = `${src}?v=${APP_VER}`;
|
|
return new Promise((resolve, reject) => {
|
|
if (document.querySelector(`script[src="${versioned}"]`)) { resolve(); return; }
|
|
const s = document.createElement('script');
|
|
s.src = versioned;
|
|
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);
|
|
if (item.closest('#sidebar')) _closeSidebar();
|
|
return;
|
|
}
|
|
|
|
// Sidebar-Logo → Willkommensseite
|
|
if (e.target.closest('.sidebar-logo-text')) {
|
|
navigate('welcome');
|
|
_closeSidebar();
|
|
return;
|
|
}
|
|
|
|
// Sidebar-User (kein data-page, damit keine Aktiv-Markierung)
|
|
if (e.target.closest('#sidebar-user')) {
|
|
navigate('settings');
|
|
_closeSidebar();
|
|
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() {
|
|
document.getElementById('sidebar')?.classList.add('open');
|
|
document.getElementById('sidebar-backdrop')?.classList.add('visible');
|
|
}
|
|
|
|
function _closeSidebar() {
|
|
document.getElementById('sidebar')?.classList.remove('open');
|
|
document.getElementById('sidebar-backdrop')?.classList.remove('visible');
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// SCHNELL-HINZUFÜGEN (+ Button)
|
|
// ----------------------------------------------------------
|
|
function _showQuickAdd() {
|
|
const loggedIn = !!state.user;
|
|
|
|
const authBtn = (quick, cls, icon, label) => loggedIn
|
|
? `<button class="btn ${cls} w-full" data-quick="${quick}">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${icon}"></use></svg> ${label}
|
|
</button>`
|
|
: `<button class="btn ${cls} w-full" style="opacity:0.5" data-quick="auth-${quick}">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#lock"></use></svg> ${label}
|
|
</button>`;
|
|
|
|
UI.modal.open({
|
|
title: 'Was möchtest du hinzufügen?',
|
|
body: `
|
|
<div class="flex flex-col gap-3">
|
|
${authBtn('diary', 'btn-secondary', 'book-open', 'Tagebuch-Eintrag')}
|
|
${authBtn('health', 'btn-secondary', 'syringe', 'Gesundheits-Eintrag')}
|
|
<button class="btn btn-danger w-full" data-quick="poison">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-octagon"></use></svg> Giftköder melden
|
|
</button>
|
|
${authBtn('walk', 'btn-nature', 'paw-print', 'Gassi-Treffen erstellen')}
|
|
</div>
|
|
${!loggedIn ? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin-top:var(--space-3)">
|
|
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#info"></use></svg>
|
|
Einige Funktionen erfordern einen Account.
|
|
</p>` : ''}
|
|
`,
|
|
});
|
|
|
|
// 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.startsWith('auth-')) { navigate('settings'); return; }
|
|
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 === '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;
|
|
// Admin/Moderator-Item einblenden
|
|
const adminItem = document.getElementById('sidebar-admin');
|
|
if (adminItem) {
|
|
const isMod = state.user.rolle === 'admin' || state.user.rolle === 'moderator'
|
|
|| state.user.is_moderator;
|
|
adminItem.style.display = isMod ? '' : 'none';
|
|
}
|
|
await _loadDogs();
|
|
}
|
|
|
|
function _onLoggedOut() {
|
|
state.user = null;
|
|
state.dogs = [];
|
|
state.activeDog = null;
|
|
|
|
// Gecachte Module geschützter Seiten leeren, damit sie beim nächsten Login
|
|
// sauber neu initialisiert werden statt den alten Zustand zu refreshen.
|
|
Object.entries(pages).forEach(([, page]) => {
|
|
if (page.requiresAuth) page.module = null;
|
|
});
|
|
|
|
_renderDogSwitcher();
|
|
|
|
// Wenn aktuelle Seite geschützt ist → zu freier Seite wechseln
|
|
if (pages[state.page]?.requiresAuth) {
|
|
navigate('map', false);
|
|
} else {
|
|
// Bleib auf der Seite, zeige aber den Gate-Screen
|
|
_loadPage(state.page);
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<img class="sidebar-logo-img" src="/icons/icon-180.png" alt="Ban Yaro">
|
|
<span class="sidebar-logo-text">Ban Yaro</span>`;
|
|
} else {
|
|
el.innerHTML = `<span class="header-title" id="header-title">Ban Yaro</span>`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const avHtml = d => d.foto_url
|
|
? `<img src="${UI.escape(d.foto_url)}" alt="${UI.escape(d.name)}">`
|
|
: `<span>🐕</span>`;
|
|
|
|
// Inaktive Hunde rechts
|
|
let othersHtml = '';
|
|
if (others.length === 1) {
|
|
othersHtml = `
|
|
<div class="dog-sw-others">
|
|
<div class="dog-sw-other" data-dog-id="${others[0].id}" title="${UI.escape(others[0].name)}">
|
|
${avHtml(others[0])}
|
|
</div>
|
|
</div>`;
|
|
} else if (others.length >= 2) {
|
|
const visible = others.slice(0, 3);
|
|
const extraCount = others.length - 3;
|
|
othersHtml = `
|
|
<div class="dog-sw-others">
|
|
<div class="dog-sw-stack" id="dog-sw-stack-${ctxId}">
|
|
${visible.map((d, i) => `
|
|
<div class="dog-sw-other dog-sw-other--${i}" data-dog-id="${d.id}" title="${UI.escape(d.name)}">
|
|
${avHtml(d)}
|
|
</div>`).join('')}
|
|
${extraCount > 0 ? `<div class="dog-sw-more">+${extraCount}</div>` : ''}
|
|
</div>
|
|
<div class="dog-quickpick hidden" id="dog-qp-${ctxId}">
|
|
${others.map(d => `
|
|
<div class="dog-qp-item" data-dog-id="${d.id}">
|
|
<div class="dog-qp-av">${avHtml(d)}</div>
|
|
<span>${UI.escape(d.name)}</span>
|
|
</div>`).join('')}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
const titleClass = ctxId === 'sb' ? 'sidebar-logo-text' : 'header-title';
|
|
el.innerHTML = `
|
|
<div class="dog-sw-active" id="dog-sw-active-${ctxId}" title="${UI.escape(dog.name)} bearbeiten">
|
|
${avHtml(dog)}
|
|
</div>
|
|
<span class="${titleClass} dog-sw-title">Ban Yaro</span>
|
|
${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();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// INITIALISIERUNG
|
|
// ----------------------------------------------------------
|
|
async function init() {
|
|
_bindNavigation();
|
|
await _checkAuth();
|
|
|
|
// Einladungslink /teilen/{token} → direkt annehmen
|
|
const inviteMatch = location.pathname.match(/^\/teilen\/([A-Za-z0-9_-]+)$/);
|
|
if (inviteMatch) {
|
|
const token = inviteMatch[1];
|
|
navigate('diary', false);
|
|
_handleInvite(token);
|
|
return;
|
|
}
|
|
|
|
// Erste Seite laden: Hash aus URL oder Standard 'diary'.
|
|
const rawHash = location.hash.replace('#', '');
|
|
const [hashPage, hashQuery] = rawHash.split('?');
|
|
const hashParams = {};
|
|
if (hashQuery) {
|
|
new URLSearchParams(hashQuery).forEach((v, k) => {
|
|
hashParams[k] = isNaN(v) ? v : Number(v);
|
|
});
|
|
}
|
|
const startPage = (hashPage && pages[hashPage]) ? hashPage : 'diary';
|
|
navigate(startPage, false, hashParams);
|
|
}
|
|
|
|
async function _handleInvite(token) {
|
|
try {
|
|
const info = await API.sharing.info(token);
|
|
if (info.accepted_at) {
|
|
UI.toast.success(`Du hast bereits Zugriff auf ${info.dog_name}.`);
|
|
history.replaceState(null, '', '/');
|
|
return;
|
|
}
|
|
const ok = await UI.modal.confirm(
|
|
`<strong>${UI.escape(info.owner_name)}</strong> möchte das Profil von
|
|
<strong>${UI.escape(info.dog_name)}</strong> mit dir teilen
|
|
(${info.role === 'editor' ? 'Lesen & Schreiben' : 'Nur lesen'}).
|
|
Möchtest du die Einladung annehmen?`
|
|
);
|
|
if (!ok) { history.replaceState(null, '', '/'); return; }
|
|
await API.sharing.accept(token);
|
|
// Hundeliste neu laden
|
|
state.dogs = await API.dogs.list();
|
|
const newDog = state.dogs.find(d => d.name === info.dog_name);
|
|
if (newDog) {
|
|
state.activeDog = newDog;
|
|
localStorage.setItem('by_active_dog', String(newDog.id));
|
|
_renderDogSwitcher();
|
|
}
|
|
history.replaceState(null, '', '/');
|
|
UI.toast.success(`${UI.escape(info.dog_name)} wurde deiner Liste hinzugefügt!`);
|
|
} catch (e) {
|
|
UI.toast.error(e.message || 'Einladungslink ungültig.');
|
|
history.replaceState(null, '', '/');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// AUTH-GATE HELPER — einheitlicher "Bitte anmelden"-Block
|
|
// ----------------------------------------------------------
|
|
function requireAuth(container, { icon = 'user', text = 'Melde dich an, um diese Funktion zu nutzen.' } = {}) {
|
|
container.innerHTML = UI.emptyState({
|
|
icon: UI.icon(icon),
|
|
title: 'Anmelden erforderlich',
|
|
text,
|
|
action: `<button class="btn btn-primary" onclick="App.navigate('settings')">Anmelden</button>`,
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// ÖFFENTLICHE API
|
|
// (andere Module können App.state, App.navigate etc. nutzen)
|
|
// ----------------------------------------------------------
|
|
return { init, navigate, state, setActiveDog, renderDogSwitcher: _renderDogSwitcher,
|
|
getInstallPrompt: () => _installPrompt, requireAuth };
|
|
|
|
})();
|
|
|
|
// App starten
|
|
document.addEventListener('DOMContentLoaded', () => App.init());
|