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)}
+ ${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 `
+
+ `;
+ }
+
+ // ----------------------------------------------------------
+ // 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
+
+ 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 `
+
+ `;
+ }
+
+ 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 };
+
+})();