diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1431cd6..ba2ab09 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -214,8 +214,7 @@ const App = (() => { function _onLoggedOut() { state.user = null; - // Zeige Login wenn nötig - // Für MVP: direkte Weiterleitung zu Einstellungen/Login + navigate('settings', false); } async function _loadDogs() { diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js new file mode 100644 index 0000000..9611520 --- /dev/null +++ b/backend/static/js/pages/dog-profile.js @@ -0,0 +1,395 @@ +/* ============================================================ + BAN YARO — Hunde-Profil + Seiten-Modul: Profil anlegen / anzeigen / bearbeiten. + ============================================================ */ + +window.Page_dog_profile = (() => { + + let _container = null; + let _appState = null; + + // ---------------------------------------------------------- + // INIT / REFRESH / LIFECYCLE + // ---------------------------------------------------------- + async function init(container, appState) { + _container = container; + _appState = appState; + await _render(); + } + + async function refresh() { + await _render(); + } + + async function onDogChange(dog) { + await _render(); + } + + // ---------------------------------------------------------- + // HAUPTRENDER + // ---------------------------------------------------------- + async function _render() { + if (!_appState.user) { + _container.innerHTML = UI.emptyState({ + icon : '🐕', + title : 'Anmelden erforderlich', + text : 'Melde dich an, um ein Hundeprofil anzulegen.', + action: ``, + }); + _container.querySelector('#profile-goto-login') + ?.addEventListener('click', () => App.navigate('settings')); + return; + } + + if (!_appState.activeDog) { + _renderCreateForm(); + } else { + _renderProfile(_appState.activeDog); + } + } + + // ---------------------------------------------------------- + // PROFIL-ANSICHT + // ---------------------------------------------------------- + function _renderProfile(dog) { + const geburtstag = dog.geburtstag + ? new Date(dog.geburtstag + 'T00:00:00') + .toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }) + : null; + + _container.innerHTML = ` +
+ + +
+ ${dog.foto_url + ? `${_esc(dog.name)}` + : `
🐕
`} + +
+ + +

${_esc(dog.name)}

+ ${dog.rasse + ? `

${_esc(dog.rasse)}

` + : `

`} + + +
+ ${geburtstag ? ` +
+
🎂 Geburtstag
+
${geburtstag}
+
+ ${_calcAlter(dog.geburtstag)} +
+
+ ` : ''} + ${dog.geschlecht ? ` +
+
${dog.geschlecht === 'm' ? '♂' : '♀'} Geschlecht
+
+ ${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'} +
+
+ ` : ''} + ${dog.gewicht_kg ? ` +
+
⚖️ Gewicht
+
${dog.gewicht_kg} kg
+
+ ` : ''} + ${dog.chip_nr ? ` +
+
💾 Chip-Nr.
+
${_esc(dog.chip_nr)}
+
+ ` : ''} +
+ + ${dog.bio ? ` +
+

+ "${_esc(dog.bio)}" +

+
+ ` : ''} + + + +
+ `; + + // Foto hochladen + document.getElementById('dp-photo-input')?.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + try { + const fd = new FormData(); + fd.append('file', file); + const result = await API.dogs.uploadPhoto(dog.id, fd); + // State in-place aktualisieren + dog.foto_url = result.foto_url; + _appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url }; + _appState.dogs = _appState.dogs.map(d => + d.id === dog.id ? _appState.activeDog : d + ); + UI.toast.success('Foto gespeichert.'); + _renderProfile(_appState.activeDog); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Hochladen.'); + } + }); + + // Bearbeiten öffnen + document.getElementById('dp-edit-btn')?.addEventListener('click', () => { + _openEditModal(dog); + }); + } + + // ---------------------------------------------------------- + // NEU ANLEGEN (direkt auf der Seite, kein Modal) + // ---------------------------------------------------------- + function _renderCreateForm() { + _container.innerHTML = ` +
+
+
🐕
+

+ Hund anlegen +

+

+ Erstelle das Profil für deinen Hund. +

+
+ ${_formHTML(null)} +
+ `; + _bindForm(null, false); + } + + // ---------------------------------------------------------- + // BEARBEITEN (Modal) + // ---------------------------------------------------------- + function _openEditModal(dog) { + UI.modal.open({ title: `${dog.name} bearbeiten`, body: _formHTML(dog) }); + _bindForm(dog, true); + } + + // ---------------------------------------------------------- + // FORMULAR HTML + // ---------------------------------------------------------- + function _formHTML(dog) { + const today = new Date().toISOString().slice(0, 10); + return ` +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+ ${dog ? `` : ''} + +
+ + ${dog ? ` +
+ +
+ ` : ''} + +
+ `; + } + + // ---------------------------------------------------------- + // FORMULAR EVENTS + // ---------------------------------------------------------- + function _bindForm(dog, inModal) { + const form = document.getElementById('dp-form'); + if (!form) return; + + document.getElementById('dp-form-cancel') + ?.addEventListener('click', UI.modal.close); + + document.getElementById('dp-delete-btn')?.addEventListener('click', async () => { + const ok = await UI.modal.confirm({ + title : `${dog.name} löschen?`, + message: 'Tagebuch-Einträge und Gesundheitsdaten werden ebenfalls gelöscht. Nicht rückgängig.', + confirmText: 'Löschen', + danger : true, + }); + if (!ok) return; + try { + await API.dogs.delete(dog.id); + _appState.dogs = _appState.dogs.filter(d => d.id !== dog.id); + _appState.activeDog = _appState.dogs[0] || null; + if (inModal) UI.modal.close(); + UI.toast.success(`${dog.name} wurde gelöscht.`); + await _render(); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Löschen.'); + } + }); + + form.addEventListener('submit', async e => { + e.preventDefault(); + const btn = form.querySelector('[type="submit"]'); + const fd = UI.formData(form); + + if (!fd.name?.trim()) { + UI.toast.warning('Bitte einen Namen eingeben.'); + return; + } + + await UI.asyncButton(btn, async () => { + const payload = { + name: fd.name.trim(), + rasse: fd.rasse || null, + geburtstag: fd.geburtstag || null, + geschlecht: fd.geschlecht || null, + gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null, + chip_nr: fd.chip_nr || null, + bio: fd.bio || null, + is_public: 'is_public' in fd, + }; + + let saved; + if (dog) { + saved = await API.dogs.update(dog.id, payload); + _appState.dogs = _appState.dogs.map(d => d.id === dog.id ? saved : d); + _appState.activeDog = saved; + if (inModal) UI.modal.close(); + UI.toast.success('Profil gespeichert.'); + } else { + saved = await API.dogs.create(payload); + _appState.dogs.push(saved); + _appState.activeDog = saved; + UI.toast.success(`${saved.name} wurde angelegt! 🎉`); + } + await _render(); + }); + }); + } + + // ---------------------------------------------------------- + // HELPER + // ---------------------------------------------------------- + function _calcAlter(geburtstag) { + const born = new Date(geburtstag + 'T00:00:00'); + const tage = Math.floor((Date.now() - born) / 86400000); + if (tage < 0) return ''; + if (tage < 30) return `${tage} Tag${tage !== 1 ? 'e' : ''} alt`; + if (tage < 365) { + const m = Math.floor(tage / 30); + return `${m} Monat${m !== 1 ? 'e' : ''} alt`; + } + const j = Math.floor(tage / 365); + const m = Math.floor((tage % 365) / 30); + return m > 0 + ? `${j} Jahr${j !== 1 ? 'e' : ''}, ${m} Monat${m !== 1 ? 'e' : ''} alt` + : `${j} Jahr${j !== 1 ? 'e' : ''} alt`; + } + + function _esc(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"'); + } + + // ---------------------------------------------------------- + // PUBLIC + // ---------------------------------------------------------- + return { init, refresh, onDogChange }; + +})(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js new file mode 100644 index 0000000..345d162 --- /dev/null +++ b/backend/static/js/pages/settings.js @@ -0,0 +1,306 @@ +/* ============================================================ + 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 = ` +
+ +
+
+
+ ${_esc(u.name.charAt(0).toUpperCase())} +
+
+
${_esc(u.name)}
+
${_esc(u.email)}
+ ${u.is_premium + ? ` + ⭐ Ban Yaro Plus + ` + : ` + Kostenlos + `} +
+
+
+ +
+
+ + + +
+
+ +
+ Ban Yaro · banyaro.app
+ Deine Daten liegen auf einem eigenen Server in Deutschland. +
+ +
+ `; + + 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.'); + } + }); + } + + // ---------------------------------------------------------- + // NICHT EINGELOGGT — Login / Registrierung + // ---------------------------------------------------------- + function _renderAuth(mode) { + _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 ` +
+
+ + +
+
+ + +
+
+ + +
+ +

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

+
+ `; + } + + 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, '&').replace(//g, '>') + .replace(/"/g, '"'); + } + + // ---------------------------------------------------------- + // PUBLIC + // ---------------------------------------------------------- + return { init, refresh }; + +})();