feat: Implement admin functionality to edit and delete comments.

This commit is contained in:
x
2026-01-25 04:31:23 +01:00
parent 322698cf74
commit 2c0f4f3397
4 changed files with 157 additions and 4 deletions

View File

@@ -891,7 +891,6 @@ html[theme="f0ck95"] #next {
} }
#comments-container { #comments-container {
margin-top: 20px;
padding: 10px; padding: 10px;
color: var(--white); color: var(--white);
max-width: 1000px; max-width: 1000px;
@@ -3844,7 +3843,6 @@ input#s_avatar {
/* Comments System */ /* Comments System */
#comments-container { #comments-container {
margin-top: 20px;
padding: 15px; padding: 15px;
background: var(--metadata-bg); background: var(--metadata-bg);
border-radius: 5px; border-radius: 5px;
@@ -4029,3 +4027,63 @@ input#s_avatar {
.comment-content a { .comment-content a {
text-decoration: underline; 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;
}

View File

@@ -54,6 +54,7 @@ class CommentSystem {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
this.isAdmin = data.is_admin || false;
this.render(data.comments, data.user_id, data.is_subscribed); this.render(data.comments, data.user_id, data.is_subscribed);
// Priority: Explicit ID > Hash // Priority: Explicit ID > Hash
@@ -171,6 +172,15 @@ class CommentSystem {
const content = isDeleted ? '<span class="deleted-msg">[deleted]</span>' : this.renderCommentContent(comment.content); const content = isDeleted ? '<span class="deleted-msg">[deleted]</span>' : this.renderCommentContent(comment.content);
const date = new Date(comment.created_at).toLocaleString(); const date = new Date(comment.created_at).toLocaleString();
// Admin buttons
let adminButtons = '';
if (this.isAdmin && !isDeleted) {
adminButtons = `
<button class="admin-edit-btn" data-id="${comment.id}" data-content="${this.escapeHtml(comment.content)}">✏️</button>
<button class="admin-delete-btn" data-id="${comment.id}">🗑️</button>
`;
}
return ` return `
<div class="comment ${isDeleted ? 'deleted' : ''}" id="c${comment.id}"> <div class="comment ${isDeleted ? 'deleted' : ''}" id="c${comment.id}">
<div class="comment-avatar"> <div class="comment-avatar">
@@ -182,6 +192,7 @@ class CommentSystem {
<span class="comment-time">${date}</span> <span class="comment-time">${date}</span>
<a href="#c${comment.id}" class="comment-permalink">#${comment.id}</a> <a href="#c${comment.id}" class="comment-permalink">#${comment.id}</a>
${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}">Reply</button>` : ''} ${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}">Reply</button>` : ''}
${adminButtons}
</div> </div>
<div class="comment-content">${content}</div> <div class="comment-content">${content}</div>
${comment.children.length > 0 ? `<div class="comment-children">${comment.children.map(c => this.renderComment(c, currentUserId)).join('')}</div>` : ''} ${comment.children.length > 0 ? `<div class="comment-children">${comment.children.map(c => this.renderComment(c, currentUserId)).join('')}</div>` : ''}
@@ -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 = `
<textarea class="edit-textarea">${currentContent}</textarea>
<div class="edit-actions">
<button class="save-edit-btn">Save</button>
<button class="cancel-edit-btn">Cancel</button>
</div>
`;
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 // Reply
this.container.querySelectorAll('.reply-btn').forEach(btn => { this.container.querySelectorAll('.reply-btn').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {

View File

@@ -181,5 +181,34 @@ export default (router, tpl) => {
} }
}); });
// Edit comment (admin only)
router.post(/\/api\/comments\/(?<id>\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; return router;
}; };

View File

@@ -7,8 +7,7 @@
<link rel="icon" type="image/gif" href="/s/img/favicon.png" /> <link rel="icon" type="image/gif" href="/s/img/favicon.png" />
<link rel="stylesheet" href="/s/css/f0ck.css?v=@mtime(/public/s/css/f0ck.css)"> <link rel="stylesheet" href="/s/css/f0ck.css?v=@mtime(/public/s/css/f0ck.css)">
<link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)"> <link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)">
@if(typeof item !== 'undefined') <script src="/s/js/marked.min.js"></script>
<script src="/s/js/marked.min.js"></script>@endif
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@if(typeof item !== 'undefined') @if(typeof item !== 'undefined')