banyaro/backend/static/js/pages/movies.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
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).
2026-05-27 07:11:27 +02:00

482 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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 AZ</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 (18001812). 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh };
})();