Feature: Filme-Suche, HdM ins Forum + Gewinner-Badge im Profil, SW by-v594

- Filme-Seite: Suchfeld (filtert live nach Titel, Rasse, Genre, Beschreibung)
- Filme-Seite: Tab "Hund des Monats" entfernt
- Forum: kompakte HdM-Kachel über der Suche (Sieger + Stimmen), Klick öffnet Abstimmungs-Modal
- Hundeprofil: goldene Badges für jeden gewonnenen Monat (🏆 Mai 2026 …)
- DB: Tabelle hund_des_monats_wins (dauerhaft, dog_id + monat + stimmen)
- Scheduler: Job am 1. des Monats 00:05 — schreibt Vormonats-Sieger, Push an Besitzer
- Dogs-API: liefert hdm_wins[] pro Hund mit
This commit is contained in:
rene 2026-05-02 08:12:29 +02:00
parent d00284184b
commit ea2a83b29e
9 changed files with 348 additions and 7 deletions

View file

@ -66,6 +66,7 @@ window.Page_forum = (() => {
_container = container;
_appState = appState;
_render();
_loadHdmCard();
_loadThreads(true);
}
@ -104,9 +105,11 @@ window.Page_forum = (() => {
data-section="map">${UI.icon('users')} Mitgliederkarte</button>
</div>
<!-- Rechte Spalte: Suche + Threads -->
<!-- Rechte Spalte: HdM-Kachel + Suche + Threads -->
<div class="forum-main-col">
<div id="forum-hdm-card"></div>
<div class="forum-search-wrap">
<input type="search" class="forum-search" id="forum-search"
placeholder="Forum durchsuchen…" autocomplete="off">
@ -175,6 +178,139 @@ window.Page_forum = (() => {
document.getElementById('forum-rules-btn').addEventListener('click', _showRules);
}
// ----------------------------------------------------------
// Hund des Monats — Kachel + Modal
// ----------------------------------------------------------
async function _loadHdmCard() {
const card = document.getElementById('forum-hdm-card');
if (!card) return;
try {
const data = await API.get('/movies/hund-des-monats');
const [year, month] = data.monat.split('-');
const monthName = new Intl.DateTimeFormat('de-DE', { month: 'long' })
.format(new Date(+year, +month - 1, 1));
const top = data.top?.[0];
const winnerLine = top
? `🥇 ${_esc(top.name)}${top.rasse ? ` · ${_esc(top.rasse)}` : ''}`
: 'Noch keine Stimmen';
const metaLine = top
? `${top.stimmen} Stimme${top.stimmen !== 1 ? 'n' : ''}`
: 'Sei der Erste!';
card.innerHTML = `
<div class="forum-hdm-tile" id="forum-hdm-tile">
<div class="forum-hdm-tile-trophy">🏆</div>
<div class="forum-hdm-tile-body">
<div class="forum-hdm-tile-title">Hund des Monats · ${_esc(monthName)}</div>
<div class="forum-hdm-tile-winner">${winnerLine}</div>
<div class="forum-hdm-tile-meta">${metaLine}</div>
</div>
<div class="forum-hdm-tile-cta">${UI.icon('arrow-right')}</div>
</div>`;
document.getElementById('forum-hdm-tile')?.addEventListener('click', () => _openHdmModal(data));
} catch {
// Kachel bleibt leer bei Fehler
}
}
async function _openHdmModal(data) {
// Immer frische Daten laden
try { data = await API.get('/movies/hund-des-monats'); } catch { /* nutze gecachte */ }
const [year, month] = data.monat.split('-');
const monthName = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' })
.format(new Date(+year, +month - 1, 1));
let voteSection = '';
if (_appState.user && _appState.dogs?.length > 0) {
const cards = _appState.dogs.map(dog => {
const isVoted = data.user_vote === dog.id;
const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-vote-av-img">`
: `<span class="hdm-vote-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`;
return `
<div class="hdm-vote-card${isVoted ? ' hdm-vote-card--voted' : ''}" data-dog-id="${dog.id}">
<div class="hdm-vote-av">${av}</div>
<div class="hdm-vote-name">${_esc(dog.name)}</div>
${dog.rasse ? `<div class="hdm-vote-rasse">${_esc(dog.rasse)}</div>` : ''}
<button class="btn${isVoted ? ' btn-primary' : ' btn-secondary'} hdm-vote-btn" data-dog-id="${dog.id}">
${isVoted ? `${UI.icon('check-circle')} Gewählt` : 'Abstimmen'}
</button>
</div>`;
}).join('');
voteSection = `
<div class="hdm-section">
<h3 class="hdm-section-title">Für welchen deiner Hunde möchtest du abstimmen?</h3>
<div class="hdm-vote-grid" id="hdm-vote-grid">${cards}</div>
</div>`;
} else if (!_appState.user) {
voteSection = `
<div class="hdm-section">
<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">
<a href="#" id="hdm-login-link" style="color:var(--c-primary);font-weight:var(--weight-semibold)">Anmelden</a>
um für deinen Hund abzustimmen.
</p>
</div>`;
}
const topList = data.top?.length
? data.top.slice(0, 5).map((dog, i) => {
const medal = ['🥇','🥈','🥉','4⃣','5⃣'][i];
const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-top-av-img">`
: `<span class="hdm-top-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`;
const vorname = dog.besitzer_name ? _esc(dog.besitzer_name.split(' ')[0]) : '';
return `
<div class="hdm-top-entry">
<span class="hdm-top-medal">${medal}</span>
<div class="hdm-top-av">${av}</div>
<div class="hdm-top-info">
<div class="hdm-top-name">${_esc(dog.name)}</div>
${dog.rasse ? `<div class="hdm-top-rasse">${_esc(dog.rasse)}</div>` : ''}
${vorname ? `<div class="hdm-top-besitzer">von ${vorname}</div>` : ''}
</div>
<div class="hdm-top-stimmen">${dog.stimmen} ${UI.icon('star')}</div>
</div>`;
}).join('')
: `<p style="color:var(--c-text-secondary);padding:var(--space-4)">Noch keine Stimmen diesen Monat. Sei der Erste!</p>`;
const body = `
<div class="hdm-header">
<div class="hdm-trophy">🏆</div>
<h2 class="hdm-title">Hund des Monats</h2>
<div class="hdm-monat">${_esc(monthName)}</div>
</div>
${voteSection}
<div class="hdm-section">
<h3 class="hdm-section-title">Top 5 diesen Monat</h3>
<div id="hdm-top-list">${topList}</div>
</div>`;
UI.modal.open({ title: '🏆 Hund des Monats', body,
footer: `<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Schließen</button>` });
document.getElementById('hdm-login-link')?.addEventListener('click', e => {
e.preventDefault(); UI.modal.close(); App.navigate('settings');
});
document.querySelectorAll('.hdm-vote-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const dogId = parseInt(btn.dataset.dogId);
await UI.asyncButton(btn, async () => {
try {
await API.post('/movies/hund-des-monats/vote', { dog_id: dogId });
UI.toast.success('Stimme abgegeben!');
UI.modal.close();
_loadHdmCard();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Abstimmen.');
}
});
});
});
}
// ----------------------------------------------------------
// Threads laden
// ----------------------------------------------------------