feat: Kino-Feature mit TMDb-Proxy, Favoriten, Admin-Token-Einstellung

This commit is contained in:
2026-06-06 16:08:54 +02:00
parent 97da66616b
commit 9969079bdd
5 changed files with 360 additions and 33 deletions

View File

@@ -1,8 +1,11 @@
import { useState } from 'react';
import { S } from '../lib.js';
import { useState, useEffect, useCallback } from 'react';
import { api, S } from '../lib.js';
const POSTER = 'https://image.tmdb.org/t/p/w342';
const TABS = [
{ id: 'kino', label: '🎬 Kino' },
{ id: 'kino', label: '🎬 Kino' },
{ id: 'demnächst', label: '🗓 Demnächst' },
{ id: 'favoriten', label: '❤ Favoriten' },
];
export default function Media() {
@@ -10,50 +13,224 @@ export default function Media() {
return (
<div style={{ padding: '0 0 40px' }}>
{/* Tab-Bar */}
<div style={{ display: 'flex', gap: 8, marginBottom: 24, flexWrap: 'wrap' }}>
{TABS.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
style={{
padding: '7px 16px',
borderRadius: 8,
border: tab === t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.12)',
background: tab === t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
color: tab === t.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
cursor: 'pointer',
fontFamily: "'Courier New', monospace",
fontSize: 13,
fontWeight: tab === t.id ? 700 : 400,
transition: 'all 0.15s',
}}
>
<button key={t.id} onClick={() => setTab(t.id)} style={{
padding: '7px 16px', borderRadius: 8, cursor: 'pointer',
border: tab === t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.12)',
background: tab === t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
color: tab === t.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
fontFamily: "'Space Mono',monospace", fontSize: 13,
fontWeight: tab === t.id ? 700 : 400, transition: 'all 0.15s',
}}>
{t.label}
</button>
))}
</div>
{tab === 'kino' && <KinoTab />}
{tab === 'kino' && <MovieGrid endpoint="/tools/media/now-playing" emptyText="Keine aktuellen Kinofilme" />}
{tab === 'demnächst' && <MovieGrid endpoint="/tools/media/upcoming" emptyText="Keine kommenden Filme" />}
{tab === 'favoriten' && <FavoritenTab />}
</div>
);
}
function KinoTab() {
// ── Gemeinsame Film-Karte ─────────────────────────────────────────────────────
function MovieCard({ movie, favIds, onToggleFav }) {
const isFav = favIds.has(movie.id);
const releaseDE = movie.release_date
? new Date(movie.release_date).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' })
: '';
return (
<div style={{
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 12,
padding: '32px 24px',
textAlign: 'center',
color: 'rgba(255,255,255,0.35)',
fontFamily: "'Courier New', monospace",
fontSize: 14,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column',
transition: 'border-color 0.15s',
}}>
<div style={{ fontSize: 40, marginBottom: 12 }}>🎬</div>
<div style={{ fontSize: 15, color: 'rgba(255,255,255,0.5)', marginBottom: 8 }}>Kino-Feature</div>
<div style={{ fontSize: 12 }}>Wird implementiert TMDb API-Key erforderlich</div>
{/* Poster */}
<div style={{ position: 'relative', aspectRatio: '2/3', background: '#111', overflow: 'hidden' }}>
{movie.poster_path
? <img src={POSTER + movie.poster_path} alt={movie.title}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
: <div style={{ width:'100%',height:'100%',display:'flex',alignItems:'center',
justifyContent:'center',fontSize:40,color:'rgba(255,255,255,0.15)' }}>🎬</div>
}
{/* Rating Badge */}
{movie.vote_average > 0 && (
<div style={{
position:'absolute', top:8, left:8,
background:'rgba(0,0,0,0.75)', borderRadius:6,
padding:'2px 7px', fontSize:11, color:'#ffe66d', fontWeight:700,
fontFamily:"'Space Mono',monospace",
}}>
{movie.vote_average.toFixed(1)}
</div>
)}
{/* Fav Button */}
<button onClick={() => onToggleFav(movie, isFav)} style={{
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
transition:'transform 0.1s',
}}>
{isFav ? '❤' : '🤍'}
</button>
</div>
{/* Info */}
<div style={{ padding: '10px 12px', flex: 1, display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
fontFamily:"'Space Mono',monospace", overflow:'hidden',
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
{movie.title}
</div>
{releaseDE && (
<div style={{ fontSize:11, color:'rgba(255,255,255,0.4)', fontFamily:"'Space Mono',monospace" }}>
{releaseDE}
</div>
)}
</div>
</div>
);
}
// ── Grid-Wrapper (now-playing / upcoming) ─────────────────────────────────────
function MovieGrid({ endpoint, emptyText }) {
const [movies, setMovies] = useState([]);
const [favIds, setFavIds] = useState(new Set());
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const loadFavs = useCallback(async () => {
try {
const data = await api('/tools/media/favorites');
setFavIds(new Set(data.map(f => f.tmdb_id)));
} catch (_) {}
}, []);
useEffect(() => {
setLoading(true); setError('');
Promise.all([
api(endpoint).then(setMovies).catch(e => setError(e.message)),
loadFavs(),
]).finally(() => setLoading(false));
}, [endpoint]);
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_de: movie.release_date
? new Date(movie.release_date).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' })
: '',
}});
setFavIds(prev => new Set([...prev, movie.id]));
}
} catch (e) { alert(e.message); }
};
if (loading) return <StatusBox> Lädt</StatusBox>;
if (error) return <StatusBox color="#ff6b9d"> {error}</StatusBox>;
if (!movies.length) return <StatusBox>{emptyText}</StatusBox>;
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: 14,
}}>
{movies.map(m => (
<MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={toggleFav} />
))}
</div>
);
}
// ── Favoriten-Tab ─────────────────────────────────────────────────────────────
function FavoritenTab() {
const [favs, setFavs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const load = useCallback(async () => {
setLoading(true); setError('');
try {
const data = await api('/tools/media/favorites');
setFavs(data);
} catch (e) { setError(e.message); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
const removeFav = async (tmdbId) => {
try {
await api(`/tools/media/favorites/${tmdbId}`, { method: 'DELETE' });
setFavs(prev => prev.filter(f => f.tmdb_id !== tmdbId));
} catch (e) { alert(e.message); }
};
if (loading) return <StatusBox> Lädt</StatusBox>;
if (error) return <StatusBox color="#ff6b9d"> {error}</StatusBox>;
if (!favs.length) return <StatusBox>Noch keine Favoriten markiere Filme mit </StatusBox>;
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: 14,
}}>
{favs.map(f => (
<div key={f.tmdb_id} style={{
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
borderRadius:12, overflow:'hidden', display:'flex', flexDirection:'column',
}}>
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
{f.poster_path
? <img src={POSTER + f.poster_path} alt={f.title}
style={{ width:'100%',height:'100%',objectFit:'cover',display:'block' }} />
: <div style={{ width:'100%',height:'100%',display:'flex',alignItems:'center',
justifyContent:'center',fontSize:40,color:'rgba(255,255,255,0.15)' }}>🎬</div>
}
<button onClick={() => removeFav(f.tmdb_id)} style={{
position:'absolute',top:6,right:6,background:'rgba(0,0,0,0.6)',
border:'none',borderRadius:'50%',width:32,height:32,cursor:'pointer',
fontSize:16,display:'flex',alignItems:'center',justifyContent:'center',
}}></button>
</div>
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
<div style={{ fontSize:13,fontWeight:700,color:'#fff',lineHeight:1.3,
fontFamily:"'Space Mono',monospace",overflow:'hidden',
display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical' }}>
{f.title}
</div>
{f.release_date_de && (
<div style={{ fontSize:11,color:'rgba(255,255,255,0.4)',fontFamily:"'Space Mono',monospace" }}>
🇩🇪 {f.release_date_de}
</div>
)}
</div>
</div>
))}
</div>
);
}
function StatusBox({ children, color }) {
return (
<div style={{
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
borderRadius:12, padding:'32px 24px', textAlign:'center',
color: color || 'rgba(255,255,255,0.35)', fontFamily:"'Space Mono',monospace", fontSize:14,
}}>
{children}
</div>
);
}