/* ============================================================ BAN YARO — Einstellungen / Account Login, Registrierung, Logout, Account-Info. ============================================================ */ window.Page_settings = (() => { let _container = null; let _appState = null; let _mode = 'login'; // 'login' | 'register' // ---------------------------------------------------------- // HUNDEPASSPHRASE — sicheres Passwort aus Hundewelt // ---------------------------------------------------------- const _PW_WOERTER = [ // Rassen 'Labrador','Pudel','Beagle','Husky','Dackel','Spitz','Mops','Boxer', 'Collie','Setter','Pointer','Retriever','Shepherd','Terrier','Welpe', // Körper & Natur 'Pfote','Schwanz','Schnauze','Schnurrbart','Fell','Nase','Ohr', // Aktivität 'Gassi','Laufen','Bellen','Springen','Graben','Schnüffeln','Spielen', 'Apportieren','Schwimmen','Hecheln','Wackeln','Toben', // Gegenstände 'Leckerli','Leine','Halsband','Ball','Napf','Knochen','Frisbee', 'Körbchen','Bürste','Leine','Stöckchen','Kauspielzeug', // Orte & Personen 'Wiese','Wald','Park','Bach','Pfütze','Tierarzt','Züchter', // Eigenschaften 'Treu','Tapfer','Mutig','Flauschig','Verspielt','Neugierig', 'Wachsam','Flink','Sanft','Lieb', // Geräusche & Aktionen 'Wuff','Jaulen','Schnuppern','Wedeln','Gähnen','Strecken', // Futter 'Trockenfutter','Nassfutter','Kausnack','Futternapf', ]; function _genPassphrase() { const pick = () => _PW_WOERTER[Math.floor(Math.random() * _PW_WOERTER.length)]; const num = Math.floor(Math.random() * 90) + 10; // 2-stellig // 3 zufällige Wörter + Zahl, mit Bindestrich const words = []; while (words.length < 3) { const w = pick(); if (!words.includes(w)) words.push(w); } return words.join('-') + '-' + num; } // ---------------------------------------------------------- // INIT / REFRESH // ---------------------------------------------------------- async function init(container, appState, params = {}) { _container = container; _appState = appState; _render(); if (params.tab === 'login') setTimeout(() => _renderAuth('login'), 50); if (params.tab === 'register') setTimeout(() => _renderAuth('register'), 50); // Frischen User-State laden damit Badges (is_founder, is_partner) aktuell sind if (_appState.user) { try { const fresh = await API.auth.me(); Object.assign(_appState.user, fresh); _render(); } catch {} } } async function refresh() { _render(); if (_appState?.user) { try { const fresh = await API.auth.me(); Object.assign(_appState.user, fresh); _render(); } catch {} } } // ---------------------------------------------------------- // ABO & TARIF // ---------------------------------------------------------- function _tierCard(u) { const tier = u.subscription_tier || 'standard'; const rolle = u.rolle || 'user'; const isAdmin = rolle === 'admin' || rolle === 'moderator'; const isPro = ['pro','pro_test'].includes(tier); const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder'; const isStandard = !isAdmin && !isPro && !isBreeder; const _badge = (label, color) => `${label}`; const _upgradeBtn = (id, label, price, color) => ``; const expires = u.subscription_expires_at; const cancelled = u.subscription_cancelled_at; const expiresDate = expires ? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) : null; const isPaid = (isPro || isBreeder) && !tier.endsWith('_test') && !isAdmin; const _expiryInfo = () => { if (!isPaid) return ''; if (cancelled && !expiresDate) { return `
Gekündigt — läuft bis Ablauf des bezahlten Zeitraums
`; } if (!expiresDate) return ''; const color = cancelled ? '#e65100' : 'var(--c-text-secondary)'; const text = cancelled ? `Gekündigt — läuft bis ${expiresDate}` : `Aktiv bis ${expiresDate}`; return `
${text}
`; }; const _cancelBtn = () => { if (!isPaid || cancelled) return ''; return ``; }; let statusHtml = ''; let actionsHtml = ''; if (isAdmin) { statusHtml = _badge('Admin', '#6366f1'); } else if (isBreeder) { statusHtml = _badge(cancelled ? 'Züchter (gekündigt)' : 'Züchter aktiv', '#C4843A'); } else if (isPro) { statusHtml = _badge(cancelled ? 'Pro (gekündigt)' : 'Pro aktiv', '#16a34a'); actionsHtml = `
${!cancelled ? _upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A') : ''}
`; } else { statusHtml = _badge('Kostenlos', '#888'); actionsHtml = `
${_upgradeBtn('settings-upgrade-pro-btn','Ban Yaro Pro','29 €/Jahr','#16a34a')} ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter','49 €/Jahr','#C4843A')}
`; } return `
Abo & Tarif
Aktueller Tarif: ${statusHtml}
${_expiryInfo()} ${actionsHtml} ${_cancelBtn()}
`; } function _showUpgradeModal(tier) { const isPro = tier === 'pro'; const label = isPro ? 'Ban Yaro Pro' : 'Züchter'; const price = isPro ? '29 €/Jahr' : '49 €/Jahr'; const color = isPro ? '#16a34a' : '#C4843A'; const _group = (title, items) => `
${title}
${items.map(f => `
${f}
`).join('')}
`; const featureList = isPro ? _group('Deine Hunde', [ 'Bis zu 10 Hunde gleichzeitig verwalten', 'Getrennte Trainingsfortschritte, Gesundheits- und Ernährungsdaten je Hund', ]) + _group('Community & Alltag', [ 'Gassi-Treffen: Fotos und Rasse der Teilnehmer sichtbar, Fotos nach dem Treffen hochladen', 'Direktnachrichten & Chat mit anderen Hundebesitzern', 'Playdate: Spielkameraden in der Nähe finden und verabreden', ]) + _group('Tools & Wissen', [ 'Ernährung: Kalorienbedarf-Rechner, BARF-Guide, Giftliste, KI-Ernährungsberater', 'Reise-Checkliste & EU-Länder-Einreiseregeln', 'Notizblock mit KI-Muster-Analyse', 'Erweiterte Karten-Layer (Wandern, Radfahren, Satellit)', 'Alle künftigen Pro-Features inklusive', ]) : `
✓ Alle Pro-Features inklusive — mehrere Hunde, Ernährung, Gassi-Community, Chat, Playdate, Reise, Karten-Layer
` + _group('Zucht-Management', [ 'Zuchtkartei: Stammdaten, Gesundheitstests (HD, ED, OCD, Augen, Herz, Patella, ZTP), Gentests (MDR1, PRA, DM, vWD)', 'Wurfverwaltung: Welpen, Gewichtsverlauf, Fotos, automatisch ausgefüllter Kaufvertrag', 'Warteliste: Interessenten mit Präferenzen (Geschlecht, Farbe, Verwendungszweck) pro Zuchthündin', 'Läufigkeit & Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, Meilensteinberechnung', 'Wurf-Buchstabe und Wurf-Name (z. B. A-Wurf, „Vatertags-Wurf")', ]) + _group('KI & Analyse', [ 'KI-Züchter-Assistent: Wurfankündigungen schreiben, Genetik-Erklärung für Käufer, Paarungsanalyse', 'Stammbaum bis 4 Generationen mit klickbaren Knoten', 'Inzucht-Koeffizient (Wright\'s Formel, Ampel-Bewertung, Probeverpaarung)', 'Tierschutz-Check automatisch bei jeder Verpaarung', 'KI-Jahresbericht mit Trends und Empfehlungen', ]) + _group('Sichtbarkeit & Export', [ 'Öffentliches Züchter-Profil unter banyaro.app/breeder/{zwingername}', 'Wurfbörse: Würfe öffentlich ankündigen, Käufer schreiben direkt an', 'Datenexport als HTML-Dossier und ODS-Tabelle (LibreOffice / Excel)', 'Privater Züchter-Bereich mit Zwingername und Logo', ]); 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 breederForm = isPro ? '' : `
Dein Zwinger
Zuchtbuch, Vereinsausweis o.ä. — kann auch per E-Mail nachgereicht werden
`; UI.modal.open({ title: `${label} freischalten`, body: `
${price}
Einmaliger Jahresbeitrag
Kündigung jederzeit möglich
${featureList}
Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden. Wir melden uns mit den Zahlungsdetails per E-Mail.
${!_appState.user?.billing_address ? `
💡 Tipp: Trag deine Rechnungsadresse im Profil ein — dann können wir die Rechnung vollständig ausstellen.
` : ''}
${breederForm}
`, footer: ` ` }); const agbBox = document.getElementById('agb-checkbox'); const widerrufBox = document.getElementById('widerruf-checkbox'); const sendBtn = document.getElementById('upgrade-request-send-btn'); if (sendBtn) sendBtn.disabled = true; const _checkBtns = () => { if (sendBtn) sendBtn.disabled = !(agbBox?.checked && widerrufBox?.checked); }; agbBox?.addEventListener('change', _checkBtns); widerrufBox?.addEventListener('change', _checkBtns); document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => { const btn = document.getElementById('upgrade-request-send-btn'); if (!btn) return; // Züchter: Formular validieren + als FormData senden if (!isPro) { const form = document.getElementById('breeder-upgrade-form'); if (form && !form.reportValidity()) return; if (form) { const fd = new FormData(form); fd.set('vdh_mitglied', form.querySelector('[name="vdh_mitglied"]').checked ? '1' : '0'); // Pflichtfelder aus Form übernehmen falls leer → leere Strings senden if (!fd.get('verein')) fd.set('verein', ''); if (!fd.get('stadt')) fd.set('stadt', ''); btn.disabled = true; btn.textContent = 'Wird gesendet…'; try { await API.breeder.apply(fd); } catch (e) { if (!e.message?.includes('bereits')) { btn.disabled = false; btn.textContent = 'Anfrage senden'; UI.toast.error(e.message || 'Fehler beim Einreichen.'); return; } } } } else { btn.disabled = true; btn.textContent = 'Wird gesendet…'; } try { const widerrufAt = new Date().toLocaleString('de-DE'); const res = await API.auth.upgradeRequest(tier, `[Widerrufsrecht akzeptiert am ${widerrufAt}]`); UI.modal.close(); if (res.already) { UI.toast.info('Deine Anfrage liegt bereits vor — wir melden uns bald.'); } else { UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.'); } if (!isPro) _loadBreederCard(); } catch (e) { btn.disabled = false; btn.textContent = 'Anfrage senden'; UI.toast.error(e.message || 'Fehler beim Senden.'); } }); } function _showCancelModal() { const u = _appState.user; const tier = u?.subscription_tier || 'standard'; const label = { pro: 'Ban Yaro Pro', breeder: 'Züchter' }[tier] || tier; const expires = u?.subscription_expires_at; const expiresDate = expires ? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) : null; UI.modal.open({ title: `${label} kündigen`, body: `
${expiresDate ? `
Dein Abo läuft noch bis ${expiresDate} — du hast bis dahin vollen Zugriff.
` : ''}
✓ Alle deine Daten (Tagebuch, Gesundheit, Notizen) bleiben vollständig erhalten
✓ Deine Hunde-Profile bleiben gespeichert
✓ Du kannst jederzeit wieder upgraden
${_appState.dogs?.length > 1 ? `
⚠ Du hast mehrere Hunde — nach dem Ablauf wählst du einen als Haupthund
` : ''}
`, footer: ` ` }); document.getElementById('cancel-sub-confirm-btn')?.addEventListener('click', async () => { const btn = document.getElementById('cancel-sub-confirm-btn'); if (!btn) return; btn.disabled = true; btn.textContent = '…'; try { await API.auth.cancelSubscription(); // User-State aktualisieren const fresh = await API.auth.me(); Object.assign(_appState.user, fresh); UI.modal.close(); const tier = fresh.subscription_tier || ''; const label = { pro: 'Pro', breeder: 'Züchter' }[tier] || tier; const exp = fresh.subscription_expires_at; const expFmt = exp ? new Date(exp).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) : null; UI.toast.success( expFmt ? `Kündigung bestätigt — ${label} läuft noch bis ${expFmt}.` : 'Kündigung bestätigt. Eine Bestätigungsmail wurde gesendet.' ); _render(); } catch (e) { btn.disabled = false; btn.textContent = 'Jetzt kündigen'; UI.toast.error(e.message || 'Fehler beim Kündigen.'); } }); } // ---------------------------------------------------------- // 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 ? `Avatar` : UI.escape(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 = `
${avatarInner}
${UI.escape(u.name)}
${UI.escape(u.email)} ${u.email_verified ? `` : `Nicht bestätigt`}
${(() => { // Tier-Source-of-Truth ist subscription_tier (was auch // _tierCard und has_pro_access nutzen). Das alte is_premium- // Flag wird bei Admin-Upgrade nicht immer mitgezogen — daher // hier den Tier nehmen. const _tier = u.subscription_tier || 'standard'; const _rolle = u.rolle || 'user'; const _isAdminLike = _rolle === 'admin' || _rolle === 'moderator'; const _isBreeder = _tier === 'breeder' || _tier === 'breeder_test' || _rolle === 'breeder'; const _isPro = _tier === 'pro' || _tier === 'pro_test'; if (_isAdminLike) { return ` Admin `; } if (_isBreeder) { return ` Züchter `; } if (_isPro) { return ` Ban Yaro Pro `; } return `Kostenlos`; })()} ${u.is_founder ? ` ${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'} ` : u.is_founder_pending ? ` Gründer-Platz reserviert ` : ''} ${u.is_partner ? ` Partner ` : ''}
Mein Profil
${memberSince ? `
Mitglied seit ${UI.escape(memberSince)}
` : ''} ${u.bio ? `
${UI.escape(u.bio)}
` : ''} ${u.wohnort ? `
📍 ${UI.escape(u.wohnort)}
` : ''} ${u.erfahrung && erfahrungLabel[u.erfahrung] ? `
${UI.escape(erfahrungLabel[u.erfahrung])}
` : ''} ${u.social_link ? `` : ''} ${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link ? `
Noch kein Profil ausgefüllt.
` : ''}
Aktivität
Lädt…
Trophäen
Lädt…
OpenStreetMap – die Karte mitverbessern
Lädt…
${_tierCard(u)}
App-Einstellungen
Dark Mode
Erscheinungsbild der App
${/SamsungBrowser/i.test(navigator.userAgent) ? `
Samsung Internet Tipps:
• Farben: Einstellungen → Webseitenansicht → Dark Mode deaktivieren.
• Vollbild: Einstellungen → Display → Navigationsleiste → Wischgesten aktivieren.
` : ''} ${(() => { const tier = u.subscription_tier || 'standard'; const hasPro = ['pro','breeder'].includes(tier) || u.rolle === 'admin' || u.rolle === 'moderator' || u.is_moderator || u.is_social_media || tier.endsWith('_test'); if (!hasPro) return ''; return `
KI-Notiz-Assistent
Erkennt Muster in deinen Notizen und macht Vorschläge
`; })()}
DWD-Regenvorhersage
2-Stunden-Vorhersage vom Deutschen Wetterdienst im Regenradar (nur Deutschland)
Goldene Gassi-Stunde täglich
Täglich um 07:00 Uhr: bestes Wetterfenster für den Gassi-Gang
Ban Yaro — das Album
Ban Yaro — Album-Cover
7 Songs zum Behalten 🎸
Das ganze Album als Download — Deutsch, Englisch oder „neo" (Electro-Remix). Behalten & teilen ausdrücklich erwünscht.
${UI.icon('download-simple')} Deutsch · 33 MB ${UI.icon('download-simple')} English · 36 MB ${UI.icon('download-simple')} neo · 33 MB
${UI.icon('arrow-square-out')} Freunde werben — dauerhafter Rabatt
10 Freunde → 20% · 20 Freunde → 30% · 50 Freunde → 50% — dauerhaft auf Ban Yaro Pro.
Lade…
App installieren
Ban Yaro · banyaro.app
Deine Daten liegen auf einem eigenen Server in Deutschland.
v${typeof APP_VERSION !== 'undefined' ? APP_VERSION : '1.0.0'} (${typeof APP_VER !== 'undefined' ? APP_VER : '—'})
`; // Verstorbene Hunde in Erinnerungen-Sektion laden API.get('/dogs/verstorben').then(dogs => { const el = document.getElementById('settings-erinnerungen-wrap'); if (!el || !dogs.length) return; el.innerHTML = dogs.map(d => { const av = d.foto_url ? `` : `
`; const jahr = d.verstorben_am ? d.verstorben_am.slice(0, 4) : ''; return ` `; }).join(''); el.querySelectorAll('.settings-erinnerung-btn').forEach(btn => { btn.addEventListener('click', () => _openGedenkseite( parseInt(btn.dataset.dogId), btn.dataset.dogName )); }); }).catch(() => {}); // OSM-Account-Verknüpfung (Modell A) — Status laden + Buttons verdrahten (function _osmLink() { const el = document.getElementById('settings-osm-body'); if (!el) return; API.get('/osm-auth/status').then(st => { if (st.linked) { el.innerHTML = `
Verknüpft als ${UI.escape(st.osm_name)}
`; el.querySelector('#settings-osm-unlink').addEventListener('click', async () => { try { await API.post('/osm-auth/unlink', {}); } catch (e) {} _osmLink(); }); // Gamification-Zähler API.get('/osm-contrib/status').then(cs => { const c = document.getElementById('settings-osm-count'); if (!c) return; const n = cs.verified_count || 0; const next = n >= cs.pro_at ? 0 : (n < 10 ? 10 - n : cs.pro_at - n); c.innerHTML = `🐾 ${n} hundefreundliche Orte eingetragen` + (next ? ` · noch ${next} bis ${n < 10 ? 'zum Kartograf-Badge' : '1 Jahr Pro'}` : ' · Ziel erreicht! 🎉'); }).catch(() => { const c = document.getElementById('settings-osm-count'); if (c) c.textContent=''; }); } else { el.innerHTML = `

Du kennst die hundefreundlichen Orte besser als jede Karte. Verknüpfe deinen kostenlosen OpenStreetMap-Account und trag mit einem Tap ein, wo dein Hund willkommen war – das hilft jedem Hundehalter nach dir. Kostenlos, gemeinnützig, keine Werbung.

Noch kein OSM-Konto? Was ist das?

OpenStreetMap ist die freie Weltkarte – von Menschen gemacht, gehört allen, keine Werbung, kein Datenverkauf. Mit einem kostenlosen Konto trägst du hundefreundliche Orte ein, die jeder Hundehalter sieht.

So geht's:

  1. Konto erstellen (Benutzername + E-Mail – kein Klarname nötig)
  2. Bestätigungs-E-Mail anklicken
  3. Hier zurück → „OSM-Konto verknüpfen"
${st.sandbox ? `

⚠️ Testphase: Dies ist eine Test-Karte. Deine Einträge verändern die echte OpenStreetMap noch nicht.

` : ''} Kostenloses OSM-Konto erstellen
`; el.querySelector('#settings-osm-link').addEventListener('click', async () => { try { const r = await API.get('/osm-auth/authorize'); if (r.authorize_url) window.location.href = r.authorize_url; } catch (e) { UI.toast?.('OSM-Anbindung noch nicht konfiguriert.'); } }); } }).catch(() => { el.innerHTML = '
OSM-Status nicht verfügbar.
'; }); })(); // 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) => `
${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!`; } // Lifetime-km Balken mit Meilenstein-Markierungen const lifetimeEl = document.getElementById('settings-lifetime-km'); if (lifetimeEl) { const km = s.total_km ?? 0; const MILESTONES = [ { km: 100, label: '100', badge: '100-km-Club', color: '#cd7f32' }, { km: 500, label: '500', badge: '500-km-Wanderer', color: '#94a3b8' }, { km: 1000, label: '1k', badge: 'Tausend-km-Held', color: '#f59e0b' }, { km: 5000, label: '5k', badge: 'Ultraläufer', color: '#cbd5e1' }, ]; const maxKm = 5000; const pct = Math.min(km / maxKm * 100, 100); const nextM = MILESTONES.find(m => km < m.km); const reachedM = MILESTONES.filter(m => km >= m.km); const lastBadge = reachedM.length ? reachedM[reachedM.length - 1] : null; const markers = MILESTONES.map(m => { const pos = (m.km / maxKm * 100).toFixed(1); const reached = km >= m.km; return `
${m.label}
`; }).join(''); lifetimeEl.innerHTML = `
🐾 Lebenswerk-km ${km} km
${markers}
${nextM ? `
Noch ${(nextM.km - km).toLocaleString('de-DE')} km bis ${nextM.badge}
` : `
Ultraläufer-Legende erreicht! 🏆
`}
`; } if (badgesEl && a.categories) { // Foto-Hintergründe für bestimmte Badge-Kategorien const _BADGE_PHOTOS = { 'schnee_held': '/img/banyaro/winter_schnee.webp', 'jahreszeiten': '/img/banyaro/herbst_bach.webp', 'wetter_tapfer': '/img/banyaro/fruehling_playdate.webp', }; // SVG-Schild für jede Kategorie (mit optionalem Foto-Hintergrund) const shield = (color, dark, emoji, opacity = 1, catId = '') => { const photo = _BADGE_PHOTOS[catId]; const clipId = `clip_${catId || Math.random().toString(36).slice(2)}`; const path = 'M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z'; if (photo && opacity === 1) { return ` ${emoji} `; } return ` ${emoji} `; }; 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, 1, cat.id) : shield('#9ca3af', '#6b7280', cat.emoji, 0.5, cat.id); // Fortschrittsbalken const progressBar = nxt ? `
${val}${cat.einheit} / ${nxt.schwelle}${cat.einheit} → ${UI.escape(nxt.name)}
` : `
Höchste Stufe erreicht! 🎉
`; return `
${shieldSvg}
${UI.escape(cat.name)} ${cur ? `${UI.escape(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 // E-Mail-Verifikation: Chip → erneut senden document.getElementById('settings-verify-chip')?.addEventListener('click', async () => { await API.post('/auth/resend-verification', {}); UI.toast.success('Bestätigungs-Mail gesendet — bitte prüfe dein Postfach.'); }); 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: `
Wird auf Rechnungen gedruckt. Straße in Zeile 1, PLZ + Ort in Zeile 2.
Wird nur für Geburtstagsgrüße in der App verwendet.
`, 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', billing_address: fd.billing_address || '', geburtstag: fd.geburtstag || '', }); Object.assign(_appState.user, updated); window.Worlds?.refresh?.(_appState); // Welten neu rendern (z.B. Geburtstags-Greeting) UI.modal.close?.(); UI.toast.success('Profil gespeichert.'); _render(); }); }); }); document.getElementById('settings-check-update')?.addEventListener('click', async () => { const btn = document.getElementById('settings-check-update'); if (!('serviceWorker' in navigator)) { UI.toast.info('Service Worker nicht verfügbar.'); return; } if (btn) btn.textContent = 'Prüfe…'; try { // Versionsnummer direkt vom API-Endpunkt holen const r = await fetch('/api/version', { cache: 'no-store' }); // r.ok prüfen: SW antwortet offline mit 503+JSON → json() wirft nicht und // serverVersion=undefined meldete fälschlich „Ban Yaro ist aktuell". if (!r.ok) throw new Error(`version ${r.status}`); const { version: serverVersion } = await r.json(); const localVersion = typeof APP_VER !== 'undefined' ? APP_VER : '0'; const reg = await navigator.serviceWorker.getRegistration(); reg?.update().catch(() => {}); // kein await — kann hängen if (serverVersion && serverVersion !== localVersion) { if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' }); UI.toast.info(`Update auf v${serverVersion} — Seite wird neu geladen…`); setTimeout(() => location.replace('/?_t=' + Date.now()), 1500); } else if (reg?.waiting) { reg.waiting.postMessage({ type: 'SKIP_WAITING' }); UI.toast.success('Update wird installiert…'); setTimeout(() => location.replace('/?_t=' + Date.now()), 1500); } else { UI.toast.success(`Ban Yaro ist aktuell — Build ${localVersion}`); } } catch { UI.toast.error('Update-Prüfung fehlgeschlagen.'); } finally { if (btn) btn.innerHTML = UI.icon('arrows-clockwise') + ' Auf Update prüfen'; } }); document.getElementById('settings-upgrade-pro-btn')?.addEventListener('click', () => { _showUpgradeModal('pro'); }); document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => { _showUpgradeModal('breeder'); }); document.getElementById('settings-cancel-sub-btn')?.addEventListener('click', () => { _showCancelModal(); }); document.getElementById('settings-worlds-btn')?.addEventListener('click', () => { if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal(); else if (window.Worlds) window.Worlds.openConfig?.(); }); document.getElementById('settings-hilfe-btn')?.addEventListener('click', () => { App.navigate('hilfe'); }); document.getElementById('settings-feedback-btn')?.addEventListener('click', () => { const sel = (id) => document.getElementById(id); const inputStyle = 'width:100%;padding:10px 12px;border:1.5px solid var(--c-border);border-radius:var(--radius-md);background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm);box-sizing:border-box'; UI.modal.open({ title: 'Feedback geben', body: `
`, footer: `
`, }); sel('feedback-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = sel('feedback-submit-btn'); const kat = sel('feedback-kat')?.value; const text = sel('feedback-text')?.value?.trim(); if (!text) { UI.toast.error('Bitte schreib etwas.'); return; } await UI.asyncButton(btn, async () => { await API.post('/feedback', { kategorie: kat, text }); UI.modal.close?.(); UI.toast.success('Vielen Dank für dein Feedback!'); }); }); }); 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-export-btn')?.addEventListener('click', async () => { const btn = document.getElementById('settings-export-btn'); await UI.asyncButton(btn, async () => { try { const resp = await fetch('/api/profile/export', { credentials: 'include', headers: { 'Authorization': `Bearer ${localStorage.getItem('by_token') || ''}` }, }); if (!resp.ok) throw new Error('Export fehlgeschlagen.'); const data = await resp.json(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `banyaro-export-${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); UI.toast.success('Export heruntergeladen.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Export.'); } }); }); document.getElementById('settings-delete-account-btn')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Konto unwiderruflich löschen?', message: 'Alle deine Daten (Tagebuch, Gesundheit, Training, Fotos) werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.', confirmText: 'Ja, Konto löschen', danger: true, }); if (!ok) return; try { await API.del('/profile/account'); _appState.user = null; _appState.dogs = []; _appState.activeDog = null; UI.toast.info('Dein Konto wurde gelöscht.'); App.navigate('welcome'); } catch { UI.toast.error('Konto konnte nicht gelöscht werden. Bitte versuche es erneut.'); } }); document.getElementById('settings-install-btn')?.addEventListener('click', () => { App.navigate('welcome', true, { install: true }); }); 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}
${UI.icon('calendar-dots')} In Kalender-App öffnen

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', async e => { const val = e.target.value; localStorage.setItem('by_theme', val); // lokaler Cache für schnellen Start 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.' ); try { await API.patch('/profile', { preferred_theme: val }); if (_appState?.user) _appState.user.preferred_theme = val; } catch { /* ignorieren — localStorage-Fallback greift */ } }); 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.'); }); // DWD-Regenvorhersage (Default AN) — ausgewertet in pages/map.js _dwdEnabled() document.getElementById('toggle-dwd-radar')?.addEventListener('change', e => { const on = e.target.checked; localStorage.setItem('by_dwd_radar', on ? '1' : '0'); const track = document.getElementById('toggle-dwd-radar-track'); const thumb = document.getElementById('toggle-dwd-radar-thumb'); if (track) track.style.background = on ? 'var(--c-primary)' : 'var(--c-border)'; if (thumb) thumb.style.left = on ? '22px' : '2px'; UI.toast.info(on ? 'DWD-Regenvorhersage aktiviert — 2h-Vorhersage im Regenradar (Deutschland).' : 'DWD-Regenvorhersage deaktiviert — Regenradar nutzt RainViewer.'); }); document.getElementById('toggle-notes-ki')?.addEventListener('change', async e => { const enabled = e.target.checked; const track = document.getElementById('toggle-notes-ki-track'); const thumb = document.getElementById('toggle-notes-ki-thumb'); if (track) track.style.background = enabled ? 'var(--c-primary)' : 'var(--c-border)'; if (thumb) thumb.style.left = enabled ? '22px' : '2px'; try { await API.patch('/profile', { notes_ki_enabled: enabled ? 1 : 0 }); _appState.user.notes_ki_enabled = enabled ? 1 : 0; UI.toast.success(enabled ? 'KI-Notiz-Assistent aktiviert.' : 'KI-Notiz-Assistent deaktiviert.'); } catch (err) { UI.toast.error(err?.message || 'Einstellung konnte nicht gespeichert werden.'); // Revert UI e.target.checked = !enabled; if (track) track.style.background = !enabled ? 'var(--c-primary)' : 'var(--c-border)'; if (thumb) thumb.style.left = !enabled ? '22px' : '2px'; } }); document.getElementById('toggle-gassi-stunde')?.addEventListener('change', async e => { const enabled = e.target.checked; const track = document.getElementById('toggle-gassi-stunde-track'); const thumb = document.getElementById('toggle-gassi-stunde-thumb'); if (track) track.style.background = enabled ? 'var(--c-primary)' : 'var(--c-border)'; if (thumb) thumb.style.left = enabled ? '22px' : '2px'; try { await API.patch('/profile', { gassi_stunde_push: enabled ? 1 : 0 }); _appState.user.gassi_stunde_push = enabled ? 1 : 0; UI.toast.success(enabled ? 'Goldene Gassi-Stunde aktiviert.' : 'Goldene Gassi-Stunde deaktiviert.'); } catch (err) { UI.toast.error(err?.message || 'Einstellung konnte nicht gespeichert werden.'); // Revert UI e.target.checked = !enabled; if (track) track.style.background = !enabled ? 'var(--c-primary)' : 'var(--c-border)'; if (thumb) thumb.style.left = !enabled ? '22px' : '2px'; } }); _loadReferral(); _loadBreederCard(); } // ---------------------------------------------------------- // ZÜCHTER-CARD — asynchron laden und in Slot rendern // ---------------------------------------------------------- async function _loadBreederCard() { const slot = document.getElementById('breeder-card-slot'); if (!slot) return; let status = null; try { status = await API.breeder.status(); } catch { // API nicht verfügbar — Card weglassen return; } const { rolle, breeder_status, profile } = status; 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)`; let statusBadge = ''; let actionBlock = ''; if (rolle === 'breeder' || rolle === 'admin') { // Verifizierte Züchter/Admins: alles Inhaltliche (Profil, KI-Assistenz, // Würfe, Zuchtkartei) lebt im Züchter-Bereich — hier nur der Verweis. slot.innerHTML = ''; return; } else if (breeder_status === 'pending') { statusBadge = ` ${UI.icon('hourglass')} Antrag wird geprüft `; } else if (breeder_status === 'rejected') { statusBadge = ` ${UI.icon('x-circle')} Abgelehnt `; actionBlock = `

Du kannst einen neuen Antrag stellen.

`; } else { // Kein Antrag, kein Profil — Card ausblenden (Upgrade-Flow läuft über Abo & Tarif) slot.innerHTML = ''; return; } slot.innerHTML = `
Züchter-Profil
${statusBadge} ${actionBlock}
`; // Button-Handler binden slot.querySelector('#breeder-reapply-btn')?.addEventListener('click', () => _showUpgradeModal('breeder')); document.getElementById('breeder-apply-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('breeder-apply-submit'); await UI.asyncButton(btn, async () => { const form = e.target; const fd = new FormData(form); // Checkbox-Wert normalisieren fd.set('vdh_mitglied', form.querySelector('[name="vdh_mitglied"]').checked ? '1' : '0'); await API.breeder.apply(fd); UI.modal.close?.(); UI.toast.success('Antrag eingereicht. Du wirst benachrichtigt sobald er geprüft wurde.'); // Card neu laden _loadBreederCard(); }); }); } // ---------------------------------------------------------- // REFERRAL — Einladungslink laden und rendern // ---------------------------------------------------------- async function _loadReferral() { const el = document.getElementById('referral-body'); if (!el) return; try { const r = await API.auth.referral(); const TIERS = [{t:10,d:20},{t:20,d:30},{t:50,d:50}]; const currentTier = r.discount_pct; const next = r.next_tier; // Fortschrittsbalken-Berechnung let barPct = 0, barLabel = ''; if (!next) { barPct = 100; barLabel = 'Maximaler Rabatt erreicht!'; } else { const prevT = TIERS.find(t => t.d === currentTier)?.t || 0; barPct = Math.round(((r.count - prevT) / (next.count - prevT)) * 100); barLabel = `Noch ${next.count - r.count} ${next.count - r.count === 1 ? 'Person' : 'Personen'} bis ${next.discount}% Rabatt`; } el.innerHTML = `
${TIERS.map(({t, d}) => { const reached = r.count >= t; return `
${d}%
ab ${t} Freunden
${reached ? `
✓ Erreicht
` : ''}
`; }).join('')}
${UI.icon('users')} ${r.count} ${r.count === 1 ? 'Freund geworben' : 'Freunde geworben'} ${currentTier > 0 ? `${currentTier}% Rabatt aktiv` : ''}
${barLabel}
${r.link}

Der Rabatt gilt für dich auf Ban Yaro Pro — dauerhaft und automatisch.

`; document.getElementById('ref-share-btn')?.addEventListener('click', async () => { const msg = `Ich bin bei Ban Yaro — der coolsten Hunde-App! Registrier dich mit meinem Link und wir wachsen zusammen 🐾`; if (navigator.share) { navigator.share({ title: 'Ban Yaro', text: msg, url: r.link }).catch(() => {}); } else { await navigator.clipboard.writeText(r.link); UI.toast.success('Link kopiert!'); } }); await App.loadScript('/js/qrcode.min.js'); new QRCode(document.getElementById('ref-qr'), { text: r.link, width: 140, height: 140, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H, }); } catch { el.innerHTML = ''; } } // ---------------------------------------------------------- // GEDENKSEITE — für verstorbene Hunde // ---------------------------------------------------------- async function _openGedenkseite(dogId, dogName) { UI.modal.open({ title: `Erinnerungen an ${UI.escape(dogName)}`, body: `
` }); let data; try { data = await API.get(`/dogs/${dogId}/gedenkseite`); } catch { UI.modal.close(); return; } const d = data; const av = d.dog.foto_url ? `` : `
`; const photoGrid = d.photos?.length ? `
${d.photos.map(url => ``).join('')}
` : ''; const statsHtml = `
${d.km_total ? `
${d.km_total}
km zusammen
` : ''} ${d.diary_count ? `
${d.diary_count}
Tagebucheinträge
` : ''} ${d.gemeinsam_tage ? `
${d.gemeinsam_tage}
gemeinsame Tage
` : ''}
`; const passed = d.dog.verstorben_am; const passedStr = passed ? new Date(passed).toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }) : ''; UI.modal.open({ title: `Erinnerungen an ${UI.escape(d.dog.name)}`, body: `
${av}
${UI.escape(d.dog.name)}
${passedStr ? `
${passedStr}
` : ''}
${statsHtml} ${photoGrid}

Der Schmerz über den Verlust eines Hundes ist real und tief. Lass dich trauern — die Erinnerungen bleiben immer bei dir.

${d.ki_abschied ? `
"${UI.escape(d.ki_abschied)}"
` : ''} `, }); } // ---------------------------------------------------------- // NICHT EINGELOGGT — Login / Registrierung // ---------------------------------------------------------- function _renderVerifyPending(email) { _container.innerHTML = `
Ban Yaro

E-Mail bestätigen

Wir haben einen Bestätigungslink an
${email}
gesendet.

Bitte prüfe dein Postfach und klicke auf den Link, um dein Konto zu aktivieren. Danach kannst du dich hier anmelden.

`; document.getElementById('verify-resend-btn2')?.addEventListener('click', async function() { this.disabled = true; this.textContent = 'Gesendet …'; try { await API.post('/auth/resend-verification', { email }); UI.toast.success('Bestätigungs-Mail erneut gesendet.'); } catch { UI.toast.error('Fehler beim Senden — bitte versuche es später erneut.'); } }); document.getElementById('verify-back-btn')?.addEventListener('click', () => _renderAuth('login')); } function _renderAuth(mode) { // Passwort-Reset über Link aus E-Mail const resetToken = sessionStorage.getItem('by_reset_token'); if (resetToken) { sessionStorage.removeItem('by_reset_token'); _renderResetPassword(resetToken); return; } _mode = mode; _container.innerHTML = `
Ban Yaro

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 `

Dieser Name ist öffentlich sichtbar und wird bei deinen Beiträgen, Pins und Kommentaren angezeigt.

🐾 Passwort-Vorschlag
Sichere Passphrase aus der Hundewelt — leicht zu merken, schwer zu knacken.

Mit der Registrierung stimmst du unseren Datenschutzhinweisen zu.
Deine Daten werden ausschließlich auf unserem Server gespeichert.

`; } 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('forgot-pw-link')?.addEventListener('click', () => { const id = 'forgot-pw-modal'; UI.modal.open({ title: 'Passwort zurücksetzen', body: `

Gib deine E-Mail-Adresse ein. Du erhältst einen Link zum Zurücksetzen deines Passworts.

`, footer: ` `, }); document.getElementById(id)?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector(`[form="${id}"]`); const email = document.getElementById('forgot-pw-email').value.trim(); await UI.asyncButton(btn, async () => { await API.post('/auth/forgot-password', { email }); UI.modal.close(); UI.toast.success('Falls ein Account existiert, haben wir dir einen Link geschickt.'); }); }); }); 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 () => { let result; try { result = await API.auth.login(fd.email, fd.password); } catch (err) { if (err.message === 'EMAIL_NOT_VERIFIED') { _renderVerifyPending(fd.email); return; } throw err; } 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: Welten initialisieren oder Onboarding (mit Skip-Option) if (_appState.activeDog) { window.Worlds?.init(_appState); } else { App.navigate('onboarding'); } }); }); } function _bindRegisterForm() { _bindPwToggle('register-pw', 'register-pw-toggle'); // Hundepassphrase-Generator initialisieren const phraseEl = document.getElementById('pw-gen-phrase'); const pwInput = document.getElementById('register-pw'); if (phraseEl) { const _refresh = () => { phraseEl.textContent = _genPassphrase(); }; _refresh(); document.getElementById('pw-gen-new')?.addEventListener('click', _refresh); document.getElementById('pw-gen-use')?.addEventListener('click', () => { const phrase = phraseEl.textContent; pwInput.value = phrase; pwInput.type = 'text'; // sichtbar machen document.getElementById('register-pw-toggle') ?.querySelector('use') ?.setAttribute('href', '/icons/phosphor.svg#eye-slash'); // kurzes visuelles Feedback const btn = document.getElementById('pw-gen-use'); btn.textContent = '✓ Übernommen'; btn.style.background = 'var(--c-success)'; setTimeout(() => { btn.textContent = 'Übernehmen'; btn.style.background = 'var(--c-primary)'; }, 1500); }); } // Referral-Code aus localStorage lesen (30-Tage-Ablauf) bzw. löschen. const _storedRefCode = () => { try { const code = localStorage.getItem('by_ref_code') || ''; if (!code) return ''; const ts = parseInt(localStorage.getItem('by_ref_code_ts') || '0', 10); if (ts && Date.now() - ts > 30 * 24 * 3600 * 1000) { _clearRefCode(); return ''; } return code; } catch { return ''; } }; const _clearRefCode = () => { try { localStorage.removeItem('by_ref_code'); localStorage.removeItem('by_ref_code_ts'); } catch {} }; // Partner-Code live validieren const partnerInput = document.getElementById('reg-partner-code'); const partnerHint = document.getElementById('reg-partner-hint'); let _partnerValid = false; if (partnerInput) { // Vorausfüllen falls via Referral-Link gesetzt (localStorage, überlebt App-Schließen) const stored = _storedRefCode(); if (stored) partnerInput.value = stored; let _debounce = null; partnerInput.addEventListener('input', () => { const code = partnerInput.value.trim().toUpperCase(); partnerInput.value = code; clearTimeout(_debounce); partnerHint.style.display = 'none'; _partnerValid = false; if (code.length < 3) return; _debounce = setTimeout(async () => { try { const info = await API.get(`/partner/codes/${encodeURIComponent(code)}/info`); if (info.redeemable) { partnerHint.textContent = info.grants_founder ? `✓ Gültiger Code von "${info.label}" — du erhältst eine lebenslange Gründer-Lizenz!` : `✓ Gültiger Einladungscode von "${info.label}"`; partnerHint.style.cssText = 'display:block;background:var(--c-success-bg,#f0fdf4);color:var(--c-success,#16a34a);padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);font-size:var(--text-xs)'; _partnerValid = true; } else { partnerHint.textContent = 'Dieser Code ist bereits vollständig eingelöst.'; partnerHint.style.cssText = 'display:block;background:#fef2f2;color:#dc2626;padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);font-size:var(--text-xs)'; } } catch { partnerHint.textContent = 'Code nicht gefunden.'; partnerHint.style.cssText = 'display:block;color:var(--c-text-muted);font-size:var(--text-xs)'; } }, 500); }); } 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 partnerCode = (fd.partner_code || '').trim().toUpperCase() || undefined; const refCode = _storedRefCode(); const finalCode = partnerCode || refCode || undefined; // QR-Token mitschicken — Backend ordnet ihn nur zu, wenn er zum Code passt const qrToken = (() => { try { return localStorage.getItem('by_qr_token') || undefined; } catch { return undefined; } })(); const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), finalCode, qrToken); if (refCode) _clearRefCode(); try { localStorage.removeItem('by_qr_token'); } catch {} if (result.pending_verification) { _renderVerifyPending(fd.email); return; } }); }); } // ---------------------------------------------------------- // 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); } // ---------------------------------------------------------- // PASSWORT ZURÜCKSETZEN // ---------------------------------------------------------- function _renderResetPassword(token) { _container.innerHTML = `
Ban Yaro

Neues Passwort

Wähle ein sicheres Passwort für deinen Account.

🐾 Passwort-Vorschlag
`; _bindPwToggle('reset-pw-input', 'reset-pw-toggle'); const phraseEl = document.getElementById('reset-gen-phrase'); const pwInput = document.getElementById('reset-pw-input'); const _refresh = () => { phraseEl.textContent = _genPassphrase(); }; _refresh(); document.getElementById('reset-gen-new')?.addEventListener('click', _refresh); document.getElementById('reset-gen-use')?.addEventListener('click', () => { pwInput.value = phraseEl.textContent; pwInput.type = 'text'; }); document.getElementById('reset-pw-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = e.target.querySelector('[type="submit"]'); const password = document.getElementById('reset-pw-input').value; await UI.asyncButton(btn, async () => { const res = await API.post('/auth/reset-password', { token, password }); if (res?.ok) { UI.toast.success('Passwort geändert! Du kannst dich jetzt anmelden.'); _renderAuth('login'); } }); }); } // ---------------------------------------------------------- // HELPER // ---------------------------------------------------------- // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh }; })();