Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
|
|
@ -463,6 +463,8 @@ window.Page_welcome = (() => {
|
|||
`).join('')}
|
||||
</div>
|
||||
|
||||
${dog?.id ? `<div id="wc-streak-widget"></div>` : ''}
|
||||
|
||||
<h2 class="wc-section-title" style="margin-top:var(--space-6)">Mehr entdecken</h2>
|
||||
<div class="wc-grid">
|
||||
${FEATURES.filter(f => !FEATURE_CARD_PAGES.has(f.page)).map(f => `
|
||||
|
|
@ -497,9 +499,85 @@ window.Page_welcome = (() => {
|
|||
_updateChipsFromDash(dash);
|
||||
_tryRouteChip(dash); // nach Chips-Update: ggf. Gassirunden-Vorschlag einfügen
|
||||
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
||||
|
||||
// Streak-Widget asynchron laden
|
||||
_loadStreakWidget(dog.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STREAK-WIDGET
|
||||
// ----------------------------------------------------------
|
||||
async function _loadStreakWidget(dogId) {
|
||||
const slot = _container.querySelector('#wc-streak-widget');
|
||||
if (!slot) return;
|
||||
|
||||
let streak;
|
||||
try {
|
||||
streak = await API.get(`/streak/${dogId}`);
|
||||
} catch { return; }
|
||||
|
||||
if (!streak || (streak.current_streak === 0 && streak.longest_streak === 0)) return;
|
||||
|
||||
slot.innerHTML = _streakWidgetHTML(streak);
|
||||
|
||||
slot.querySelector('#wc-streak-leaderboard-btn')?.addEventListener('click', async () => {
|
||||
const modalEl = UI.modal.open({
|
||||
title: '🔥 Trainings-Bestenliste',
|
||||
body: '<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-4)">Wird geladen…</p>',
|
||||
});
|
||||
let board;
|
||||
try { board = await API.get('/streak/leaderboard'); } catch { board = []; }
|
||||
const bodyEl = modalEl?.querySelector('.modal-body');
|
||||
if (bodyEl) bodyEl.innerHTML = _leaderboardHTML(board);
|
||||
});
|
||||
}
|
||||
|
||||
function _streakWidgetHTML(s) {
|
||||
const cur = s.current_streak || 0;
|
||||
const best = s.longest_streak || 0;
|
||||
return `
|
||||
<div class="wc-streak-card">
|
||||
<div class="wc-streak-flame-wrap">
|
||||
<span class="wc-streak-flame">🔥</span>
|
||||
<span class="wc-streak-number">${cur}</span>
|
||||
</div>
|
||||
<div class="wc-streak-info">
|
||||
<div class="wc-streak-label">Tage in Folge trainiert</div>
|
||||
<div class="wc-streak-best">Rekord: ${best} ${best === 1 ? 'Tag' : 'Tage'}</div>
|
||||
</div>
|
||||
<button class="wc-streak-lb-btn" id="wc-streak-leaderboard-btn" title="Bestenliste">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trophy"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _leaderboardHTML(rows) {
|
||||
if (!rows || !rows.length) {
|
||||
return '<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-4)">Noch keine Einträge.</p>';
|
||||
}
|
||||
const medals = ['🥇', '🥈', '🥉'];
|
||||
return `
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${rows.map((r, i) => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border-subtle)">
|
||||
<span style="font-size:1.4rem;width:28px;text-align:center;flex-shrink:0">${medals[i] || (i + 1) + '.'}</span>
|
||||
${r.foto_url
|
||||
? `<img src="${UI.escape(r.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0" alt="">`
|
||||
: `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-primary-subtle);flex-shrink:0"></div>`}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${UI.escape(r.dog_name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(r.rasse || '')}${r.user_name ? ' · ' + UI.escape(r.user_name) : ''}</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
|
||||
<span style="font-size:1.1rem">🔥</span>
|
||||
<span style="font-weight:var(--weight-bold);font-size:var(--text-base);color:var(--c-primary)">${r.current_streak}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _updateHeroFromDash(dash, dog) {
|
||||
const heroBox = _container.querySelector('#wc-hero-box');
|
||||
if (!heroBox) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue