banyaro/backend/static/js/pages/settings.js
rene 097295c628 Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration
- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
2026-04-15 21:33:53 +02:00

340 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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
// ----------------------------------------------------------
function _renderAccount() {
const u = _appState.user;
_container.innerHTML = `
<div style="max-width:400px;margin:0 auto;padding:var(--space-4) 0">
<div class="card" style="padding:var(--space-5);margin-bottom:var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-4)">
<div style="width:56px;height:56px;border-radius:50%;
background:var(--c-primary);color:#fff;
display:flex;align-items:center;justify-content:center;
font-size:1.5rem;font-weight:700;flex-shrink:0">
${_esc(u.name.charAt(0).toUpperCase())}
</div>
<div>
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(u.name)}</div>
<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${_esc(u.email)}</div>
${u.is_premium
? `<span class="badge badge-primary" style="margin-top:var(--space-1)">
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#star"></use></svg> Ban Yaro Plus
</span>`
: `<span class="badge" style="margin-top:var(--space-1);
color:var(--c-text-secondary)">
Kostenlos
</span>`}
</div>
</div>
</div>
<div class="card" style="margin-bottom:var(--space-4)">
<div class="card-body" style="padding:0">
<div class="sidebar-item" data-page="dog-profile"
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
<span>Hunde-Profile</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>
<div class="sidebar-item" id="settings-push-btn"
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bell"></use></svg>
<span>Push-Benachrichtigungen</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>
<div class="sidebar-item" id="settings-logout-btn"
style="padding:var(--space-4);border-radius:0;cursor:pointer;
color:var(--c-danger)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sign-out"></use></svg>
<span>Abmelden</span>
</div>
</div>
</div>
<div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4);
font-size:var(--text-xs);font-weight:600;
color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:0.05em;border-bottom:1px solid var(--c-border)">
App-Einstellungen
</div>
<div class="card-body" style="padding:0">
<div style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#eye-slash"></use></svg>
<div style="flex:1">
<div style="font-weight:500">Pocket-Modus beim Aufzeichnen</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Schwarzes Overlay hält den Bildschirm aktiv (GPS läuft) — ideal für die Hosentasche.
Helligkeit auf Minimum reduzieren für optimalen Akku-Schutz.
</div>
</div>
<label class="toggle" style="flex-shrink:0">
<input type="checkbox" id="toggle-pocket-mode"
${localStorage.getItem('by_pocket_mode') === 'true' ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div style="text-align:center;color:var(--c-text-secondary);
font-size:var(--text-xs)">
Ban Yaro · banyaro.app<br>
Deine Daten liegen auf einem eigenen Server in Deutschland.
</div>
</div>
`;
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-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('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.');
});
}
// ----------------------------------------------------------
// NICHT EINGELOGGT — Login / Registrierung
// ----------------------------------------------------------
function _renderAuth(mode) {
_mode = mode;
_container.innerHTML = `
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) var(--space-4)">
<!-- Logo -->
<div style="text-align:center;margin-bottom:var(--space-6)">
<img src="/icons/icon-180.png" alt="Ban Yaro"
style="width:72px;height:72px;border-radius:var(--radius-lg);
margin-bottom:var(--space-3)">
<h1 style="font-size:var(--text-2xl);font-weight:700;margin:0">Ban Yaro</h1>
<p style="color:var(--c-text-secondary);margin:var(--space-1) 0 0">
Alles rund um deinen Hund
</p>
</div>
<!-- Tab-Toggle -->
<div style="display:flex;background:var(--c-surface-2);
border-radius:var(--radius-md);padding:3px;
margin-bottom:var(--space-5)">
<button id="tab-login"
class="btn flex-1 ${mode === 'login' ? 'btn-primary' : 'btn-ghost'}"
style="border-radius:calc(var(--radius-md) - 2px)">
Anmelden
</button>
<button id="tab-register"
class="btn flex-1 ${mode === 'register' ? 'btn-primary' : 'btn-ghost'}"
style="border-radius:calc(var(--radius-md) - 2px)">
Registrieren
</button>
</div>
${mode === 'login' ? _loginFormHTML() : _registerFormHTML()}
</div>
`;
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 `
<form id="auth-form" autocomplete="on" novalidate>
<div class="form-group">
<label class="form-label">E-Mail</label>
<input class="form-control" type="email" name="email"
placeholder="deine@email.de" autocomplete="email" required>
</div>
<div class="form-group">
<label class="form-label">Passwort</label>
<input class="form-control" type="password" name="password"
placeholder="Passwort" autocomplete="current-password" required>
</div>
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
Anmelden
</button>
</form>
`;
}
function _registerFormHTML() {
return `
<form id="auth-form" autocomplete="on" novalidate>
<div class="form-group">
<label class="form-label">Dein Name</label>
<input class="form-control" type="text" name="name"
placeholder="z. B. Maria" autocomplete="name" required>
</div>
<div class="form-group">
<label class="form-label">E-Mail</label>
<input class="form-control" type="email" name="email"
placeholder="deine@email.de" autocomplete="email" required>
</div>
<div class="form-group">
<label class="form-label">Passwort</label>
<input class="form-control" type="password" name="password"
placeholder="Mindestens 8 Zeichen" autocomplete="new-password"
minlength="8" required>
</div>
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
Konto erstellen
</button>
<p style="text-align:center;font-size:var(--text-xs);
color:var(--c-text-secondary);margin-top:var(--space-3)">
Mit der Registrierung stimmst du unseren Datenschutzhinweisen zu.<br>
Deine Daten werden ausschließlich auf unserem Server gespeichert.
</p>
</form>
`;
}
function _bindLoginForm() {
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 */ }
UI.toast.success(`Willkommen zurück, ${_appState.user.name}!`);
// Nach Login: Tagebuch oder Profil anlegen
if (_appState.activeDog) {
App.navigate('diary');
} else {
App.navigate('dog-profile');
}
});
});
}
function _bindRegisterForm() {
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 result = await API.auth.register(fd.email, fd.password, fd.name.trim());
localStorage.setItem('by_token', result.token);
_appState.user = await API.auth.me();
document.getElementById('sidebar-username').textContent = _appState.user.name;
_appState.dogs = [];
_appState.activeDog = null;
UI.toast.success(`Willkommen bei Ban Yaro, ${_appState.user.name}! 🐕`);
// Direkt zur Profil-Anlage
App.navigate('dog-profile');
});
});
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh };
})();