/* ============================================================
BAN YARO — Einstellungen / Account
Login, Registrierung, Logout, Account-Info.
============================================================ */
window.Page_settings = (() => {
let _container = null;
let _appState = null;
let _mode = 'login'; // 'login' | 'register'
// ----------------------------------------------------------
// INIT / REFRESH
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
}
function refresh() {
_render();
}
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render() {
if (_appState.user) {
_renderAccount();
} else {
_renderAuth(_mode);
}
}
// ----------------------------------------------------------
// EINGELOGGT — Account-Übersicht
// ----------------------------------------------------------
async function _renderAccount() {
const u = _appState.user;
// Avatar: Bild oder Buchstabe
const avatarInner = u.avatar_url
? `
`
: _esc(u.name.charAt(0).toUpperCase());
// Mitglied seit
const memberSince = (() => {
if (!u.created_at) return '';
const d = new Date(u.created_at);
return d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
})();
// Erfahrungs-Labels
const erfahrungLabel = {
einsteiger: 'Einsteiger (erster Hund)',
erfahren: 'Erfahrener Hundehalter',
trainer: 'Trainer / Ausbilder',
zuechter: 'Züchter',
};
_container.innerHTML = `
${_esc(u.name)}
${_esc(u.email)}
${u.is_premium
? `
Ban Yaro Plus
`
: `
Kostenlos
`}
Mein Profil
${memberSince
? `
Mitglied seit ${_esc(memberSince)}
`
: ''}
${u.bio
? `
${_esc(u.bio)}
`
: ''}
${u.wohnort
? `
📍 ${_esc(u.wohnort)}
`
: ''}
${u.erfahrung && erfahrungLabel[u.erfahrung]
? `
${_esc(erfahrungLabel[u.erfahrung])}
`
: ''}
${u.social_link
? `
`
: ''}
${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link
? `
Noch kein Profil ausgefüllt.
`
: ''}
App-Einstellungen
Dark Mode
Erscheinungsbild der App
${UI.icon('arrow-square-out')} App empfehlen
Lade Freunde ein — jede erfolgreiche Einladung wird in deinem Profil angezeigt.
Lade…
Ban Yaro · banyaro.app
Deine Daten liegen auf einem eigenen Server in Deutschland.
`;
// Achievements laden (Streak + Stats + Badges)
API.get('/achievements/me').then(a => {
const statsEl = document.getElementById('settings-stats-body');
const badgesEl = document.getElementById('settings-badges-body');
if (!statsEl) return;
const s = a.stats || {}, streak = a.streak || {};
const stat = (val, label) => `
`;
statsEl.innerHTML =
stat((s.total_km ?? 0) + ' km', 'gelaufen') +
stat(s.routen ?? 0, 'Routen') +
stat(s.pois ?? 0, 'POIs') +
stat('#' + (a.rang ?? '–'), 'Rang');
const streakEl = document.getElementById('settings-streak');
if (streakEl) {
const cur = streak.current || 0, mx = streak.max || 0;
streakEl.innerHTML = cur > 0
? `🔥
${cur} Tage Streak
${mx > cur ? `Best: ${mx}` : ''}`
: `🔥 Noch kein Streak — heute aktiv werden!`;
}
if (badgesEl && a.categories) {
// SVG-Schild für jede Kategorie
const shield = (color, dark, emoji, opacity = 1) => `
`;
badgesEl.innerHTML = (a.categories || []).map(cat => {
const cur = cat.current_tier;
const nxt = cat.next_tier;
const val = cat.current_value;
// Alle Stufen als kleine Punkte
const dots = (cat.alle_stufen || []).map(s =>
``
).join('');
// Aktuelles Schild
const shieldSvg = cur
? shield(cur.color, cur.dark, cat.emoji)
: shield('#9ca3af', '#6b7280', cat.emoji, 0.5);
// Fortschrittsbalken
const progressBar = nxt ? `
${val}${cat.einheit} / ${nxt.schwelle}${cat.einheit} → ${_esc(nxt.name)}
` : `
Höchste Stufe erreicht! 🎉
`;
return `
${shieldSvg}
${_esc(cat.name)}
${cur ? `${_esc(cur.name)}` : ''}
${dots}
${progressBar}
`;
}).join('');
}
// Neue Badges als Toast
if (a.new_badges?.length) {
a.new_badges.forEach(b => {
UI.toast.success(`${b.emoji} ${b.name} — ${b.tier} freigeschaltet!`);
});
}
}).catch(() => {
const el = document.getElementById('settings-stats-body');
if (el) el.innerHTML = '–
';
});
// Avatar-Hover-Overlay
const avatarBtn = document.getElementById('settings-avatar-btn');
const avatarOverlay = avatarBtn?.querySelector('.avatar-overlay');
if (avatarBtn && avatarOverlay) {
avatarBtn.addEventListener('mouseenter', () => { avatarOverlay.style.opacity = '1'; });
avatarBtn.addEventListener('mouseleave', () => { avatarOverlay.style.opacity = '0'; });
}
// Avatar-Upload
avatarBtn?.addEventListener('click', () => {
document.getElementById('settings-avatar-input')?.click();
});
document.getElementById('settings-avatar-input')?.addEventListener('change', async e => {
const file = e.target.files?.[0];
if (!file) return;
try {
const fd = new FormData();
fd.append('file', file);
const res = await API.post('/profile/avatar', fd);
_appState.user.avatar_url = res.avatar_url;
UI.toast.success('Avatar aktualisiert.');
_render();
} catch {
UI.toast.error('Avatar-Upload fehlgeschlagen.');
}
});
// Profil bearbeiten
document.getElementById('settings-profile-edit-btn')?.addEventListener('click', () => {
const u = _appState.user;
const inputStyle = `width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
font-size:var(--text-sm);font-family:inherit;
background:var(--c-surface);color:var(--c-text)`;
const erfahrungOpts = [
['', 'Bitte wählen...'],
['einsteiger', 'Einsteiger (erster Hund)'],
['erfahren', 'Erfahrener Hundehalter'],
['trainer', 'Trainer / Ausbilder'],
['zuechter', 'Züchter'],
].map(([val, label]) =>
``
).join('');
const sichtbarkeitOpts = [
['public', 'Öffentlich'],
['friends', 'Nur Freunde'],
['private', 'Privat'],
].map(([val, label]) =>
``
).join('');
UI.modal.open({
title: 'Profil bearbeiten',
body: `
`,
footer: `
`,
});
document.getElementById('profile-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.querySelector('[form="profile-form"]');
const fd = UI.formData(e.target);
await UI.asyncButton(btn, async () => {
const updated = await API.patch('/profile', {
real_name: fd.real_name || '',
bio: fd.bio || '',
wohnort: fd.wohnort || '',
erfahrung: fd.erfahrung || '',
social_link: fd.social_link || '',
profil_sichtbarkeit: fd.profil_sichtbarkeit || 'public',
});
Object.assign(_appState.user, updated);
UI.modal.close?.();
UI.toast.success('Profil gespeichert.');
_render();
});
});
});
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
const ok = await UI.modal.confirm({
title : 'Abmelden?',
message: 'Du wirst aus deinem Konto abgemeldet.',
confirmText: 'Abmelden',
});
if (!ok) return;
try {
await API.auth.logout();
} catch { /* cookie wird trotzdem gelöscht */ }
_appState.user = null;
_appState.dogs = [];
_appState.activeDog = null;
UI.toast.info('Du wurdest abgemeldet.');
_render();
});
document.getElementById('settings-install-btn')?.addEventListener('click', () => {
App.navigate('welcome');
});
document.getElementById('settings-push-btn')?.addEventListener('click', async () => {
try {
await API.subscribeToPush();
UI.toast.success('Push-Benachrichtigungen aktiviert.');
} catch {
UI.toast.warning('Push-Benachrichtigungen konnten nicht aktiviert werden.');
}
});
document.getElementById('settings-calendar-btn')?.addEventListener('click', async () => {
try {
const { token } = await API.webcal.getToken();
const url = `webcal://${location.host}/api/webcal/${token}.ics`;
const httpsUrl = `https://${location.host}/api/webcal/${token}.ics`;
UI.modal.open({
title: `${UI.icon('calendar-dots')} Kalender abonnieren`,
body: `
Abonniere deinen persönlichen Ban-Yaro-Kalender. Er enthält Impf-Erinnerungen,
Läufigkeits-Termine, Events und Gassi-Treffen — immer aktuell.
${httpsUrl}
Tipp: iOS → Einstellungen › Kalender › Accounts › Account hinzufügen › Andere › Kalenderabo
`,
});
document.getElementById('cal-copy-btn')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(httpsUrl);
UI.toast.success('URL kopiert.');
} catch {
UI.toast.warning('Kopieren nicht möglich — URL oben manuell kopieren.');
}
});
} catch(err) {
console.error('Kalender-Fehler:', err);
UI.toast.error('Kalender-Token konnte nicht geladen werden: ' + (err?.message || err));
}
});
document.getElementById('select-theme')?.addEventListener('change', e => {
const val = e.target.value;
localStorage.setItem('by_theme', val);
const html = document.documentElement;
if (val === 'dark') {
html.setAttribute('data-theme', 'dark');
} else if (val === 'light') {
html.setAttribute('data-theme', 'light');
} else {
html.removeAttribute('data-theme');
}
UI.toast.info(
val === 'dark' ? 'Dark Mode aktiviert.' :
val === 'light' ? 'Hell-Modus aktiviert.' :
'Theme folgt der Systemeinstellung.'
);
});
document.getElementById('toggle-pocket-mode')?.addEventListener('change', e => {
localStorage.setItem('by_pocket_mode', String(e.target.checked));
UI.toast.info(e.target.checked
? 'Pocket-Modus aktiviert — Bildschirm bleibt bei Aufzeichnung an.'
: 'Pocket-Modus deaktiviert.');
});
_loadReferral();
}
// ----------------------------------------------------------
// REFERRAL — Einladungslink laden und rendern
// ----------------------------------------------------------
async function _loadReferral() {
const el = document.getElementById('referral-body');
if (!el) return;
try {
const r = await API.auth.referral();
el.innerHTML = `
${r.link}
${UI.icon('users')} ${r.count} ${r.count === 1 ? 'Person' : 'Personen'} über deinen Link registriert
${r.count > 0 ? `
${r.count >= 1 ? `${UI.icon('star')} Botschafter` : ''}
${r.count >= 5 ? `${UI.icon('star')} Super-Botschafter` : ''}
${r.count >= 10 ? `${UI.icon('star')} Top-Botschafter` : ''}
` : ''}
`;
document.getElementById('ref-share-btn')?.addEventListener('click', async () => {
if (navigator.share) {
navigator.share({ title: 'Ban Yaro — Die Hunde-App', text: 'Schau dir Ban Yaro an!', url: r.link }).catch(() => {});
} else {
await navigator.clipboard.writeText(r.link);
UI.toast.success('Link kopiert!');
}
});
} catch { el.innerHTML = ''; }
}
// ----------------------------------------------------------
// NICHT EINGELOGGT — Login / Registrierung
// ----------------------------------------------------------
function _renderAuth(mode) {
_mode = mode;
_container.innerHTML = `
Ban Yaro
Alles rund um deinen Hund
${mode === 'login' ? _loginFormHTML() : _registerFormHTML()}
`;
document.getElementById('tab-login')
?.addEventListener('click', () => _renderAuth('login'));
document.getElementById('tab-register')
?.addEventListener('click', () => _renderAuth('register'));
if (mode === 'login') {
_bindLoginForm();
} else {
_bindRegisterForm();
}
}
function _loginFormHTML() {
return `
`;
}
function _registerFormHTML() {
return `
`;
}
function _bindPwToggle(inputId, btnId) {
const input = document.getElementById(inputId);
const btn = document.getElementById(btnId);
if (!input || !btn) return;
btn.addEventListener('click', () => {
const visible = input.type === 'text';
input.type = visible ? 'password' : 'text';
btn.setAttribute('aria-label', visible ? 'Passwort anzeigen' : 'Passwort verbergen');
btn.querySelector('use').setAttribute('href',
visible ? '/icons/phosphor.svg#eye' : '/icons/phosphor.svg#eye-slash');
});
}
function _bindLoginForm() {
_bindPwToggle('login-pw', 'login-pw-toggle');
document.getElementById('auth-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = e.target.querySelector('[type="submit"]');
const fd = UI.formData(e.target);
await UI.asyncButton(btn, async () => {
const result = await API.auth.login(fd.email, fd.password);
localStorage.setItem('by_token', result.token);
// User-Daten laden
_appState.user = await API.auth.me();
document.getElementById('sidebar-username').textContent = _appState.user.name;
// Hunde laden
try {
_appState.dogs = await API.dogs.list();
_appState.activeDog = _appState.dogs[0] || null;
} catch { /* keine Hunde = okay */ }
document.getElementById('header-login-btn')?.remove();
UI.toast.success(`Willkommen zurück, ${_appState.user.name}!`);
// Push-Benachrichtigungen anbieten wenn noch nicht entschieden
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
_offerPushNotifications();
}
// Nach Login: Tagebuch oder Profil anlegen
if (_appState.activeDog) {
App.navigate('diary');
} else {
App.navigate('dog-profile');
}
});
});
}
function _bindRegisterForm() {
_bindPwToggle('register-pw', 'register-pw-toggle');
document.getElementById('auth-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = e.target.querySelector('[type="submit"]');
const fd = UI.formData(e.target);
if (!fd.name?.trim()) {
UI.toast.warning('Bitte einen Namen eingeben.');
return;
}
if ((fd.password || '').length < 8) {
UI.toast.warning('Passwort muss mindestens 8 Zeichen lang sein.');
return;
}
await UI.asyncButton(btn, async () => {
const refCode = sessionStorage.getItem('by_ref_code') || '';
const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), refCode || undefined);
localStorage.setItem('by_token', result.token);
if (refCode) sessionStorage.removeItem('by_ref_code');
_appState.user = await API.auth.me();
document.getElementById('sidebar-username').textContent = _appState.user.name;
_appState.dogs = [];
_appState.activeDog = null;
document.getElementById('header-login-btn')?.remove();
UI.toast.success(`Willkommen bei Ban Yaro, ${_appState.user.name}!`);
App.showOnboarding();
});
});
}
// ----------------------------------------------------------
// PUSH-BENACHRICHTIGUNGEN ANBIETEN (nach Login)
// ----------------------------------------------------------
function _offerPushNotifications() {
// Kleiner Toast-Banner mit Ja-Button — nicht-invasiv
const toastEl = document.createElement('div');
toastEl.id = 'push-offer-banner';
toastEl.style.cssText = [
'position:fixed',
'bottom:calc(var(--nav-h, 64px) + var(--space-3))',
'left:50%',
'transform:translateX(-50%)',
'background:var(--c-surface)',
'border:1.5px solid var(--c-border)',
'border-radius:var(--radius-lg)',
'box-shadow:var(--shadow-lg)',
'padding:var(--space-3) var(--space-4)',
'display:flex',
'align-items:center',
'gap:var(--space-3)',
'font-size:var(--text-sm)',
'z-index:1100',
'max-width:340px',
'width:calc(100% - var(--space-8))',
].join(';');
toastEl.innerHTML = `
Push-Benachrichtigungen aktivieren?
`;
document.body.appendChild(toastEl);
const remove = () => toastEl.remove();
document.getElementById('push-offer-yes')?.addEventListener('click', async () => {
remove();
try {
await API.subscribeToPush();
UI.toast.success('Push-Benachrichtigungen aktiviert.');
} catch {
UI.toast.warning('Push-Benachrichtigungen konnten nicht aktiviert werden.');
}
});
document.getElementById('push-offer-no')?.addEventListener('click', remove);
// Automatisch ausblenden nach 12 Sekunden
setTimeout(remove, 12000);
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return str.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh };
})();