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:
parent
d00284184b
commit
ea2a83b29e
9 changed files with 348 additions and 7 deletions
|
|
@ -1644,3 +1644,16 @@ def _migrate(conn_factory):
|
||||||
conn.execute("UPDATE training_exercises SET js_exercise_id=? WHERE id=?", (js_id, row['id']))
|
conn.execute("UPDATE training_exercises SET js_exercise_id=? WHERE id=?", (js_id, row['id']))
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_te_js_id ON training_exercises(js_exercise_id)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_te_js_id ON training_exercises(js_exercise_id)")
|
||||||
logger.info("Migration: training_exercises.js_exercise_id hinzugefügt, 'Fuß' bereinigt.")
|
logger.info("Migration: training_exercises.js_exercise_id hinzugefügt, 'Fuß' bereinigt.")
|
||||||
|
|
||||||
|
# Hund des Monats — dauerhafte Gewinner-Tabelle
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS hund_des_monats_wins (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
|
||||||
|
monat TEXT NOT NULL,
|
||||||
|
stimmen INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(dog_id, monat)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_hdm_wins_dog ON hund_des_monats_wins(dog_id);
|
||||||
|
""")
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,21 @@ async def list_dogs(user=Depends(get_current_user)):
|
||||||
d = dict(r)
|
d = dict(r)
|
||||||
d["is_guest"] = True
|
d["is_guest"] = True
|
||||||
result.append(d)
|
result.append(d)
|
||||||
|
|
||||||
|
# HdM-Siege pro Hund anhängen
|
||||||
|
if result:
|
||||||
|
dog_ids = [d["id"] for d in result]
|
||||||
|
with db() as conn:
|
||||||
|
wins_rows = conn.execute(
|
||||||
|
f"SELECT dog_id, monat FROM hund_des_monats_wins WHERE dog_id IN ({','.join('?'*len(dog_ids))}) ORDER BY monat DESC",
|
||||||
|
dog_ids,
|
||||||
|
).fetchall()
|
||||||
|
wins_map: dict[int, list[str]] = {}
|
||||||
|
for w in wins_rows:
|
||||||
|
wins_map.setdefault(w["dog_id"], []).append(w["monat"])
|
||||||
|
for d in result:
|
||||||
|
d["hdm_wins"] = wins_map.get(d["id"], [])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,14 @@ def start():
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
misfire_grace_time=3600,
|
misfire_grace_time=3600,
|
||||||
)
|
)
|
||||||
|
# 1. des Monats 00:05 — Hund des Monats Sieger festlegen
|
||||||
|
_scheduler.add_job(
|
||||||
|
_job_hdm_winner,
|
||||||
|
CronTrigger(day=1, hour=0, minute=5),
|
||||||
|
id="hdm_winner",
|
||||||
|
replace_existing=True,
|
||||||
|
misfire_grace_time=3600,
|
||||||
|
)
|
||||||
_scheduler.start()
|
_scheduler.start()
|
||||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).")
|
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).")
|
||||||
|
|
||||||
|
|
@ -1110,3 +1118,57 @@ def _compute_milestone(today: date, bday: date, dog_name: str):
|
||||||
return titel, text
|
return titel, text
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# JOB: Hund des Monats — Sieger des Vormonats festlegen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def _job_hdm_winner():
|
||||||
|
"""Läuft am 1. des Monats 00:05 und schreibt den Sieger des Vormonats."""
|
||||||
|
today = datetime.now(tz=_TZ)
|
||||||
|
# Vormonat berechnen
|
||||||
|
first_this = today.replace(day=1)
|
||||||
|
last_month = (first_this - timedelta(days=1)).replace(day=1)
|
||||||
|
monat = last_month.strftime("%Y-%m")
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
# Schon eingetragen?
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM hund_des_monats_wins WHERE monat=?", (monat,)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
logger.info(f"HdM-Winner {monat}: bereits eingetragen, übersprungen.")
|
||||||
|
_log_job("hdm_winner", "ok", f"bereits vorhanden für {monat}")
|
||||||
|
return
|
||||||
|
|
||||||
|
winner = conn.execute("""
|
||||||
|
SELECT v.dog_id, d.name, d.user_id, COUNT(v.id) AS stimmen
|
||||||
|
FROM hund_des_monats_votes v
|
||||||
|
JOIN dogs d ON d.id = v.dog_id
|
||||||
|
WHERE v.monat = ?
|
||||||
|
GROUP BY v.dog_id
|
||||||
|
ORDER BY stimmen DESC
|
||||||
|
LIMIT 1
|
||||||
|
""", (monat,)).fetchone()
|
||||||
|
|
||||||
|
if not winner:
|
||||||
|
logger.info(f"HdM-Winner {monat}: keine Stimmen, kein Sieger.")
|
||||||
|
_log_job("hdm_winner", "ok", f"keine Stimmen für {monat}")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO hund_des_monats_wins (dog_id, monat, stimmen) VALUES (?, ?, ?)",
|
||||||
|
(winner["dog_id"], monat, winner["stimmen"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
month_label = last_month.strftime("%B %Y")
|
||||||
|
send_push_to_user(winner["user_id"], {
|
||||||
|
"type": "hdm_winner",
|
||||||
|
"title": f"🏆 {winner['name']} ist Hund des Monats!",
|
||||||
|
"body": f"{winner['name']} hat den {month_label} gewonnen — herzlichen Glückwunsch!",
|
||||||
|
"data": {"page": "forum"},
|
||||||
|
"tag": f"hdm-{monat}",
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"HdM-Winner {monat}: Hund {winner['dog_id']} ('{winner['name']}', {winner['stimmen']} Stimmen) eingetragen.")
|
||||||
|
_log_job("hdm_winner", "ok", f"{monat}: {winner['name']} ({winner['stimmen']} Stimmen)")
|
||||||
|
|
|
||||||
|
|
@ -4200,6 +4200,59 @@ html.modal-open {
|
||||||
.forum-category-badge--tauschboerse { background: #fce7f3; color: #9d174d; }
|
.forum-category-badge--tauschboerse { background: #fce7f3; color: #9d174d; }
|
||||||
|
|
||||||
/* Search */
|
/* Search */
|
||||||
|
/* Hund des Monats — kompakte Forum-Kachel */
|
||||||
|
.forum-hdm-tile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
background: linear-gradient(135deg, var(--c-surface-2) 0%, var(--c-bg) 100%);
|
||||||
|
border: 1.5px solid var(--c-border-light);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
min-width: 0;
|
||||||
|
transition: border-color .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.forum-hdm-tile:hover { border-color: var(--c-primary); box-shadow: var(--shadow-sm); }
|
||||||
|
.forum-hdm-tile-trophy { font-size: 1.5rem; flex-shrink: 0; }
|
||||||
|
.forum-hdm-tile-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.forum-hdm-tile-title {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.forum-hdm-tile-winner {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--c-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.forum-hdm-tile-meta {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.forum-hdm-tile-cta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--c-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.forum-search-wrap {
|
.forum-search-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
@ -4918,6 +4971,25 @@ html.modal-open {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter-Row */
|
/* Filter-Row */
|
||||||
|
.movies-search-row {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-3) 0 var(--space-1);
|
||||||
|
}
|
||||||
|
.movies-search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-3);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.movies-search-input {
|
||||||
|
padding-left: calc(var(--space-3) + 16px + var(--space-2)) !important;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.movies-filter-row {
|
.movies-filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
|
@ -5972,6 +6044,21 @@ html.modal-open {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hund des Monats — Profil-Badge */
|
||||||
|
.dp-hdm-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
|
color: #78350f;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
letter-spacing: .02em;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Foto-Editor Modal --- */
|
/* --- Foto-Editor Modal --- */
|
||||||
.photo-editor { display: flex; flex-direction: column; gap: var(--space-3); align-items: center; }
|
.photo-editor { display: flex; flex-direction: column; gap: var(--space-3); align-items: center; }
|
||||||
.photo-editor-preview {
|
.photo-editor-preview {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '591'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '594'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,19 @@ window.Page_dog_profile = (() => {
|
||||||
<h2 style="font-size:var(--text-2xl);font-weight:700;
|
<h2 style="font-size:var(--text-2xl);font-weight:700;
|
||||||
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
|
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
|
||||||
${dog.rasse
|
${dog.rasse
|
||||||
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-5)">${_esc(dog.rasse)}</p>`
|
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-2)">${_esc(dog.rasse)}</p>`
|
||||||
: `<p style="margin:0 0 var(--space-5)"></p>`}
|
: `<p style="margin:0 0 var(--space-2)"></p>`}
|
||||||
|
|
||||||
|
${(dog.hdm_wins?.length) ? `
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);justify-content:center;margin-bottom:var(--space-5)">
|
||||||
|
${dog.hdm_wins.map(m => {
|
||||||
|
const [y, mo] = m.split('-');
|
||||||
|
const label = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' })
|
||||||
|
.format(new Date(+y, +mo - 1, 1));
|
||||||
|
return `<span class="dp-hdm-badge" title="Hund des Monats ${label}">🏆 ${label}</span>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : `<div style="margin-bottom:var(--space-5)"></div>`}
|
||||||
|
|
||||||
<!-- Info-Grid -->
|
<!-- Info-Grid -->
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ window.Page_forum = (() => {
|
||||||
_container = container;
|
_container = container;
|
||||||
_appState = appState;
|
_appState = appState;
|
||||||
_render();
|
_render();
|
||||||
|
_loadHdmCard();
|
||||||
_loadThreads(true);
|
_loadThreads(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,9 +105,11 @@ window.Page_forum = (() => {
|
||||||
data-section="map">${UI.icon('users')} Mitgliederkarte</button>
|
data-section="map">${UI.icon('users')} Mitgliederkarte</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rechte Spalte: Suche + Threads -->
|
<!-- Rechte Spalte: HdM-Kachel + Suche + Threads -->
|
||||||
<div class="forum-main-col">
|
<div class="forum-main-col">
|
||||||
|
|
||||||
|
<div id="forum-hdm-card"></div>
|
||||||
|
|
||||||
<div class="forum-search-wrap">
|
<div class="forum-search-wrap">
|
||||||
<input type="search" class="forum-search" id="forum-search"
|
<input type="search" class="forum-search" id="forum-search"
|
||||||
placeholder="Forum durchsuchen…" autocomplete="off">
|
placeholder="Forum durchsuchen…" autocomplete="off">
|
||||||
|
|
@ -175,6 +178,139 @@ window.Page_forum = (() => {
|
||||||
document.getElementById('forum-rules-btn').addEventListener('click', _showRules);
|
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
|
// Threads laden
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ window.Page_movies = (() => {
|
||||||
let _filter = 'alle';
|
let _filter = 'alle';
|
||||||
let _typ = 'alle'; // alle | film | serie | doku
|
let _typ = 'alle'; // alle | film | serie | doku
|
||||||
let _sort = 'default'; // default | titel | jahr_desc | jahr_asc | imdb | bewertung
|
let _sort = 'default'; // default | titel | jahr_desc | jahr_asc | imdb | bewertung
|
||||||
|
let _search = '';
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// INIT
|
// INIT
|
||||||
|
|
@ -41,7 +42,6 @@ window.Page_movies = (() => {
|
||||||
<div class="movies-tabs">
|
<div class="movies-tabs">
|
||||||
<button class="movies-tab${_activeTab === 'filme' ? ' movies-tab--active' : ''}" data-tab="filme">${UI.icon('film-slate')} Filme</button>
|
<button class="movies-tab${_activeTab === 'filme' ? ' movies-tab--active' : ''}" data-tab="filme">${UI.icon('film-slate')} Filme</button>
|
||||||
<button class="movies-tab${_activeTab === 'promis' ? ' movies-tab--active' : ''}" data-tab="promis">${UI.icon('star')} Berühmtheiten</button>
|
<button class="movies-tab${_activeTab === 'promis' ? ' movies-tab--active' : ''}" data-tab="promis">${UI.icon('star')} Berühmtheiten</button>
|
||||||
<button class="movies-tab${_activeTab === 'hdm' ? ' movies-tab--active' : ''}" data-tab="hdm">${UI.icon('paw-print')} Hund des Monats</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="movies-tab-content"></div>
|
<div id="movies-tab-content"></div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -66,7 +66,6 @@ window.Page_movies = (() => {
|
||||||
|
|
||||||
if (_activeTab === 'filme') await _renderFilme(content);
|
if (_activeTab === 'filme') await _renderFilme(content);
|
||||||
if (_activeTab === 'promis') _renderPromis(content);
|
if (_activeTab === 'promis') _renderPromis(content);
|
||||||
if (_activeTab === 'hdm') await _renderHundDesMonats(content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -86,6 +85,11 @@ window.Page_movies = (() => {
|
||||||
|
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="movies-controls">
|
<div class="movies-controls">
|
||||||
|
<div class="movies-search-row">
|
||||||
|
<svg class="ph-icon movies-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||||
|
<input type="search" id="movies-search" class="form-control movies-search-input"
|
||||||
|
placeholder="Film, Serie oder Rasse suchen …" value="${_esc(_search)}" autocomplete="off">
|
||||||
|
</div>
|
||||||
<div class="movies-filter-row">
|
<div class="movies-filter-row">
|
||||||
<button class="movies-filter-btn${_filter === 'alle' ? ' movies-filter-btn--active' : ''}" data-filter="alle">Alle</button>
|
<button class="movies-filter-btn${_filter === 'alle' ? ' movies-filter-btn--active' : ''}" data-filter="alle">Alle</button>
|
||||||
<button class="movies-filter-btn${_filter === 'stirbt' ? ' movies-filter-btn--active' : ''}" data-filter="stirbt"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#warning"></use></svg> Hund stirbt</button>
|
<button class="movies-filter-btn${_filter === 'stirbt' ? ' movies-filter-btn--active' : ''}" data-filter="stirbt"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#warning"></use></svg> Hund stirbt</button>
|
||||||
|
|
@ -135,6 +139,11 @@ window.Page_movies = (() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
content.querySelector('#movies-search')?.addEventListener('input', e => {
|
||||||
|
_search = e.target.value.trim().toLowerCase();
|
||||||
|
_renderMovieGrid(content.querySelector('#movie-grid'));
|
||||||
|
});
|
||||||
|
|
||||||
content.querySelector('#movies-sort')?.addEventListener('change', async e => {
|
content.querySelector('#movies-sort')?.addEventListener('change', async e => {
|
||||||
_sort = e.target.value;
|
_sort = e.target.value;
|
||||||
const grid = content.querySelector('#movie-grid');
|
const grid = content.querySelector('#movie-grid');
|
||||||
|
|
@ -153,6 +162,14 @@ window.Page_movies = (() => {
|
||||||
if (_filter === 'stirbt') list = list.filter(f => f.stirbt_der_hund);
|
if (_filter === 'stirbt') list = list.filter(f => f.stirbt_der_hund);
|
||||||
if (_filter === 'ueberlebt') list = list.filter(f => !f.stirbt_der_hund);
|
if (_filter === 'ueberlebt') list = list.filter(f => !f.stirbt_der_hund);
|
||||||
if (_filter === 'top') list = list.filter(f => (f.imdb_rating || 0) >= 7.5 || f.bewertung_avg >= 4.0);
|
if (_filter === 'top') list = list.filter(f => (f.imdb_rating || 0) >= 7.5 || f.bewertung_avg >= 4.0);
|
||||||
|
if (_search) {
|
||||||
|
list = list.filter(f =>
|
||||||
|
(f.titel || '').toLowerCase().includes(_search) ||
|
||||||
|
(f.hund_rasse || '').toLowerCase().includes(_search) ||
|
||||||
|
(f.genre || '').toLowerCase().includes(_search) ||
|
||||||
|
(f.beschreibung || '').toLowerCase().includes(_search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const countEl = document.getElementById('movies-count');
|
const countEl = document.getElementById('movies-count');
|
||||||
if (countEl) countEl.textContent = `${list.length} Einträge`;
|
if (countEl) countEl.textContent = `${list.length} Einträge`;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v591';
|
const CACHE_VERSION = 'by-v594';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue