` : ''}
`,
});
// 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'); setTimeout(() => pages['diary'].module?.openNew?.(), 400); }
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
if (action === 'lost') { navigate('lost'); setTimeout(() => pages['lost'].module?.openNew?.(), 400); }
}, 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;
document.getElementById('header-login-btn')?.remove();
_updateHeaderUserBtn(true);
// 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';
}
const moderationItem = document.getElementById('sidebar-moderation');
if (moderationItem) {
const isMod = state.user.rolle === 'admin' || state.user.rolle === 'moderator'
|| state.user.is_moderator;
moderationItem.style.display = isMod ? '' : 'none';
}
const isBreeder = state.user.rolle === 'breeder' || state.user.rolle === 'admin';
const breederSection = document.getElementById('sidebar-breeder-section');
if (breederSection) breederSection.style.display = isBreeder ? '' : 'none';
const socialItem = document.getElementById('sidebar-social');
if (socialItem) {
const isSocial = state.user.is_social_media || state.user.rolle === 'admin';
socialItem.style.display = isSocial ? '' : 'none';
}
await _loadDogs();
// Eingeloggter User ohne Hund → Onboarding-Wizard (einmalig)
if (state.dogs.length === 0 && !localStorage.getItem('by_onboarding_done')) {
navigate('onboarding');
}
// Abo abgelaufen mit mehreren Hunden → Haupthund auswählen (nur wenn explizit 1, nicht "0" string)
if (state.user.needs_dog_selection === 1 && state.dogs.length > 1) {
_showDogSelectionModal();
}
// Theme aus DB-Profil übernehmen (überschreibt localStorage-Wert)
_applyUserTheme(state.user);
// Drei Welten nach Login starten (falls noch nicht initialisiert)
if (window.Worlds) window.Worlds.init(state);
_showVerifyBanner();
_showAndroidBetaBanner();
_updateNotifBadge();
_updateChatBadge();
_checkNearbyAlerts();
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
setInterval(_checkNearbyAlerts, 5 * 60_000);
// App-Heartbeat: last_seen aktualisieren (Nutzungsfrequenz für Admin)
const _sendHeartbeat = () => API.post('/auth/heartbeat', {}).catch(() => {});
_sendHeartbeat();
setInterval(_sendHeartbeat, 5 * 60_000);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
_updateNotifBadge();
_updateChatBadge();
_checkNearbyAlerts();
_sendHeartbeat();
if (state.page === 'chat') {
pages['chat']?.module?.refresh?.();
}
}
});
const pendingInvite = sessionStorage.getItem('pending_invite');
if (pendingInvite) {
sessionStorage.removeItem('pending_invite');
_handleInvite(pendingInvite);
}
}
async function _checkNearbyAlerts() {
try {
const pos = await new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000, maximumAge: 120_000 })
);
const { latitude: lat, longitude: lon } = pos.coords;
await API.get(`/alerts?lat=${lat}&lon=${lon}`);
// Standort-Update für Push-Subscriptions (serverseitig in /alerts gespeichert)
} catch {
// Kein Standort verfügbar — ignorieren
}
}
async function _updateNotifBadge() {
if (!state.user) return;
try {
const b = await API.notifications.badge();
document.getElementById('chat-nav-badge')?.classList.toggle('hidden', !b.personal);
} catch { /* ignorieren */ }
}
async function _updateChatBadge() {
if (!state.user) return;
try {
const convs = await API.chat.conversations();
const total = convs.reduce((s, c) => s + (c.unread_count || 0), 0);
const badge = document.getElementById('chat-badge');
if (badge) { badge.textContent = total; badge.style.display = total > 0 ? '' : 'none'; }
} catch { /* ignorieren */ }
}
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();
_updateHeaderUserBtn(false);
window.Worlds?.hide();
document.getElementById('worlds-back')?.classList.remove('worlds-back-visible');
navigate('welcome', false);
}
function _applyUserTheme(user) {
const theme = user?.preferred_theme;
if (!theme || theme === 'system') { _syncThemeColor(); return; }
localStorage.setItem('by_theme', theme);
const html = document.documentElement;
if (theme === 'dark') html.setAttribute('data-theme', 'dark');
else if (theme === 'light') html.setAttribute('data-theme', 'light');
_syncThemeColor();
}
function _syncThemeColor() {
const isAndroid = /android/i.test(navigator.userAgent);
const isDark = isAndroid
|| document.documentElement.getAttribute('data-theme') === 'dark'
|| (window.matchMedia('(prefers-color-scheme: dark)').matches
&& document.documentElement.getAttribute('data-theme') !== 'light');
document.getElementById('meta-theme-color')?.setAttribute('content', isDark ? '#0f1623' : '#C4843A');
}
function _showDogSelectionModal() {
const dogs = state.dogs;
const optionHtml = dogs.map(d => `
`).join('');
UI.modal.open({
title: 'Haupthund auswählen',
body: `
Dein Abo ist ausgelaufen. Wähle einen Haupthund für deinen kostenlosen Account.
Alle anderen Hunde-Profile bleiben vollständig gespeichert — du kannst sie nach
einem erneuten Upgrade wieder aktivieren.
`,
footer: `
`
});
document.getElementById('dog-select-confirm')?.addEventListener('click', async () => {
const chosen = document.querySelector('[name="select-dog"]:checked')?.value;
if (!chosen) { UI.toast.warning('Bitte einen Hund auswählen.'); return; }
const btn = document.getElementById('dog-select-confirm');
btn.disabled = true; btn.textContent = '…';
try {
await API.auth.selectPrimaryDog(parseInt(chosen));
state.user.needs_dog_selection = 0;
state.activeDog = state.dogs.find(d => String(d.id) === chosen) || state.dogs[0];
localStorage.setItem('by_active_dog', String(state.activeDog.id));
UI.modal.close();
UI.toast.success('Haupthund festgelegt.');
_renderDogSwitcher();
} catch (e) {
btn.disabled = false; btn.textContent = 'Auswahl bestätigen';
UI.toast.error(e.message || 'Fehler.');
}
});
}
function _showAndroidBetaBanner() {
// Nur auf Android, nur einmalig, nur für eingeloggte Nutzer
if (!/android/i.test(navigator.userAgent)) return;
if (localStorage.getItem('by_android_beta_dismissed')) return;
setTimeout(() => {
UI.toast.info(
'📱 Play Store Beta: Hilf uns beim Android-Test! Schreib an support@banyaro.app',
20000
);
localStorage.setItem('by_android_beta_dismissed', '1');
}, 5000);
}
function _showVerifyBanner() {
const banner = document.getElementById('verify-banner');
if (!banner) return;
if (!state.user || state.user.email_verified) {
banner.style.display = 'none';
return;
}
const dismissed = sessionStorage.getItem('by_verify_dismissed');
if (dismissed) return;
banner.style.display = 'flex';
document.getElementById('verify-resend-btn')?.addEventListener('click', async () => {
await API.post('/auth/resend-verification', { email: state.user.email });
UI.toast.success('Bestätigungs-Mail erneut gesendet.');
}, { once: true });
document.getElementById('verify-banner-close')?.addEventListener('click', () => {
banner.style.display = 'none';
sessionStorage.setItem('by_verify_dismissed', '1');
}, { once: true });
}
function _updateHeaderUserBtn(loggedIn) {
const btn = document.getElementById('header-user-btn');
const icon = document.getElementById('header-user-icon');
if (!btn) return;
if (loggedIn) {
const av = state.user?.avatar_url;
if (av) {
btn.innerHTML = ``;
} else {
btn.innerHTML = ``;
}
btn.style.borderColor = 'var(--c-primary)';
btn.title = 'Mein Profil';
} else {
btn.innerHTML = `
`;
btn.style.borderColor = 'var(--c-border)';
btn.title = 'Anmelden';
}
}
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 */ }
}
// ----------------------------------------------------------
// ONBOARDING — Willkommens-Modal für neue User
// ----------------------------------------------------------
function _showOnboardingModal() {
UI.modal.open({
title: `${UI.icon('paw-print')} Willkommen bei Ban Yaro!`,
body: `
Schön, dass du dabei bist! Ban Yaro hilft dir, alles rund um
deinen Hund im Blick zu behalten — Spaziergänge, Gesundheit,
Termine und vieles mehr.
Fang jetzt an und leg ein Profil für deinen Hund an.
`,
footer: `
`,
});
document.getElementById('onboarding-start-btn')?.addEventListener('click', () => {
UI.modal.close();
navigate('dog-profile');
});
}
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`;
} else {
el.innerHTML = `Ban Yaro`;
}
return;
}
const avHtml = d => d.foto_url
? ``
: `🐕`;
// Inaktive Hunde rechts
let othersHtml = '';
if (others.length === 1) {
othersHtml = `