From 55da2dbc2d90a1f092fdc8e749beebc662f4b364 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 00:55:09 +0200 Subject: [PATCH] Kommentarsystem: Antworten, Pflichtfelder, Like-Benachrichtigung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Antworten auf Kommentare (Threading mit parent_id) - Pflichtfeld E-Mail ODER Handynummer bei Beiträgen und Kommentaren - Benachrichtigung bei Antworten auf eigene Kommentare (E-Mail/WhatsApp) - Like-Benachrichtigung an Beitragsersteller (E-Mail/WhatsApp) - Verschachtelte Kommentardarstellung mit Antwort-Button - Web-Submit-Overlay um Phone-Feld + Validierung erweitert - DB-Migration: comments.parent_id/email/phone, submissions.phone --- app.py | 272 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 225 insertions(+), 47 deletions(-) diff --git a/app.py b/app.py index ff8f69b..5e56612 100644 --- a/app.py +++ b/app.py @@ -2219,8 +2219,11 @@ def public_frontend(): - + + + +

🔔 E-Mail oder Handynummer erforderlich, damit wir dich bei Antworten & Likes benachrichtigen können.

@@ -2285,6 +2288,7 @@ def public_frontend(): sSend.addEventListener('click', async () => { const name = document.getElementById('subName').value.trim(); const email = document.getElementById('subEmail').value.trim(); + const phone = document.getElementById('subPhone').value.trim(); const text = document.getElementById('subText').value.trim(); const fileInput = document.getElementById('subPhoto'); const file = fileInput.files[0] || document.getElementById('subVideo').files[0] || document.getElementById('subAudio').files[0] || document.getElementById('subGallery').files[0]; @@ -2297,6 +2301,12 @@ def public_frontend(): return; } + if (!email && !phone) { + sStatus.textContent = '⚠️ Bitte E-Mail oder Handynummer angeben, damit wir dich benachrichtigen können.'; + sStatus.style.color = '#dc3545'; + return; + } + sSend.disabled = true; sSend.textContent = '⏳ Wird gesendet...'; sStatus.textContent = ''; @@ -2304,6 +2314,7 @@ def public_frontend(): const formData = new FormData(); if (name) formData.append('name', name); if (email) formData.append('email', email); + if (phone) formData.append('phone', phone); if (text) formData.append('text', text); if (file) formData.append('media', file); if (halle) formData.append('halle', halle); @@ -2326,6 +2337,7 @@ def public_frontend(): sStatus.style.color = '#20228a'; document.getElementById('subName').value = ''; document.getElementById('subEmail').value = ''; + document.getElementById('subPhone').value = ''; document.getElementById('subText').value = ''; fileInput.value = ''; document.getElementById('subVideo').value = ''; @@ -3033,12 +3045,17 @@ def api_submit(): halle = (request.form.get('halle') or '').strip()[:20] spiel_titel = (request.form.get('spiel_titel') or '').strip()[:200] email = (request.form.get('email') or '').strip()[:200] + phone = (request.form.get('phone') or '').strip()[:50] # Require at least text or a file file = request.files.get('media') if not text and (not file or not file.filename): return jsonify({'error': 'Bitte Text oder eine Datei (Foto/Video/Audio) angeben.'}), 400 + # Require email OR phone for notifications + if not email and not phone: + return jsonify({'error': 'Bitte E-Mail-Adresse oder Handynummer angeben, damit wir dich bei Antworten benachrichtigen können.'}), 400 + media_path = None msg_type = 'text' @@ -3113,9 +3130,9 @@ def api_submit(): db.execute( """INSERT INTO submissions - (channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email) - VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?)""", - ('web', sender_id, sender_name, msg_type, text or None, media_path, halle_valid, category, spiel_titel or None, email or None), + (channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone) + VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?, ?)""", + ('web', sender_id, sender_name, msg_type, text or None, media_path, halle_valid, category, spiel_titel or None, email or None, phone or None), ) db.commit() sub_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] @@ -3214,22 +3231,39 @@ def public_detail(sub_id: int): # Comments comment_rows = db.execute( - "SELECT author_name, content, created_at FROM comments WHERE submission_id = ? ORDER BY created_at ASC", + "SELECT id, author_name, content, created_at, parent_id FROM comments WHERE submission_id = ? ORDER BY created_at ASC", (sub_id,), ).fetchall() - comments_html = "" + + # Build nested structure: top-level + children + top_level = [c for c in comment_rows if not c["parent_id"]] + children = {} for c in comment_rows: - c_author = (c["author_name"] or "Anonym").strip() - c_date = c["created_at"] or "" - try: - cd = datetime.strptime(c_date, "%Y-%m-%d %H:%M:%S") - c_date = cd.strftime("%d.%m.%y %H:%M") - except Exception: - pass - comments_html += f"""
+ if c["parent_id"]: + children.setdefault(c["parent_id"], []).append(c) + + def render_comments(comments, indent=0): + html = "" + for c in comments: + c_author = (c["author_name"] or "Anonym").strip() + c_date = c["created_at"] or "" + try: + cd = datetime.strptime(c_date, "%Y-%m-%d %H:%M:%S") + c_date = cd.strftime("%d.%m.%y %H:%M") + except Exception: + pass + margin = "margin-left:1.5rem;" if indent > 0 else "" + html += f"""
{c_author} {c_date}

{c["content"]}

+
""" + # Recursively render children + if c["id"] in children: + html += render_comments(children[c["id"]], indent + 1) + return html + + comments_html = render_comments(top_level) return f""" @@ -3274,6 +3308,12 @@ def public_detail(sub_id: int): .comment-submit-btn:hover {{ background:#161e6e; }} .comment-submit-btn:active {{ transform:scale(0.97); }} .comment-submit-btn:disabled {{ opacity:0.6; pointer-events:none; }} + .reply-btn {{ background:none; border:1px solid #ccc; border-radius:6px; padding:0.2rem 0.6rem; font-size:0.72rem; color:#666; cursor:pointer; margin-top:0.3rem; font-family:inherit; transition:all 0.15s; }} + .reply-btn:hover {{ border-color:#20228a; color:#20228a; background:rgba(32,34,138,0.04); }} + .reply-info {{ font-size:0.75rem; color:#20228a; margin-bottom:0.3rem; font-weight:600; display:none; }} + .comment-email, .comment-phone {{ padding:0.6rem 0.9rem; border:1.5px solid #ddd; border-radius:10px; font-size:0.85rem; font-family:inherit; }} + .comment-email:focus, .comment-phone:focus {{ outline:none; border-color:#20228a; }} + .comment-field-hint {{ font-size:0.7rem; color:#999; margin-top:-0.3rem; margin-bottom:0.3rem; }} .detail-content {{ font-size:1.05rem; line-height:1.7; color:#333; white-space:pre-wrap; word-break:break-word; }} .detail-media {{ margin-top:1.5rem; }} .site-footer {{ text-align:center; padding:2rem; border-top:1px solid #eee; margin-top:3rem; }} @@ -3315,8 +3355,13 @@ def public_detail(sub_id: int):

💬 Kommentare ({len(comment_rows)})

{comments_html}
+
↩ Antwort an × abbrechen
+ +
🔔 Damit wir dich bei Antworten benachrichtigen können (E-Mail oder Nummer erforderlich):
+ +
@@ -3362,6 +3407,19 @@ def public_detail(sub_id: int): .catch(function() {{ btn.disabled = false; }}); }}); }})(); +function replyTo(commentId, authorName) {{ + document.getElementById('commentParentId').value = commentId; + document.getElementById('replyTarget').textContent = authorName; + document.getElementById('replyInfo').style.display = 'block'; + document.getElementById('commentForm').content.focus(); + window.scrollTo({{ top: document.getElementById('commentForm').offsetTop - 100, behavior: 'smooth' }}); +}} +function cancelReply() {{ + document.getElementById('commentParentId').value = ''; + document.getElementById('replyTarget').textContent = ''; + document.getElementById('replyInfo').style.display = 'none'; + return false; +}} function submitComment(event, subId) {{ event.preventDefault(); var form = document.getElementById('commentForm'); @@ -3369,14 +3427,26 @@ function submitComment(event, subId) {{ var btn = form.querySelector('.comment-submit-btn'); var author = form.author.value.trim(); var content = form.content.value.trim(); + var email = form.email.value.trim(); + var phone = form.phone.value.trim(); + var parentId = form.parent_id.value; if (!content) return false; + if (!email && !phone) {{ + status.style.display = 'block'; + status.style.background = '#fff3e0'; + status.style.color = '#e65100'; + status.textContent = '⚠️ Bitte E-Mail oder Handynummer angeben, damit wir dich benachrichtigen können.'; + return false; + }} btn.disabled = true; btn.textContent = 'Wird gesendet...'; status.style.display = 'none'; + var body = {{author: author, content: content, email: email, phone: phone}}; + if (parentId) body.parent_id = parseInt(parentId); fetch('/api/comment/' + subId, {{ method: 'POST', headers: {{'Content-Type': 'application/json'}}, - body: JSON.stringify({{author: author, content: content}}) + body: JSON.stringify(body) }}) .then(function(r) {{ return r.json(); }}) .then(function(data) {{ @@ -3386,6 +3456,7 @@ function submitComment(event, subId) {{ status.style.color = '#2e7d32'; status.textContent = '✅ Kommentar gesendet!'; form.reset(); + cancelReply(); // Reload after short delay to show the new comment setTimeout(function() {{ location.reload(); }}, 1200); }} else {{ @@ -3417,7 +3488,7 @@ function submitComment(event, subId) {{ def api_like(sub_id: int): db = get_db() row = db.execute( - "SELECT id, likes FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", + "SELECT id, likes, channel, sender_id, sender_name, email, phone, spiel_titel FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", (sub_id,), ).fetchone() if not row: @@ -3431,6 +3502,40 @@ def api_like(sub_id: int): db.commit() new_likes = db.execute("SELECT likes FROM submissions WHERE id = ?", (sub_id,)).fetchone()[0] + # Send notification to submission author + base = request.host_url.rstrip("/") + link = f"{base}/beitrag/{sub_id}" + post_label = row["spiel_titel"] or row["sender_name"] or f"Beitrag #{sub_id}" + + # WhatsApp notification + if row["channel"] == "whatsapp" and row["sender_id"]: + try: + msg = f"❤️ *Dein Beitrag wurde geliked!*\n\n{post_label}\n\n👍 {new_likes} Likes\n👉 {link}" + send_whatsapp_message(row["sender_id"], msg) + except Exception: + pass + if row["phone"]: + try: + msg = f"❤️ *Dein Beitrag wurde geliked!*\n\n{post_label}\n\n👍 {new_likes} Likes\n👉 {link}" + send_whatsapp_message(row["phone"], msg) + except Exception: + pass + + # Email notification + if row["email"]: + try: + subject = f"❤️ Dein BSN-Beitrag wurde geliked ({new_likes} Likes)" + body = ( + f"Hallo,\n\n" + f"Dein Beitrag \"{post_label}\" hat jetzt {new_likes} Likes.\n\n" + f"👉 {link}\n\n" + f"Viele Grüße,\n" + f"Boardgame Social / brettspiel-news.de\n" + ) + _send_email(row["email"], subject, body) + except Exception: + pass + resp = jsonify({"likes": new_likes or 0, "already": False}) resp.set_cookie(f"liked_{sub_id}", "1", max_age=365*24*60*60, httponly=True, samesite="Lax") return resp @@ -3440,7 +3545,7 @@ def api_like(sub_id: int): def api_comment(sub_id: int): db = get_db() row = db.execute( - "SELECT id, channel, sender_id, sender_name, email FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", + "SELECT id, channel, sender_id, sender_name, email, phone FROM submissions WHERE id = ? AND status='published' AND deleted_at IS NULL", (sub_id,), ).fetchone() if not row: @@ -3449,49 +3554,122 @@ def api_comment(sub_id: int): data = request.get_json(force=True, silent=True) or {} author = (data.get("author") or "").strip()[:80] content = (data.get("content") or "").strip()[:2000] + email = (data.get("email") or "").strip()[:200] + phone = (data.get("phone") or "").strip()[:50] + parent_id = data.get("parent_id") + if parent_id is not None: + try: + parent_id = int(parent_id) + except (ValueError, TypeError): + parent_id = None + if not content: return jsonify({"error": "Kommentar darf nicht leer sein"}), 400 + # Require email OR phone for notification + if not email and not phone: + return jsonify({"error": "Bitte E-Mail-Adresse oder Handynummer angeben, damit wir dich bei Antworten benachrichtigen können."}), 400 + db.execute( - "INSERT INTO comments (submission_id, author_name, content) VALUES (?, ?, ?)", - (sub_id, author or None, content), + "INSERT INTO comments (submission_id, author_name, content, parent_id, email, phone) VALUES (?, ?, ?, ?, ?, ?)", + (sub_id, author or None, content, parent_id, email or None, phone or None), ) db.commit() + comment_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] # Build notification link base = request.host_url.rstrip("/") link = f"{base}/beitrag/{sub_id}" - # WhatsApp notification (only for WhatsApp submissions) - if row["channel"] == "whatsapp" and row["sender_id"]: - try: - msg = ( - f"💬 *Neuer Kommentar zu deinem Beitrag!*\n\n" - f"{'*' + (author or 'Jemand') + '* schreibt' if author else 'Jemand hat kommentiert'}:\n" - f"_{content[:200]}{'...' if len(content) > 200 else ''}_\n\n" - f"👉 {link}" - ) - send_whatsapp_message(row["sender_id"], msg) - except Exception: - pass # silently ignore notification failures + # 1. Notify parent comment author (if replying to a comment) + if parent_id: + parent = db.execute( + "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ?", + (parent_id, sub_id), + ).fetchone() + if parent: + p_author = parent["author_name"] or "Jemand" + # WhatsApp to parent comment author + if parent["phone"]: + try: + msg = ( + f"💬 *Antwort auf deinen Kommentar!*\n\n" + f"{'*' + (author or 'Jemand') + '* hat geantwortet' if author else 'Jemand hat geantwortet'}:\n" + f"_{content[:200]}{'...' if len(content) > 200 else ''}_\n\n" + f"👉 {link}" + ) + send_whatsapp_message(parent["phone"], msg) + except Exception: + pass + # Email to parent comment author + if parent["email"]: + try: + subject = f"💬 Antwort auf deinen BSN-Kommentar" + body = ( + f"Hallo,\n\n" + f"{author or 'Jemand'} hat auf deinen Kommentar geantwortet:\n\n" + f"\"{content[:300]}{'...' if len(content) > 300 else ''}\"\n\n" + f"👉 {link}\n\n" + f"Viele Grüße,\n" + f"Boardgame Social / brettspiel-news.de\n" + ) + _send_email(parent["email"], subject, body) + except Exception: + pass - # Email notification (only for web submissions with email) - if row["channel"] == "web" and row["email"]: - try: - subject = f"💬 Neuer Kommentar zu deinem BSN-Beitrag #{sub_id}" - body = ( - f"Hallo,\n\n" - f"{author or 'Jemand'} hat deinen Beitrag kommentiert:\n\n" - f"\"{content[:300]}{'...' if len(content) > 300 else ''}\"\n\n" - f"👉 {link}\n\n" - f"Viele Grüße,\n" - f"Boardgame Social / brettspiel-news.de\n" - ) - _send_email(row["email"], subject, body) - except Exception: - pass # silently ignore notification failures + # 2. Notify submission author (unless they're replying to themselves) + notify_submission_author = True + if parent_id: + parent = db.execute( + "SELECT author_name, email, phone FROM comments WHERE id = ? AND submission_id = ?", + (parent_id, sub_id), + ).fetchone() + if parent and (parent["email"] == row["email"] or parent["phone"] == row["phone"]): + notify_submission_author = False # Conversation with themselves - return jsonify({"ok": True}), 201 + if notify_submission_author: + # WhatsApp notification + if row["channel"] == "whatsapp" and row["sender_id"]: + try: + msg = ( + f"💬 *Neuer Kommentar zu deinem Beitrag!*\n\n" + f"{'*' + (author or 'Jemand') + '* schreibt' if author else 'Jemand hat kommentiert'}:\n" + f"_{content[:200]}{'...' if len(content) > 200 else ''}_\n\n" + f"👉 {link}" + ) + send_whatsapp_message(row["sender_id"], msg) + except Exception: + pass + # Email to submission author + if row["email"]: + try: + subject = f"💬 Neuer Kommentar zu deinem BSN-Beitrag #{sub_id}" + body = ( + f"Hallo,\n\n" + f"{author or 'Jemand'} hat deinen Beitrag kommentiert:\n\n" + f"\"{content[:300]}{'...' if len(content) > 300 else ''}\"\n\n" + f"👉 {link}\n\n" + f"Viele Grüße,\n" + f"Boardgame Social / brettspiel-news.de\n" + ) + _send_email(row["email"], subject, body) + except Exception: + pass + + # WhatsApp to submission author via phone + if row["phone"]: + try: + msg = ( + f"💬 *Neuer Kommentar zu deinem Beitrag!*\n\n" + f"{'*' + (author or 'Jemand') + '* schreibt' if author else 'Jemand hat kommentiert'}:\n" + f"_{content[:200]}{'...' if len(content) > 200 else ''}_\n\n" + f"👉 {link}" + ) + send_whatsapp_message(row["phone"], msg) + except Exception: + pass + + return jsonify({"ok": True, "comment_id": comment_id}), 201 if __name__ == "__main__":