banyaro/backend/static/js/pages/onboarding.js
rene 553e9e7854 Sprint 12+13: Tagebuch Day-One-Redesign, Notiz-Feature, Icon-Fixes, SW by-v405
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
2026-04-25 20:44:46 +02:00

461 lines
17 KiB
JavaScript

/* ============================================================
BAN YARO — Onboarding-Wizard
3-Schritt-Wizard für neue User ohne Hund.
============================================================ */
window.Page_onboarding = (() => {
let _container = null;
let _appState = null;
let _step = 1; // 1 = Willkommen, 2 = Hund anlegen, 3 = Fertig
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_step = 1;
_render();
}
function refresh() {
// Wenn User nach Abschluss zurücknavigiert und schon fertig ist → Tagebuch
if (localStorage.getItem('by_onboarding_done')) {
App.navigate('diary');
return;
}
_render();
}
function onDogChange() {}
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div style="max-width:480px;margin:0 auto;padding:var(--space-6) var(--space-4) var(--space-8)">
<!-- Schritt-Indikator -->
<div style="display:flex;align-items:center;justify-content:center;
gap:var(--space-2);margin-bottom:var(--space-8)">
${[1, 2, 3].map(n => `
<div style="display:flex;align-items:center;gap:var(--space-2)">
<div style="width:32px;height:32px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
font-size:var(--text-sm);font-weight:var(--weight-bold);
transition:background 0.2s,color 0.2s;
${n === _step
? 'background:var(--c-primary);color:#fff;'
: n < _step
? 'background:var(--c-success);color:#fff;'
: 'background:var(--c-bg-secondary);color:var(--c-text-secondary);'}">
${n < _step
? `<svg style="width:16px;height:16px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg>`
: n}
</div>
${n < 3 ? `<div style="width:40px;height:2px;
background:${n < _step ? 'var(--c-success)' : 'var(--c-border)'};
border-radius:1px;transition:background 0.2s"></div>` : ''}
</div>
`).join('')}
</div>
<!-- Schritt-Inhalt -->
<div id="onboarding-step-content">
${_stepContent()}
</div>
</div>
`;
_bindEvents();
}
function _stepContent() {
if (_step === 1) return _step1();
if (_step === 2) return _step2();
if (_step === 3) return _step3();
return '';
}
// ----------------------------------------------------------
// SCHRITT 1 — Willkommen
// ----------------------------------------------------------
function _step1() {
return `
<div style="text-align:center">
<!-- Logo -->
<div style="margin-bottom:var(--space-6)">
<img src="/icons/icon-180.png" alt="Ban Yaro"
style="width:96px;height:96px;border-radius:var(--radius-xl);
box-shadow:var(--shadow-md)">
</div>
<!-- Überschrift -->
<h1 style="font-size:var(--text-2xl);font-weight:var(--weight-bold);
color:var(--c-text);margin:0 0 var(--space-4)">
Willkommen bei Ban Yaro!
</h1>
<!-- Beschreibung -->
<p style="font-size:var(--text-base);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-3)">
Ban Yaro ist dein digitaler Begleiter für alles rund um deinen Hund —
Tagebuch, Gesundheit, Karte und Community in einer App.
</p>
<p style="font-size:var(--text-base);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-8)">
In nur zwei Schritten richtest du dein Profil ein und bist sofort startklar.
</p>
<!-- Feature-Highlights -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
margin-bottom:var(--space-8);text-align:left">
${[
['book-open', 'Tagebuch', 'Momente & Fotos'],
['syringe', 'Gesundheit', 'Impfungen & Arzt'],
['map-trifold', 'Karte', 'Orte & Routen'],
['users', 'Community', 'Freunde & Treffen'],
].map(([icon, title, desc]) => `
<div style="display:flex;gap:var(--space-3);align-items:center;
background:var(--c-bg-secondary);border-radius:var(--radius-md);
padding:var(--space-3)">
<div style="width:36px;height:36px;border-radius:var(--radius-md);
background:var(--c-primary-subtle);flex-shrink:0;
display:flex;align-items:center;justify-content:center">
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
</div>
<div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${title}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${desc}</div>
</div>
</div>
`).join('')}
</div>
<!-- Buttons -->
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
<button class="btn btn-primary" id="ob-next-btn" style="width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-right"></use></svg>
Los geht's
</button>
<button class="btn btn-ghost" id="ob-skip-btn" style="width:100%">
Überspringen
</button>
</div>
</div>
`;
}
// ----------------------------------------------------------
// SCHRITT 2 — Hund anlegen
// ----------------------------------------------------------
function _step2() {
const today = new Date().toISOString().slice(0, 10);
return `
<div>
<!-- Überschrift -->
<div style="text-align:center;margin-bottom:var(--space-6)">
<div style="width:64px;height:64px;border-radius:50%;
background:var(--c-primary-subtle);margin:0 auto var(--space-4);
display:flex;align-items:center;justify-content:center">
<svg style="fill:currentColor;width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#dog"></use>
</svg>
</div>
<h2 style="font-size:var(--text-xl);font-weight:var(--weight-bold);
color:var(--c-text);margin:0 0 var(--space-2)">
Dein erster Hund
</h2>
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0">
Nur der Name ist Pflicht — alles andere kannst du später ergänzen.
</p>
</div>
<!-- Formular -->
<form id="ob-dog-form" autocomplete="off">
<div class="form-group">
<label class="form-label">Name *</label>
<input class="form-control" type="text" name="name" id="ob-dog-name"
placeholder="z. B. Luna, Max, Bello …" required autofocus>
</div>
<div class="form-group">
<label class="form-label">
Rasse
<span style="color:var(--c-text-secondary);font-weight:400">(optional)</span>
</label>
<input class="form-control" type="text" name="rasse"
placeholder="z. B. Golden Retriever, Mischling …">
</div>
<div class="form-group">
<label class="form-label">
Geburtstag
<span style="color:var(--c-text-secondary);font-weight:400">(optional)</span>
</label>
<input class="form-control" type="date" name="geburtstag" max="${today}">
</div>
<!-- Foto-Upload -->
<div class="form-group">
<label class="form-label">
Foto
<span style="color:var(--c-text-secondary);font-weight:400">(optional)</span>
</label>
<div id="ob-photo-preview" style="display:none;margin-bottom:var(--space-2)">
<img id="ob-photo-img" src="" alt="Vorschau"
style="width:80px;height:80px;object-fit:cover;
border-radius:50%;border:2px solid var(--c-border)">
</div>
<label class="btn btn-secondary" style="width:100%;cursor:pointer;
display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
<span id="ob-photo-label">Foto auswählen</span>
<input type="file" name="foto" id="ob-photo-input"
accept="image/*" style="display:none">
</label>
</div>
</form>
<!-- Buttons -->
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-6)">
<button class="btn btn-secondary" id="ob-back-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-left"></use></svg>
</button>
<button type="submit" form="ob-dog-form" class="btn btn-primary" id="ob-save-btn"
style="flex:1">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
Hund anlegen
</button>
</div>
<div style="text-align:center;margin-top:var(--space-3)">
<button class="btn btn-ghost" id="ob-skip-btn" style="font-size:var(--text-sm)">
Ohne Hund fortfahren
</button>
</div>
</div>
`;
}
// ----------------------------------------------------------
// SCHRITT 3 — Fertig
// ----------------------------------------------------------
function _step3() {
const dogName = _appState.activeDog?.name;
return `
<div style="text-align:center">
<!-- Erfolgs-Icon -->
<div style="margin-bottom:var(--space-6)">
<div style="width:80px;height:80px;border-radius:50%;
background:var(--c-success-subtle,#dcfce7);margin:0 auto var(--space-4);
display:flex;align-items:center;justify-content:center">
<svg style="fill:currentColor;width:40px;height:40px;color:var(--c-success)" aria-hidden="true">
<use href="/icons/phosphor.svg#check-circle"></use>
</svg>
</div>
</div>
<!-- Text -->
<h2 style="font-size:var(--text-2xl);font-weight:var(--weight-bold);
color:var(--c-text);margin:0 0 var(--space-4)">
Dein Profil ist bereit!
</h2>
${dogName ? `
<p style="font-size:var(--text-base);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-3)">
<strong>${_esc(dogName)}</strong> ist jetzt in Ban Yaro.
Du kannst jetzt Einträge im Tagebuch anlegen, die Gesundheit pflegen
und viele weitere Funktionen nutzen.
</p>
` : `
<p style="font-size:var(--text-base);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-3)">
Ban Yaro ist bereit. Du kannst jetzt die Karte, das Wiki und viele
weitere Funktionen erkunden.
</p>
`}
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-8)">
Du kannst dein Hundeprofil jederzeit unter
<strong>Mein Hund</strong> bearbeiten und ergänzen.
</p>
<!-- CTA -->
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
<button class="btn btn-primary" id="ob-diary-btn" style="width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
Zum Tagebuch
</button>
${dogName ? `
<button class="btn btn-secondary" id="ob-profile-btn" style="width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
Profil vervollständigen
</button>
` : ''}
</div>
</div>
`;
}
// ----------------------------------------------------------
// EVENTS
// ----------------------------------------------------------
function _bindEvents() {
// Weiter-Button (Schritt 1)
_container.querySelector('#ob-next-btn')?.addEventListener('click', () => {
_step = 2;
_render();
});
// Zurück-Button (Schritt 2)
_container.querySelector('#ob-back-btn')?.addEventListener('click', () => {
_step = 1;
_render();
});
// Überspringen
_container.querySelector('#ob-skip-btn')?.addEventListener('click', () => {
_finish();
});
// Foto-Vorschau
_container.querySelector('#ob-photo-input')?.addEventListener('change', e => {
const file = e.target.files?.[0];
if (!file) return;
const url = URL.createObjectURL(file);
const preview = _container.querySelector('#ob-photo-preview');
const img = _container.querySelector('#ob-photo-img');
const label = _container.querySelector('#ob-photo-label');
if (preview) preview.style.display = '';
if (img) img.src = url;
if (label) label.textContent = file.name.length > 20
? file.name.slice(0, 17) + '...'
: file.name;
});
// Formular abschicken (Schritt 2)
_container.querySelector('#ob-dog-form')?.addEventListener('submit', async e => {
e.preventDefault();
await _saveDog(e.target);
});
// Zu Tagebuch (Schritt 3)
_container.querySelector('#ob-diary-btn')?.addEventListener('click', () => {
App.navigate('diary');
});
// Zu Hund-Profil (Schritt 3)
_container.querySelector('#ob-profile-btn')?.addEventListener('click', () => {
App.navigate('dog-profile');
});
}
// ----------------------------------------------------------
// HUND SPEICHERN
// ----------------------------------------------------------
async function _saveDog(form) {
const saveBtn = _container.querySelector('#ob-save-btn');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.innerHTML = `
<svg class="ph-icon spin" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
Wird angelegt…
`;
}
try {
const data = new FormData(form);
const payload = {
name: data.get('name')?.trim(),
rasse: data.get('rasse')?.trim() || null,
geburtstag: data.get('geburtstag') || null,
};
if (!payload.name) {
UI.toast.error('Bitte gib einen Namen ein.');
return;
}
// Hund anlegen
const dog = await API.dogs.create(payload);
// Foto hochladen (falls vorhanden)
const fotoFile = data.get('foto');
if (fotoFile && fotoFile.size > 0) {
try {
const fd = new FormData();
fd.append('file', fotoFile);
await API.dogs.uploadPhoto(dog.id, fd);
} catch {
// Foto-Upload-Fehler ist nicht kritisch
UI.toast.warning('Hund angelegt, Foto konnte nicht hochgeladen werden.');
}
}
// State aktualisieren
const dogs = await API.dogs.list();
_appState.dogs = dogs;
const newDog = dogs.find(d => d.id === dog.id) || dogs[0];
_appState.activeDog = newDog;
if (newDog) {
localStorage.setItem('by_active_dog', String(newDog.id));
}
App.renderDogSwitcher();
UI.toast.success(`${_esc(dog.name)} wurde angelegt!`);
_step = 3;
_render();
} catch (err) {
UI.toast.error(err.message || 'Hund konnte nicht angelegt werden.');
} finally {
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.innerHTML = `
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
Hund anlegen
`;
}
}
}
// ----------------------------------------------------------
// ABSCHLUSS
// ----------------------------------------------------------
function _finish() {
localStorage.setItem('by_onboarding_done', '1');
if (_appState.dogs.length > 0) {
App.navigate('diary');
} else {
App.navigate('map');
}
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _esc(s) {
return UI.escape(s || '');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh, onDogChange };
})();