feat: Implement admin functionality to edit and delete comments.
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user