feat: Suchen-Tab mit Filmsuche, Such-Endpoint mit Sprach-/Duplikat-Filter
This commit is contained in:
@@ -291,6 +291,29 @@ router.get('/upcoming', authenticate, async (req, res) => {
|
|||||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/tools/media/search?q=TITEL – Filmsuche
|
||||||
|
router.get('/search', authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const q = (req.query.q || '').trim();
|
||||||
|
if (!q || q.length < 2) return res.json([]);
|
||||||
|
const [results, genreMap] = await Promise.all([
|
||||||
|
tmdb('/search/movie', { query: q, include_adult: 'false' }),
|
||||||
|
getGenreMap(),
|
||||||
|
]);
|
||||||
|
const seen = new Set();
|
||||||
|
const filtered = (results.results || [])
|
||||||
|
.filter(m => {
|
||||||
|
if (BLOCKED_LANGS.has(m.original_language)) return false;
|
||||||
|
if (seen.has(m.id)) return false;
|
||||||
|
seen.add(m.id);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.slice(0, 20)
|
||||||
|
.map(m => enrichMovie(m, genreMap, null, null));
|
||||||
|
res.json(filtered);
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// Modal: TMDb-Daten sofort, xREL via separaten Endpoint
|
// Modal: TMDb-Daten sofort, xREL via separaten Endpoint
|
||||||
router.get('/movie/:id', authenticate, async (req, res) => {
|
router.get('/movie/:id', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const TOOL_DEFS = [
|
|||||||
{ id: 'kino', label: '🎬 Kino', desc: 'Aktuell im Kino (DE) – alle laufenden Filme', 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: 'demnächst', label: '🗓 Demnächst', desc: 'Neustarts wochenweise – nächste Wochen', ready: true },
|
||||||
{ id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
|
{ id: 'favoriten', label: '❤ Favoriten', desc: 'Deine gespeicherten Lieblingsfilme', ready: true },
|
||||||
|
{ id: 'suchen', label: '🔍 Suchen', desc: 'Filme suchen und Infos abrufen', ready: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getIcon = lbl => [...lbl][0] ?? '🎬';
|
const getIcon = lbl => [...lbl][0] ?? '🎬';
|
||||||
@@ -128,6 +129,7 @@ export default function Media({ toast, mobile }) {
|
|||||||
{activeTool === 'kino' && <KinoGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
|
{activeTool === 'kino' && <KinoGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
|
||||||
{activeTool === 'demnächst' && <DemnächstGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
|
{activeTool === 'demnächst' && <DemnächstGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
|
||||||
{activeTool === 'favoriten' && <FavoritenKalender onOpenModal={setModal} />}
|
{activeTool === 'favoriten' && <FavoritenKalender onOpenModal={setModal} />}
|
||||||
|
{activeTool === 'suchen' && <SucheGrid onOpenModal={setModal} xrelIds={xrelIds} onXrelLoaded={ids => setXrelIds(prev => new Set([...prev, ...ids]))} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{modal && createPortal(
|
{modal && createPortal(
|
||||||
@@ -367,6 +369,93 @@ function DemnächstGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Suche ────────────────────────────────────────────────────────────────────
|
||||||
|
function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [results, setResults] = useState(null); // null=noch nicht gesucht
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [favIds, toggleFav] = useFavIds();
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
const doSearch = async (q = query) => {
|
||||||
|
const trimmed = q.trim();
|
||||||
|
if (!trimmed || trimmed.length < 2) return;
|
||||||
|
setLoading(true); setError(''); setResults(null);
|
||||||
|
try {
|
||||||
|
const list = await api(`/tools/media/search?q=${encodeURIComponent(trimmed)}`);
|
||||||
|
setResults(list);
|
||||||
|
// xREL-Badges aus Cache
|
||||||
|
if (list.length) {
|
||||||
|
const ids = list.map(m => m.id).join(',');
|
||||||
|
api(`/tools/media/xrel-cached?ids=${ids}`)
|
||||||
|
.then(r => {
|
||||||
|
const hits = Object.entries(r).filter(([,v])=>v).map(([k])=>Number(k));
|
||||||
|
if (hits.length) onXrelLoaded?.(hits);
|
||||||
|
}).catch(()=>{});
|
||||||
|
}
|
||||||
|
} catch(e) { setError(e.message); }
|
||||||
|
finally { setLoading(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggle = async (movie, isFav) => {
|
||||||
|
try { await toggleFav(movie, isFav); } catch(e) { alert(e.message); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKey = e => { if (e.key === 'Enter') doSearch(); };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Suchfeld */}
|
||||||
|
<div style={{ display:'flex', gap:8, marginBottom:20 }}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
onKeyDown={onKey}
|
||||||
|
placeholder="Filmtitel eingeben…"
|
||||||
|
style={{
|
||||||
|
flex:1, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.15)',
|
||||||
|
borderRadius:8, padding:'9px 14px', color:'#fff', fontFamily:"'Space Mono',monospace",
|
||||||
|
fontSize:13, outline:'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => doSearch()}
|
||||||
|
disabled={loading || query.trim().length < 2}
|
||||||
|
style={{
|
||||||
|
background:'rgba(78,205,196,0.15)', border:'1px solid rgba(78,205,196,0.3)',
|
||||||
|
color:'#4ecdc4', borderRadius:8, padding:'9px 18px', cursor:'pointer',
|
||||||
|
fontFamily:"'Space Mono',monospace", fontSize:12, fontWeight:700,
|
||||||
|
opacity: loading || query.trim().length < 2 ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '⏳' : '🔍 Suchen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <ErrorBox msg={error} />}
|
||||||
|
|
||||||
|
{results === null && !loading && (
|
||||||
|
<StatusBox>Filmtitel eingeben und Suchen drücken</StatusBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{results !== null && results.length === 0 && (
|
||||||
|
<StatusBox>Keine Filme gefunden für „{query}"</StatusBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{results !== null && results.length > 0 && (
|
||||||
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(130px,1fr))', gap:12 }}>
|
||||||
|
{results.map(m => (
|
||||||
|
<MovieCard key={m.id} movie={m} favIds={favIds} hasXrel={xrelIds?.has(m.id)}
|
||||||
|
onToggleFav={onToggle} onOpenModal={onOpenModal} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Favoriten Kalender ────────────────────────────────────────────────────────
|
// ── Favoriten Kalender ────────────────────────────────────────────────────────
|
||||||
function FavoritenKalender({ onOpenModal }) {
|
function FavoritenKalender({ onOpenModal }) {
|
||||||
const [favs, setFavs] = useState([]);
|
const [favs, setFavs] = useState([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user