Frontend Sprint 3+4: Dog-Switcher, Health-Seite, Multi-Dog Tagebuch
- app.js: vollständiger Dog-Switcher (Avatar im Header/Sidebar, Quickpicker bei 3+ Hunden, setActiveDog, localStorage-Persistenz), iOS Ghost-Click Fix, Loading-Guard, Logout State Reset - index.html: Dog-Switcher HTML, Favicon-Links, Sidebar "+ Neu erstellen", Navigation Tab Karte → Gesundheit - health.js (neu): vollständiges Health-Frontend mit Tabs (Impfung, Entwurmung, Tierarzt, Medikament, Gewicht-Kurve, Allergie, Dokument), Ampel-System, KI-Zusammenfassung - dog-profile.js: "+ Weiteren Hund anlegen" Button + _openCreateModal(), Event-Delegation statt direkter Listener (kein Doppelaufruf) - diary.js: Dog-Picker im Formular, Avatar-Reihe auf Karten, Dog-Chips im Detail-Modal, dog_ids im API-Payload - poison.js: Erledigt-Dialog mit Grundauswahl (beseitigt/fehlerhaft/anderes) - api.js: health-Endpoints (list, create, update, delete, upload, ki) - ui.js: confirm() Fix (resolve vor close) - layout.css: Dog-Switcher Styles, scrollbare Sidebar-Nav, User-Item fix - components.css: Health-Styles, Diary Dog-Picker, Ampel-Punkte, Gewicht-SVG - icons/: Favicon-Set (ico, 16px, 32px, 180px, 192px, 512px)
This commit is contained in:
parent
6f48ec581d
commit
d8b9561fff
16 changed files with 1597 additions and 91 deletions
|
|
@ -56,8 +56,9 @@ const App = (() => {
|
|||
document.querySelectorAll(`[data-page="${pageId}"]`)
|
||||
.forEach(el => el.classList.add('active'));
|
||||
|
||||
// Header-Titel setzen
|
||||
document.getElementById('header-title').textContent = pages[pageId].title;
|
||||
// Header-Titel setzen (nur wenn kein Dog-Switcher aktiv ist)
|
||||
const titleEl = document.getElementById('header-title');
|
||||
if (titleEl) titleEl.textContent = pages[pageId].title;
|
||||
|
||||
// History
|
||||
if (pushHistory) {
|
||||
|
|
@ -79,8 +80,12 @@ const App = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Guard: verhindert doppelten Load bei gleichzeitigen navigate()-Aufrufen
|
||||
if (page._loading) return;
|
||||
page._loading = true;
|
||||
|
||||
const container = document.querySelector(`#page-${pageId} .page-body`);
|
||||
if (!container) return;
|
||||
if (!container) { page._loading = false; return; }
|
||||
|
||||
// Skeleton während Laden
|
||||
container.innerHTML = UI.skeleton(4);
|
||||
|
|
@ -108,6 +113,8 @@ const App = (() => {
|
|||
text: 'Diese Seite ist noch in Entwicklung.',
|
||||
});
|
||||
page.module = {};
|
||||
} finally {
|
||||
page._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,8 +141,14 @@ const App = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// + Button
|
||||
if (e.target.closest('#nav-add')) {
|
||||
// Sidebar-User (kein data-page, damit keine Aktiv-Markierung)
|
||||
if (e.target.closest('#sidebar-user')) {
|
||||
navigate('settings');
|
||||
return;
|
||||
}
|
||||
|
||||
// + Button (Mobile Bottom-Nav + Desktop Sidebar)
|
||||
if (e.target.closest('#nav-add') || e.target.closest('#sidebar-add')) {
|
||||
_showQuickAdd();
|
||||
}
|
||||
});
|
||||
|
|
@ -146,11 +159,8 @@ const App = (() => {
|
|||
navigate(page, false);
|
||||
});
|
||||
|
||||
// Initial: URL-Hash auslesen
|
||||
const hash = location.hash.replace('#', '');
|
||||
if (hash && pages[hash]) {
|
||||
navigate(hash, false);
|
||||
}
|
||||
// Hash-Navigation wird in init() nach _checkAuth() behandelt (nicht hier),
|
||||
// damit kein doppelter _loadPage()-Aufruf entsteht.
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -184,13 +194,18 @@ const App = (() => {
|
|||
document.querySelector('#modal-container').addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-quick]');
|
||||
if (!btn) return;
|
||||
UI.modal.close();
|
||||
const action = btn.dataset.quick;
|
||||
if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); }
|
||||
if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); }
|
||||
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
|
||||
if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
UI.modal.close();
|
||||
// Kurzes Delay wegen iOS Ghost-Click: nach modal.close() feuert iOS
|
||||
// ~300ms später ein synthetisches Click-Event an derselben Position.
|
||||
// Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort.
|
||||
setTimeout(() => {
|
||||
if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); }
|
||||
if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); }
|
||||
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
|
||||
if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
}, 350);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
|
|
@ -201,19 +216,22 @@ const App = (() => {
|
|||
try {
|
||||
const user = await API.auth.me();
|
||||
state.user = user;
|
||||
_onLoggedIn();
|
||||
await _onLoggedIn();
|
||||
} catch {
|
||||
_onLoggedOut();
|
||||
}
|
||||
}
|
||||
|
||||
function _onLoggedIn() {
|
||||
async function _onLoggedIn() {
|
||||
document.getElementById('sidebar-username').textContent = state.user.name;
|
||||
_loadDogs();
|
||||
await _loadDogs();
|
||||
}
|
||||
|
||||
function _onLoggedOut() {
|
||||
state.user = null;
|
||||
state.user = null;
|
||||
state.dogs = [];
|
||||
state.activeDog = null;
|
||||
_renderDogSwitcher();
|
||||
navigate('settings', false);
|
||||
}
|
||||
|
||||
|
|
@ -221,18 +239,142 @@ const App = (() => {
|
|||
try {
|
||||
state.dogs = await API.dogs.list();
|
||||
if (state.dogs.length > 0) {
|
||||
state.activeDog = state.dogs[0];
|
||||
// Zuletzt aktiven Hund aus localStorage wiederherstellen
|
||||
const savedId = parseInt(localStorage.getItem('by_active_dog') || '0');
|
||||
state.activeDog = state.dogs.find(d => d.id === savedId) || state.dogs[0];
|
||||
}
|
||||
// Seitenmodule über neuen Hund informieren
|
||||
_renderDogSwitcher();
|
||||
_notifyDogChange();
|
||||
} catch { /* kein Hund vorhanden */ }
|
||||
}
|
||||
|
||||
function _notifyDogChange() {
|
||||
// Alle geladenen Seiten-Module über Hundwechsel informieren
|
||||
Object.values(pages).forEach(p => p.module?.onDogChange?.(state.activeDog));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HUNDE-SWITCHER (Header Mobile + Sidebar-Logo Desktop)
|
||||
// ----------------------------------------------------------
|
||||
function _renderDogSwitcher() {
|
||||
_renderSwitcherInto(document.getElementById('header-dog-switcher'), 'hdr');
|
||||
_renderSwitcherInto(document.getElementById('sidebar-dog-switcher'), 'sb');
|
||||
}
|
||||
|
||||
function _renderSwitcherInto(el, ctxId) {
|
||||
if (!el) return;
|
||||
|
||||
const dog = state.activeDog;
|
||||
const others = state.dogs.filter(d => d.id !== dog?.id);
|
||||
|
||||
// Fallback: kein User oder kein Hund → Standardlogo
|
||||
if (!state.user || !dog) {
|
||||
if (ctxId === 'sb') {
|
||||
el.innerHTML = `
|
||||
<img class="sidebar-logo-img" src="/icons/icon-180.png" alt="Ban Yaro">
|
||||
<span class="sidebar-logo-text">Ban Yaro</span>`;
|
||||
} else {
|
||||
el.innerHTML = `<span class="header-title" id="header-title">Ban Yaro</span>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const avHtml = d => d.foto_url
|
||||
? `<img src="${_esc(d.foto_url)}" alt="${_esc(d.name)}">`
|
||||
: `<span>🐕</span>`;
|
||||
|
||||
// Inaktive Hunde rechts
|
||||
let othersHtml = '';
|
||||
if (others.length === 1) {
|
||||
othersHtml = `
|
||||
<div class="dog-sw-others">
|
||||
<div class="dog-sw-other" data-dog-id="${others[0].id}" title="${_esc(others[0].name)}">
|
||||
${avHtml(others[0])}
|
||||
</div>
|
||||
</div>`;
|
||||
} else if (others.length >= 2) {
|
||||
const visible = others.slice(0, 3);
|
||||
const extraCount = others.length - 3;
|
||||
othersHtml = `
|
||||
<div class="dog-sw-others">
|
||||
<div class="dog-sw-stack" id="dog-sw-stack-${ctxId}">
|
||||
${visible.map((d, i) => `
|
||||
<div class="dog-sw-other dog-sw-other--${i}" data-dog-id="${d.id}" title="${_esc(d.name)}">
|
||||
${avHtml(d)}
|
||||
</div>`).join('')}
|
||||
${extraCount > 0 ? `<div class="dog-sw-more">+${extraCount}</div>` : ''}
|
||||
</div>
|
||||
<div class="dog-quickpick hidden" id="dog-qp-${ctxId}">
|
||||
${others.map(d => `
|
||||
<div class="dog-qp-item" data-dog-id="${d.id}">
|
||||
<div class="dog-qp-av">${avHtml(d)}</div>
|
||||
<span>${_esc(d.name)}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const titleClass = ctxId === 'sb' ? 'sidebar-logo-text' : 'header-title';
|
||||
el.innerHTML = `
|
||||
<div class="dog-sw-active" id="dog-sw-active-${ctxId}" title="${_esc(dog.name)} bearbeiten">
|
||||
${avHtml(dog)}
|
||||
</div>
|
||||
<span class="${titleClass} dog-sw-title">Ban Yaro</span>
|
||||
${othersHtml}`;
|
||||
|
||||
// Klick aktiver Avatar → Hund-Profil
|
||||
el.querySelector(`#dog-sw-active-${ctxId}`)?.addEventListener('click', () => {
|
||||
navigate('dog-profile');
|
||||
});
|
||||
|
||||
// 1 anderer Hund → direkter Tausch
|
||||
if (others.length === 1) {
|
||||
el.querySelector('.dog-sw-other')?.addEventListener('click', () => {
|
||||
setActiveDog(others[0].id);
|
||||
});
|
||||
}
|
||||
|
||||
// 2+ andere Hunde → Stack klick öffnet Quickpicker
|
||||
if (others.length >= 2) {
|
||||
const stack = el.querySelector(`#dog-sw-stack-${ctxId}`);
|
||||
const qp = el.querySelector(`#dog-qp-${ctxId}`);
|
||||
stack?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
// Alle anderen Quickpicker schließen
|
||||
document.querySelectorAll('.dog-quickpick').forEach(q => {
|
||||
if (q !== qp) q.classList.add('hidden');
|
||||
});
|
||||
qp?.classList.toggle('hidden');
|
||||
});
|
||||
el.querySelectorAll('.dog-qp-item').forEach(item => {
|
||||
item.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
setActiveDog(parseInt(item.dataset.dogId));
|
||||
qp?.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Quickpicker schließen bei Klick außerhalb
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dog-quickpick').forEach(q => q.classList.add('hidden'));
|
||||
});
|
||||
|
||||
function setActiveDog(dogId) {
|
||||
const dog = state.dogs.find(d => d.id === dogId);
|
||||
if (!dog || dog.id === state.activeDog?.id) return;
|
||||
state.activeDog = dog;
|
||||
localStorage.setItem('by_active_dog', String(dogId));
|
||||
_renderDogSwitcher();
|
||||
_notifyDogChange();
|
||||
}
|
||||
|
||||
function _esc(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// INITIALISIERUNG
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -240,16 +382,19 @@ const App = (() => {
|
|||
_bindNavigation();
|
||||
await _checkAuth();
|
||||
|
||||
// Erste Seite laden (Hash oder Standard: diary)
|
||||
const startPage = location.hash.replace('#', '') || 'diary';
|
||||
navigate(pages[startPage] ? startPage : 'diary', false);
|
||||
// Erste Seite laden: Hash aus URL oder Standard 'diary'.
|
||||
// Bewusst NACH _checkAuth(), damit _loadPage() nur einmal aufgerufen wird
|
||||
// (vorher war Hash-Navigation auch in _bindNavigation() → doppelter Aufruf).
|
||||
const hash = location.hash.replace('#', '');
|
||||
const startPage = (hash && pages[hash]) ? hash : 'diary';
|
||||
navigate(startPage, false);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ÖFFENTLICHE API
|
||||
// (andere Module können App.state, App.navigate etc. nutzen)
|
||||
// ----------------------------------------------------------
|
||||
return { init, navigate, state };
|
||||
return { init, navigate, state, renderDogSwitcher: _renderDogSwitcher };
|
||||
|
||||
})();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue