PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
482 lines
23 KiB
JavaScript
482 lines
23 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Hunde-Filme
|
||
Seiten-Modul: Film-Datenbank, Promi-Hunde, Hund des Monats.
|
||
============================================================ */
|
||
|
||
window.Page_movies = (() => {
|
||
|
||
// ----------------------------------------------------------
|
||
// MODUL-STATE
|
||
// ----------------------------------------------------------
|
||
let _container = null;
|
||
let _appState = null;
|
||
let _filme = [];
|
||
let _activeTab = 'filme';
|
||
let _filter = 'alle';
|
||
let _typ = 'alle'; // alle | film | serie | doku
|
||
let _sort = 'default'; // default | titel | jahr_desc | jahr_asc | imdb | bewertung
|
||
let _search = '';
|
||
|
||
// ----------------------------------------------------------
|
||
// INIT
|
||
// ----------------------------------------------------------
|
||
async function init(container, appState) {
|
||
_container = container;
|
||
_appState = appState;
|
||
await _render();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// REFRESH
|
||
// ----------------------------------------------------------
|
||
async function refresh() {
|
||
_filme = [];
|
||
await _render();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// RENDER — Haupt-Layout mit Tabs
|
||
// ----------------------------------------------------------
|
||
async function _render() {
|
||
_container.innerHTML = `
|
||
<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 === 'promis' ? ' movies-tab--active' : ''}" data-tab="promis">${UI.icon('star')} Berühmtheiten</button>
|
||
</div>
|
||
<div id="movies-tab-content"></div>
|
||
`;
|
||
|
||
_container.querySelectorAll('.movies-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_activeTab = btn.dataset.tab;
|
||
_container.querySelectorAll('.movies-tab').forEach(b => b.classList.remove('movies-tab--active'));
|
||
btn.classList.add('movies-tab--active');
|
||
_renderTab();
|
||
});
|
||
});
|
||
|
||
await _renderTab();
|
||
}
|
||
|
||
async function _renderTab() {
|
||
const content = _container.querySelector('#movies-tab-content');
|
||
if (!content) return;
|
||
|
||
content.innerHTML = UI.skeleton(3);
|
||
|
||
if (_activeTab === 'filme') await _renderFilme(content);
|
||
if (_activeTab === 'promis') _renderPromis(content);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB 1: FILME
|
||
// ----------------------------------------------------------
|
||
async function _loadFilme() {
|
||
_filme = await API.get(`/movies/filme?sort=${_sort}&typ=${_typ}`);
|
||
}
|
||
|
||
async function _renderFilme(content) {
|
||
try {
|
||
await _loadFilme();
|
||
} catch {
|
||
content.innerHTML = UI.emptyState({ icon: 'film-slate', title: 'Filme konnten nicht geladen werden', text: 'Bitte versuche es erneut.' });
|
||
return;
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<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">
|
||
<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 === 'ueberlebt' ? ' movies-filter-btn--active' : ''}" data-filter="ueberlebt"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> Hund überlebt</button>
|
||
<button class="movies-filter-btn${_filter === 'top' ? ' movies-filter-btn--active' : ''}" data-filter="top"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg> Top</button>
|
||
</div>
|
||
<div class="movies-filter-row mt-2">
|
||
<button class="movies-filter-btn movies-type-btn${_typ === 'alle' ? ' movies-filter-btn--active' : ''}" data-typ="alle">Alle</button>
|
||
<button class="movies-filter-btn movies-type-btn${_typ === 'film' ? ' movies-filter-btn--active' : ''}" data-typ="film"><svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#film-slate"></use></svg> Filme</button>
|
||
<button class="movies-filter-btn movies-type-btn${_typ === 'serie' ? ' movies-filter-btn--active' : ''}" data-typ="serie"><svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#list"></use></svg> Serien</button>
|
||
<button class="movies-filter-btn movies-type-btn${_typ === 'doku' ? ' movies-filter-btn--active' : ''}" data-typ="doku"><svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#camera"></use></svg> Dokus</button>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-top:var(--space-2)">
|
||
<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px;color:var(--c-text-muted)"><use href="/icons/phosphor.svg#list"></use></svg>
|
||
<select id="movies-sort" class="form-control" style="flex:1;font-size:var(--text-sm);padding:var(--space-2) var(--space-3)">
|
||
<option value="default" ${_sort==='default' ?'selected':''}>Empfohlen</option>
|
||
<option value="bewertung" ${_sort==='bewertung' ?'selected':''}>Community-Bewertung</option>
|
||
<option value="imdb" ${_sort==='imdb' ?'selected':''}>IMDb-Bewertung</option>
|
||
<option value="jahr_desc" ${_sort==='jahr_desc' ?'selected':''}>Neueste zuerst</option>
|
||
<option value="jahr_asc" ${_sort==='jahr_asc' ?'selected':''}>Älteste zuerst</option>
|
||
<option value="titel" ${_sort==='titel' ?'selected':''}>Titel A–Z</option>
|
||
</select>
|
||
<span id="movies-count" style="font-size:var(--text-xs);color:var(--c-text-muted);white-space:nowrap"></span>
|
||
</div>
|
||
</div>
|
||
<div class="movie-grid" id="movie-grid"></div>
|
||
`;
|
||
|
||
content.querySelectorAll('.movies-filter-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_filter = btn.dataset.filter;
|
||
content.querySelectorAll('.movies-filter-btn').forEach(b => b.classList.remove('movies-filter-btn--active'));
|
||
btn.classList.add('movies-filter-btn--active');
|
||
_renderMovieGrid(content.querySelector('#movie-grid'));
|
||
});
|
||
});
|
||
|
||
content.querySelectorAll('.movies-type-btn').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
_typ = btn.dataset.typ;
|
||
content.querySelectorAll('.movies-type-btn').forEach(b => b.classList.remove('movies-filter-btn--active'));
|
||
btn.classList.add('movies-filter-btn--active');
|
||
const grid = content.querySelector('#movie-grid');
|
||
grid.innerHTML = UI.skeleton(3);
|
||
await _loadFilme();
|
||
_renderMovieGrid(grid);
|
||
});
|
||
});
|
||
|
||
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 => {
|
||
_sort = e.target.value;
|
||
const grid = content.querySelector('#movie-grid');
|
||
grid.innerHTML = UI.skeleton(3);
|
||
await _loadFilme();
|
||
_renderMovieGrid(grid);
|
||
});
|
||
|
||
_renderMovieGrid(content.querySelector('#movie-grid'));
|
||
}
|
||
|
||
function _renderMovieGrid(grid) {
|
||
if (!grid) return;
|
||
let list = [..._filme];
|
||
|
||
if (_filter === 'stirbt') 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 (_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');
|
||
if (countEl) countEl.textContent = `${list.length} Einträge`;
|
||
|
||
if (list.length === 0) {
|
||
grid.innerHTML = `<div style="grid-column:1/-1;padding:var(--space-8);text-align:center;color:var(--c-text-secondary)">Keine Filme für diesen Filter.</div>`;
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = list.map(f => _movieCard(f)).join('');
|
||
|
||
grid.querySelectorAll('.movie-card').forEach(card => {
|
||
card.addEventListener('click', (e) => {
|
||
if (e.target.closest('.movie-star-rating')) return;
|
||
const id = card.dataset.filmId;
|
||
const film = _filme.find(f => f.id === id);
|
||
if (film) _openMovieModal(film);
|
||
});
|
||
});
|
||
|
||
_bindStarRatings(grid);
|
||
}
|
||
|
||
function _movieCard(film) {
|
||
const stirbt = film.stirbt_der_hund;
|
||
const tag = stirbt
|
||
? `<div class="movie-tag-stirbt"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Hund stirbt</div>`
|
||
: `<div class="movie-tag-ueberlebt"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> Hund überlebt</div>`;
|
||
const stars = _starsHtml(film.bewertung_avg, film.id, film.user_rating, false);
|
||
const _ico = name => `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px;vertical-align:middle"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
|
||
const typLabel = film.typ === 'serie' ? `${_ico('list')} Serie` : film.typ === 'doku' ? `${_ico('camera')} Doku` : '';
|
||
const imdb = film.imdb_rating ? `<span class="text-xs-muted">IMDb ${film.imdb_rating}</span>` : '';
|
||
const streaming = film.streaming ? `<span class="text-xs-muted">${_esc(film.streaming)}</span>` : '';
|
||
|
||
return `
|
||
<div class="movie-card" data-film-id="${_esc(film.id)}">
|
||
<div class="movie-card-emoji">${film.bild_emoji}</div>
|
||
<div class="movie-card-body">
|
||
<div class="movie-card-title">${_esc(film.titel)} <span class="movie-card-year">(${film.jahr})</span></div>
|
||
<div class="movie-card-genre" style="display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap">
|
||
<span>${_esc(film.genre)}</span>${typLabel ? `<span class="text-xs-muted">${typLabel}</span>` : ''}
|
||
</div>
|
||
<div class="movie-card-rasse"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${_esc(film.hund_rasse)}</div>
|
||
${tag}
|
||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-1)">${imdb}${streaming}</div>
|
||
<div class="movie-card-stars">${stars}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _openMovieModal(film) {
|
||
const stirbt = film.stirbt_der_hund;
|
||
const bannerClass = stirbt ? 'movie-tag-stirbt' : 'movie-tag-ueberlebt';
|
||
const bannerText = stirbt ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> ACHTUNG: Der Hund stirbt!' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> Der Hund überlebt';
|
||
|
||
const stars = _starsHtml(film.bewertung_avg, film.id, film.user_rating, true);
|
||
const loginHint = !_appState.user
|
||
? `<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-2)">Anmelden um zu bewerten</p>`
|
||
: '';
|
||
|
||
const body = `
|
||
<div class="movie-modal-emoji">${film.bild_emoji}</div>
|
||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
|
||
<span class="badge badge-primary">${_esc(film.genre)}</span>
|
||
<span class="badge"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${_esc(film.hund_rasse)}</span>
|
||
<span class="badge">${film.jahr}</span>
|
||
</div>
|
||
<div class="${bannerClass}" style="margin-bottom:var(--space-4);font-size:var(--text-base)">${bannerText}</div>
|
||
<p style="line-height:1.6;color:var(--c-text);margin-bottom:var(--space-5)">${_esc(film.beschreibung)}</p>
|
||
<div class="mb-2">
|
||
<strong>Community-Bewertung:</strong>
|
||
</div>
|
||
<div id="modal-stars-${_esc(film.id)}">${stars}</div>
|
||
<div id="modal-avg-${_esc(film.id)}" style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)">
|
||
Ø ${film.bewertung_avg} von ${film.bewertung_cnt || 0} Bewertungen
|
||
</div>
|
||
${loginHint}
|
||
`;
|
||
|
||
UI.modal.open({ title: film.titel, body });
|
||
|
||
const starsEl = document.getElementById(`modal-stars-${film.id}`);
|
||
if (starsEl && _appState.user) {
|
||
_bindStarRatingsEl(starsEl, film.id, true);
|
||
}
|
||
}
|
||
|
||
function _starsHtml(avg, filmId, userRating, interactive) {
|
||
const filled = Math.round(avg);
|
||
const stars = [1,2,3,4,5].map(i => {
|
||
const active = i <= (userRating || filled) ? ' movie-star--active' : '';
|
||
return `<span class="movie-star${active}" data-film-id="${_esc(filmId)}" data-val="${i}"><svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px"><use href="/icons/phosphor.svg#star"></use></svg></span>`;
|
||
}).join('');
|
||
return `<div class="movie-star-rating" data-film-id="${_esc(filmId)}">${stars} <span class="movie-star-avg">${avg}</span></div>`;
|
||
}
|
||
|
||
function _bindStarRatings(container) {
|
||
container.querySelectorAll('.movie-star-rating').forEach(el => {
|
||
_bindStarRatingsEl(el, el.dataset.filmId, false);
|
||
});
|
||
}
|
||
|
||
function _bindStarRatingsEl(el, filmId, inModal) {
|
||
if (!_appState.user) return;
|
||
|
||
const stars = el.querySelectorAll('.movie-star');
|
||
|
||
stars.forEach(star => {
|
||
star.addEventListener('mouseenter', () => {
|
||
const val = parseInt(star.dataset.val);
|
||
stars.forEach((s, i) => s.classList.toggle('movie-star--active', i < val));
|
||
});
|
||
|
||
star.addEventListener('mouseleave', () => {
|
||
const film = _filme.find(f => f.id === filmId);
|
||
const cur = film?.user_rating || 0;
|
||
stars.forEach((s, i) => s.classList.toggle('movie-star--active', i < cur));
|
||
});
|
||
|
||
star.addEventListener('click', async () => {
|
||
const val = parseInt(star.dataset.val);
|
||
try {
|
||
const res = await API.post(`/movies/filme/${filmId}/vote`, { bewertung: val });
|
||
// Update in _filme array
|
||
const idx = _filme.findIndex(f => f.id === filmId);
|
||
if (idx !== -1) {
|
||
_filme[idx].user_rating = val;
|
||
_filme[idx].bewertung_avg = res.bewertung_avg;
|
||
_filme[idx].bewertung_cnt = res.bewertung_cnt;
|
||
}
|
||
// Update star display
|
||
stars.forEach((s, i) => s.classList.toggle('movie-star--active', i < val));
|
||
const avgEl = el.querySelector('.movie-star-avg') ||
|
||
(inModal ? document.getElementById(`modal-avg-${filmId}`) : null);
|
||
if (el.querySelector('.movie-star-avg')) {
|
||
el.querySelector('.movie-star-avg').textContent = res.bewertung_avg;
|
||
}
|
||
if (inModal) {
|
||
const avgInfo = document.getElementById(`modal-avg-${filmId}`);
|
||
if (avgInfo) avgInfo.textContent = `Ø ${res.bewertung_avg} von ${res.bewertung_cnt} Bewertungen`;
|
||
}
|
||
UI.toast.success('Bewertung gespeichert!');
|
||
} catch {
|
||
UI.toast.error('Bewertung konnte nicht gespeichert werden.');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB 2: BERÜHMTHEITEN (hardcoded, kein Backend)
|
||
// ----------------------------------------------------------
|
||
const PROMIS = [
|
||
{ name: "Hachikō", rasse: "Akita Inu", bekannt_fuer: "9 Jahre lang täglich auf seinen verstorbenen Herrchen am Bahnhof Shibuya gewartet. Statue in Tokio.", emoji: "🗿" },
|
||
{ name: "Rin Tin Tin", rasse: "Deutscher Schäferhund", bekannt_fuer: "Filmhund der 1920er-Jahre. Rettete Warner Bros. vor dem Bankrott. Erster Hundestar Hollywoods.", emoji: "🎬" },
|
||
{ name: "Laika", rasse: "Mischling", bekannt_fuer: "Erstes Lebewesen im Weltall (Sputnik 2, 1957). Wurde zur sowjetischen Weltraumpionierin.", emoji: "🚀" },
|
||
{ name: "Endal", rasse: "Labrador", bekannt_fuer: "Assistenzhund in England. Erster Hund der eine EC-Karte am Geldautomaten benutzte.", emoji: "💳" },
|
||
{ name: "Barry", rasse: "Bernhardiner", bekannt_fuer: "Legendärer Rettungshund der Alpen (1800–1812). Soll 40 Menschen das Leben gerettet haben.", emoji: "🏔️" },
|
||
{ name: "Greyfriars Bobby", rasse: "Skye Terrier", bekannt_fuer: "14 Jahre lang das Grab seines Herrchens in Edinburgh bewacht. Statue und Pub benannt nach ihm.", emoji: "⛪" },
|
||
];
|
||
|
||
function _renderPromis(content) {
|
||
content.innerHTML = `
|
||
<div style="padding:var(--space-2) 0">
|
||
${PROMIS.map(p => `
|
||
<div class="movie-promi-card">
|
||
<div class="movie-promi-emoji">${p.emoji}</div>
|
||
<div class="movie-promi-body">
|
||
<div class="movie-promi-name">${_esc(p.name)}</div>
|
||
<div class="movie-promi-rasse">${_esc(p.rasse)}</div>
|
||
<div class="movie-promi-text">${_esc(p.bekannt_fuer)}</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TAB 3: HUND DES MONATS
|
||
// ----------------------------------------------------------
|
||
async function _renderHundDesMonats(content) {
|
||
let data;
|
||
try {
|
||
data = await API.get('/movies/hund-des-monats');
|
||
} catch {
|
||
content.innerHTML = UI.emptyState({ icon: '🏆', title: 'Fehler beim Laden', text: 'Bitte versuche es erneut.' });
|
||
return;
|
||
}
|
||
|
||
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 voteCards = _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 ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> 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">${voteCards}</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 > 0
|
||
? data.top.slice(0, 5).map((dog, i) => {
|
||
const medal = ['🥇','🥈','🥉','4️⃣','5️⃣'][i] || `${i+1}.`;
|
||
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} <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg></div>
|
||
</div>
|
||
`;
|
||
}).join('')
|
||
: `<p style="color:var(--c-text-secondary);padding:var(--space-4)">Noch keine Stimmen diesen Monat. Sei der Erste!</p>`;
|
||
|
||
content.innerHTML = `
|
||
<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>
|
||
`;
|
||
|
||
// Login-Link
|
||
content.querySelector('#hdm-login-link')?.addEventListener('click', e => {
|
||
e.preventDefault();
|
||
App.navigate('settings');
|
||
});
|
||
|
||
// Vote-Buttons
|
||
content.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!');
|
||
// Refresh the tab
|
||
await _renderHundDesMonats(content);
|
||
} catch (err) {
|
||
UI.toast.error(err.message || 'Fehler beim Abstimmen.');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER
|
||
// ----------------------------------------------------------
|
||
function _esc(str) {
|
||
if (!str && str !== 0) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh };
|
||
|
||
})();
|