Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
1054 lines
48 KiB
JavaScript
1054 lines
48 KiB
JavaScript
/* ============================================================
|
||
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) {
|
||
_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
|
||
? `<img src="${_esc(u.avatar_url)}" alt="Avatar"
|
||
style="width:56px;height:56px;border-radius:50%;object-fit:cover;display:block">`
|
||
: _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 = `
|
||
<div style="width:100%;max-width:640px;margin:0 auto;box-sizing:border-box;overflow-x:hidden;align-self:center">
|
||
|
||
<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 id="settings-avatar-btn"
|
||
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;
|
||
cursor:pointer;overflow:hidden;position:relative">
|
||
${avatarInner}
|
||
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.25);
|
||
display:flex;align-items:center;justify-content:center;
|
||
opacity:0;transition:opacity .15s"
|
||
class="avatar-overlay">
|
||
<svg style="width:20px;height:20px;color:#fff" fill="currentColor" viewBox="0 0 256 256">
|
||
<use href="/icons/phosphor.svg#camera"></use>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<input type="file" id="settings-avatar-input" accept="image/*"
|
||
style="display:none">
|
||
<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>
|
||
|
||
<!-- Mein Profil -->
|
||
<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);
|
||
display:flex;align-items:center;justify-content:space-between">
|
||
<span>Mein Profil</span>
|
||
<button id="settings-profile-edit-btn"
|
||
class="btn btn-ghost"
|
||
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2)">
|
||
Profil bearbeiten
|
||
</button>
|
||
</div>
|
||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
|
||
${memberSince
|
||
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
Mitglied seit ${_esc(memberSince)}
|
||
</div>`
|
||
: ''}
|
||
${u.bio
|
||
? `<div style="font-size:var(--text-sm)">${_esc(u.bio)}</div>`
|
||
: ''}
|
||
${u.wohnort
|
||
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
📍 ${_esc(u.wohnort)}
|
||
</div>`
|
||
: ''}
|
||
${u.erfahrung && erfahrungLabel[u.erfahrung]
|
||
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
${_esc(erfahrungLabel[u.erfahrung])}
|
||
</div>`
|
||
: ''}
|
||
${u.social_link
|
||
? `<div style="font-size:var(--text-sm)">
|
||
<a href="${_esc(u.social_link)}" target="_blank" rel="noopener"
|
||
style="color:var(--c-primary)">${_esc(u.social_link)}</a>
|
||
</div>`
|
||
: ''}
|
||
${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link
|
||
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
Noch kein Profil ausgefüllt.
|
||
</div>`
|
||
: ''}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="settings-stats-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)">Aktivität</div>
|
||
<div id="settings-stats-body" style="padding:var(--space-4);display:flex;justify-content:space-around">
|
||
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt…</div>
|
||
</div>
|
||
<div id="settings-streak" style="display:flex;align-items:center;gap:8px;
|
||
padding:0 var(--space-4) var(--space-3);flex-wrap:wrap"></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)">Trophäen</div>
|
||
<div id="settings-badges-body" style="padding:var(--space-4)">
|
||
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt…</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-calendar-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#calendar-dots"></use></svg>
|
||
<span>Kalender abonnieren</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">
|
||
|
||
<!-- Dark-Mode-Auswahl -->
|
||
<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#moon"></use></svg>
|
||
<div style="flex:1">
|
||
<div style="font-weight:500">Dark Mode</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
Erscheinungsbild der App
|
||
</div>
|
||
</div>
|
||
<select id="select-theme"
|
||
style="padding:var(--space-1) var(--space-2);
|
||
border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);
|
||
background:var(--c-surface);
|
||
color:var(--c-text);
|
||
font-size:var(--text-sm);
|
||
font-family:inherit;
|
||
cursor:pointer">
|
||
<option value="system" ${(localStorage.getItem('by_theme')||'system') === 'system' ? 'selected' : ''}>System</option>
|
||
<option value="light" ${localStorage.getItem('by_theme') === 'light' ? 'selected' : ''}>Hell</option>
|
||
<option value="dark" ${localStorage.getItem('by_theme') === 'dark' ? 'selected' : ''}>Dunkel</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- KI-Notiz-Assistent -->
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4)">
|
||
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#brain"></use></svg>
|
||
<div style="flex:1">
|
||
<div style="font-weight:500">KI-Notiz-Assistent</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
Erkennt Muster in deinen Notizen und macht Vorschläge
|
||
</div>
|
||
</div>
|
||
<label style="position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0">
|
||
<input type="checkbox" id="toggle-notes-ki"
|
||
style="opacity:0;width:0;height:0;position:absolute"
|
||
${u.notes_ki_enabled ? 'checked' : ''}>
|
||
<span style="position:absolute;cursor:pointer;inset:0;border-radius:12px;
|
||
background:var(--c-border);transition:.2s"
|
||
id="toggle-notes-ki-track"></span>
|
||
<span id="toggle-notes-ki-thumb"
|
||
style="position:absolute;top:2px;left:${u.notes_ki_enabled ? '22px' : '2px'};
|
||
width:20px;height:20px;border-radius:50%;
|
||
background:#fff;transition:.2s;
|
||
box-shadow:0 1px 3px rgba(0,0,0,.3)"></span>
|
||
</label>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- App empfehlen -->
|
||
<div class="card" style="margin-bottom:var(--space-5)" id="referral-card">
|
||
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
|
||
<div style="font-weight:600;margin-bottom:2px">${UI.icon('arrow-square-out')} App empfehlen</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
Lade Freunde ein — jede erfolgreiche Einladung wird in deinem Profil angezeigt.
|
||
</div>
|
||
</div>
|
||
<div id="referral-body" style="padding:var(--space-4)">Lade…</div>
|
||
</div>
|
||
|
||
<!-- App installieren -->
|
||
<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 installieren
|
||
</div>
|
||
<div class="card-body" style="padding:0">
|
||
<div class="sidebar-item" id="settings-install-btn"
|
||
style="padding:var(--space-4);border-radius:0;cursor:pointer">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
|
||
<span>Installations-Anleitung</span>
|
||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||
</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>
|
||
`;
|
||
|
||
// 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) => `
|
||
<div style="text-align:center">
|
||
<div style="font-size:1.3rem;font-weight:700;color:var(--c-text)">${val}</div>
|
||
<div style="font-size:10px;color:var(--c-text-secondary);text-transform:uppercase;letter-spacing:.05em;margin-top:2px">${label}</div>
|
||
</div>`;
|
||
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
|
||
? `<span style="font-size:1.3rem">🔥</span>
|
||
<span style="font-weight:700;font-size:1.05rem">${cur} Tage Streak</span>
|
||
${mx > cur ? `<span style="color:var(--c-text-muted);font-size:11px;margin-left:auto">Best: ${mx}</span>` : ''}`
|
||
: `<span style="color:var(--c-text-muted);font-size:var(--text-sm)">🔥 Noch kein Streak — heute aktiv werden!</span>`;
|
||
}
|
||
|
||
if (badgesEl && a.categories) {
|
||
// SVG-Schild für jede Kategorie
|
||
const shield = (color, dark, emoji, opacity = 1) => `
|
||
<svg viewBox="0 0 60 72" xmlns="http://www.w3.org/2000/svg"
|
||
style="width:56px;height:56px;filter:drop-shadow(0 2px 6px ${color}66)">
|
||
<defs>
|
||
<linearGradient id="g_${color.replace('#','')}" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stop-color="${color}"/>
|
||
<stop offset="100%" stop-color="${dark}"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<path d="M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z"
|
||
fill="url(#g_${color.replace('#','')})" opacity="${opacity}"/>
|
||
<path d="M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z"
|
||
fill="none" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/>
|
||
<text x="30" y="43" text-anchor="middle" dominant-baseline="middle"
|
||
font-size="22" style="user-select:none">${emoji}</text>
|
||
</svg>`;
|
||
|
||
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 =>
|
||
`<div title="${_esc(s.name)}" style="width:8px;height:8px;border-radius:50%;
|
||
background:${s.earned ? s.color : 'var(--c-border)'}"></div>`
|
||
).join('');
|
||
|
||
// Aktuelles Schild
|
||
const shieldSvg = cur
|
||
? shield(cur.color, cur.dark, cat.emoji)
|
||
: shield('#9ca3af', '#6b7280', cat.emoji, 0.5);
|
||
|
||
// Fortschrittsbalken
|
||
const progressBar = nxt ? `
|
||
<div style="font-size:10px;color:var(--c-text-muted);margin-top:4px">
|
||
${val}${cat.einheit} / ${nxt.schwelle}${cat.einheit} → ${_esc(nxt.name)}
|
||
</div>
|
||
<div style="height:4px;background:var(--c-border);border-radius:2px;margin-top:4px;overflow:hidden">
|
||
<div style="height:100%;width:${cat.progress}%;background:${nxt.color};border-radius:2px;transition:width .4s"></div>
|
||
</div>` : `
|
||
<div style="font-size:10px;color:var(--c-primary);font-weight:600;margin-top:4px">
|
||
Höchste Stufe erreicht! 🎉
|
||
</div>`;
|
||
|
||
return `
|
||
<div style="display:flex;gap:14px;align-items:flex-start;padding:12px 0;
|
||
border-bottom:1px solid var(--c-border)">
|
||
${shieldSvg}
|
||
<div style="flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px">
|
||
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(cat.name)}</span>
|
||
${cur ? `<span style="font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;
|
||
background:${cur.color};color:${cur.text}">${_esc(cur.name)}</span>` : ''}
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-bottom:6px">${dots}</div>
|
||
${progressBar}
|
||
</div>
|
||
</div>`;
|
||
}).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 = '<div style="color:var(--c-text-muted);font-size:var(--text-sm)">–</div>';
|
||
});
|
||
|
||
// 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]) =>
|
||
`<option value="${_esc(val)}" ${u.erfahrung === val ? 'selected' : ''}>${_esc(label)}</option>`
|
||
).join('');
|
||
|
||
const sichtbarkeitOpts = [
|
||
['public', 'Öffentlich'],
|
||
['friends', 'Nur Freunde'],
|
||
['private', 'Privat'],
|
||
].map(([val, label]) =>
|
||
`<option value="${_esc(val)}" ${(u.profil_sichtbarkeit || 'public') === val ? 'selected' : ''}>${_esc(label)}</option>`
|
||
).join('');
|
||
|
||
UI.modal.open({
|
||
title: 'Profil bearbeiten',
|
||
body: `
|
||
<form id="profile-form" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Echter Name (privat)</label>
|
||
<input name="real_name" type="text" maxlength="80"
|
||
placeholder="z. B. Maria Müller"
|
||
value="${_esc(u.real_name || '')}"
|
||
style="${inputStyle}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Bio</label>
|
||
<textarea name="bio" maxlength="300" rows="4"
|
||
placeholder="Kurze Vorstellung (max. 300 Zeichen)"
|
||
style="${inputStyle};resize:vertical">${_esc(u.bio || '')}</textarea>
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Wohnort</label>
|
||
<input name="wohnort" type="text" maxlength="60"
|
||
placeholder="z.B. München"
|
||
value="${_esc(u.wohnort || '')}"
|
||
style="${inputStyle}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Erfahrung</label>
|
||
<select name="erfahrung" style="${inputStyle}">${erfahrungOpts}</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Social-Link</label>
|
||
<input name="social_link" type="url" maxlength="120"
|
||
placeholder="https://instagram.com/dein-hundeaccount"
|
||
value="${_esc(u.social_link || '')}"
|
||
style="${inputStyle}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Profil-Sichtbarkeit</label>
|
||
<select name="profil_sichtbarkeit" style="${inputStyle}">${sichtbarkeitOpts}</select>
|
||
</div>
|
||
</form>
|
||
`,
|
||
footer: `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
<button type="submit" form="profile-form" class="btn btn-primary" style="width:100%">Speichern</button>
|
||
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
|
||
</div>
|
||
`,
|
||
});
|
||
|
||
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: `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||
Abonniere deinen persönlichen Ban-Yaro-Kalender. Er enthält Impf-Erinnerungen,
|
||
Läufigkeits-Termine, Events und Gassi-Treffen — immer aktuell.
|
||
</p>
|
||
<div style="background:var(--c-bg);border-radius:var(--radius-md);
|
||
padding:var(--space-3) var(--space-4);
|
||
font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
word-break:break-all;margin-bottom:var(--space-4)">
|
||
${httpsUrl}
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||
<a href="${url}"
|
||
class="btn btn-primary"
|
||
style="text-align:center">
|
||
${UI.icon('calendar-dots')} In Kalender-App öffnen
|
||
</a>
|
||
<button class="btn btn-secondary" id="cal-copy-btn">
|
||
${UI.icon('clipboard-text')} URL kopieren
|
||
</button>
|
||
</div>
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-4)">
|
||
Tipp: iOS → Einstellungen › Kalender › Accounts › Account hinzufügen › Andere › Kalenderabo
|
||
</p>
|
||
`,
|
||
});
|
||
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.');
|
||
});
|
||
|
||
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';
|
||
}
|
||
});
|
||
|
||
_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 = `
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||
<div style="flex:1;min-width:0;background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
padding:var(--space-2) var(--space-3);font-family:monospace;font-size:var(--text-sm);
|
||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${r.link}</div>
|
||
<button class="btn btn-primary btn-sm" id="ref-share-btn">${UI.icon('arrow-square-out')} Teilen</button>
|
||
</div>
|
||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
${UI.icon('users')} <strong>${r.count}</strong> ${r.count === 1 ? 'Person' : 'Personen'} über deinen Link registriert
|
||
</div>
|
||
${r.count > 0 ? `
|
||
<div style="margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||
${r.count >= 1 ? `<span class="badge badge-primary">${UI.icon('star')} Botschafter</span>` : ''}
|
||
${r.count >= 5 ? `<span class="badge badge-primary">${UI.icon('star')} Super-Botschafter</span>` : ''}
|
||
${r.count >= 10 ? `<span class="badge badge-primary">${UI.icon('star')} Top-Botschafter</span>` : ''}
|
||
</div>` : ''}
|
||
`;
|
||
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 = `
|
||
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) 0">
|
||
|
||
<!-- 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>
|
||
<div style="position:relative">
|
||
<input class="form-control" type="password" name="password" id="login-pw"
|
||
placeholder="Passwort" autocomplete="current-password" required
|
||
style="padding-right:var(--space-10)">
|
||
<button type="button" id="login-pw-toggle"
|
||
class="btn btn-ghost btn-icon"
|
||
aria-label="Passwort anzeigen"
|
||
style="position:absolute;right:var(--space-1);top:50%;transform:translateY(-50%);
|
||
color:var(--c-text-muted);padding:var(--space-2)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#eye"></use></svg>
|
||
</button>
|
||
</div>
|
||
</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">Benutzername</label>
|
||
<input class="form-control" type="text" name="name"
|
||
placeholder="z. B. bellas_mama" autocomplete="username" required
|
||
pattern="^\\S+$" title="Kein Leerzeichen erlaubt">
|
||
<p style="margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
Dieser Name ist öffentlich sichtbar und wird bei deinen Beiträgen, Pins und Kommentaren angezeigt.
|
||
</p>
|
||
</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>
|
||
<div style="position:relative">
|
||
<input class="form-control" type="password" name="password" id="register-pw"
|
||
placeholder="Mindestens 8 Zeichen" autocomplete="new-password"
|
||
minlength="8" required style="padding-right:var(--space-10)">
|
||
<button type="button" id="register-pw-toggle"
|
||
class="btn btn-ghost btn-icon"
|
||
aria-label="Passwort anzeigen"
|
||
style="position:absolute;right:var(--space-1);top:50%;transform:translateY(-50%);
|
||
color:var(--c-text-muted);padding:var(--space-2)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#eye"></use></svg>
|
||
</button>
|
||
</div>
|
||
<!-- Hundepassphrase-Generator -->
|
||
<div id="pw-gen-box" style="margin-top:var(--space-2);padding:var(--space-3);
|
||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
border-left:3px solid var(--c-primary)">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)">
|
||
<span style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);
|
||
text-transform:uppercase;letter-spacing:.05em">🐾 Passwort-Vorschlag</span>
|
||
<button type="button" id="pw-gen-new"
|
||
style="margin-left:auto;font-size:var(--text-xs);color:var(--c-primary);
|
||
background:none;border:none;cursor:pointer;padding:0;font-weight:600">
|
||
↺ neu
|
||
</button>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||
<code id="pw-gen-phrase"
|
||
style="flex:1;font-size:var(--text-sm);font-weight:700;
|
||
color:var(--c-text);letter-spacing:.02em;word-break:break-all"></code>
|
||
<button type="button" id="pw-gen-use"
|
||
style="flex-shrink:0;font-size:var(--text-xs);font-weight:600;
|
||
padding:4px 10px;border-radius:var(--radius-md);
|
||
background:var(--c-primary);color:#fff;border:none;cursor:pointer">
|
||
Übernehmen
|
||
</button>
|
||
</div>
|
||
<div style="font-size:10px;color:var(--c-text-muted);margin-top:var(--space-1)">
|
||
Sichere Passphrase aus der Hundewelt — leicht zu merken, schwer zu knacken.
|
||
</div>
|
||
</div>
|
||
</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 _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');
|
||
|
||
// 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);
|
||
});
|
||
}
|
||
|
||
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 = `
|
||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary)">
|
||
<use href="/icons/phosphor.svg#bell-ringing"></use>
|
||
</svg>
|
||
<span style="flex:1;line-height:1.4">Push-Benachrichtigungen aktivieren?</span>
|
||
<button id="push-offer-yes" class="btn btn-primary" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-3);flex-shrink:0">Ja</button>
|
||
<button id="push-offer-no" class="btn btn-ghost btn-icon" aria-label="Schließen" style="flex-shrink:0">
|
||
<svg class="ph-icon" style="width:16px;height:16px" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||
</button>
|
||
`;
|
||
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, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh };
|
||
|
||
})();
|