Sprint 12: UI-Vereinheitlichung + Läufigkeits-Tracker

- by-tabs/by-tab: einheitliche Tab/Pill-Navigation in allen Seiten
- by-section-label, by-toolbar: einheitliche Section-Labels und Toolbars
- Design-Tokens: fehlende --c-amber, --c-primary-soft ergänzt, Fallback-Werte entfernt
- sitting.js: sitting-layout für konsistentes flush-Layout (wie walks)
- Läufigkeits-Tracker: neuer Health-Tab für Hündinnen mit Zyklusvorhersage,
  Timeline vergangener Läufigkeiten, Erinnerungen und auto-berechnetem Nächst-Datum
- emptyState-Bug: icon-Parameter muss SVG sein, nicht Icon-Name (dog/bell/warning gefixt)
- SW-Cache: by-v103, APP_VER: 79
This commit is contained in:
rene 2026-04-16 22:31:33 +02:00
parent 32d630d5a1
commit b58789373c
30 changed files with 4344 additions and 523 deletions

View file

@ -174,14 +174,16 @@ window.Page_routes = (() => {
sec.className = 'rk-map-section';
sec.innerHTML = `
<div class="rk-map-bar">
<button class="btn btn-secondary btn-sm" id="rk-map-back" title="Zurück zur Liste">${UI.icon('arrow-left')}</button>
<input class="rk-map-loc-input" id="rk-map-loc" type="search"
placeholder="🔍 Ort suchen…" autocomplete="off">
<button class="btn btn-secondary btn-sm" id="rk-map-gps" title="Mein Standort">📍</button>
<button class="btn btn-secondary btn-sm" id="rk-map-gps" title="Mein Standort">${UI.icon('map-pin')}</button>
</div>
<div id="rk-search-map" style="height:${mapH}px;width:100%"></div>
<div id="rk-search-map" style="flex:1;width:100%"></div>
<div id="rk-map-hint" class="rk-map-hint">Route antippen um Details zu sehen</div>
`;
document.body.appendChild(sec);
document.getElementById('rk-map-back')?.addEventListener('click', () => _switchView('list'));
// Wie _initMiniMaps: pollen bis window.L bereit ist
_pollAndInitSearchMap();
@ -392,14 +394,14 @@ window.Page_routes = (() => {
<h3 class="rk-empty-title">Deine erste Gassi-Route</h3>
<p class="rk-empty-text">Zeichne deine Lieblingsstrecken auf mit Streckendaten, Fotos und Hundetauglichkeit.</p>
<div class="rk-empty-features">
<div class="rk-empty-feature"><span>🗺</span><span>GPS-Aufzeichnung</span></div>
<div class="rk-empty-feature"><span>📷</span><span>Fotos entlang der Strecke</span></div>
<div class="rk-empty-feature">${UI.icon('map-trifold')}<span>GPS-Aufzeichnung</span></div>
<div class="rk-empty-feature">${UI.icon('camera')}<span>Fotos entlang der Strecke</span></div>
<div class="rk-empty-feature"><span>🐾</span><span>Hundetauglichkeit bewerten</span></div>
<div class="rk-empty-feature"><span></span><span>GPX-Download für Navi</span></div>
<div class="rk-empty-feature"><span>📍</span><span>Restaurants & Parkplätze</span></div>
<div class="rk-empty-feature"><span>🔒</span><span>Privat oder öffentlich</span></div>
<div class="rk-empty-feature">${UI.icon('download-simple')}<span>GPX-Download für Navi</span></div>
<div class="rk-empty-feature">${UI.icon('map-pin')}<span>Restaurants & Parkplätze</span></div>
<div class="rk-empty-feature">${UI.icon('lock')}<span>Privat oder öffentlich</span></div>
</div>
<button class="btn btn-primary btn-lg" id="rk-empty-rec">🔴 Erste Route aufzeichnen</button>
<button class="btn btn-primary btn-lg" id="rk-empty-rec">${UI.icon('path')} Erste Route aufzeichnen</button>
</div>`;
document.getElementById('rk-empty-rec')?.addEventListener('click', () => {
App.navigate('map');
@ -438,7 +440,7 @@ window.Page_routes = (() => {
// Karte HTML
// ----------------------------------------------------------
function _cardHTML(r) {
const privBadge = !r.is_public ? '<span class="rk-badge rk-badge--private">🔒 Privat</span>' : '';
const privBadge = !r.is_public ? `<span class="rk-badge rk-badge--private">${UI.icon('lock')} Privat</span>` : '';
const diffLabel = DIFFICULTY_LABEL[r.schwierigkeit] || '';
const terrain = TERRAIN_LABEL[r.untergrund] || '';
const paws = HUNDE_LABEL[r.hunde_tauglichkeit] || '';
@ -457,22 +459,22 @@ window.Page_routes = (() => {
<div class="rk-card-body">
<div class="rk-card-name">${_esc(r.name)}</div>
<div class="rk-card-stats">
${dist ? `<span>🗺️ ${dist}</span>` : ''}
${dur ? `<span> ${dur}</span>` : ''}
${dist ? `<span>${UI.icon('map-trifold')} ${dist}</span>` : ''}
${dur ? `<span>${UI.icon('timer')} ${dur}</span>` : ''}
${terrain ? `<span>${terrain}</span>` : ''}
${paws ? `<span title="Hundetauglichkeit">${paws}</span>` : ''}
</div>
<div class="rk-card-tags">
${privBadge}
${diffLabel ? `<span class="rk-badge rk-badge--${r.schwierigkeit}">${diffLabel}</span>` : ''}
${r.schatten ? '<span class="rk-badge">🌳 Schatten</span>' : ''}
${r.leine_empfohlen ? '<span class="rk-badge">🔗 Leine</span>' : ''}
${r.schatten ? `<span class="rk-badge">${UI.icon('tree')} Schatten</span>` : ''}
${r.leine_empfohlen ? `<span class="rk-badge">${UI.icon('link')} Leine</span>` : ''}
</div>
<div class="rk-card-footer">
<div class="rk-stars">${_starsHTML(r.id, r.bewertung||0, r.anz_bewertungen||0)}</div>
<div class="rk-card-actions">
<span class="rk-card-author">${_esc(r.user_name||'Anonym')}</span>
<button class="rk-dl-btn" data-id="${r.id}"> GPX</button>
<button class="rk-dl-btn" data-id="${r.id}">${UI.icon('download-simple')} GPX</button>
</div>
</div>
</div>
@ -579,7 +581,7 @@ window.Page_routes = (() => {
</label>` : ''}
</div>` :
isOwn ? `<label class="rk-photo-add-empty">
📷 Foto hinzufügen
${UI.icon('camera')} Foto hinzufügen
<input type="file" id="rk-photo-input" accept="image/*" style="display:none">
</label>` : '';
@ -588,14 +590,14 @@ window.Page_routes = (() => {
margin-bottom:var(--space-3);background:var(--c-surface-2)"></div>
${photoGallery}
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin:var(--space-3) 0">
${route.distanz_km ? `<span class="rk-badge rk-badge--info">🗺️ ${route.distanz_km.toFixed(2)} km</span>` : ''}
${route.dauer_min ? `<span class="rk-badge rk-badge--info"> ${_fmtDur(route.dauer_min)}</span>` : ''}
${route.distanz_km ? `<span class="rk-badge rk-badge--info">${UI.icon('map-trifold')} ${route.distanz_km.toFixed(2)} km</span>` : ''}
${route.dauer_min ? `<span class="rk-badge rk-badge--info">${UI.icon('timer')} ${_fmtDur(route.dauer_min)}</span>` : ''}
${route.schwierigkeit ? `<span class="rk-badge rk-badge--${route.schwierigkeit}">${DIFFICULTY_LABEL[route.schwierigkeit]||route.schwierigkeit}</span>` : ''}
${route.untergrund ? `<span class="rk-badge">${TERRAIN_LABEL[route.untergrund]||route.untergrund}</span>` : ''}
${paws ? `<span class="rk-badge rk-badge--dog" title="Hundetauglichkeit">${paws}</span>` : ''}
${route.schatten ? '<span class="rk-badge">🌳 Schatten</span>' : ''}
${route.leine_empfohlen ? '<span class="rk-badge">🔗 Leine empfohlen</span>' : ''}
${!route.is_public ? '<span class="rk-badge rk-badge--private">🔒 Privat</span>' : ''}
${route.schatten ? `<span class="rk-badge">${UI.icon('tree')} Schatten</span>` : ''}
${route.leine_empfohlen ? `<span class="rk-badge">${UI.icon('link')} Leine empfohlen</span>` : ''}
${!route.is_public ? `<span class="rk-badge rk-badge--private">${UI.icon('lock')} Privat</span>` : ''}
</div>
${route.beschreibung ? `<p style="color:var(--c-text-secondary);margin-bottom:var(--space-3)">${_esc(route.beschreibung)}</p>` : ''}
<div id="rk-nearby" class="rk-nearby-section">
@ -603,16 +605,16 @@ window.Page_routes = (() => {
</div>
<p style="color:var(--c-text-muted);font-size:0.75rem;margin-top:var(--space-2)">
${track.length} GPS-Punkte · von ${_esc(route.user_name||'Anonym')}
${route.bewertung ? ` · ${route.bewertung.toFixed(1)} (${route.anz_bewertungen})` : ''}
${route.bewertung ? ` · ${UI.icon('star')} ${route.bewertung.toFixed(1)} (${route.anz_bewertungen})` : ''}
</p>
`;
const footer = `
<button type="button" class="btn btn-secondary" id="rd-gpx"> GPX</button>
<button type="button" class="btn btn-secondary" id="rd-gpx">${UI.icon('download-simple')} GPX</button>
${isOwn ? `<button type="button" class="btn btn-ghost" id="rd-vis" title="${route.is_public?'Privat machen':'Öffentlich machen'}">
${route.is_public?'🔒 Privat':'🌍 Öffentlich'}
${route.is_public ? UI.icon('lock')+' Privat' : UI.icon('globe')+' Öffentlich'}
</button>
<button type="button" class="btn btn-ghost" id="rd-del" style="color:var(--c-danger)">🗑</button>` : ''}
<button type="button" class="btn btn-ghost" id="rd-del" style="color:var(--c-danger)">${UI.icon('trash')}</button>` : ''}
<button type="button" class="btn btn-primary flex-1" id="rd-close">Schließen</button>
`;
@ -627,7 +629,7 @@ window.Page_routes = (() => {
await API.routes.update(route.id, { is_public: !route.is_public });
route.is_public = !route.is_public;
const btn = document.getElementById('rd-vis');
if (btn) btn.textContent = route.is_public ? '🔒 Privat' : '🌍 Öffentlich';
if (btn) btn.innerHTML = route.is_public ? UI.icon('lock')+' Privat' : UI.icon('globe')+' Öffentlich';
const r = _data.find(x => x.id === route.id);
if (r) r.is_public = route.is_public;
_applyFilter();
@ -744,15 +746,15 @@ window.Page_routes = (() => {
});
el.innerHTML = `
<div class="rk-nearby-title">📍 Entlang der Route</div>
<div class="rk-nearby-title">${UI.icon('map-pin')} Entlang der Route</div>
${Object.values(byType).map(group => `
<div class="rk-nearby-group">
<div class="rk-nearby-group-label">${group.icon} ${_esc(group.label)} (${group.items.length})</div>
${group.items.slice(0, 5).map(p => `
<div class="rk-nearby-item">
<span class="rk-nearby-name">${_esc(p.name || group.label)}</span>
${p.opening_hours ? `<span class="rk-nearby-detail">🕐 ${_esc(p.opening_hours)}</span>` : ''}
${p.phone ? `<a href="tel:${_esc(p.phone)}" class="rk-nearby-detail rk-nearby-phone">📞 ${_esc(p.phone)}</a>` : ''}
${p.opening_hours ? `<span class="rk-nearby-detail">${UI.icon('clock')} ${_esc(p.opening_hours)}</span>` : ''}
${p.phone ? `<a href="tel:${_esc(p.phone)}" class="rk-nearby-detail rk-nearby-phone">${UI.icon('phone')} ${_esc(p.phone)}</a>` : ''}
</div>
`).join('')}
${group.items.length > 5 ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 0">+${group.items.length-5} weitere</div>` : ''}
@ -977,9 +979,9 @@ window.Page_routes = (() => {
const body = `
<div class="rk-import-preview">${preview}</div>
<div class="rk-import-stats">
<span>📍 ${track.length} Punkte</span>
<span>🗺 ${distanz_km.toFixed(2)} km</span>
${dauer_min ? `<span> ${_fmtDur(dauer_min)}</span>` : ''}
<span>${UI.icon('map-pin')} ${track.length} Punkte</span>
<span>${UI.icon('map-trifold')} ${distanz_km.toFixed(2)} km</span>
${dauer_min ? `<span>${UI.icon('timer')} ${_fmtDur(dauer_min)}</span>` : ''}
<span class="rk-badge rk-badge--info">${source}</span>
</div>
<form id="rk-import-form" style="display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-4)">
@ -1036,7 +1038,7 @@ window.Page_routes = (() => {
const footer = `
<button type="button" class="btn btn-ghost" id="ri-cancel">Abbrechen</button>
<button type="button" class="btn btn-primary flex-1" id="ri-save">💾 Route speichern</button>
<button type="button" class="btn btn-primary flex-1" id="ri-save">${UI.icon('floppy-disk')} Route speichern</button>
`;
UI.modal.open({ title: '📥 Route importieren', body, footer });
@ -1083,7 +1085,7 @@ window.Page_routes = (() => {
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Route speichern';
saveBtn.innerHTML = UI.icon('floppy-disk') + ' Route speichern';
}
});
}