UX: Würfe-Seite — Card-Design, Stats-Header, Countdown, bessere Aktionen (SW by-v893)
This commit is contained in:
parent
99842909e4
commit
5a639d47a9
5 changed files with 121 additions and 68 deletions
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)} ·
|
||||
${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar
|
||||
· ${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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue