diff --git a/backend/routes/forum.py b/backend/routes/forum.py index d75cf28..6f312c9 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -37,6 +37,13 @@ class ThreadPatch(BaseModel): is_pinned: Optional[int] = None is_locked: Optional[int] = None +class ThreadUpdate(BaseModel): + titel: Optional[str] = None + text: Optional[str] = None + +class PostUpdate(BaseModel): + text: str + class LikeBody(BaseModel): target_type: str # 'thread' | 'post' target_id: int @@ -330,6 +337,38 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current return pd +# ------------------------------------------------------------------ +# PATCH /api/forum/threads/{id}/content — Besitzer: titel/text +# ------------------------------------------------------------------ +@router.patch("/threads/{thread_id}/content") +async def update_thread_content(thread_id: int, data: ThreadUpdate, user=Depends(get_current_user)): + with db() as conn: + t = conn.execute("SELECT user_id FROM forum_threads WHERE id=?", (thread_id,)).fetchone() + if not t: raise HTTPException(404, "Thread nicht gefunden.") + if t["user_id"] != user["id"]: raise HTTPException(403, "Nicht dein Beitrag.") + updates, vals = [], [] + if data.titel is not None and data.titel.strip(): + updates.append("titel=?"); vals.append(data.titel.strip()) + if data.text is not None: + updates.append("text=?"); vals.append(data.text.strip()) + if not updates: return {"ok": True} + conn.execute(f"UPDATE forum_threads SET {','.join(updates)} WHERE id=?", (*vals, thread_id)) + return {"ok": True} + + +# ------------------------------------------------------------------ +# PATCH /api/forum/posts/{id} — Besitzer: text +# ------------------------------------------------------------------ +@router.patch("/posts/{post_id}") +async def update_post(post_id: int, data: PostUpdate, user=Depends(get_current_user)): + with db() as conn: + p = conn.execute("SELECT user_id FROM forum_posts WHERE id=?", (post_id,)).fetchone() + if not p: raise HTTPException(404, "Beitrag nicht gefunden.") + if p["user_id"] != user["id"]: raise HTTPException(403, "Nicht dein Beitrag.") + conn.execute("UPDATE forum_posts SET text=? WHERE id=?", (data.text.strip(), post_id)) + return {"ok": True} + + # ------------------------------------------------------------------ # DELETE /api/forum/posts/{id} # ------------------------------------------------------------------ diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css index e96a858..0737819 100644 --- a/backend/static/css/layout.css +++ b/backend/static/css/layout.css @@ -235,6 +235,7 @@ justify-content: center; padding: 0 3px; border: 2px solid var(--c-surface); + pointer-events: none; } .nav-item-label { diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 03f8efd..2065553 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -320,6 +320,8 @@ const API = (() => { patchThread(id, data) { return patch(`/forum/threads/${id}`, data); }, addPost(threadId, data){ return post(`/forum/threads/${threadId}/posts`, data); }, deletePost(id) { return del(`/forum/posts/${id}`); }, + updateThread(id, data) { return patch(`/forum/threads/${id}/content`, data); }, + updatePost(id, data) { return patch(`/forum/posts/${id}`, data); }, uploadThreadFoto(id, file) { const fd = new FormData(); fd.append('file', file); return upload(`/forum/threads/${id}/fotos`, fd); diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b49f2b3..6ebc64f 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '214'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '215'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { @@ -427,6 +427,15 @@ const App = (() => { _updateNotifBadge(); _updateChatBadge(); setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + _updateNotifBadge(); + _updateChatBadge(); + if (state.page === 'chat') { + pages['chat']?.module?.refresh?.(); + } + } + }); const pendingInvite = sessionStorage.getItem('pending_invite'); if (pendingInvite) { diff --git a/backend/static/js/pages/chat.js b/backend/static/js/pages/chat.js index 32dc179..51c3a42 100644 --- a/backend/static/js/pages/chat.js +++ b/backend/static/js/pages/chat.js @@ -446,9 +446,15 @@ window.Page_chat = (() => { }); } + async function refresh() { + await _loadList(); + await _updateChatBadge(); + } + // ---------------------------------------------------------- return { init, + refresh, _showList, _openThread, _send, diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index a6dd3bd..c7ccb88 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -387,6 +387,7 @@ window.Page_forum = (() => { const footer = _appState.user ? ` ${(isOwn || isMod) ? `` : ''} + ${isOwn ? `` : ''} ${(!thread.is_locked && _appState.user) ? `` : ''} ` : ``; @@ -435,6 +436,11 @@ window.Page_forum = (() => { } catch (err) { UI.toast.error(err.message); } }); + // Edit thread (owner) + document.getElementById('ft-edit-thread')?.addEventListener('click', () => { + _showEditThreadModal(thread); + }); + // Moderator: pin/lock/delete document.querySelector('.forum-mod-pin')?.addEventListener('click', async () => { try { @@ -564,7 +570,8 @@ window.Page_forum = (() => { ${UI.icon('heart')} ${p.likes || 0} ${(!isOwn && uid) ? `` : ''} - ${canDelete ? `` : ''} + ${isOwn ? `` : ''} + ${canDelete ? `` : ''} `; } @@ -596,6 +603,16 @@ window.Page_forum = (() => { }); }); + // Edit (owner) + container.querySelectorAll('.forum-post-edit:not([data-bound])').forEach(btn => { + btn.dataset.bound = '1'; + btn.addEventListener('click', () => { + const postId = parseInt(btn.dataset.postId); + const currentText = btn.dataset.text || ''; + _showEditPostModal(postId, currentText, container, threadId, uid, isMod); + }); + }); + // Delete container.querySelectorAll('.forum-post-delete:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; @@ -995,6 +1012,78 @@ window.Page_forum = (() => { }); } + // ---------------------------------------------------------- + // Post bearbeiten (Besitzer) + // ---------------------------------------------------------- + function _showEditPostModal(postId, currentText, container, threadId, uid, isMod) { + const id = 'forum-edit-post-form'; + UI.modal.open({ + title: 'Antwort bearbeiten', + body: `
`, + footer: ` + + `, + }); + document.getElementById(id)?.addEventListener('submit', async e => { + e.preventDefault(); + const text = new FormData(e.target).get('text')?.trim(); + if (!text) return; + const btn = document.querySelector(`[form="${id}"][type="submit"]`); + await UI.asyncButton(btn, async () => { + await API.forum.updatePost(postId, { text }); + UI.modal.close(); + // Post-Text im DOM aktualisieren + if (container) { + const postEl = container.querySelector(`[data-post-id="${postId}"]`); + const textEl = postEl?.querySelector('.forum-post-text'); + if (textEl) textEl.textContent = text; + // data-text auf Edit-Button aktualisieren + const editBtn = postEl?.querySelector('.forum-post-edit'); + if (editBtn) editBtn.dataset.text = text; + } + UI.toast.success('Antwort aktualisiert.'); + }); + }); + } + + // ---------------------------------------------------------- + // Thread bearbeiten (Besitzer) + // ---------------------------------------------------------- + function _showEditThreadModal(thread) { + const id = 'forum-edit-thread-form'; + UI.modal.open({ + title: 'Beitrag bearbeiten', + body: ``, + footer: ` + + `, + }); + document.getElementById(id)?.addEventListener('submit', async e => { + e.preventDefault(); + const fd = new FormData(e.target); + const btn = document.querySelector(`[form="${id}"][type="submit"]`); + await UI.asyncButton(btn, async () => { + await API.forum.updateThread(thread.id, { titel: fd.get('titel'), text: fd.get('text') }); + UI.modal.close(); + _loadThreads(true); + UI.toast.success('Beitrag aktualisiert.'); + }); + }); + } + function openNew() { if (!_appState?.user) { UI.toast.info('Bitte erst anmelden.'); return; } _showCreateForm(); diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 4bf6b4d..0310dee 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -742,7 +742,8 @@ window.Page_routes = (() => {