diff --git a/backend/static/css/design-system.css b/backend/static/css/design-system.css index 15f8df2..dc1a0b2 100644 --- a/backend/static/css/design-system.css +++ b/backend/static/css/design-system.css @@ -115,21 +115,70 @@ --safe-right: env(safe-area-inset-right, 0px); } -/* Dark Mode — vorbereitet, nicht aktiv */ +/* Dark Mode — System-Präferenz (nur wenn kein manuelles Theme gesetzt) */ @media (prefers-color-scheme: dark) { - :root { - --c-bg: #1A1410; - --c-surface: #241C14; - --c-surface-2: #2E2418; - --c-surface-3: #3A2E20; - --c-border: #4A3C2C; - --c-border-light: #3A2E20; - --c-text: #F0EAE0; + :root:not([data-theme="light"]):not([data-theme="dark"]) { + --c-bg: #1A1410; + --c-surface: #241C14; + --c-surface-2: #2E2418; + --c-surface-3: #3A2E20; + --c-border: #4A3C2C; + --c-border-light: #3A2E20; + + --c-primary-subtle: #2A1C0A; + --c-primary-soft: #2E1E08; + + --c-nature-subtle: #1A2214; + --c-sky-subtle: #141C22; + --c-danger-subtle: #2A100A; + --c-success-subtle: #122010; + --c-warning-subtle: #261A08; + --c-info-subtle: #10182A; + + --c-text: #F0EAE0; --c-text-secondary: #C0B0A0; - --c-text-muted: #806A58; + --c-text-muted: #806A58; + --c-text-inverse: #2A1F14; + + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30); + --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.40), 0 2px 4px rgba(0, 0, 0, 0.25); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45), 0 4px 8px rgba(0, 0, 0, 0.30); + --shadow-xl: 0 16px 40px rgba(0, 0, 0, 0.50), 0 8px 16px rgba(0, 0, 0, 0.35); } } +/* Manuelles Dark-Theme via data-theme="dark" (überschreibt auch prefers-color-scheme: light) */ +:root[data-theme="dark"] { + --c-bg: #1A1410; + --c-surface: #241C14; + --c-surface-2: #2E2418; + --c-surface-3: #3A2E20; + --c-border: #4A3C2C; + --c-border-light: #3A2E20; + + --c-primary-subtle: #2A1C0A; + --c-primary-soft: #2E1E08; + + --c-nature-subtle: #1A2214; + --c-sky-subtle: #141C22; + --c-danger-subtle: #2A100A; + --c-success-subtle: #122010; + --c-warning-subtle: #261A08; + --c-info-subtle: #10182A; + + --c-text: #F0EAE0; + --c-text-secondary: #C0B0A0; + --c-text-muted: #806A58; + --c-text-inverse: #2A1F14; + + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30); + --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.40), 0 2px 4px rgba(0, 0, 0, 0.25); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45), 0 4px 8px rgba(0, 0, 0, 0.30); + --shadow-xl: 0 16px 40px rgba(0, 0, 0, 0.50), 0 8px 16px rgba(0, 0, 0, 0.35); +} + /* ------------------------------------------------------------ 2. RESET & BASE ------------------------------------------------------------ */ diff --git a/backend/static/index.html b/backend/static/index.html index f46531d..a02751d 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -170,6 +170,10 @@
+
+
+
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js index e36649b..8ec5eb4 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '175'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '179'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { @@ -33,6 +33,7 @@ const App = (() => { // ---------------------------------------------------------- const pages = { welcome: { title: 'Willkommen', module: null }, + onboarding: { title: 'Einrichtung', module: null, requiresAuth: true }, diary: { title: 'Tagebuch', module: null, requiresAuth: true }, health: { title: 'Gesundheit', module: null, requiresAuth: true }, 'dog-profile': { title: 'Mein Hund', module: null, requiresAuth: true }, @@ -413,9 +414,9 @@ const App = (() => { } await _loadDogs(); - // Eingeloggter User ohne Hund (z.B. nach Reload) → direkt zur Hund-Anlage - if (state.dogs.length === 0) { - navigate('dog-profile'); + // Eingeloggter User ohne Hund → Onboarding-Wizard (einmalig) + if (state.dogs.length === 0 && !localStorage.getItem('by_onboarding_done')) { + navigate('onboarding'); } _updateNotifBadge(); diff --git a/backend/static/js/pages/onboarding.js b/backend/static/js/pages/onboarding.js new file mode 100644 index 0000000..6cfda8b --- /dev/null +++ b/backend/static/js/pages/onboarding.js @@ -0,0 +1,461 @@ +/* ============================================================ + 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 = ` +
+ + +
+ ${[1, 2, 3].map(n => ` +
+
+ ${n < _step + ? `` + : n} +
+ ${n < 3 ? `
` : ''} +
+ `).join('')} +
+ + +
+ ${_stepContent()} +
+ +
+ `; + + _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 ` +
+ + +
+ Ban Yaro +
+ + +

+ Willkommen bei Ban Yaro! +

+ + +

+ Ban Yaro ist dein digitaler Begleiter für alles rund um deinen Hund — + Tagebuch, Gesundheit, Karte und Community in einer App. +

+

+ In nur zwei Schritten richtest du dein Profil ein und bist sofort startklar. +

+ + +
+ ${[ + ['book-open', 'Tagebuch', 'Momente & Fotos'], + ['syringe', 'Gesundheit', 'Impfungen & Arzt'], + ['map-trifold', 'Karte', 'Orte & Routen'], + ['users', 'Community', 'Freunde & Treffen'], + ].map(([icon, title, desc]) => ` +
+
+ +
+
+
${title}
+
${desc}
+
+
+ `).join('')} +
+ + +
+ + +
+ +
+ `; + } + + // ---------------------------------------------------------- + // SCHRITT 2 — Hund anlegen + // ---------------------------------------------------------- + function _step2() { + const today = new Date().toISOString().slice(0, 10); + return ` +
+ + +
+
+ +
+

+ Dein erster Hund +

+

+ Nur der Name ist Pflicht — alles andere kannst du später ergänzen. +

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ +
+ `; + } + + // ---------------------------------------------------------- + // SCHRITT 3 — Fertig + // ---------------------------------------------------------- + function _step3() { + const dogName = _appState.activeDog?.name; + return ` +
+ + +
+
+ +
+
+ + +

+ Dein Profil ist bereit! +

+ ${dogName ? ` +

+ ${_esc(dogName)} ist jetzt in Ban Yaro. + Du kannst jetzt Einträge im Tagebuch anlegen, die Gesundheit pflegen + und viele weitere Funktionen nutzen. +

+ ` : ` +

+ Ban Yaro ist bereit. Du kannst jetzt die Karte, das Wiki und viele + weitere Funktionen erkunden. +

+ `} +

+ Du kannst dein Hundeprofil jederzeit unter + Mein Hund bearbeiten und ergänzen. +

+ + +
+ + ${dogName ? ` + + ` : ''} +
+ +
+ `; + } + + // ---------------------------------------------------------- + // 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 = ` + + 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 = ` + + 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 }; + +})(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 8478bca..6aeb9d9 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -182,6 +182,32 @@ window.Page_settings = (() => { App-Einstellungen
+ + +
+ +
+
Dark Mode
+
+ Erscheinungsbild der App +
+
+ +
+
@@ -420,6 +446,24 @@ window.Page_settings = (() => { } }); + 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