UX: Würfe-Seite — Card-Design, Stats-Header, Countdown, bessere Aktionen (SW by-v893)

This commit is contained in:
rene 2026-05-13 16:59:03 +02:00
parent 99842909e4
commit 5a639d47a9
5 changed files with 121 additions and 68 deletions

View file

@ -404,7 +404,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
APP_VER = "892" # muss mit APP_VER in app.js übereinstimmen
APP_VER = "893" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():

View file

@ -583,10 +583,10 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=892"></script>
<script src="/js/ui.js?v=892"></script>
<script src="/js/app.js?v=892"></script>
<script src="/js/worlds.js?v=892"></script>
<script src="/js/api.js?v=893"></script>
<script src="/js/ui.js?v=893"></script>
<script src="/js/app.js?v=893"></script>
<script src="/js/worlds.js?v=893"></script>
<!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '892'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '893'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -100,6 +100,7 @@ window.Page_litters = (() => {
${UI.icon('plus')} Neuer Wurf
</button>
</div>
<div id="litters-stats" style="display:none;gap:var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap"></div>
<div id="litters-list">
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt</p>
</div>
@ -132,6 +133,33 @@ window.Page_litters = (() => {
// ----------------------------------------------------------
// Würfe-Liste rendern
// ----------------------------------------------------------
function _renderStats() {
const bar = document.getElementById('litters-stats');
if (!bar || !_litters.length) return;
const total = _litters.length;
const aktiv = _litters.filter(l => l.status === 'verfuegbar' || l.status === 'geboren').length;
const geplant = _litters.filter(l => l.status === 'geplant').length;
const welpen = _litters.reduce((s, l) => s + (l.welpen_gesamt || 0), 0);
const verfuegb = _litters.reduce((s, l) => s + (l.welpen_verfuegbar || 0), 0);
const statItems = [
{ icon: 'list-bullets', label: 'Würfe gesamt', val: total },
{ icon: 'baby', label: 'Aktiv', val: aktiv, color: 'var(--c-success)' },
{ icon: 'calendar-dots',label: 'Geplant', val: geplant },
{ icon: 'dog', label: 'Welpen', val: welpen },
{ icon: 'tag', label: 'Verfügbar', val: verfuegb, color: verfuegb > 0 ? 'var(--c-primary)' : undefined },
];
bar.style.display = 'flex';
bar.innerHTML = statItems.map(s => `
<div style="background:var(--c-bg-secondary);border:1px solid var(--c-border);border-radius:var(--radius-md);
padding:var(--space-3) var(--space-4);display:flex;align-items:center;gap:var(--space-2);flex:1;min-width:100px">
<span style="color:${s.color || 'var(--c-text-muted)'};opacity:.8">${UI.icon(s.icon)}</span>
<div>
<div style="font-size:var(--text-lg);font-weight:700;color:${s.color || 'var(--c-text);line-height:1'}">${s.val}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">${s.label}</div>
</div>
</div>`).join('');
}
function _renderList() {
const el = document.getElementById('litters-list');
if (!el) return;
@ -149,6 +177,7 @@ window.Page_litters = (() => {
return;
}
_renderStats();
el.innerHTML = _litters.map(l => _litterCardHTML(l)).join('');
// Events
@ -218,74 +247,96 @@ window.Page_litters = (() => {
if (_openId) _togglePuppies(_openId, true);
}
function _daysUntil(dateStr) {
if (!dateStr) return null;
const diff = Math.ceil((new Date(dateStr) - new Date()) / 86400000);
return diff;
}
function _litterCardHTML(l) {
const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
const datumLabel = l.geburt_datum
? `Geburt: ${_fmtDate(l.geburt_datum)}`
: l.erwartetes_datum
? `Erwartet: ${_fmtDate(l.erwartetes_datum)}`
: '—';
const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => _esc(n)).join(' × ') || '—';
const elternLabel = [l.vater_name, l.mutter_name]
.filter(Boolean)
.map(n => _esc(n))
.join(' × ') || '—';
// Datum + Countdown
let datumChip = '';
const refDate = l.geburt_datum || l.erwartetes_datum;
if (refDate) {
const days = _daysUntil(refDate);
const label = l.geburt_datum ? `Geburt ${_fmtDate(l.geburt_datum)}` : `Erwartet ${_fmtDate(l.erwartetes_datum)}`;
let countdownHtml = '';
if (days !== null && !l.geburt_datum) {
const c = days < 0 ? `<span style="color:var(--c-danger)">überfällig</span>`
: days === 0 ? `<span style="color:var(--c-success)">heute!</span>`
: days <= 7 ? `<span style="color:var(--c-warning,#f59e0b)">${days}d</span>`
: `<span style="color:var(--c-text-muted)">${days}d</span>`;
countdownHtml = ` · ${c}`;
}
datumChip = `<span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('calendar-dots')} ${label}${countdownHtml}</span>`;
}
const sichtbarLabel = l.sichtbar
? `<span style="color:var(--c-success);font-size:var(--text-xs)">${UI.icon('eye')} Öffentlich</span>`
: `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.icon('eye-slash')} Nicht öffentlich</span>`;
const sichtbarChip = l.sichtbar
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-success)">${UI.icon('eye')} Öffentlich</span>`
: `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-muted)">${UI.icon('eye-slash')} Nicht öffentlich</span>`;
const welpenChip = `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar</span>`;
const preisChip = l.preis_spanne
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}</span>`
: '';
return `
<div class="litters-card" id="litter-card-${l.id}">
<div class="litters-card-header">
<div style="flex:1;min-width:0">
<div class="litters-card-title">
${elternLabel}
${_statusBadge(l.status)}
<div class="litters-card" id="litter-card-${l.id}"
style="background:var(--c-bg-secondary);border:1px solid var(--c-border);border-radius:var(--radius-lg);
margin-bottom:var(--space-3);overflow:hidden">
<!-- Card-Header -->
<div style="padding:var(--space-4) var(--space-4) var(--space-3);border-bottom:1px solid var(--c-border)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap">
<div style="min-width:0">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-2)">
<span style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${elternLabel}</span>
${_statusBadge(l.status)}
${sichtbarChip}
</div>
<div style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap">
${datumChip}
${welpenChip}
${preisChip}
</div>
</div>
<div class="litters-card-meta">
${UI.icon('calendar-dots')} ${_esc(datumLabel)} &nbsp;·&nbsp;
${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar
&nbsp;·&nbsp; ${sichtbarLabel}
<div style="display:flex;align-items:center;gap:var(--space-1);flex-shrink:0;flex-wrap:wrap">
<button class="btn btn-ghost btn-sm litters-card-toggle" data-id="${l.id}" title="Welpen">
${UI.icon('caret-down')} Welpen
</button>
<button class="btn btn-ghost btn-sm litters-waitlist-btn" data-id="${l.id}" title="Warteliste">
${UI.icon('list-bullets')} Warteliste
</button>
<button class="btn btn-ghost btn-sm litters-photos-btn" data-id="${l.id}" title="Wurf-Fotos">
${UI.icon('images')}
</button>
<button class="btn btn-ghost btn-sm litters-parent-photos-btn" data-id="${l.id}" title="Elterntier-Fotos">
${UI.icon('users')}
</button>
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? `
<button class="btn btn-ghost btn-sm litters-ki-announce-btn" data-id="${l.id}" title="KI: Wurfankündigung">
${UI.icon('sparkle')}
</button>` : ''}
<button class="btn btn-ghost btn-sm litters-edit-btn" data-id="${l.id}" title="Bearbeiten">
${UI.icon('pencil-simple')}
</button>
<button class="btn btn-ghost btn-sm litters-delete-btn" data-id="${l.id}" title="Löschen"
style="color:var(--c-danger)">
${UI.icon('trash')}
</button>
</div>
${l.preis_spanne ? `<div class="litters-card-meta">${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}</div>` : ''}
</div>
<div class="litters-card-actions">
<button class="btn btn-ghost btn-sm litters-card-toggle" data-id="${l.id}"
title="Welpen anzeigen">
${UI.icon('caret-down')} Welpen
</button>
<button class="btn btn-ghost btn-sm litters-waitlist-btn" data-id="${l.id}"
title="Warteliste">
${UI.icon('list-bullets')} Warteliste
</button>
<button class="btn btn-ghost btn-sm litters-photos-btn" data-id="${l.id}"
title="Wurf-Fotos verwalten">
${UI.icon('images')} Fotos
</button>
<button class="btn btn-ghost btn-sm litters-parent-photos-btn" data-id="${l.id}"
title="Elterntier-Fotos verwalten">
${UI.icon('users')} Eltern
</button>
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? `
<button class="btn btn-ghost btn-sm litters-ki-announce-btn" data-id="${l.id}"
title="KI: Wurfankündigung schreiben">
${UI.icon('sparkle')} Ankündigung
</button>` : ''}
<button class="btn btn-ghost btn-sm litters-edit-btn" data-id="${l.id}"
title="Bearbeiten">
${UI.icon('pencil-simple')}
</button>
<button class="btn btn-ghost btn-sm litters-delete-btn" data-id="${l.id}"
title="Löschen" style="color:var(--c-danger)">
${UI.icon('trash')}
</button>
</div>
${l.beschreibung ? `<p style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(l.beschreibung)}</p>` : ''}
</div>
${l.beschreibung ? `<div class="litters-card-desc">${_esc(l.beschreibung)}</div>` : ''}
<div class="litters-puppies-wrap" id="puppies-wrap-${l.id}" style="display:none">
<div class="litters-puppies-inner" id="puppies-inner-${l.id}">
<!-- Welpen-Bereich -->
<div id="puppies-wrap-${l.id}" style="display:none;padding:var(--space-3) var(--space-4)">
<div id="puppies-inner-${l.id}">
<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</p>
</div>
<button class="btn btn-secondary btn-sm litters-add-puppy-btn" data-id="${l.id}"
@ -293,8 +344,10 @@ window.Page_litters = (() => {
${UI.icon('plus')} Welpen hinzufügen
</button>
</div>
<div class="litters-waitlist-wrap" id="waitlist-wrap-${l.id}" style="display:none">
<div class="litters-waitlist-inner" id="waitlist-inner-${l.id}">
<!-- Wartelisten-Bereich -->
<div id="waitlist-wrap-${l.id}" style="display:none;padding:var(--space-3) var(--space-4)">
<div id="waitlist-inner-${l.id}">
<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</p>
</div>
<button class="btn btn-secondary btn-sm litters-add-waitlist-btn" data-id="${l.id}"

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v892';
const CACHE_VERSION = 'by-v893';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache