fix: media – authenticate middleware, doppelter Route-Load, DevTools-Layout
This commit is contained in:
@@ -69,7 +69,6 @@ fs.readdirSync(toolsDir, { withFileTypes: true })
|
|||||||
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
|
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
|
||||||
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
|
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
|
||||||
app.use('/api/upload-shares', require('./tools/dateien/upload-share'));
|
app.use('/api/upload-shares', require('./tools/dateien/upload-share'));
|
||||||
app.use('/api/tools/media', require('./tools/media/routes'));
|
|
||||||
app.use('/api/search', require('./routes/search'));
|
app.use('/api/search', require('./routes/search'));
|
||||||
|
|
||||||
// Öffentliche Upload-Seite: React-App ausliefern – App erkennt /u/ Pfad selbst
|
// Öffentliche Upload-Seite: React-App ausliefern – App erkennt /u/ Pfad selbst
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const db = require('../../db');
|
const db = require('../../db');
|
||||||
const { requireAdmin } = require('../../middleware/auth');
|
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||||
|
|
||||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||||
|
|
||||||
@@ -23,8 +23,7 @@ async function tmdb(path, params = {}) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/tools/media/now-playing
|
router.get('/now-playing', authenticate, async (req, res) => {
|
||||||
router.get('/now-playing', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const data = await tmdb('/movie/now_playing');
|
const data = await tmdb('/movie/now_playing');
|
||||||
res.json(data.results.slice(0, 10));
|
res.json(data.results.slice(0, 10));
|
||||||
@@ -33,8 +32,7 @@ router.get('/now-playing', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/upcoming
|
router.get('/upcoming', authenticate, async (req, res) => {
|
||||||
router.get('/upcoming', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const data = await tmdb('/movie/upcoming');
|
const data = await tmdb('/movie/upcoming');
|
||||||
res.json(data.results.slice(0, 10));
|
res.json(data.results.slice(0, 10));
|
||||||
@@ -43,16 +41,14 @@ router.get('/upcoming', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/favorites
|
router.get('/favorites', authenticate, (req, res) => {
|
||||||
router.get('/favorites', (req, res) => {
|
|
||||||
const rows = db.prepare(
|
const rows = db.prepare(
|
||||||
'SELECT * FROM movie_favorites WHERE user_id=? ORDER BY added_at DESC'
|
'SELECT * FROM movie_favorites WHERE user_id=? ORDER BY added_at DESC'
|
||||||
).all(req.user.id);
|
).all(req.user.id);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/tools/media/favorites
|
router.post('/favorites', authenticate, (req, res) => {
|
||||||
router.post('/favorites', (req, res) => {
|
|
||||||
const { tmdb_id, title, poster_path, release_date_de } = req.body;
|
const { tmdb_id, title, poster_path, release_date_de } = req.body;
|
||||||
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
|
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
|
||||||
try {
|
try {
|
||||||
@@ -65,21 +61,18 @@ router.post('/favorites', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/tools/media/favorites/:tmdb_id
|
router.delete('/favorites/:tmdb_id', authenticate, (req, res) => {
|
||||||
router.delete('/favorites/:tmdb_id', (req, res) => {
|
|
||||||
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?')
|
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?')
|
||||||
.run(req.user.id, Number(req.params.tmdb_id));
|
.run(req.user.id, Number(req.params.tmdb_id));
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/settings (admin only)
|
router.get('/settings', authenticate, requireAdmin, (req, res) => {
|
||||||
router.get('/settings', requireAdmin, (req, res) => {
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
res.json({ configured: !!token });
|
res.json({ configured: !!token });
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /api/tools/media/settings (admin only)
|
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||||
router.put('/settings', requireAdmin, (req, res) => {
|
|
||||||
const { tmdb_token } = req.body;
|
const { tmdb_token } = req.body;
|
||||||
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
|
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());
|
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run('tmdb_token', tmdb_token.trim());
|
||||||
|
|||||||
@@ -1,40 +1,113 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { api, S } from '../lib.js';
|
import { S, api } from '../lib.js';
|
||||||
|
|
||||||
const POSTER = 'https://image.tmdb.org/t/p/w342';
|
const POSTER = 'https://image.tmdb.org/t/p/w342';
|
||||||
const TABS = [
|
|
||||||
{ id: 'kino', label: '🎬 Kino' },
|
const TOOL_DEFS = [
|
||||||
{ id: 'demnächst', label: '🗓 Demnächst' },
|
{ id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino – Top 10 (DE)', ready: true },
|
||||||
{ id: 'favoriten', label: '❤ Favoriten' },
|
{ id: 'demnächst', label: '🗓 Demnächst', desc: 'Bald im Kino – Vorschau (DE)', ready: true },
|
||||||
|
{ id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Media() {
|
const getIcon = lbl => [...lbl][0] ?? '🎬';
|
||||||
const [tab, setTab] = useState('kino');
|
const getText = lbl => { const c = [...lbl]; return c.slice(1).join('').trim() || lbl; };
|
||||||
|
|
||||||
|
export default function Media({ toast, mobile, nav }) {
|
||||||
|
const [activeTool, setActiveTool] = useState('kino');
|
||||||
|
const chipScrollRef = useRef(null);
|
||||||
|
const [chipScroll, setChipScroll] = useState({ left: false, right: true });
|
||||||
|
|
||||||
|
const onChipScroll = () => {
|
||||||
|
const el = chipScrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
setChipScroll({
|
||||||
|
left: el.scrollLeft > 4,
|
||||||
|
right: el.scrollLeft < el.scrollWidth - el.clientWidth - 4,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const active = TOOL_DEFS.find(t => t.id === activeTool);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 0 40px' }}>
|
<div style={{ padding: mobile ? '14px 14px 90px' : '36px 44px', maxWidth: 860 }}>
|
||||||
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:20, margin:'0 0 20px' }}>
|
<div style={{ display:'flex', alignItems:'center', gap:12, marginBottom:16, flexWrap:'wrap' }}>
|
||||||
🎬 Media
|
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize: mobile?17:22, margin:0 }}>Media</h1>
|
||||||
</h1>
|
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10 }}>Kino · Vorschau · Favoriten</span>
|
||||||
|
|
||||||
<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, 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>
|
</div>
|
||||||
|
|
||||||
{tab === 'kino' && <MovieGrid endpoint="/tools/media/now-playing" emptyText="Keine aktuellen Kinofilme" />}
|
{/* Tool-Auswahl */}
|
||||||
{tab === 'demnächst' && <MovieGrid endpoint="/tools/media/upcoming" emptyText="Keine kommenden Filme" />}
|
{mobile ? (
|
||||||
{tab === 'favoriten' && <FavoritenTab />}
|
<div style={{ marginBottom:16 }}>
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap:4 }}>
|
||||||
|
{chipScroll.left && (
|
||||||
|
<button onClick={() => chipScrollRef.current?.scrollBy({ left:-160, behavior:'smooth' })}
|
||||||
|
style={{ flexShrink:0, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.12)',
|
||||||
|
borderRadius:20, color:'rgba(255,255,255,0.6)', cursor:'pointer', padding:'6px 8px', fontSize:14, lineHeight:1 }}>‹</button>
|
||||||
|
)}
|
||||||
|
<div ref={chipScrollRef} onScroll={onChipScroll}
|
||||||
|
style={{ display:'flex', gap:6, overflowX:'auto', flex:1,
|
||||||
|
padding:'3px 2px 6px', scrollbarWidth:'none', WebkitOverflowScrolling:'touch' }}>
|
||||||
|
{TOOL_DEFS.map(t => (
|
||||||
|
<button key={t.id} onClick={() => setActiveTool(t.id)} style={{
|
||||||
|
display:'flex', alignItems:'center', gap:6, flexShrink:0,
|
||||||
|
padding:'7px 12px', borderRadius:20, fontFamily:'monospace', fontSize:11,
|
||||||
|
cursor:'pointer', whiteSpace:'nowrap',
|
||||||
|
background: activeTool===t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.05)',
|
||||||
|
border: activeTool===t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.1)',
|
||||||
|
color: activeTool===t.id ? '#4ecdc4' : 'rgba(255,255,255,0.75)',
|
||||||
|
fontWeight: activeTool===t.id ? 700 : 400,
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize:15 }}>{getIcon(t.label)}</span>
|
||||||
|
<span>{getText(t.label)}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{chipScroll.right && (
|
||||||
|
<button onClick={() => chipScrollRef.current?.scrollBy({ left:160, behavior:'smooth' })}
|
||||||
|
style={{ flexShrink:0, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.12)',
|
||||||
|
borderRadius:20, color:'rgba(255,255,255,0.6)', cursor:'pointer', padding:'6px 8px', fontSize:14, lineHeight:1 }}>›</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:5, marginBottom:16 }}>
|
||||||
|
{TOOL_DEFS.map(t => (
|
||||||
|
<button key={t.id} title={t.desc} onClick={() => setActiveTool(t.id)} style={{
|
||||||
|
display:'flex', alignItems:'center', gap:9,
|
||||||
|
padding:'8px 11px', borderRadius:9, fontFamily:'monospace',
|
||||||
|
cursor:'pointer', textAlign:'left', border:'none',
|
||||||
|
background: activeTool===t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
||||||
|
outline: activeTool===t.id ? '1px solid #4ecdc4' : 'none',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize:17, flexShrink:0, lineHeight:1, minWidth:22, textAlign:'center' }}>{getIcon(t.label)}</span>
|
||||||
|
<div style={{ minWidth:0, flex:1 }}>
|
||||||
|
<div style={{
|
||||||
|
color: activeTool===t.id ? '#4ecdc4' : 'rgba(255,255,255,0.85)',
|
||||||
|
fontSize:12, fontWeight: activeTool===t.id?700:500,
|
||||||
|
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap',
|
||||||
|
}}>
|
||||||
|
{getText(t.label)}
|
||||||
|
</div>
|
||||||
|
<div style={{ color:'rgba(255,255,255,0.28)', fontSize:9, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', marginTop:1 }}>
|
||||||
|
{t.desc}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Inhalts-Card */}
|
||||||
|
<div style={{ ...S.card }}>
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:18, flexWrap:'wrap' }}>
|
||||||
|
<div style={{ ...S.head, marginBottom:0 }}>{active?.label}</div>
|
||||||
|
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10 }}>{active?.desc}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTool === 'kino' && <KinoGrid endpoint="/tools/media/now-playing" emptyText="Keine aktuellen Kinofilme gefunden" />}
|
||||||
|
{activeTool === 'demnächst' && <KinoGrid endpoint="/tools/media/upcoming" emptyText="Keine kommenden Filme gefunden" />}
|
||||||
|
{activeTool === 'favoriten' && <FavoritenGrid />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,41 +122,37 @@ function MovieCard({ movie, favIds, onToggleFav }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
||||||
borderRadius:12, overflow:'hidden', display:'flex', flexDirection:'column',
|
borderRadius:10, overflow:'hidden', display:'flex', flexDirection:'column',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
||||||
{movie.poster_path
|
{movie.poster_path
|
||||||
? <img src={POSTER + movie.poster_path} alt={movie.title}
|
? <img src={POSTER + movie.poster_path} alt={movie.title}
|
||||||
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||||
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
||||||
justifyContent:'center', fontSize:40, color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>🎬</div>
|
||||||
}
|
}
|
||||||
{movie.vote_average > 0 && (
|
{movie.vote_average > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position:'absolute', top:8, left:8,
|
position:'absolute', top:6, left:6,
|
||||||
background:'rgba(0,0,0,0.75)', borderRadius:6,
|
background:'rgba(0,0,0,0.8)', borderRadius:5,
|
||||||
padding:'2px 7px', fontSize:11, color:'#ffe66d', fontWeight:700,
|
padding:'2px 6px', fontSize:10, color:'#ffe66d', fontWeight:700,
|
||||||
fontFamily:"'Space Mono',monospace",
|
fontFamily:"'Space Mono',monospace",
|
||||||
}}>
|
}}>★ {movie.vote_average.toFixed(1)}</div>
|
||||||
★ {movie.vote_average.toFixed(1)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<button onClick={() => onToggleFav(movie, isFav)} style={{
|
<button onClick={() => onToggleFav(movie, isFav)} style={{
|
||||||
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
|
position:'absolute', top:5, right:5, background:'rgba(0,0,0,0.65)',
|
||||||
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
|
border:'none', borderRadius:'50%', width:30, height:30, cursor:'pointer',
|
||||||
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
|
fontSize:14, display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
}}>
|
}}>{isFav ? '❤' : '🤍'}</button>
|
||||||
{isFav ? '❤' : '🤍'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
|
<div style={{ padding:'8px 10px', flex:1, display:'flex', flexDirection:'column', gap:3 }}>
|
||||||
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
|
<div style={{
|
||||||
|
fontSize:12, fontWeight:700, color:'#fff', lineHeight:1.3,
|
||||||
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
||||||
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
|
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
|
||||||
{movie.title}
|
}}>{movie.title}</div>
|
||||||
</div>
|
|
||||||
{releaseDE && (
|
{releaseDE && (
|
||||||
<div style={{ fontSize:11, color:'rgba(255,255,255,0.4)', fontFamily:"'Space Mono',monospace" }}>
|
<div style={{ fontSize:10, color:'rgba(255,255,255,0.35)', fontFamily:"'Space Mono',monospace" }}>
|
||||||
{releaseDE}
|
{releaseDE}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -92,8 +161,8 @@ function MovieCard({ movie, favIds, onToggleFav }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid (now-playing / upcoming) ─────────────────────────────────────────────
|
// ── Kino/Demnächst Grid ───────────────────────────────────────────────────────
|
||||||
function MovieGrid({ endpoint, emptyText }) {
|
function KinoGrid({ endpoint, emptyText }) {
|
||||||
const [movies, setMovies] = useState([]);
|
const [movies, setMovies] = useState([]);
|
||||||
const [favIds, setFavIds] = useState(new Set());
|
const [favIds, setFavIds] = useState(new Set());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -134,21 +203,23 @@ function MovieGrid({ endpoint, emptyText }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
||||||
if (error) return <StatusBox color="#ff6b9d">
|
if (error) return (
|
||||||
⚠ {error}
|
<StatusBox color="#ff6b9d">
|
||||||
{error.includes('Token') && (
|
<div>⚠ {error}</div>
|
||||||
<div style={{ marginTop:8, fontSize:12, color:'rgba(255,255,255,0.4)' }}>
|
{error.includes('Token') && (
|
||||||
→ Einstellungen → Benutzer → <span style={{ color:'#4ecdc4' }}>TMDB API (KINO-FEATURE)</span>
|
<div style={{ marginTop:8, fontSize:11, color:'rgba(255,255,255,0.4)' }}>
|
||||||
</div>
|
Token eintragen: Einstellungen → Benutzer → <span style={{ color:'#4ecdc4' }}>TMDB API</span>
|
||||||
)}
|
</div>
|
||||||
</StatusBox>;
|
)}
|
||||||
|
</StatusBox>
|
||||||
|
);
|
||||||
if (!movies.length) return <StatusBox>{emptyText}</StatusBox>;
|
if (!movies.length) return <StatusBox>{emptyText}</StatusBox>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display:'grid',
|
display:'grid',
|
||||||
gridTemplateColumns:'repeat(auto-fill, minmax(140px, 1fr))',
|
gridTemplateColumns:'repeat(auto-fill, minmax(130px, 1fr))',
|
||||||
gap:14,
|
gap:12,
|
||||||
}}>
|
}}>
|
||||||
{movies.map(m => (
|
{movies.map(m => (
|
||||||
<MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={toggleFav} />
|
<MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={toggleFav} />
|
||||||
@@ -157,8 +228,8 @@ function MovieGrid({ endpoint, emptyText }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Favoriten ─────────────────────────────────────────────────────────────────
|
// ── Favoriten Grid ────────────────────────────────────────────────────────────
|
||||||
function FavoritenTab() {
|
function FavoritenGrid() {
|
||||||
const [favs, setFavs] = useState([]);
|
const [favs, setFavs] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -180,33 +251,33 @@ function FavoritenTab() {
|
|||||||
if (!favs.length) return <StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>;
|
if (!favs.length) return <StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(140px, 1fr))', gap:14 }}>
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px, 1fr))', gap:12 }}>
|
||||||
{favs.map(f => (
|
{favs.map(f => (
|
||||||
<div key={f.tmdb_id} style={{
|
<div key={f.tmdb_id} style={{
|
||||||
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
||||||
borderRadius:12, overflow:'hidden', display:'flex', flexDirection:'column',
|
borderRadius:10, overflow:'hidden', display:'flex', flexDirection:'column',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
||||||
{f.poster_path
|
{f.poster_path
|
||||||
? <img src={POSTER + f.poster_path} alt={f.title}
|
? <img src={POSTER + f.poster_path} alt={f.title}
|
||||||
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||||
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
||||||
justifyContent:'center', fontSize:40, color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>🎬</div>
|
||||||
}
|
}
|
||||||
<button onClick={() => removeFav(f.tmdb_id)} style={{
|
<button onClick={() => removeFav(f.tmdb_id)} style={{
|
||||||
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
|
position:'absolute', top:5, right:5, background:'rgba(0,0,0,0.65)',
|
||||||
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
|
border:'none', borderRadius:'50%', width:30, height:30, cursor:'pointer',
|
||||||
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
|
fontSize:14, display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
}}>❤</button>
|
}}>❤</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
|
<div style={{ padding:'8px 10px', flex:1, display:'flex', flexDirection:'column', gap:3 }}>
|
||||||
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
|
<div style={{
|
||||||
|
fontSize:12, fontWeight:700, color:'#fff', lineHeight:1.3,
|
||||||
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
||||||
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
|
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
|
||||||
{f.title}
|
}}>{f.title}</div>
|
||||||
</div>
|
|
||||||
{f.release_date_de && (
|
{f.release_date_de && (
|
||||||
<div style={{ fontSize:11, color:'rgba(255,255,255,0.4)', fontFamily:"'Space Mono',monospace" }}>
|
<div style={{ fontSize:10, color:'rgba(255,255,255,0.35)', fontFamily:"'Space Mono',monospace" }}>
|
||||||
🇩🇪 {f.release_date_de}
|
🇩🇪 {f.release_date_de}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -220,9 +291,9 @@ function FavoritenTab() {
|
|||||||
function StatusBox({ children, color }) {
|
function StatusBox({ children, color }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
padding:'28px 20px', textAlign:'center',
|
||||||
borderRadius:12, padding:'32px 24px', textAlign:'center',
|
color: color || 'rgba(255,255,255,0.35)',
|
||||||
color: color || 'rgba(255,255,255,0.35)', fontFamily:"'Space Mono',monospace", fontSize:14,
|
fontFamily:"'Space Mono',monospace", fontSize:13,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user