Herz-Icon (monochrom BSN-Blau) + Kommentar-Likes
- Like-Icon: Thumbs-Up → modernes Herz-Outline (Feather-Style) in #20228a - Alle 4 Stellen ersetzt (Frontpage Cards, Card-JS, Detailseite, Detail-JS) - DB-Migration: ALTER TABLE comments ADD COLUMN likes - API: POST /api/comment-like/<id> mit Cookie-Dedup - Like-Button in jedem Kommentar (clike-count) - CSS .comment-like-btn analog zu .stat-like-btn - JS: localStorage likedComments für Persistenz - SQL-Query: comments SELECT um likes erweitert
This commit is contained in:
@@ -2882,7 +2882,7 @@ def public_frontend():
|
|||||||
<div class="card-stats">
|
<div class="card-stats">
|
||||||
<span class="stat-views"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>""" + str(r['views'] or 0) + """</span>
|
<span class="stat-views"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>""" + str(r['views'] or 0) + """</span>
|
||||||
<span class="stat-comments"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""" + str(r['comment_count'] or 0) + """</span>
|
<span class="stat-comments"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""" + str(r['comment_count'] or 0) + """</span>
|
||||||
<button class="stat-like-btn" data-id=""" + str(r['id']) + """ data-likes=""" + str(r['likes'] or 0) + """><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11h11.5a2.5 2.5 0 0 1 2.5 2.5v.5a2.5 2.5 0 0 1-2.5 2.5h-2.5"/><path d="M16 16.5V19a3 3 0 0 1-6 1.5L7 11"/></svg> """ + str(r['likes'] or 0) + """</button>
|
<button class="stat-like-btn" data-id=""" + str(r['id']) + """ data-likes=""" + str(r['likes'] or 0) + """><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:2px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> """ + str(r['likes'] or 0) + """</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="story-meta">
|
<div class="story-meta">
|
||||||
<span class="story-type">""" + type_emoji + """</span>
|
<span class="story-type">""" + type_emoji + """</span>
|
||||||
@@ -2902,7 +2902,7 @@ def public_frontend():
|
|||||||
<div class="card-stats">
|
<div class="card-stats">
|
||||||
<span class="stat-views"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>""" + str(r['views'] or 0) + """</span>
|
<span class="stat-views"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>""" + str(r['views'] or 0) + """</span>
|
||||||
<span class="stat-comments"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""" + str(r['comment_count'] or 0) + """</span>
|
<span class="stat-comments"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>""" + str(r['comment_count'] or 0) + """</span>
|
||||||
<button class="stat-like-btn" data-id=""" + str(r['id']) + """ data-likes=""" + str(r['likes'] or 0) + """><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11h11.5a2.5 2.5 0 0 1 2.5 2.5v.5a2.5 2.5 0 0 1-2.5 2.5h-2.5"/><path d="M16 16.5V19a3 3 0 0 1-6 1.5L7 11"/></svg> """ + str(r['likes'] or 0) + """</button>
|
<button class="stat-like-btn" data-id=""" + str(r['id']) + """ data-likes=""" + str(r['likes'] or 0) + """><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:2px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> """ + str(r['likes'] or 0) + """</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="story-meta">
|
<div class="story-meta">
|
||||||
<span class="story-type">""" + type_emoji + """</span>
|
<span class="story-type">""" + type_emoji + """</span>
|
||||||
@@ -3514,7 +3514,7 @@ def public_frontend():
|
|||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (!data.already) {
|
if (!data.already) {
|
||||||
this.classList.add('liked');
|
this.classList.add('liked');
|
||||||
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11h11.5a2.5 2.5 0 0 1 2.5 2.5v.5a2.5 2.5 0 0 1-2.5 2.5h-2.5"/><path d="M16 16.5V19a3 3 0 0 1-6 1.5L7 11"/></svg> ' + data.likes;
|
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:2px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> ' + data.likes;
|
||||||
this.setAttribute('data-likes', data.likes);
|
this.setAttribute('data-likes', data.likes);
|
||||||
likedPosts.push(parseInt(id));
|
likedPosts.push(parseInt(id));
|
||||||
localStorage.setItem('likedPosts', JSON.stringify(likedPosts));
|
localStorage.setItem('likedPosts', JSON.stringify(likedPosts));
|
||||||
@@ -4643,7 +4643,7 @@ def public_detail(sub_id: int):
|
|||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
comment_rows = db.execute(
|
comment_rows = db.execute(
|
||||||
"SELECT id, author_name, content, created_at, parent_id FROM comments WHERE submission_id = ? AND status='published' ORDER BY created_at ASC",
|
"SELECT id, author_name, content, created_at, parent_id, likes FROM comments WHERE submission_id = ? AND status='published' ORDER BY created_at ASC",
|
||||||
(sub_id,),
|
(sub_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
@@ -4668,7 +4668,10 @@ def public_detail(sub_id: int):
|
|||||||
html += f"""<div class="comment-item" style="{margin}">
|
html += f"""<div class="comment-item" style="{margin}">
|
||||||
<div class="comment-header"><strong class="comment-author">{c_author}</strong> <span class="comment-date">{c_date}</span></div>
|
<div class="comment-header"><strong class="comment-author">{c_author}</strong> <span class="comment-date">{c_date}</span></div>
|
||||||
<p class="comment-text">{c["content"]}</p>
|
<p class="comment-text">{c["content"]}</p>
|
||||||
|
<div class="comment-actions">
|
||||||
|
<button class="comment-like-btn" data-cid="{c['id']}" data-clikes="{c['likes'] or 0}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:3px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> <span class="clike-count">{c['likes'] or 0}</span></button>
|
||||||
<button class="reply-btn" onclick="replyTo({c['id']}, '{c_author}')">↩ Antworten</button>
|
<button class="reply-btn" onclick="replyTo({c['id']}, '{c_author}')">↩ Antworten</button>
|
||||||
|
</div>
|
||||||
</div>"""
|
</div>"""
|
||||||
# Recursively render children
|
# Recursively render children
|
||||||
if c["id"] in children:
|
if c["id"] in children:
|
||||||
@@ -4736,6 +4739,11 @@ def public_detail(sub_id: int):
|
|||||||
.comment-submit-btn:disabled {{ opacity:0.6; pointer-events:none; }}
|
.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 {{ 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-btn:hover {{ border-color:#20228a; color:#20228a; background:rgba(32,34,138,0.04); }}
|
||||||
|
.comment-actions {{ display:flex; align-items:center; gap:0.4rem; margin-top:0.4rem; }}
|
||||||
|
.comment-like-btn {{ background:none; border:1.5px solid #e0dcd3; border-radius:999px; padding:0.15rem 0.5rem; font-size:0.68rem; cursor:pointer; color:#888; font-family:inherit; transition:all 0.15s; display:inline-flex; align-items:center; -webkit-tap-highlight-color:transparent; }}
|
||||||
|
.comment-like-btn:hover {{ border-color:#20228a; color:#20228a; background:rgba(32,34,138,0.05); }}
|
||||||
|
.comment-like-btn.liked {{ background:#20228a; color:#fff; border-color:#20228a; font-weight:600; }}
|
||||||
|
.comment-like-btn:active {{ transform:scale(0.94); }}
|
||||||
.reply-info {{ font-size:0.75rem; color:#20228a; margin-bottom:0.3rem; font-weight:600; display:none; }}
|
.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, .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-email:focus, .comment-phone:focus {{ outline:none; border-color:#20228a; }}
|
||||||
@@ -4775,7 +4783,7 @@ def public_detail(sub_id: int):
|
|||||||
{"".join([f'<div class="detail-content">{content}</div>' if content else ''])}
|
{"".join([f'<div class="detail-content">{content}</div>' if content else ''])}
|
||||||
<div style="display:flex;align-items:center;gap:1rem;margin-top:1.5rem;padding-top:1rem;border-top:1px solid #f0ede6;">
|
<div style="display:flex;align-items:center;gap:1rem;margin-top:1.5rem;padding-top:1rem;border-top:1px solid #f0ede6;">
|
||||||
<span style="font-size:0.8rem;color:#999;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> {row['views'] or 0} Aufrufe</span>
|
<span style="font-size:0.8rem;color:#999;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> {row['views'] or 0} Aufrufe</span>
|
||||||
<button id="detail-like-btn" data-id="{sub_id}" class="detail-like-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11h11.5a2.5 2.5 0 0 1 2.5 2.5v.5a2.5 2.5 0 0 1-2.5 2.5h-2.5"/><path d="M16 16.5V19a3 3 0 0 1-6 1.5L7 11"/></svg> {row['likes'] or 0}</button>
|
<button id="detail-like-btn" data-id="{sub_id}" class="detail-like-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:2px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> {row['likes'] or 0}</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<section class="comments-section">
|
<section class="comments-section">
|
||||||
@@ -4822,7 +4830,7 @@ def public_detail(sub_id: int):
|
|||||||
.then(function(data) {{
|
.then(function(data) {{
|
||||||
if (!data.already) {{
|
if (!data.already) {{
|
||||||
btn.classList.add('liked');
|
btn.classList.add('liked');
|
||||||
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" style="vertical-align:middle;margin-right:2px;"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11h11.5a2.5 2.5 0 0 1 2.5 2.5v.5a2.5 2.5 0 0 1-2.5 2.5h-2.5"/><path d="M16 16.5V19a3 3 0 0 1-6 1.5L7 11"/></svg> ' + data.likes;
|
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#20228a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:2px;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> ' + data.likes;
|
||||||
btn.style.background = '#20228a';
|
btn.style.background = '#20228a';
|
||||||
btn.style.color = '#fff';
|
btn.style.color = '#fff';
|
||||||
btn.style.borderColor = '#20228a';
|
btn.style.borderColor = '#20228a';
|
||||||
@@ -4905,6 +4913,34 @@ function submitComment(event, subId) {{
|
|||||||
}});
|
}});
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// ── Comment Like Buttons ────────────────────────────────────
|
||||||
|
(function() {{
|
||||||
|
var likedComments = JSON.parse(localStorage.getItem('likedComments') || '[]');
|
||||||
|
document.querySelectorAll('.comment-like-btn').forEach(function(btn) {{
|
||||||
|
var cid = parseInt(btn.getAttribute('data-cid'));
|
||||||
|
if (likedComments.indexOf(cid) !== -1) {{
|
||||||
|
btn.classList.add('liked');
|
||||||
|
}}
|
||||||
|
btn.addEventListener('click', function() {{
|
||||||
|
var id = parseInt(this.getAttribute('data-cid'));
|
||||||
|
if (likedComments.indexOf(id) !== -1) return;
|
||||||
|
fetch('/api/comment-like/' + id, {{method:'POST'}})
|
||||||
|
.then(function(r) {{ return r.json(); }})
|
||||||
|
.then(function(data) {{
|
||||||
|
if (!data.already) {{
|
||||||
|
this.classList.add('liked');
|
||||||
|
var count = this.querySelector('.clike-count');
|
||||||
|
if (count) count.textContent = data.likes;
|
||||||
|
this.setAttribute('data-clikes', data.likes);
|
||||||
|
likedComments.push(id);
|
||||||
|
localStorage.setItem('likedComments', JSON.stringify(likedComments));
|
||||||
|
}}
|
||||||
|
}}.bind(this))
|
||||||
|
.catch(function() {{}});
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="offcanvas-backdrop" id="offcanvasBackdrop"></div>
|
<div class="offcanvas-backdrop" id="offcanvasBackdrop"></div>
|
||||||
@@ -5018,6 +5054,30 @@ def api_like(sub_id: int):
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/comment-like/<int:comment_id>", methods=["POST"])
|
||||||
|
def api_comment_like(comment_id: int):
|
||||||
|
"""Like a comment (cookie-based dedup)."""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT id, likes FROM comments WHERE id = ? AND status='published'",
|
||||||
|
(comment_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "Nicht gefunden"}), 404
|
||||||
|
|
||||||
|
liked = request.cookies.get(f"cliked_{comment_id}", "")
|
||||||
|
if liked == "1":
|
||||||
|
return jsonify({"likes": row["likes"] or 0, "already": True})
|
||||||
|
|
||||||
|
db.execute("UPDATE comments SET likes = likes + 1 WHERE id = ?", (comment_id,))
|
||||||
|
db.commit()
|
||||||
|
new_likes = db.execute("SELECT likes FROM comments WHERE id = ?", (comment_id,)).fetchone()[0]
|
||||||
|
|
||||||
|
resp = jsonify({"likes": new_likes or 0, "already": False})
|
||||||
|
resp.set_cookie(f"cliked_{comment_id}", "1", max_age=365*24*60*60, httponly=True, samesite="Lax")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/comment/<int:sub_id>", methods=["POST"])
|
@app.route("/api/comment/<int:sub_id>", methods=["POST"])
|
||||||
def api_comment(sub_id: int):
|
def api_comment(sub_id: int):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|||||||
Reference in New Issue
Block a user