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

@ -43,14 +43,16 @@ window.Page_sitting = (() => {
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="sitting-tabs" id="sit-tabs">
<button class="sitting-tab active" data-sit-tab="suchen">${UI.icon('magnifying-glass')} Sitter finden</button>
${_state.user ? `
<button class="sitting-tab" data-sit-tab="profil">${UI.icon('user')} Mein Profil</button>
<button class="sitting-tab" data-sit-tab="anfragen">${UI.icon('bell')} Anfragen</button>
` : ''}
<div class="sitting-layout">
<div class="sitting-tabs by-tabs" id="sit-tabs">
<button class="by-tab active" data-sit-tab="suchen">${UI.icon('magnifying-glass')} Sitter finden</button>
${_state.user ? `
<button class="by-tab" data-sit-tab="profil">${UI.icon('user')} Mein Profil</button>
<button class="by-tab" data-sit-tab="anfragen">${UI.icon('bell')} Anfragen</button>
` : ''}
</div>
<div id="sit-content" class="sitting-content"></div>
</div>
<div id="sit-content" class="sitting-content"></div>
`;
_container.addEventListener('click', _onClick);
}
@ -95,7 +97,7 @@ window.Page_sitting = (() => {
// ---- Tab: Sitter suchen ----
function _renderSuchen(el) {
if (!_sitters.length) {
el.innerHTML = UI.emptyState({ icon: 'dog', title: 'Keine Sitter', text: 'Noch keine Sitter in deiner Nähe registriert.' });
el.innerHTML = UI.emptyState({ icon: UI.icon('dog'), title: 'Keine Sitter', text: 'Noch keine Sitter in deiner Nähe registriert.' });
return;
}
el.innerHTML = `
@ -138,7 +140,7 @@ window.Page_sitting = (() => {
<div style="font-size:3rem">${UI.icon('paw-print')}</div>
<h3>Werde Hundesitter</h3>
<p>Biete anderen Hundebesitzern deine Dienste an und verdiene etwas dazu.</p>
<button class="btn btn-primary" id="sit-create-profil-btn">Profil erstellen</button>
<button class="btn btn-primary" id="sit-create-profil-btn">${UI.icon('plus')} Profil erstellen</button>
</div>
`;
return;
@ -149,7 +151,7 @@ window.Page_sitting = (() => {
<div class="sitting-my-profil">
<div class="sitting-profil-header">
<div class="sitting-profil-status ${s.aktiv ? 'active' : 'inactive'}">
${s.aktiv ? `${UI.icon('check')} Aktiv` : 'Pausiert'}
${s.aktiv ? `${UI.icon('check')} Aktiv` : `${UI.icon('pause')} Pausiert`}
</div>
<button class="btn btn-secondary btn-sm" id="sit-edit-profil-btn">${UI.icon('pencil-simple')} Bearbeiten</button>
</div>
@ -172,31 +174,36 @@ window.Page_sitting = (() => {
let html = '';
if (inbox.length) {
html += `<div class="sitting-section-label">${UI.icon('bell')} Eingehende Anfragen (als Sitter)</div>`;
html += `<div class="by-section-label">${UI.icon('bell')} Eingehende Anfragen (als Sitter)</div>`;
html += inbox.map(r => _requestCardHTML(r, 'inbox')).join('');
}
if (myReqs.length) {
html += `<div class="sitting-section-label" style="margin-top:var(--space-4)">${UI.icon('upload')} Meine Anfragen</div>`;
html += `<div class="by-section-label" style="margin-top:var(--space-4)">${UI.icon('upload')} Meine Anfragen</div>`;
html += myReqs.map(r => _requestCardHTML(r, 'sent')).join('');
}
if (!inbox.length && !myReqs.length) {
html = UI.emptyState({ icon: 'bell', title: 'Keine Anfragen', text: 'Noch keine Sitting-Anfragen vorhanden.' });
html = UI.emptyState({ icon: UI.icon('bell'), title: 'Keine Anfragen', text: 'Noch keine Sitting-Anfragen vorhanden.' });
}
el.innerHTML = html;
}
const STATUS_ICON = { offen: 'clock', angenommen: 'check-circle', abgelehnt: 'x-circle', abgebrochen: 'minus-circle' };
const STATUS_LABEL = { offen: 'Offen', angenommen: 'Angenommen', abgelehnt: 'Abgelehnt', abgebrochen: 'Abgebrochen' };
const STATUS_COLOR = { offen: '#f59e0b', angenommen: '#10b981', abgelehnt: '#ef4444', abgebrochen: '#6b7280' };
function _requestCardHTML(r, mode) {
const STATUS_COLOR = { offen: '#f59e0b', angenommen: '#10b981', abgelehnt: '#ef4444', abgebrochen: '#6b7280' };
const color = STATUS_COLOR[r.status] || '#6b7280';
const icon = STATUS_ICON[r.status] || 'question';
const label = STATUS_LABEL[r.status] || r.status;
const name = mode === 'inbox' ? r.anfragender_name : r.sitter_name;
return `
<div class="sitting-request-card" data-sit-req="${r.id}" data-sit-req-mode="${mode}">
<div class="sitting-req-header">
<span class="sitting-req-name">${UI.escHtml(name || '?')}</span>
<span class="sitting-req-status" style="color:${color}">${r.status}</span>
<span class="sitting-req-status" style="color:${color}">${UI.icon(icon)} ${label}</span>
</div>
<div class="sitting-req-dates">${UI.icon('calendar-dots')} ${r.von} ${r.bis}</div>
${r.nachricht ? `<div class="sitting-req-msg">${UI.escHtml(r.nachricht)}</div>` : ''}
@ -295,7 +302,7 @@ window.Page_sitting = (() => {
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-anfrage-submit">Anfrage senden</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-anfrage-submit">${UI.icon('paper-plane-tilt')} Anfrage senden</button>
`;
UI.modal.open({ title: 'Anfrage senden', body, footer });
@ -313,7 +320,7 @@ window.Page_sitting = (() => {
dog_ids: dogIds,
nachricht: fd.get('nachricht') || null,
};
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '…'; }
try {
await API.sitting.sendRequest(data);
UI.modal.close();
@ -322,7 +329,7 @@ window.Page_sitting = (() => {
} catch (err) {
UI.toast(err.message, 'error');
} finally {
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Anfrage senden'; }
if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = `${UI.icon('paper-plane-tilt')} Anfrage senden`; }
}
});
}
@ -385,7 +392,7 @@ window.Page_sitting = (() => {
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-profil-submit">
${s ? 'Speichern' : 'Profil erstellen'}
${s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`}
</button>
`;
UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer });
@ -416,7 +423,7 @@ window.Page_sitting = (() => {
};
if (s) data.aktiv = form.querySelector('[name="aktiv"]')?.checked ? 1 : 0;
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '…'; }
try {
if (s) {
await API.sitting.updateMe(data);
@ -429,7 +436,7 @@ window.Page_sitting = (() => {
} catch (err) {
UI.toast(err.message, 'error');
} finally {
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = s ? 'Speichern' : 'Profil erstellen'; }
if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`; }
}
});
}