Filme: DB-Migration, 68 Einträge, Sort + Typ-Filter

- movies-Tabelle in SQLite (statt hardcoded Liste)
- seed_movies(): 68 Filme/Serien/Dokus beim ersten Start
- Felder: titel, originaltitel, jahr, genre, typ, hund_rasse,
  stirbt_der_hund, beschreibung, bild_emoji, imdb_rating, streaming
- GET /api/movies/filme?sort=&typ= — serverseitig sortiert
  Sort: default | titel | jahr_desc | jahr_asc | imdb | bewertung
  Typ: alle | film | serie | doku
- Admin-CRUD: POST/PATCH/DELETE /api/movies/filme
- Frontend: Sort-Dropdown, Typ-Filter-Buttons (Filme/Serien/Dokus),
  Zähler, IMDb-Rating + Streaming auf der Karte
- Promis ebenfalls erweitert (10 statt 6 Einträge)
This commit is contained in:
rene 2026-05-01 08:50:01 +02:00
parent de1677154f
commit 59856e61a1
4 changed files with 350 additions and 113 deletions

View file

@ -13,6 +13,8 @@ window.Page_movies = (() => {
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
// ----------------------------------------------------------
// INIT
@ -70,20 +72,44 @@ window.Page_movies = (() => {
// ----------------------------------------------------------
// TAB 1: FILME
// ----------------------------------------------------------
async function _loadFilme() {
_filme = await API.get(`/movies/filme?sort=${_sort}&typ=${_typ}`);
}
async function _renderFilme(content) {
try {
_filme = await API.get('/movies/filme');
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-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><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg>+</button>
<div class="movies-controls">
<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" style="margin-top:var(--space-2)">
<button class="movies-type-btn${_typ === 'alle' ? ' movies-filter-btn--active' : ''}" data-typ="alle">Alle Typen</button>
<button class="movies-type-btn${_typ === 'film' ? ' movies-filter-btn--active' : ''}" data-typ="film">🎬 Filme</button>
<button class="movies-type-btn${_typ === 'serie' ? ' movies-filter-btn--active' : ''}" data-typ="serie">📺 Serien</button>
<button class="movies-type-btn${_typ === 'doku' ? ' movies-filter-btn--active' : ''}" data-typ="doku">🎥 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#sort-ascending"></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>
`;
@ -97,6 +123,26 @@ window.Page_movies = (() => {
});
});
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-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'));
}
@ -106,7 +152,10 @@ window.Page_movies = (() => {
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.bewertung_avg >= 4.0);
if (_filter === 'top') list = list.filter(f => (f.imdb_rating || 0) >= 7.5 || f.bewertung_avg >= 4.0);
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>`;
@ -130,18 +179,24 @@ window.Page_movies = (() => {
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> ACHTUNG: Der Hund stirbt</div>`
: `<div class="movie-tag-ueberlebt"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> Der Hund überlebt</div>`;
? `<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 typLabel = film.typ === 'serie' ? '📺 Serie' : film.typ === 'doku' ? '🎥 Doku' : '';
const imdb = film.imdb_rating ? `<span style="font-size:var(--text-xs);color:var(--c-text-muted)">IMDb ${film.imdb_rating}</span>` : '';
const streaming = film.streaming ? `<span style="font-size:var(--text-xs);color:var(--c-text-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">${_esc(film.genre)}</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 style="font-size:var(--text-xs);color:var(--c-text-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>