feat: Implement admin functionality to edit and delete comments.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 ? '<span class="deleted-msg">[deleted]</span>' : this.renderCommentContent(comment.content);
|
||||
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 `
|
||||
<div class="comment ${isDeleted ? 'deleted' : ''}" id="c${comment.id}">
|
||||
<div class="comment-avatar">
|
||||
@@ -182,6 +192,7 @@ class CommentSystem {
|
||||
<span class="comment-time">${date}</span>
|
||||
<a href="#c${comment.id}" class="comment-permalink">#${comment.id}</a>
|
||||
${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}">Reply</button>` : ''}
|
||||
${adminButtons}
|
||||
</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>` : ''}
|
||||
@@ -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
|
||||
this.container.querySelectorAll('.reply-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
<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/w0bm.css?v=@mtime(/public/s/css/w0bm.css)">
|
||||
@if(typeof item !== 'undefined')
|
||||
<script src="/s/js/marked.min.js"></script>@endif
|
||||
<script src="/s/js/marked.min.js"></script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@if(typeof item !== 'undefined')
|
||||
|
||||
Reference in New Issue
Block a user