From 031c6028ac0040912a563ac2cbd4ea765b2bbf3c Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 2 May 2026 08:44:59 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20HdM=20Community-Vote=20=E2=80=94=20a?= =?UTF-8?q?lle=20=C3=B6ffentlichen=20Hunde=20w=C3=A4hlbar,=20eigene=20ausg?= =?UTF-8?q?enommen,=20SW=20by-v597?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/movies.py | 32 ++++++- backend/static/css/components.css | 8 ++ backend/static/js/app.js | 2 +- backend/static/js/pages/forum.js | 134 +++++++++++++++++++----------- backend/static/sw.js | 2 +- 5 files changed, 127 insertions(+), 51 deletions(-) diff --git a/backend/routes/movies.py b/backend/routes/movies.py index e4f306e..da6c682 100644 --- a/backend/routes/movies.py +++ b/backend/routes/movies.py @@ -386,6 +386,34 @@ async def get_hund_des_monats(user=Depends(get_current_user_optional)): return {"monat": monat, "top": [dict(r) for r in rows], "user_vote": user_vote} +@router.get("/hund-des-monats/kandidaten") +async def get_hdm_kandidaten(user=Depends(get_current_user)): + """Alle öffentlichen Hunde anderer User, mit aktuellem Stimmenstand.""" + monat = datetime.now().strftime("%Y-%m") + with db() as conn: + rows = conn.execute(""" + SELECT d.id, d.name, d.rasse, d.foto_url, + u.name AS besitzer_name, + COALESCE(v.stimmen, 0) AS stimmen + FROM dogs d + JOIN users u ON u.id = d.user_id + LEFT JOIN ( + SELECT dog_id, COUNT(*) AS stimmen + FROM hund_des_monats_votes + WHERE monat = ? + GROUP BY dog_id + ) v ON v.dog_id = d.id + WHERE d.is_public = 1 + AND d.user_id != ? + ORDER BY + CASE WHEN d.foto_url IS NOT NULL THEN 0 ELSE 1 END, + stimmen DESC, + d.name ASC + LIMIT 60 + """, (monat, user["id"])).fetchall() + return [dict(r) for r in rows] + + @router.post("/hund-des-monats/vote") async def vote_hund_des_monats(data: HundDesMonatsVoteRequest, user=Depends(get_current_user)): monat = datetime.now().strftime("%Y-%m") @@ -393,7 +421,9 @@ async def vote_hund_des_monats(data: HundDesMonatsVoteRequest, user=Depends(get_ dog = conn.execute("SELECT id, user_id, is_public FROM dogs WHERE id=?", (data.dog_id,)).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") - if dog["user_id"] != user["id"] and not dog["is_public"]: + if dog["user_id"] == user["id"]: + raise HTTPException(403, "Du kannst nicht für deinen eigenen Hund abstimmen.") + if not dog["is_public"]: raise HTTPException(403, "Dieser Hund ist nicht öffentlich.") conn.execute(""" INSERT INTO hund_des_monats_votes (user_id, dog_id, monat) diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 5264274..3582760 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -5241,11 +5241,19 @@ html.modal-open { margin-bottom: var(--space-3); } +/* Kandidaten-Suche */ +.hdm-kandidaten-search { + margin-bottom: var(--space-3); +} + /* Vote-Grid */ .hdm-vote-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); + max-height: 340px; + overflow-y: auto; + padding-right: var(--space-1); } .hdm-vote-card { diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 18e94c3..55a80eb 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '596'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '597'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index 1b5abd7..8c28a1b 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -232,45 +232,12 @@ window.Page_forum = (() => { } async function _openHdmModal(data) { - // Immer frische Daten laden - try { data = await API.get('/movies/hund-des-monats'); } catch { /* nutze gecachte */ } + try { data = await API.get('/movies/hund-des-monats'); } catch { /* gecachte Daten */ } 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 - ? `${_esc(dog.name)}` - : `${_esc(dog.name.charAt(0).toUpperCase())}`; - return ` -
-
${av}
-
${_esc(dog.name)}
- ${dog.rasse ? `
${_esc(dog.rasse)}
` : ''} - -
`; - }).join(''); - voteSection = ` -
-

Für welchen deiner Hunde möchtest du abstimmen?

-
${cards}
-
`; - } else if (!_appState.user) { - voteSection = ` -
-

- Anmelden - um für deinen Hund abzustimmen. -

-
`; - } - const topList = data.top?.length ? data.top.slice(0, 5).map((dog, i) => { const medal = ['🥇','🥈','🥉','4️⃣','5️⃣'][i]; @@ -290,7 +257,26 @@ window.Page_forum = (() => {
${dog.stimmen} ${UI.icon('star')}
`; }).join('') - : `

Noch keine Stimmen diesen Monat. Sei der Erste!

`; + : `

Noch keine Stimmen. Sei der Erste!

`; + + const voteHint = !_appState.user + ? `
+

+ Anmelden + um abstimmen zu können. +

+
` + : `
+

Für welchen Hund möchtest du abstimmen?

+ +
+ ${UI.skeleton(3)} +
+
`; const body = `
@@ -298,7 +284,7 @@ window.Page_forum = (() => {

Hund des Monats

${_esc(monthName)}
- ${voteSection} + ${voteHint}

Top 5 diesen Monat

${topList}
@@ -311,20 +297,72 @@ window.Page_forum = (() => { 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.'); - } + if (!_appState.user) return; + + // Kandidaten laden und rendern + let _kandidaten = []; + const _renderKandidaten = (list) => { + const grid = document.getElementById('hdm-kandidaten-grid'); + if (!grid) return; + if (!list.length) { + grid.innerHTML = `

Keine Hunde gefunden.

`; + return; + } + grid.innerHTML = list.map(dog => { + const isVoted = data.user_vote === dog.id; + const av = dog.foto_url + ? `${_esc(dog.name)}` + : `${_esc(dog.name.charAt(0).toUpperCase())}`; + const vorname = dog.besitzer_name ? _esc(dog.besitzer_name.split(' ')[0]) : ''; + return ` +
+
${av}
+
${_esc(dog.name)}
+ ${dog.rasse ? `
${_esc(dog.rasse)}
` : ''} + ${vorname ? `
von ${vorname}
` : ''} + ${dog.stimmen > 0 ? `
${dog.stimmen} ${UI.icon('star')}
` : ''} + +
`; + }).join(''); + + grid.querySelectorAll('.hdm-vote-btn:not([disabled])').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 }); + data.user_vote = dogId; + UI.toast.success('Stimme abgegeben!'); + UI.modal.close(); + _loadHdmCard(); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Abstimmen.'); + } + }); }); }); + }; + + try { + _kandidaten = await API.get('/movies/hund-des-monats/kandidaten'); + } catch { + document.getElementById('hdm-kandidaten-grid').innerHTML = + `

Kandidaten konnten nicht geladen werden.

`; + return; + } + _renderKandidaten(_kandidaten); + + document.getElementById('hdm-search')?.addEventListener('input', e => { + const q = e.target.value.trim().toLowerCase(); + _renderKandidaten(q + ? _kandidaten.filter(d => + (d.name || '').toLowerCase().includes(q) || + (d.rasse || '').toLowerCase().includes(q)) + : _kandidaten + ); }); } diff --git a/backend/static/sw.js b/backend/static/sw.js index 1a56aac..bc46029 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v596'; +const CACHE_VERSION = 'by-v597'; 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