diff --git a/backend/src/index.js b/backend/src/index.js
index 8842041..c16df2f 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -69,7 +69,6 @@ fs.readdirSync(toolsDir, { withFileTypes: true })
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
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'));
// Γffentliche Upload-Seite: React-App ausliefern β App erkennt /u/ Pfad selbst
diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js
index 66a8c34..029268b 100644
--- a/backend/src/tools/media/routes.js
+++ b/backend/src/tools/media/routes.js
@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('../../db');
-const { requireAdmin } = require('../../middleware/auth');
+const { authenticate, requireAdmin } = require('../../middleware/auth');
const TMDB_BASE = 'https://api.themoviedb.org/3';
@@ -23,8 +23,7 @@ async function tmdb(path, params = {}) {
return res.json();
}
-// GET /api/tools/media/now-playing
-router.get('/now-playing', async (req, res) => {
+router.get('/now-playing', authenticate, async (req, res) => {
try {
const data = await tmdb('/movie/now_playing');
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', async (req, res) => {
+router.get('/upcoming', authenticate, async (req, res) => {
try {
const data = await tmdb('/movie/upcoming');
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', (req, res) => {
+router.get('/favorites', authenticate, (req, res) => {
const rows = db.prepare(
'SELECT * FROM movie_favorites WHERE user_id=? ORDER BY added_at DESC'
).all(req.user.id);
res.json(rows);
});
-// POST /api/tools/media/favorites
-router.post('/favorites', (req, res) => {
+router.post('/favorites', authenticate, (req, res) => {
const { tmdb_id, title, poster_path, release_date_de } = req.body;
if (!tmdb_id) return res.status(400).json({ error: 'tmdb_id fehlt' });
try {
@@ -65,21 +61,18 @@ router.post('/favorites', (req, res) => {
}
});
-// DELETE /api/tools/media/favorites/:tmdb_id
-router.delete('/favorites/:tmdb_id', (req, res) => {
+router.delete('/favorites/:tmdb_id', authenticate, (req, res) => {
db.prepare('DELETE FROM movie_favorites WHERE user_id=? AND tmdb_id=?')
.run(req.user.id, Number(req.params.tmdb_id));
res.json({ ok: true });
});
-// GET /api/tools/media/settings (admin only)
-router.get('/settings', requireAdmin, (req, res) => {
+router.get('/settings', authenticate, requireAdmin, (req, res) => {
const token = getToken();
res.json({ configured: !!token });
});
-// PUT /api/tools/media/settings (admin only)
-router.put('/settings', requireAdmin, (req, res) => {
+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());
diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx
index 0cf3c9b..f82b3c6 100644
--- a/frontend/src/tools/media.jsx
+++ b/frontend/src/tools/media.jsx
@@ -1,40 +1,113 @@
-import { useState, useEffect, useCallback } from 'react';
-import { api, S } from '../lib.js';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { S, api } from '../lib.js';
const POSTER = 'https://image.tmdb.org/t/p/w342';
-const TABS = [
- { id: 'kino', label: 'π¬ Kino' },
- { id: 'demnΓ€chst', label: 'π DemnΓ€chst' },
- { id: 'favoriten', label: 'β€ Favoriten' },
+
+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 },
];
-export default function Media() {
- const [tab, setTab] = useState('kino');
+const getIcon = lbl => [...lbl][0] ?? 'π¬';
+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 (
-
-
- π¬ Media
-
-
-
- {TABS.map(t => (
-
- ))}
+
+
+
Media
+ Kino Β· Vorschau Β· Favoriten
- {tab === 'kino' &&
}
- {tab === 'demnΓ€chst' &&
}
- {tab === 'favoriten' &&
}
+ {/* Tool-Auswahl */}
+ {mobile ? (
+
+
+ {chipScroll.left && (
+
+ )}
+
+ {TOOL_DEFS.map(t => (
+
+ ))}
+
+ {chipScroll.right && (
+
+ )}
+
+
+ ) : (
+
+ {TOOL_DEFS.map(t => (
+
+ ))}
+
+ )}
+
+ {/* Inhalts-Card */}
+
+
+
{active?.label}
+
{active?.desc}
+
+
+ {activeTool === 'kino' &&
}
+ {activeTool === 'demnΓ€chst' &&
}
+ {activeTool === 'favoriten' &&
}
+
);
}
@@ -49,41 +122,37 @@ function MovieCard({ movie, favIds, onToggleFav }) {
return (
{movie.poster_path
?

:
π¬
+ justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>π¬
}
{movie.vote_average > 0 && (
- β
{movie.vote_average.toFixed(1)}
-
+ }}>β
{movie.vote_average.toFixed(1)}
)}
+ position:'absolute', top:5, right:5, background:'rgba(0,0,0,0.65)',
+ border:'none', borderRadius:'50%', width:30, height:30, cursor:'pointer',
+ fontSize:14, display:'flex', alignItems:'center', justifyContent:'center',
+ }}>{isFav ? 'β€' : 'π€'}
-
-
+
- {movie.title}
-
+ display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
+ }}>{movie.title}
{releaseDE && (
-
+
{releaseDE}
)}
@@ -92,8 +161,8 @@ function MovieCard({ movie, favIds, onToggleFav }) {
);
}
-// ββ Grid (now-playing / upcoming) βββββββββββββββββββββββββββββββββββββββββββββ
-function MovieGrid({ endpoint, emptyText }) {
+// ββ Kino/DemnΓ€chst Grid βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function KinoGrid({ endpoint, emptyText }) {
const [movies, setMovies] = useState([]);
const [favIds, setFavIds] = useState(new Set());
const [loading, setLoading] = useState(true);
@@ -134,21 +203,23 @@ function MovieGrid({ endpoint, emptyText }) {
};
if (loading) return
β³ LΓ€dtβ¦;
- if (error) return
- β {error}
- {error.includes('Token') && (
-
- β Einstellungen β Benutzer β TMDB API (KINO-FEATURE)
-
- )}
- ;
+ if (error) return (
+
+ β {error}
+ {error.includes('Token') && (
+
+ Token eintragen: Einstellungen β Benutzer β TMDB API
+
+ )}
+
+ );
if (!movies.length) return
{emptyText};
return (
{movies.map(m => (
@@ -157,8 +228,8 @@ function MovieGrid({ endpoint, emptyText }) {
);
}
-// ββ Favoriten βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-function FavoritenTab() {
+// ββ Favoriten Grid ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function FavoritenGrid() {
const [favs, setFavs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -180,33 +251,33 @@ function FavoritenTab() {
if (!favs.length) return
Noch keine Favoriten β markiere Filme mit β€;
return (
-
+
{favs.map(f => (
{f.poster_path
?

:
π¬
+ justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>π¬
}
-
-
+
- {f.title}
-
+ display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
+ }}>{f.title}
{f.release_date_de && (
-
+
π©πͺ {f.release_date_de}
)}
@@ -220,9 +291,9 @@ function FavoritenTab() {
function StatusBox({ children, color }) {
return (
{children}