feat: Serien-Suche, Admin-Favoriten, Pushover, Dashboard-Badge
This commit is contained in:
@@ -556,6 +556,12 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='mov
|
|||||||
db.exec("ALTER TABLE movie_favorites ADD COLUMN genres TEXT NOT NULL DEFAULT '[]'");
|
db.exec("ALTER TABLE movie_favorites ADD COLUMN genres TEXT NOT NULL DEFAULT '[]'");
|
||||||
if (!cols.includes('fsk'))
|
if (!cols.includes('fsk'))
|
||||||
db.exec("ALTER TABLE movie_favorites ADD COLUMN fsk TEXT NOT NULL DEFAULT ''");
|
db.exec("ALTER TABLE movie_favorites ADD COLUMN fsk TEXT NOT NULL DEFAULT ''");
|
||||||
|
if (!cols.includes('acknowledged'))
|
||||||
|
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged INTEGER NOT NULL DEFAULT 0");
|
||||||
|
if (!cols.includes('acknowledged_at'))
|
||||||
|
db.exec("ALTER TABLE movie_favorites ADD COLUMN acknowledged_at DATETIME DEFAULT NULL");
|
||||||
|
if (!cols.includes('media_type'))
|
||||||
|
db.exec("ALTER TABLE movie_favorites ADD COLUMN media_type TEXT NOT NULL DEFAULT 'movie'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// xREL Badge Cache (persistent, überlebt Container-Restarts)
|
// xREL Badge Cache (persistent, überlebt Container-Restarts)
|
||||||
|
|||||||
@@ -320,4 +320,11 @@ router.delete('/push-schedules/:id', authenticate, (req, res) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Unquittierte Favoriten für Admin-Dashboard-Badge
|
||||||
|
router.get('/media-favorites/unacked', authenticate, (req, res) => {
|
||||||
|
if (req.user.role !== 'admin') return res.json({ count: 0 });
|
||||||
|
const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get();
|
||||||
|
res.json({ count: count.n });
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -320,25 +320,35 @@ router.get('/search', authenticate, async (req, res) => {
|
|||||||
const searchParams = { query: q, include_adult: 'false' };
|
const searchParams = { query: q, include_adult: 'false' };
|
||||||
if (yearFilter) searchParams.year = yearFilter;
|
if (yearFilter) searchParams.year = yearFilter;
|
||||||
|
|
||||||
const [results, genreMap] = await Promise.all([
|
const [movieResults, tvResults, genreMap] = await Promise.all([
|
||||||
tmdb('/search/movie', searchParams),
|
tmdb('/search/movie', searchParams),
|
||||||
|
tmdb('/search/tv', { query: q, include_adult: 'false', ...(yearFilter ? { first_air_date_year: yearFilter } : {}) }),
|
||||||
getGenreMap(),
|
getGenreMap(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const filtered = (results.results || [])
|
const movies = (movieResults.results || [])
|
||||||
.filter(m => {
|
.filter(m => { if (BLOCKED_LANGS.has(m.original_language)) return false; if (seen.has('m'+m.id)) return false; seen.add('m'+m.id); return true; })
|
||||||
if (BLOCKED_LANGS.has(m.original_language)) return false;
|
.map(m => ({ ...enrichMovie(m, genreMap, null, null), media_type: 'movie' }));
|
||||||
if (seen.has(m.id)) return false;
|
|
||||||
seen.add(m.id);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.slice(0, 20)
|
|
||||||
.map(m => enrichMovie(m, genreMap, null, null));
|
|
||||||
|
|
||||||
// Neueste zuerst (Backend sortiert, Frontend sortiert auch – konsistent)
|
const tvShows = (tvResults.results || [])
|
||||||
filtered.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''));
|
.filter(m => { if (BLOCKED_LANGS.has(m.original_language)) return false; if (seen.has('t'+m.id)) return false; seen.add('t'+m.id); return true; })
|
||||||
res.json(filtered);
|
.map(m => ({
|
||||||
|
id: m.id, media_type: 'tv',
|
||||||
|
title: m.name || m.original_name,
|
||||||
|
original_title: m.original_name,
|
||||||
|
release_date: m.first_air_date || '',
|
||||||
|
poster_path: m.poster_path || '',
|
||||||
|
genres: (m.genre_ids||[]).map(id => genreMap[id]).filter(Boolean),
|
||||||
|
vote_average: m.vote_average,
|
||||||
|
original_language: m.original_language,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const combined = [...movies, ...tvShows]
|
||||||
|
.sort((a,b) => (b.release_date||'').localeCompare(a.release_date||''))
|
||||||
|
.slice(0, 30);
|
||||||
|
|
||||||
|
res.json(combined);
|
||||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -436,18 +446,71 @@ router.get('/favorites', authenticate, (req, res) => {
|
|||||||
res.json(db.prepare('SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC').all(req.user.id));
|
res.json(db.prepare('SELECT * FROM movie_favorites WHERE user_id=? ORDER BY release_date ASC').all(req.user.id));
|
||||||
});
|
});
|
||||||
router.post('/favorites', authenticate, (req, res) => {
|
router.post('/favorites', authenticate, (req, res) => {
|
||||||
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk } = req.body;
|
const { tmdb_id, title, poster_path, release_date_de, release_date, genres, fsk, media_type } = 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 {
|
||||||
db.prepare(`INSERT OR IGNORE INTO movie_favorites (user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk) VALUES (?,?,?,?,?,?,?,?)`)
|
const r = db.prepare(`INSERT OR IGNORE INTO movie_favorites
|
||||||
.run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'', release_date||'', JSON.stringify(genres||[]), fsk||'');
|
(user_id,tmdb_id,title,poster_path,release_date_de,release_date,genres,fsk,media_type,added_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`)
|
||||||
|
.run(req.user.id, tmdb_id, title||'', poster_path||'', release_date_de||'', release_date||'', JSON.stringify(genres||[]), fsk||'', media_type||'movie');
|
||||||
|
|
||||||
|
if (r.changes > 0) {
|
||||||
|
// Pushover-Benachrichtigung an Admin (fire-and-forget, nie emergency)
|
||||||
|
try {
|
||||||
|
const pushToken = db.prepare("SELECT value FROM admin_settings WHERE key='pushover_token'").get()?.value;
|
||||||
|
const pushUser = db.prepare("SELECT value FROM admin_settings WHERE key='pushover_user'").get()?.value;
|
||||||
|
const user = db.prepare('SELECT username FROM users WHERE id=?').get(req.user.id);
|
||||||
|
if (pushToken && pushUser) {
|
||||||
|
fetch('https://api.pushover.net/1/messages.json', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: pushToken, user: pushUser,
|
||||||
|
message: `${user?.username || 'Jemand'} hat "${title}" zur Merkliste hinzugefügt`,
|
||||||
|
title: '⭐ Neue Favoriten',
|
||||||
|
priority: 0,
|
||||||
|
}),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch(e) { res.status(500).json({ error: e.message }); }
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/favorites/:tmdb_id', authenticate, (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));
|
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 });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin: alle Favoriten aller User
|
||||||
|
router.get('/favorites/all', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const favs = db.prepare(`
|
||||||
|
SELECT f.*, u.username
|
||||||
|
FROM movie_favorites f
|
||||||
|
JOIN users u ON u.id = f.user_id
|
||||||
|
ORDER BY f.acknowledged ASC, f.added_at DESC
|
||||||
|
`).all();
|
||||||
|
res.json(favs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin: Favoritenanzahl unquittiert (für Dashboard)
|
||||||
|
router.get('/favorites/unacked', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get();
|
||||||
|
res.json({ count: count.n });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin: Favorit quittieren
|
||||||
|
router.post('/favorites/:id/acknowledge', authenticate, requireAdmin, (req, res) => {
|
||||||
|
db.prepare('UPDATE movie_favorites SET acknowledged=1, acknowledged_at=CURRENT_TIMESTAMP WHERE id=?').run(req.params.id);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin: alle quittieren
|
||||||
|
router.post('/favorites/acknowledge-all', authenticate, requireAdmin, (req, res) => {
|
||||||
|
db.prepare('UPDATE movie_favorites SET acknowledged=1, acknowledged_at=CURRENT_TIMESTAMP WHERE acknowledged=0').run();
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
router.get('/settings', authenticate, requireAdmin, (req, res) => res.json({ configured: !!getToken() }));
|
router.get('/settings', authenticate, requireAdmin, (req, res) => res.json({ configured: !!getToken() }));
|
||||||
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||||||
const { tmdb_token } = req.body;
|
const { tmdb_token } = req.body;
|
||||||
|
|||||||
@@ -1903,7 +1903,7 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
|
function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, unackedFavs=0, onAckFavs, user }) {
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
const [showBoard, setShowBoard] = useState(false);
|
const [showBoard, setShowBoard] = useState(false);
|
||||||
const [showChangelog, setShowChangelog] = useState(false);
|
const [showChangelog, setShowChangelog] = useState(false);
|
||||||
@@ -1981,6 +1981,16 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr
|
|||||||
borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 5px #ffe66d' }}/>
|
borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 5px #ffe66d' }}/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Admin: unquittierte Favoriten */}
|
||||||
|
{user?.role==='admin' && unackedFavs > 0 && (
|
||||||
|
<button onClick={()=>{ setActive('media'); onAckFavs(); }} style={{
|
||||||
|
background:'rgba(245,158,11,0.12)', border:'1px solid rgba(245,158,11,0.35)',
|
||||||
|
borderRadius:7, color:'#f59e0b', cursor:'pointer',
|
||||||
|
padding:'4px 10px', fontSize:9, fontFamily:'monospace', letterSpacing:1,
|
||||||
|
}}>
|
||||||
|
⭐ {unackedFavs} neue Favorit{unackedFavs!==1?'en':''}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -3608,6 +3618,7 @@ export default function App() {
|
|||||||
const [unreadBoard, setUnreadBoard] = useState(0);
|
const [unreadBoard, setUnreadBoard] = useState(0);
|
||||||
const [unreadPromoted, setUnreadPromoted] = useState(0);
|
const [unreadPromoted, setUnreadPromoted] = useState(0);
|
||||||
const [unreadChangelog,setUnreadChangelog]= useState(0);
|
const [unreadChangelog,setUnreadChangelog]= useState(0);
|
||||||
|
const [unackedFavs, setUnackedFavs] = useState(0);
|
||||||
|
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) {
|
|||||||
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
||||||
justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>🎬</div>
|
justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>🎬</div>
|
||||||
}
|
}
|
||||||
|
{/* TV-Badge */}
|
||||||
|
{movie.media_type === 'tv' && (
|
||||||
|
<div style={{ position:'absolute', top:6, left:6, background:'rgba(96,165,250,0.85)',
|
||||||
|
borderRadius:5, padding:'2px 7px', fontSize:9, fontWeight:700,
|
||||||
|
fontFamily:'monospace', color:'#fff', letterSpacing:1 }}>
|
||||||
|
SERIE
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Top-Badge */}
|
{/* Top-Badge */}
|
||||||
{isTop && (
|
{isTop && (
|
||||||
<div style={{ position:'absolute', bottom:6, left:6, background:'rgba(0,0,0,0.85)',
|
<div style={{ position:'absolute', bottom:6, left:6, background:'rgba(0,0,0,0.85)',
|
||||||
@@ -257,6 +265,7 @@ function useFavIds() {
|
|||||||
release_date: movie.release_date || '',
|
release_date: movie.release_date || '',
|
||||||
release_date_de: fmtDE(movie.release_date),
|
release_date_de: fmtDE(movie.release_date),
|
||||||
genres: movie.genres || [], fsk: movie.fsk || '',
|
genres: movie.genres || [], fsk: movie.fsk || '',
|
||||||
|
media_type: movie.media_type || 'movie',
|
||||||
}});
|
}});
|
||||||
setFavIds(prev => new Set([...prev, movie.id]));
|
setFavIds(prev => new Set([...prev, movie.id]));
|
||||||
}
|
}
|
||||||
@@ -422,7 +431,7 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
onKeyDown={onKey}
|
onKeyDown={onKey}
|
||||||
placeholder="Filmtitel eingeben…"
|
placeholder="Film- oder Serientitel eingeben…"
|
||||||
style={{
|
style={{
|
||||||
flex:1, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.15)',
|
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",
|
borderRadius:8, padding:'9px 14px', color:'#fff', fontFamily:"'Space Mono',monospace",
|
||||||
@@ -468,14 +477,28 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
|
|||||||
// ── Favoriten Kalender ────────────────────────────────────────────────────────
|
// ── Favoriten Kalender ────────────────────────────────────────────────────────
|
||||||
function FavoritenKalender({ onOpenModal }) {
|
function FavoritenKalender({ onOpenModal }) {
|
||||||
const [favs, setFavs] = useState([]);
|
const [favs, setFavs] = useState([]);
|
||||||
|
const [allFavs, setAllFavs] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [adminTab, setAdminTab]= useState(false);
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true); setError('');
|
setLoading(true); setError('');
|
||||||
api('/tools/media/favorites').then(setFavs).catch(e => setError(e.message)).finally(() => setLoading(false));
|
Promise.all([
|
||||||
|
api('/tools/media/favorites'),
|
||||||
|
api('/tools/media/favorites/all').catch(() => null),
|
||||||
|
]).then(([mine, all]) => {
|
||||||
|
setFavs(mine);
|
||||||
|
if (all) { setAllFavs(all); setIsAdmin(true); }
|
||||||
|
}).catch(e => setError(e.message)).finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const ackAll = async () => {
|
||||||
|
await api('/tools/media/favorites/acknowledge-all', { method:'POST', body:{} });
|
||||||
|
setAllFavs(prev => prev.map(f => ({ ...f, acknowledged: 1 })));
|
||||||
|
};
|
||||||
|
|
||||||
const removeFav = async (tmdbId) => {
|
const removeFav = async (tmdbId) => {
|
||||||
try {
|
try {
|
||||||
await api(`/tools/media/favorites/${tmdbId}`, { method:'DELETE' });
|
await api(`/tools/media/favorites/${tmdbId}`, { method:'DELETE' });
|
||||||
@@ -485,7 +508,79 @@ function FavoritenKalender({ onOpenModal }) {
|
|||||||
|
|
||||||
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
||||||
if (error) return <ErrorBox msg={error} />;
|
if (error) return <ErrorBox msg={error} />;
|
||||||
if (!favs.length) return <StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>;
|
|
||||||
|
// Admin-Ansicht: alle Favoriten aller User
|
||||||
|
if (isAdmin && adminTab) {
|
||||||
|
const unacked = allFavs.filter(f => !f.acknowledged).length;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{display:'flex',gap:8,marginBottom:16,alignItems:'center'}}>
|
||||||
|
<button onClick={()=>setAdminTab(false)}
|
||||||
|
style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',
|
||||||
|
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
|
||||||
|
← Meine Favoriten
|
||||||
|
</button>
|
||||||
|
{unacked > 0 && (
|
||||||
|
<button onClick={ackAll}
|
||||||
|
style={{background:'rgba(245,158,11,0.12)',border:'1px solid rgba(245,158,11,0.35)',
|
||||||
|
borderRadius:7,padding:'5px 12px',color:'#f59e0b',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
|
||||||
|
✓ Alle {unacked} quittieren
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{display:'flex',flexDirection:'column',gap:6}}>
|
||||||
|
{allFavs.map(f => (
|
||||||
|
<div key={f.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',
|
||||||
|
background: !f.acknowledged ? 'rgba(245,158,11,0.06)' : 'rgba(255,255,255,0.02)',
|
||||||
|
border:`1px solid ${!f.acknowledged ? 'rgba(245,158,11,0.25)' : 'rgba(255,255,255,0.06)'}`,
|
||||||
|
borderRadius:8}}>
|
||||||
|
{f.poster_path
|
||||||
|
? <img src={POSTER+f.poster_path} alt={f.title} style={{width:32,height:48,objectFit:'cover',borderRadius:4,flexShrink:0}}/>
|
||||||
|
: <div style={{width:32,height:48,background:'#111',borderRadius:4,flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',fontSize:14}}>
|
||||||
|
{f.media_type==='tv'?'📺':'🎬'}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div style={{flex:1,minWidth:0}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.85)',fontFamily:'monospace',fontSize:12,
|
||||||
|
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
|
||||||
|
{f.title}
|
||||||
|
{f.media_type==='tv' && <span style={{marginLeft:6,color:'#60a5fa',fontSize:9}}>SERIE</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||||||
|
👤 {f.username} · ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
|
||||||
|
{!!f.acknowledged && <span style={{marginLeft:6,color:'rgba(255,255,255,0.2)'}}>✓ quittiert</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!f.acknowledged && (
|
||||||
|
<button onClick={async(e)=>{e.stopPropagation();
|
||||||
|
await api(`/tools/media/favorites/${f.id}/acknowledge`,{method:'POST',body:{}});
|
||||||
|
setAllFavs(prev=>prev.map(x=>x.id===f.id?{...x,acknowledged:1}:x));
|
||||||
|
}}
|
||||||
|
style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',
|
||||||
|
borderRadius:6,padding:'3px 10px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
|
||||||
|
✓
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!allFavs.length && <StatusBox>Noch keine Favoriten von irgendjemandem.</StatusBox>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!favs.length) return (
|
||||||
|
<div>
|
||||||
|
{isAdmin && (
|
||||||
|
<button onClick={()=>setAdminTab(true)}
|
||||||
|
style={{marginBottom:12,background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
|
||||||
|
👥 Alle Favoriten ({allFavs.length})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const groups = {};
|
const groups = {};
|
||||||
for (const f of favs) {
|
for (const f of favs) {
|
||||||
@@ -498,6 +593,20 @@ function FavoritenKalender({ onOpenModal }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display:'flex', flexDirection:'column', gap:24 }}>
|
<div style={{ display:'flex', flexDirection:'column', gap:24 }}>
|
||||||
|
{isAdmin && (
|
||||||
|
<button onClick={()=>setAdminTab(true)}
|
||||||
|
style={{marginBottom:4,background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||||||
|
display:'flex',alignItems:'center',gap:6,width:'fit-content'}}>
|
||||||
|
👥 Alle Favoriten ({allFavs.length})
|
||||||
|
{allFavs.filter(f=>!f.acknowledged).length > 0 && (
|
||||||
|
<span style={{background:'rgba(245,158,11,0.2)',border:'1px solid rgba(245,158,11,0.4)',
|
||||||
|
borderRadius:10,padding:'1px 7px',color:'#f59e0b',fontSize:9}}>
|
||||||
|
{allFavs.filter(f=>!f.acknowledged).length} neu
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
|
{Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>
|
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user