From 8425688160fb38605ecf27b61e92584f6b29ff2c Mon Sep 17 00:00:00 2001 From: Dicken Date: Sat, 4 Jul 2026 12:49:19 +0200 Subject: [PATCH] feat: Whiteboard Dot in Liste, Favoriten eingeklappt + Modal bei Klick --- backend/src/tools/whiteboard/routes.js | 7 ++ frontend/src/tools/media.jsx | 162 +++++++++++++++---------- frontend/src/tools/whiteboard.jsx | 21 +++- 3 files changed, 124 insertions(+), 66 deletions(-) diff --git a/backend/src/tools/whiteboard/routes.js b/backend/src/tools/whiteboard/routes.js index 6f9540d..b7c3d06 100644 --- a/backend/src/tools/whiteboard/routes.js +++ b/backend/src/tools/whiteboard/routes.js @@ -76,6 +76,13 @@ router.get('/unread', authenticate, (req, res) => { res.json({ count }); }); +// ── Unread IDs (welche Whiteboards konkret) ─────────────────────────────────── +router.get('/unread-ids', authenticate, (req, res) => { + const me = uid(req); + const rows = db.prepare('SELECT whiteboard_id FROM whiteboard_unread WHERE user_id=?').all(me); + res.json({ ids: rows.map(r => r.whiteboard_id) }); +}); + // ── Liste ───────────────────────────────────────────────────────────────────── router.get('/', authenticate, (req, res) => { const me = uid(req); diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index 7d104c8..4ef0b2f 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -678,6 +678,50 @@ function FavCard({ f, onOpenModal, onRemove }) { } // ── Favoriten Kalender ──────────────────────────────────────────────────────── +// ── Admin User Group mit einklappbaren quittierten Favoriten ───────────────── +function AdminUserGroup({ username, unackedItems, ackedItems, FavRow }) { + const [ackedOpen, setAckedOpen] = useState(false); + const total = unackedItems.length + ackedItems.length; + return ( +
+
+ + 👤 {username} + + + {total} Favorit{total!==1?'en':''} + + {unackedItems.length > 0 && ( + + {unackedItems.length} neu + + )} +
+
+ {unackedItems.map(f => )} +
+ {ackedItems.length > 0 && ( +
+ + {ackedOpen && ( +
+ {ackedItems.map(f => )} +
+ )} +
+ )} +
+ ); +} + function FavoritenKalender({ onOpenModal }) { const [favs, setFavs] = useState([]); const [allFavs, setAllFavs] = useState([]); @@ -712,7 +756,7 @@ function FavoritenKalender({ onOpenModal }) { if (loading) return ⏳ Lädt…; if (error) return ; - // Admin-Ansicht: alle Favoriten nach User gruppiert + // Admin-Ansicht: alle Favoriten nach User gruppiert, quittierte eingeklappt if (isAdmin && adminTab) { const unacked = allFavs.filter(f => !f.acknowledged).length; const userGroups = {}; @@ -720,6 +764,52 @@ function FavoritenKalender({ onOpenModal }) { if (!userGroups[f.username]) userGroups[f.username] = []; userGroups[f.username].push(f); }); + + // FavRow: einzelne Favoriten-Zeile mit Modal-Klick + const FavRow = ({ f }) => ( +
onOpenModal({ id: f.tmdb_id, media_type: f.media_type || 'movie' })}> + {f.poster_path + ? {f.title} + :
+ {f.media_type==='tv'?'📺':'🎬'} +
+ } +
+
+ {f.title} + {f.media_type==='tv' && SERIE} +
+
+ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'} + {!!f.acknowledged && ✓ quittiert} +
+
+ {!f.acknowledged ? ( + + ) : ( + + )} +
+ ); + return (
@@ -744,69 +834,13 @@ function FavoritenKalender({ onOpenModal }) {
{!allFavs.length ? Noch keine Favoriten von irgendjemandem. - : Object.entries(userGroups).map(([username, items]) => ( -
-
- - 👤 {username} - - - {items.length} Favorit{items.length!==1?'en':''} - - {items.filter(f=>!f.acknowledged).length > 0 && ( - - {items.filter(f=>!f.acknowledged).length} neu - - )} -
-
- {items.map(f => ( -
- {f.poster_path - ? {f.title} - :
- {f.media_type==='tv'?'📺':'🎬'} -
- } -
-
- {f.title} - {f.media_type==='tv' && SERIE} -
-
- ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'} - {!!f.acknowledged && } -
-
- {!f.acknowledged ? ( - - ) : ( - - )} -
- ))} -
-
- )) + : Object.entries(userGroups).map(([username, items]) => { + const unackedItems = items.filter(f => !f.acknowledged); + const ackedItems = items.filter(f => f.acknowledged); + return ( + + ); + }) }
); diff --git a/frontend/src/tools/whiteboard.jsx b/frontend/src/tools/whiteboard.jsx index 3784b45..43791b8 100644 --- a/frontend/src/tools/whiteboard.jsx +++ b/frontend/src/tools/whiteboard.jsx @@ -265,6 +265,7 @@ export default function Whiteboard({ toast, mobile }) { // View const [view, setView] = useState('list'); const [boards, setBoards] = useState([]); + const [unreadIds, setUnreadIds] = useState(new Set()); const [loading, setLoading] = useState(true); const [current, setCurrent] = useState(null); const [creating, setCreating] = useState(false); @@ -379,7 +380,16 @@ export default function Whiteboard({ toast, mobile }) { // ── Laden / Speichern ──────────────────────────────────────────────────── const loadBoards = useCallback(() => { setLoading(true); - api('/tools/whiteboard').then(d=>setBoards(d.whiteboards||[])).catch(()=>toast('Fehler','error')).finally(()=>setLoading(false)); + Promise.all([ + api('/tools/whiteboard'), + api('/tools/whiteboard/unread').catch(() => ({ count: 0 })), + ]).then(([d, u]) => { + setBoards(d.whiteboards || []); + // unread count kommt global — wir markieren alle als "möglicherweise unread" + // getGameForUser gibt uns keine IDs, also nutzen wir den globalen count als Indikator + }).catch(() => toast('Fehler', 'error')).finally(() => setLoading(false)); + // Einzelne unread IDs vom Backend holen + api('/tools/whiteboard/unread-ids').then(d => setUnreadIds(new Set(d.ids || []))).catch(() => {}); }, []); const renameBoard = async (id, title) => { @@ -394,6 +404,7 @@ export default function Whiteboard({ toast, mobile }) { useEffect(()=>{ loadBoards(); },[loadBoards]); const openBoard = async (board) => { + setUnreadIds(prev => { const s = new Set(prev); s.delete(board.id); return s; }); try { const d = await api(`/tools/whiteboard/${board.id}/data`); const els = JSON.parse(typeof d.elements==='string'?d.elements:JSON.stringify(d.elements||[])); @@ -684,7 +695,13 @@ export default function Whiteboard({ toast, mobile }) { {boards.map(b=>(
{ if(editingId!==b.id) openBoard(b); }}> -
🖊
+
+ 🖊 + {unreadIds.has(b.id) && ( + + )} +
{editingId===b.id?( setEditTitle(e.target.value)}