diff --git a/public/s/css/f0ck.css b/public/s/css/f0ck.css index c13fceb..1b1dab0 100644 --- a/public/s/css/f0ck.css +++ b/public/s/css/f0ck.css @@ -891,7 +891,6 @@ html[theme="f0ck95"] #next { } #comments-container { - margin-top: 20px; padding: 10px; color: var(--white); max-width: 1000px; @@ -3844,7 +3843,6 @@ input#s_avatar { /* Comments System */ #comments-container { - margin-top: 20px; padding: 15px; background: var(--metadata-bg); border-radius: 5px; @@ -4028,4 +4026,64 @@ input#s_avatar { .comment-content a { text-decoration: underline; +} + +/* Admin buttons */ +.admin-edit-btn, +.admin-delete-btn { + background: none; + border: none; + cursor: pointer; + font-size: 0.9em; + padding: 0 4px; + margin-left: 5px; + opacity: 0.6; + transition: opacity 0.2s; +} + +.admin-edit-btn:hover, +.admin-delete-btn:hover { + opacity: 1; +} + +.admin-delete-btn:hover { + color: #e74c3c; +} + +/* Edit mode */ +.edit-textarea { + width: 100%; + min-height: 80px; + background: var(--bg); + border: 1px solid var(--accent); + color: var(--white); + padding: 10px; + border-radius: 4px; + font-family: inherit; + resize: vertical; + margin-bottom: 8px; +} + +.edit-actions { + display: flex; + gap: 8px; +} + +.save-edit-btn, +.cancel-edit-btn { + padding: 6px 12px; + border: none; + border-radius: 3px; + cursor: pointer; + font-weight: bold; +} + +.save-edit-btn { + background: var(--accent); + color: var(--black); +} + +.cancel-edit-btn { + background: #666; + color: white; } \ No newline at end of file diff --git a/public/s/js/comments.js b/public/s/js/comments.js index 37b770e..16f294e 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -54,6 +54,7 @@ class CommentSystem { const data = await res.json(); if (data.success) { + this.isAdmin = data.is_admin || false; this.render(data.comments, data.user_id, data.is_subscribed); // Priority: Explicit ID > Hash @@ -171,6 +172,15 @@ class CommentSystem { const content = isDeleted ? '[deleted]' : this.renderCommentContent(comment.content); const date = new Date(comment.created_at).toLocaleString(); + // Admin buttons + let adminButtons = ''; + if (this.isAdmin && !isDeleted) { + adminButtons = ` + + + `; + } + return `
@@ -182,6 +192,7 @@ class CommentSystem { ${date} #${comment.id} ${!isDeleted && currentUserId ? `` : ''} + ${adminButtons}
${content}
${comment.children.length > 0 ? `
${comment.children.map(c => this.renderComment(c, currentUserId)).join('')}
` : ''} @@ -238,6 +249,62 @@ class CommentSystem { }); }); + // Admin Delete + this.container.querySelectorAll('.admin-delete-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + if (!confirm('Admin: Delete this comment?')) return; + const id = e.target.dataset.id; + const res = await fetch(`/api/comments/${id}/delete`, { method: 'POST' }); + const json = await res.json(); + if (json.success) this.loadComments(id); + else alert('Failed to delete: ' + (json.message || 'Error')); + }); + }); + + // Admin Edit + this.container.querySelectorAll('.admin-edit-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = e.target.dataset.id; + const currentContent = e.target.dataset.content; + const commentEl = document.getElementById('c' + id); + const contentEl = commentEl.querySelector('.comment-content'); + + // Replace content with textarea + const originalHtml = contentEl.innerHTML; + contentEl.innerHTML = ` + +
+ + +
+ `; + + contentEl.querySelector('.cancel-edit-btn').addEventListener('click', () => { + contentEl.innerHTML = originalHtml; + }); + + contentEl.querySelector('.save-edit-btn').addEventListener('click', async () => { + const newContent = contentEl.querySelector('.edit-textarea').value; + if (!newContent.trim()) return alert('Cannot be empty'); + + const params = new URLSearchParams(); + params.append('content', newContent); + + const res = await fetch(`/api/comments/${id}/edit`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params + }); + const json = await res.json(); + if (json.success) { + this.loadComments(id); + } else { + alert('Failed to edit: ' + (json.message || 'Error')); + } + }); + }); + }); + // Reply this.container.querySelectorAll('.reply-btn').forEach(btn => { btn.addEventListener('click', (e) => { diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs index ba3971e..d278eeb 100644 --- a/src/inc/routes/comments.mjs +++ b/src/inc/routes/comments.mjs @@ -181,5 +181,34 @@ export default (router, tpl) => { } }); + // Edit comment (admin only) + router.post(/\/api\/comments\/(?\d+)\/edit/, async (req, res) => { + if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) }); + if (!req.session.admin) return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Admin only" }) }); + + const commentId = req.params.id; + const body = req.post || {}; + const content = body.content; + + if (!content || !content.trim()) { + return res.reply({ body: JSON.stringify({ success: false, message: "Empty content" }) }); + } + + try { + const comment = await db`SELECT id FROM comments WHERE id = ${commentId}`; + if (!comment.length) return res.reply({ code: 404, body: JSON.stringify({ success: false, message: "Not found" }) }); + + await db`UPDATE comments SET content = ${content}, updated_at = NOW() WHERE id = ${commentId}`; + + return res.reply({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ success: true }) + }); + } catch (e) { + console.error(e); + return res.reply({ code: 500, body: JSON.stringify({ success: false }) }); + } + }); + return router; }; diff --git a/views/snippets/header.html b/views/snippets/header.html index 28cee05..2d4d86b 100644 --- a/views/snippets/header.html +++ b/views/snippets/header.html @@ -7,8 +7,7 @@ - @if(typeof item !== 'undefined') - @endif + @if(typeof item !== 'undefined')