diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js
index 7fcb382..1d990a6 100644
--- a/backend/src/tools/media/routes.js
+++ b/backend/src/tools/media/routes.js
@@ -23,7 +23,7 @@ async function tmdb(path, params = {}) {
return res.json();
}
-// Genre-Cache (in-memory, wird bei erstem Aufruf befüllt)
+// Genre-Cache
let genreCache = null;
async function getGenreMap() {
if (genreCache) return genreCache;
@@ -33,109 +33,139 @@ async function getGenreMap() {
return genreCache;
}
-// FSK aus release_dates extrahieren
function extractFSK(releaseDates) {
const de = releaseDates?.results?.find(r => r.iso_3166_1 === 'DE');
if (!de) return null;
- const cert = de.release_dates.find(r => r.certification)?.certification;
- return cert || null;
+ return de.release_dates.find(r => r.certification)?.certification || null;
}
-// Einen Film anreichern (Genres + FSK aus append_to_response)
-function enrichMovie(movie, genreMap, releaseDates) {
+// Alle Seiten einer TMDb-Liste holen (bis max. maxPages)
+async function fetchAllPages(path, maxPages = 5) {
+ const first = await tmdb(path, { page: 1 });
+ const total = Math.min(first.total_pages || 1, maxPages);
+ let results = [...first.results];
+ const extra = await Promise.all(
+ Array.from({ length: total - 1 }, (_, i) => tmdb(path, { page: i + 2 }))
+ );
+ for (const p of extra) results = results.concat(p.results);
+ return results;
+}
+
+function enrichMovie(movie, genreMap, fsk, rank) {
return {
- id: movie.id,
- title: movie.title || movie.original_title,
- original_title: movie.original_title,
+ id: movie.id,
+ title: movie.title || movie.original_title,
+ original_title: movie.original_title,
original_language: movie.original_language,
- poster_path: movie.poster_path,
- backdrop_path: movie.backdrop_path,
- overview: movie.overview,
- release_date: movie.release_date,
- vote_average: movie.vote_average,
- genres: (movie.genre_ids || movie.genres?.map(g=>g.id) || [])
- .map(id => genreMap[id]).filter(Boolean),
- fsk: extractFSK(releaseDates),
+ poster_path: movie.poster_path,
+ overview: movie.overview,
+ release_date: movie.release_date,
+ vote_average: movie.vote_average,
+ genres: (movie.genre_ids || movie.genres?.map(g => g.id) || [])
+ .map(id => genreMap[id]).filter(Boolean),
+ fsk: fsk || null,
+ rank: rank || null,
};
}
-// GET /api/tools/media/now-playing
+// FSK für eine Liste von Filmen parallel holen
+async function withFSK(movies, genreMap, topN = 10) {
+ return Promise.all(movies.map(async (m, i) => {
+ let fsk = null;
+ try {
+ const detail = await tmdb(`/movie/${m.id}/release_dates`);
+ fsk = extractFSK(detail);
+ } catch (_) {}
+ return enrichMovie(m, genreMap, fsk, i < topN ? i + 1 : null);
+ }));
+}
+
+// GET /api/tools/media/now-playing – alles was aktuell in DE läuft
router.get('/now-playing', authenticate, async (req, res) => {
try {
- const [listData, genreMap] = await Promise.all([
- tmdb('/movie/now_playing'),
+ const [all, genreMap] = await Promise.all([
+ fetchAllPages('/movie/now_playing', 3),
getGenreMap(),
]);
- // Türkische Produktionen rausfiltern, max 10
- const filtered = listData.results
- .filter(m => m.original_language !== 'tr')
- .slice(0, 10);
-
- // FSK für jeden Film parallel holen
- const withFSK = await Promise.all(filtered.map(async (m, idx) => {
- try {
- const detail = await tmdb(`/movie/${m.id}/release_dates`);
- return { ...enrichMovie(m, genreMap, detail), rank: idx + 1 };
- } catch {
- return { ...enrichMovie(m, genreMap, null), rank: idx + 1 };
- }
- }));
- res.json(withFSK);
+ const filtered = all.filter(m => m.original_language !== 'tr');
+ const enriched = await withFSK(filtered, genreMap, 10);
+ res.json(enriched);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
-// GET /api/tools/media/upcoming
+// GET /api/tools/media/upcoming – wochenweise Neustarts, alle verfügbaren
router.get('/upcoming', authenticate, async (req, res) => {
try {
- const [listData, genreMap] = await Promise.all([
- tmdb('/movie/upcoming'),
+ const [all, genreMap] = await Promise.all([
+ fetchAllPages('/movie/upcoming', 4),
getGenreMap(),
]);
- const filtered = listData.results
- .filter(m => m.original_language !== 'tr')
- .slice(0, 10);
+ const filtered = all.filter(m => m.original_language !== 'tr');
- const withFSK = await Promise.all(filtered.map(async (m, idx) => {
- try {
- const detail = await tmdb(`/movie/${m.id}/release_dates`);
- return { ...enrichMovie(m, genreMap, detail), rank: idx + 1 };
- } catch {
- return { ...enrichMovie(m, genreMap, null), rank: idx + 1 };
- }
- }));
- res.json(withFSK);
+ // Deduplizieren nach ID
+ const seen = new Set();
+ const unique = filtered.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });
+
+ // Nach Erscheinungsdatum sortieren
+ unique.sort((a, b) => (a.release_date || '').localeCompare(b.release_date || ''));
+
+ // Wochenweise gruppieren (Montag der jeweiligen Woche als Key)
+ const groups = {};
+ for (const m of unique) {
+ if (!m.release_date) continue;
+ const d = new Date(m.release_date);
+ // Montag der Woche berechnen
+ const day = d.getDay(); // 0=So
+ const diff = (day === 0 ? -6 : 1 - day);
+ const mon = new Date(d);
+ mon.setDate(d.getDate() + diff);
+ const key = mon.toISOString().slice(0, 10);
+ if (!groups[key]) groups[key] = [];
+ groups[key].push(m);
+ }
+
+ // Anreichern: nur die erste Gruppe bekommt FSK (spart API-Calls), Rest ohne
+ const result = {};
+ let globalRank = 0;
+ for (const [week, movies] of Object.entries(groups)) {
+ result[week] = await Promise.all(movies.map(async m => {
+ globalRank++;
+ let fsk = null;
+ try { const d = await tmdb(`/movie/${m.id}/release_dates`); fsk = extractFSK(d); } catch (_) {}
+ return enrichMovie(m, genreMap, fsk, null);
+ }));
+ }
+ res.json(result); // { "2026-06-02": [...], "2026-06-09": [...], ... }
} catch (e) {
res.status(500).json({ error: e.message });
}
});
-// GET /api/tools/media/movie/:id – Details + Credits für Modal
+// GET /api/tools/media/movie/:id – Detail + Credits für Modal
router.get('/movie/:id', authenticate, async (req, res) => {
try {
const [detail, genreMap] = await Promise.all([
tmdb(`/movie/${req.params.id}`, { append_to_response: 'credits,release_dates' }),
getGenreMap(),
]);
- const cast = (detail.credits?.cast || []).slice(0, 8).map(a => a.name);
- const director = detail.credits?.crew?.find(c => c.job === 'Director')?.name || null;
res.json({
- id: detail.id,
- title: detail.title || detail.original_title,
+ id: detail.id,
+ title: detail.title || detail.original_title,
original_title: detail.original_title,
- tagline: detail.tagline,
- overview: detail.overview,
- poster_path: detail.poster_path,
+ tagline: detail.tagline,
+ overview: detail.overview,
+ poster_path: detail.poster_path,
backdrop_path: detail.backdrop_path,
- release_date: detail.release_date,
- runtime: detail.runtime,
- vote_average: detail.vote_average,
- vote_count: detail.vote_count,
- genres: (detail.genres || []).map(g => g.name),
- fsk: extractFSK(detail.release_dates),
- cast,
- director,
+ release_date: detail.release_date,
+ runtime: detail.runtime,
+ vote_average: detail.vote_average,
+ vote_count: detail.vote_count,
+ genres: (detail.genres || []).map(g => g.name),
+ fsk: extractFSK(detail.release_dates),
+ cast: (detail.credits?.cast || []).slice(0, 8).map(a => a.name),
+ director: detail.credits?.crew?.find(c => c.job === 'Director')?.name || null,
});
} catch (e) {
res.status(500).json({ error: e.message });
@@ -184,7 +214,7 @@ router.put('/settings', authenticate, requireAdmin, (req, res) => {
const { tmdb_token } = req.body;
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run('tmdb_token', tmdb_token.trim());
- genreCache = null; // Cache invalidieren
+ genreCache = null;
res.json({ ok: true });
});
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index a2bff3d..c6cf09e 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1513,6 +1513,10 @@ const SEARCH_TOOL_INDEX = [
{type:'tool',label:'Code-Schnipsel',icon:'📄',sub:'Werkzeuge',id:'codeschnipsel',keywords:['code','snippet','skript','programmierung','source']},
{type:'tool',label:'CAD-Skizzen',icon:'📐',sub:'Werkzeuge',id:'skizze',keywords:['cad','skizze','zeichnung','sketch']},
{type:'tool',label:'Dev-Tools',icon:'🔧',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']},
+ {type:'tool',label:'Media',icon:'🎬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnächst','favoriten','cinema']},
+ {type:'tool',label:'Kino – Aktuell',icon:'🎬',sub:'Media',id:'media',keywords:['kino','kinocharts','charts','laufend','now playing']},
+ {type:'tool',label:'Kino – Demnächst',icon:'🗓',sub:'Media',id:'media',keywords:['demnächst','neustart','upcoming','vorschau','kino']},
+ {type:'tool',label:'Kino – Favoriten',icon:'❤',sub:'Media',id:'media',keywords:['favoriten','film favoriten','gemerkte filme','watchlist']},
// ── Dashboard-Widgets ──────────────────────────────────────────────────
{type:'tool',label:'Schnellzugriff',icon:'⚡',sub:'Dashboard',id:'dashboard',keywords:['quick','links','favoriten','shortcuts']},
{type:'tool',label:'Kalender',icon:'📅',sub:'Dashboard',id:'dashboard',keywords:['kalender','termine','events','ical','abo']},
diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx
index 49b1456..f114272 100644
--- a/frontend/src/tools/media.jsx
+++ b/frontend/src/tools/media.jsx
@@ -1,13 +1,14 @@
import { useState, useEffect, useCallback, useRef } from 'react';
+import { createPortal } from 'react-dom';
import { S, api } from '../lib.js';
const POSTER = 'https://image.tmdb.org/t/p/w342';
const BACKDROP = 'https://image.tmdb.org/t/p/w780';
const TOOL_DEFS = [
- { id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino – Top 10 (DE)', ready: true },
- { id: 'demnächst', label: '🗓 Demnächst', desc: 'Bald im Kino – Vorschau (DE)', ready: true },
- { id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
+ { id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino (DE) – alle laufenden Filme', ready: true },
+ { id: 'demnächst', label: '🗓 Demnächst', desc: 'Neustarts wochenweise – nächste Wochen', ready: true },
+ { id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
];
const getIcon = lbl => [...lbl][0] ?? '🎬';
@@ -16,6 +17,15 @@ const getText = lbl => { const c = [...lbl]; return c.slice(1).join('').trim() |
const fmtDE = d => d ? new Date(d).toLocaleDateString('de-DE',
{ day:'2-digit', month:'2-digit', year:'numeric' }) : '';
+const fmtWeek = dateStr => {
+ const d = new Date(dateStr);
+ // KW berechnen
+ const jan4 = new Date(d.getFullYear(), 0, 4);
+ const kw = Math.ceil(((d - jan4) / 86400000 + jan4.getDay() + 1) / 7);
+ const end = new Date(d); end.setDate(d.getDate() + 6);
+ return `KW ${kw} · ${d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'})} – ${end.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})}`;
+};
+
const FSK_COLOR = fsk => {
if (!fsk) return null;
const n = parseInt(fsk);
@@ -29,7 +39,7 @@ const FSK_COLOR = fsk => {
export default function Media({ toast, mobile }) {
const [activeTool, setActiveTool] = useState('kino');
- const [modal, setModal] = useState(null); // tmdb_id
+ const [modal, setModal] = useState(null);
const chipScrollRef = useRef(null);
const [chipScroll, setChipScroll] = useState({ left: false, right: true });
@@ -42,13 +52,12 @@ export default function Media({ toast, mobile }) {
const active = TOOL_DEFS.find(t => t.id === activeTool);
return (
-
+
Media
Kino · Vorschau · Favoriten
- {/* Tool-Auswahl */}
{mobile ? (
@@ -113,28 +122,30 @@ export default function Media({ toast, mobile }) {
{active?.desc}
- {activeTool === 'kino' &&
}
- {activeTool === 'demnächst' &&
}
+ {activeTool === 'kino' &&
}
+ {activeTool === 'demnächst' &&
}
{activeTool === 'favoriten' &&
}
- {modal &&
setModal(null)} mobile={mobile} />}
+ {modal && createPortal(
+ setModal(null)} mobile={mobile} />,
+ document.body
+ )}
);
}
// ── Film-Karte ────────────────────────────────────────────────────────────────
function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
- const isFav = favIds.has(movie.id);
+ const isFav = favIds.has(movie.id);
+ const isTop = movie.rank != null && movie.rank <= 10;
const fskStyle = FSK_COLOR(movie.fsk);
return (
- {/* Poster */}
onOpenModal(movie.id)}>
{movie.poster_path
@@ -143,11 +154,14 @@ function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
:
🎬
}
- {/* Rank */}
- {movie.rank && (
+ {/* Top-Badge */}
+ {isTop && (
#{movie.rank}
+ borderRadius:5, padding:'2px 8px', fontSize:13, fontWeight:900,
+ fontFamily:"'Space Mono',monospace",
+ color: movie.rank === 1 ? '#ffd700' : movie.rank <= 3 ? '#c0c0c0' : '#fff' }}>
+ #{movie.rank}
+
)}
{/* Rating */}
{movie.vote_average > 0 && (
@@ -160,7 +174,7 @@ function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
- FSK {movie.fsk}
+ {movie.fsk}
)}
{/* Fav */}
@@ -170,10 +184,13 @@ function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
fontSize:14, display:'flex', alignItems:'center', justifyContent:'center',
}}>{isFav ? '❤' : '🤍'}
-
- {/* Info */}
onOpenModal(movie.id)}>
+ {isTop && (
+
+ ★ TOP {movie.rank <= 10 ? '10' : ''}
+
+ )}
@@ -195,63 +212,113 @@ function MovieCard({ movie, favIds, onToggleFav, onOpenModal }) {
);
}
-// ── Kino/Demnächst Grid ───────────────────────────────────────────────────────
-function KinoGrid({ endpoint, emptyText, onOpenModal }) {
- const [movies, setMovies] = useState([]);
- const [favIds, setFavIds] = useState(new Set());
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
-
- const loadFavs = useCallback(async () => {
+// ── Favoriten-Hook ────────────────────────────────────────────────────────────
+function useFavIds() {
+ const [favIds, setFavIds] = useState(new Set());
+ const load = useCallback(async () => {
try { const d = await api('/tools/media/favorites'); setFavIds(new Set(d.map(f => f.tmdb_id))); }
catch (_) {}
}, []);
+ useEffect(() => { load(); }, []);
+
+ const toggle = async (movie, isFav) => {
+ if (isFav) {
+ await api(`/tools/media/favorites/${movie.id}`, { method:'DELETE' });
+ setFavIds(prev => { const s = new Set(prev); s.delete(movie.id); return s; });
+ } else {
+ await api('/tools/media/favorites', { body: {
+ tmdb_id: movie.id, title: movie.title,
+ poster_path: movie.poster_path || '',
+ release_date: movie.release_date || '',
+ release_date_de: fmtDE(movie.release_date),
+ genres: movie.genres || [], fsk: movie.fsk || '',
+ }});
+ setFavIds(prev => new Set([...prev, movie.id]));
+ }
+ };
+ return [favIds, toggle];
+}
+
+// ── Kino Grid – alle laufenden Filme ─────────────────────────────────────────
+function KinoGrid({ onOpenModal }) {
+ const [movies, setMovies] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [favIds, toggleFav] = useFavIds();
useEffect(() => {
setLoading(true); setError('');
- Promise.all([
- api(endpoint).then(setMovies).catch(e => setError(e.message)),
- loadFavs(),
- ]).finally(() => setLoading(false));
- }, [endpoint]);
+ api('/tools/media/now-playing').then(setMovies).catch(e => setError(e.message)).finally(() => setLoading(false));
+ }, []);
- const toggleFav = async (movie, isFav) => {
- try {
- if (isFav) {
- await api(`/tools/media/favorites/${movie.id}`, { method:'DELETE' });
- setFavIds(prev => { const s = new Set(prev); s.delete(movie.id); return s; });
- } else {
- await api('/tools/media/favorites', { body: {
- tmdb_id: movie.id,
- title: movie.title,
- poster_path: movie.poster_path || '',
- release_date: movie.release_date || '',
- release_date_de: fmtDE(movie.release_date),
- genres: movie.genres || [],
- fsk: movie.fsk || '',
- }});
- setFavIds(prev => new Set([...prev, movie.id]));
- }
- } catch (e) { alert(e.message); }
+ const onToggle = async (movie, isFav) => {
+ try { await toggleFav(movie, isFav); } catch (e) { alert(e.message); }
};
- if (loading) return
⏳ Lädt…;
- if (error) return (
-
- ⚠ {error}
- {error.includes('Token') && (
-
- Token eintragen: Einstellungen → Benutzer → TMDB API
-
- )}
-
- );
- if (!movies.length) return
{emptyText};
+ if (loading) return
⏳ Lädt Kinocharts…;
+ if (error) return
;
+ if (!movies.length) return
Keine Filme gefunden;
+
+ const top10 = movies.filter(m => m.rank != null);
+ const rest = movies.filter(m => m.rank == null);
return (
-
- {movies.map(m => (
-
+
+ {top10.length > 0 && (
+ <>
+
★ TOP 10 – MEISTGESEHEN
+
+ {top10.map(m => )}
+
+ >
+ )}
+ {rest.length > 0 && (
+ <>
+
WEITERE FILME IM KINO
+
+ {rest.map(m => )}
+
+ >
+ )}
+
+ );
+}
+
+// ── Demnächst – wochenweise Neustarts ────────────────────────────────────────
+function DemnächstGrid({ onOpenModal }) {
+ const [groups, setGroups] = useState({});
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [favIds, toggleFav] = useFavIds();
+
+ useEffect(() => {
+ setLoading(true); setError('');
+ api('/tools/media/upcoming').then(setGroups).catch(e => setError(e.message)).finally(() => setLoading(false));
+ }, []);
+
+ const onToggle = async (movie, isFav) => {
+ try { await toggleFav(movie, isFav); } catch (e) { alert(e.message); }
+ };
+
+ if (loading) return
⏳ Lädt Neustarts…;
+ if (error) return
;
+
+ const entries = Object.entries(groups).sort(([a],[b]) => a.localeCompare(b));
+ if (!entries.length) return
Keine kommenden Filme gefunden;
+
+ return (
+
+ {entries.map(([weekStart, movies]) => (
+
+
+
NEUSTART {fmtWeek(weekStart)}
+
+
{movies.length} Film{movies.length!==1?'e':''}
+
+
+ {movies.map(m => )}
+
+
))}
);
@@ -276,14 +343,13 @@ function FavoritenKalender({ onOpenModal }) {
};
if (loading) return
⏳ Lädt…;
- if (error) return
⚠ {error};
+ if (error) return
;
if (!favs.length) return
Noch keine Favoriten – markiere Filme mit ❤;
- // Gruppieren nach Jahr+Monat
const groups = {};
for (const f of favs) {
- const d = f.release_date ? new Date(f.release_date) : null;
- const key = d ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}` : 'unbekannt';
+ const d = f.release_date ? new Date(f.release_date) : null;
+ const key = d ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}` : 'unbekannt';
const label = d ? d.toLocaleDateString('de-DE', { month:'long', year:'numeric' }) : 'Unbekannt';
if (!groups[key]) groups[key] = { label, items:[] };
groups[key].items.push(f);
@@ -293,33 +359,27 @@ function FavoritenKalender({ onOpenModal }) {
{Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
- {/* Monats-Überschrift */}
{group.label.toUpperCase()}
{group.items.length} Film{group.items.length!==1?'e':''}
- {/* Film-Zeilen */}
{group.items.map(f => {
- const genres = (() => { try { return JSON.parse(f.genres||'[]'); } catch { return []; } })();
+ const genres = (() => { try { return JSON.parse(f.genres||'[]'); } catch { return []; } })();
const fskStyle = FSK_COLOR(f.fsk);
return (
-
onOpenModal(f.tmdb_id)} style={{
display:'flex', gap:12, alignItems:'flex-start',
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.06)',
borderRadius:8, padding:'10px 12px', cursor:'pointer',
- }} onClick={() => onOpenModal(f.tmdb_id)}>
- {/* Mini-Poster */}
+ }}>
{f.poster_path
- ?

- :
🎬
+ ?

+ :
🎬
}
- {/* Text */}
{f.title}
@@ -337,11 +397,9 @@ function FavoritenKalender({ onOpenModal }) {
fontWeight:900, fontFamily:'monospace' }}>FSK {f.fsk}
)}
- {/* Entfernen */}
);
@@ -353,7 +411,7 @@ function FavoritenKalender({ onOpenModal }) {
);
}
-// ── Film-Detail-Modal ─────────────────────────────────────────────────────────
+// ── Film-Detail-Modal (via Portal) ────────────────────────────────────────────
function MovieModal({ tmdbId, onClose, mobile }) {
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(true);
@@ -362,94 +420,83 @@ function MovieModal({ tmdbId, onClose, mobile }) {
useEffect(() => {
setLoading(true); setError('');
api(`/tools/media/movie/${tmdbId}`)
- .then(setDetail)
- .catch(e => setError(e.message))
- .finally(() => setLoading(false));
+ .then(setDetail).catch(e => setError(e.message)).finally(() => setLoading(false));
+
+ // Body-Scroll sperren
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ return () => { document.body.style.overflow = prev; };
}, [tmdbId]);
+ // ESC schließt
+ useEffect(() => {
+ const h = e => { if (e.key === 'Escape') onClose(); };
+ document.addEventListener('keydown', h);
+ return () => document.removeEventListener('keydown', h);
+ }, [onClose]);
+
const fskStyle = detail ? FSK_COLOR(detail.fsk) : null;
- // Overlay
return (
-
e.stopPropagation()}
- onTouchMove={e => e.stopPropagation()}
- style={{
- background:'#1a1a1f', border:'1px solid rgba(255,255,255,0.1)',
- borderRadius: mobile ? '16px 16px 0 0' : 16,
- width: mobile ? '100%' : 560,
- maxHeight: mobile ? '90vh' : '85vh',
- height: mobile ? '90vh' : 'auto',
- overflowY:'scroll',
- WebkitOverflowScrolling:'touch',
- position:'relative',
- }}>
+
e.stopPropagation()} style={{
+ background:'#1a1a1f', border:'1px solid rgba(255,255,255,0.12)',
+ borderRadius: mobile ? '16px 16px 0 0' : 16,
+ width: mobile ? '100%' : 560,
+ maxHeight: mobile ? '88vh' : '85vh',
+ overflowY:'auto',
+ WebkitOverflowScrolling:'touch',
+ position:'relative',
+ }}>
{/* Backdrop */}
{detail?.backdrop_path && (
-
+

-
+ style={{ width:'100%', height:'100%', objectFit:'cover', display:'block', opacity:0.45 }} />
+
)}
{/* Schließen */}
-
+
{loading &&
⏳ Lädt…}
- {error &&
⚠ {error}}
- {detail && (
+ {error &&
}
+ {detail && (<>
{/* Poster */}
-
+
{detail.poster_path
- ?

- :
🎬
+ ?

+ :
🎬
}
-
{/* Info */}
-
{detail.title}
{detail.tagline && (
-
- {detail.tagline}
-
+
{detail.tagline}
)}
- {/* Badges */}
-
- {detail.vote_average > 0 && (
-
★ {detail.vote_average.toFixed(1)}
- )}
- {detail.runtime > 0 && (
-
⏱ {detail.runtime} Min
- )}
- {fskStyle && (
-
FSK {detail.fsk}
- )}
- {detail.release_date && (
-
🇩🇪 {fmtDE(detail.release_date)}
- )}
+
+ {detail.vote_average > 0 && ★ {detail.vote_average.toFixed(1)}}
+ {detail.runtime > 0 && ⏱ {detail.runtime} Min}
+ {fskStyle && FSK {detail.fsk}}
+ {detail.release_date && 🇩🇪 {fmtDE(detail.release_date)}}
- {/* Genres */}
{detail.genres?.length > 0 && (
-
+
{detail.genres.map(g => (
{g}
@@ -458,37 +505,33 @@ function MovieModal({ tmdbId, onClose, mobile }) {
)}
- )}
- {/* Plot */}
- {detail?.overview && (
-
-
HANDLUNG
-
{detail.overview}
-
- )}
-
- {/* Regie */}
- {detail?.director && (
-
-
REGIE
-
{detail.director}
-
- )}
-
- {/* Cast */}
- {detail?.cast?.length > 0 && (
-
-
BESETZUNG
-
- {detail.cast.map(name => (
-
{name}
- ))}
+ {detail.overview && (
+
+
HANDLUNG
+
{detail.overview}
-
- )}
+ )}
+ {detail.director && (
+
+
REGIE
+
{detail.director}
+
+ )}
+ {detail.cast?.length > 0 && (
+
+
BESETZUNG
+
+ {detail.cast.map(name => (
+
+ {name}
+
+ ))}
+
+
+ )}
+ >)}
@@ -504,11 +547,25 @@ function Badge({ children, bg, color }) {
);
}
-function StatusBox({ children, color }) {
+function StatusBox({ children }) {
return (
+ color:'rgba(255,255,255,0.35)', fontFamily:"'Space Mono',monospace", fontSize:13 }}>
{children}
);
}
+
+function ErrorBox({ msg }) {
+ return (
+
+
⚠ {msg}
+ {msg?.includes('Token') && (
+
+ Token eintragen: Einstellungen → Benutzer → TMDB API
+
+ )}
+
+ );
+}