${UI.escape(dog.name)}
${othersHtml}`;
// Klick aktiver Avatar → Welcome-Seite; Klick Name → Hund-Profil
el.querySelector(`#dog-sw-active-${ctxId}`)?.addEventListener('click', () => {
navigate('welcome');
});
el.querySelector(`.dog-sw-title`)?.addEventListener('click', () => {
navigate('dog-profile');
if (ctxId === 'sb') _closeSidebar();
});
// 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() {
// Spezielle Hash-Parameter → in App bleiben (kein /info-Redirect)
const _rawHash = location.hash.replace('#', '');
const _hashQuery = _rawHash.split('?')[1] || '';
const _hashP = new URLSearchParams(_hashQuery);
if (_hashP.get('verified') || _hashP.get('token') || location.pathname.startsWith('/teilen/')) {
sessionStorage.setItem('by_stay_in_app', '1');
}
// Referral-Code aus URL ?ref=CODE speichern
const urlParams = new URLSearchParams(window.location.search);
const refCode = urlParams.get('ref');
if (refCode) {
sessionStorage.setItem('by_ref_code', refCode.toUpperCase());
// URL bereinigen ohne Reload
history.replaceState({}, '', window.location.pathname + window.location.hash);
}
_bindNavigation();
try { localStorage.removeItem('by_wissen_open'); } catch (_) {}
_initVersionCheck();
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);
});
}
// Passwort-Reset: #reset-password?token=xxx
if (hashPage === 'reset-password' && hashParams.token) {
sessionStorage.setItem('by_reset_token', hashParams.token);
history.replaceState(null, '', '/');
navigate('settings', false);
return;
}
// E-Mail-Verifikation: Redirect von /api/auth/verify-email/{token}
if (hashParams.verified === '1' || hashParams.verified === 1) {
if (state.user) state.user.email_verified = 1;
document.getElementById('verify-banner')?.style?.setProperty('display', 'none');
UI.toast.success('E-Mail-Adresse erfolgreich bestätigt!');
history.replaceState(null, '', '/');
} else if (hashParams.verified === 'error') {
UI.toast.error('Ungültiger oder abgelaufener Bestätigungs-Link.');
history.replaceState(null, '', '/');
}
const startPage = (hashPage && pages[hashPage]) ? hashPage : 'welcome';
navigate(state.user ? startPage : 'welcome', false, hashParams);
if (window.Worlds && state.user) window.Worlds.init(state);
}
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;
}
if (!state.user) {
sessionStorage.setItem('pending_invite', token);
history.replaceState(null, '', '/');
navigate('settings', false);
UI.toast.info('Bitte melde dich an, um die Einladung anzunehmen.');
return;
}
const ok = await UI.modal.confirm({
message: `${UI.escape(info.owner_name)} möchte das Profil von
${UI.escape(info.dog_name)} 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);
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: ``,
});
}
// ----------------------------------------------------------
// VERSION-CHECK
let _updateBannerShown = false;
async function _checkVersion() {
try {
const r = await fetch('/api/version', { cache: 'no-store' });
if (!r.ok) return;
const { version } = await r.json();
if (version && version !== APP_VER && !_updateBannerShown) {
_updateBannerShown = true;
_showUpdateBanner(version);
}
} catch { /* offline — ignorieren */ }
}
function _showUpdateBanner(newVersion) {
const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent);
const existing = document.getElementById('app-update-banner');
if (existing) return;
const banner = document.createElement('div');
banner.id = 'app-update-banner';
banner.style.cssText = [
'position:fixed;bottom:calc(env(safe-area-inset-bottom,0px) + 72px);left:12px;right:12px',
'z-index:9000;background:var(--c-primary);color:#fff;border-radius:16px',
'padding:14px 16px;box-shadow:0 4px 20px rgba(0,0,0,0.3)',
'display:flex;flex-direction:column;gap:10px',
].join(';');
banner.innerHTML = `
Neue Version verfügbar (v${newVersion})
Tippe auf Aktualisieren um die neueste Version zu laden.
${isIos
? `Falls die App nach dem Aktualisieren noch die alte Version zeigt: 1. Drücke lange auf das App-Icon am Homescreen 2. Wähle „App entfernen" (nur das Symbol, keine Daten) 3. Öffne banyaro.app in Safari und füge die App erneut hinzu`
: `Falls die Seite noch die alte Version zeigt:
Drücke Cmd+Shift+R (Mac) bzw. Ctrl+Shift+R (Windows/Android Chrome) für einen harten Reload.`}
`;
document.body.appendChild(banner);
banner.querySelector('#upd-btn-close').addEventListener('click', () => banner.remove());
banner.querySelector('#upd-btn-reload').addEventListener('click', () => {
location.href = '/?_nocache=' + Date.now();
});
}
function _initVersionCheck() {
// Beim Start nach 10 Sekunden prüfen (nicht sofort — Prio für Auth)
setTimeout(_checkVersion, 10_000);
// Dann alle 30 Minuten
setInterval(_checkVersion, 30 * 60_000);
// Beim Wiedereinstieg in die App
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') _checkVersion();
});
// Nach Reload: war das ein Update-Reload? Falls Version immer noch alt → iOS-Hinweis
const reloadVer = sessionStorage.getItem('by_update_reload');
if (reloadVer && reloadVer === APP_VER) {
// Version hat sich nicht geändert nach Reload → iOS-Cache-Problem
sessionStorage.removeItem('by_update_reload');
setTimeout(() => {
fetch('/api/version', { cache: 'no-store' })
.then(r => r.json())
.then(({ version }) => {
if (version && version !== APP_VER) {
_updateBannerShown = true;
_showUpdateBanner(version);
// iOS-Hinweis sofort aufklappen
setTimeout(() => {
document.getElementById('upd-ios-hint')?.style.setProperty('display', 'block');
}, 300);
}
}).catch(() => {});
}, 2000);
}
}
// ----------------------------------------------------------
// ÖFFENTLICHE API
// (andere Module können App.state, App.navigate etc. nutzen)
// ----------------------------------------------------------
function callModule(pageId, method, ...args) {
navigate(pageId);
setTimeout(() => pages[pageId]?.module?.[method]?.(...args), 500);
}
return { init, navigate, callModule, state, setActiveDog,
hasPro: (user) => _hasPro(user ?? state.user),
renderDogSwitcher: _renderDogSwitcher,
getInstallPrompt: () => _installPrompt, requireAuth,
showOnboarding: _showOnboardingModal,
updateNotifBadge: _updateNotifBadge,
checkNearbyAlerts: _checkNearbyAlerts,
loadScript: _loadScript,
_triggerUpdateBanner: _showUpdateBanner };
})();
window.App = App; // Worlds kann App.navigate() aufrufen
// App starten
document.addEventListener('DOMContentLoaded', () => {
App.init();
if (IS_STAGING) {
document.title = '⚗️ ' + document.title;
// Nach App.init() Styles direkt setzen — sonst überschreibt init sie
const _applyStaging = () => {
const nav = document.getElementById('bottom-nav');
if (!nav) return;
nav.style.cssText += ';background:#2d1b69!important;border-top-color:#7c3aed!important;box-shadow:0 -2px 12px rgba(124,58,237,0.4)!important';
nav.querySelectorAll('.nav-item-label').forEach(el => el.style.color = 'rgba(196,181,253,0.75)');
nav.querySelectorAll('.plus-btn, .nav-item-center button').forEach(el => el.style.background = '#7c3aed');
};
_applyStaging();
setTimeout(_applyStaging, 400); // nochmal nach vollständigem Render
}
});