Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions flask_setup/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ def _register_comment_routes(app: Flask, adapters: dict) -> None:
methods=["POST"],
endpoint="comment.delete_comment",
)
app.add_url_rule(
"/articles/<int:article_id>/comments/<int:comment_id>/edit",
view_func=com.edit_comment,
methods=["POST"],
endpoint="comment.edit_comment",
)
app.add_url_rule(
"/articles/<int:article_id>/comments/<int:comment_id>/delete-permanent",
view_func=com.hard_delete_comment,
methods=["POST"],
endpoint="comment.hard_delete_comment",
)


def _register_auth_routes(app: Flask, adapters: dict) -> None:
Expand Down
97 changes: 80 additions & 17 deletions frontend/static/css/article.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,26 @@
border-radius: 9999px;
object-fit: cover;
vertical-align: middle;
margin-right: var(--spacing-xs);
}

.meta-author-initial {
width: 3rem;
height: 3rem;
border-radius: var(--radius-full);
background: var(--surface-container-high);
color: var(--primary);
border: 1px solid var(--outline);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
flex-shrink: 0;
}

.meta-author-avatar-link {
display: flex;
text-decoration: none;
}

a.meta-author:hover {
Expand Down Expand Up @@ -600,8 +619,9 @@ input[type=number] {
flex-shrink: 0;
width: 3rem;
height: 3rem;
border-radius: 9999px;
background: var(--primary-container);
border-radius: var(--radius-full);
background: var(--surface-container-high);
border: 1px solid var(--outline);
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -611,12 +631,14 @@ input[type=number] {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}

.comment-avatar-initial {
font-size: var(--font-body-base);
font-weight: 600;
color: var(--on-primary-container);
font-size: 1.2rem;
font-weight: bold;
color: var(--primary);
line-height: 1;
}

Expand All @@ -627,14 +649,6 @@ input[type=number] {
border-radius: 9999px;
}

.comment-avatar-deleted {
background: var(--surface-container-high);
}

.comment-avatar-deleted .comment-avatar-initial {
color: var(--on-surface-variant);
}

.comment-content {
flex: 1;
min-width: 0;
Expand Down Expand Up @@ -773,9 +787,52 @@ a.comment-author:hover {
}
}

.comment-delete-form {
display: inline;
margin-left: 0.625rem;
.comment-edited {
font-size: var(--font-body-xs);
color: var(--on-surface-variant);
font-style: italic;
}

.edit-form-container {
margin-top: var(--spacing-sm);
}

.edit-form {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}

.edit-editor {
width: 100%;
box-sizing: border-box;
resize: vertical;
min-height: 4rem;
font-size: var(--font-body-base);
}

.edit-actions {
display: flex;
gap: var(--spacing-xs);
}

.cancel-edit {
cursor: pointer;
}

.comment-edit-toggle {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
font-size: var(--font-body-xs);
font-family: var(--font-mono);
padding: 0;
text-transform: uppercase;
}

.comment-edit-toggle:hover {
text-decoration: underline;
}

.comment-delete-btn {
Expand All @@ -798,6 +855,12 @@ a.comment-author:hover {
transform: none;
}

.comment-edited-line {
font-size: var(--font-body-xs);
color: var(--on-surface-variant);
margin-top: var(--spacing-xs);
}

.comment-empty {
color: var(--on-surface-variant);
font-size: var(--font-body-base);
Expand Down
85 changes: 85 additions & 0 deletions frontend/static/scripts/comment-reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
}
});

document.querySelectorAll('.edit-form-container').forEach(other => {
if (other.style.display === 'block') {
other.style.display = 'none';
const otherId = other.id.replace('edit-form-', '');
const otherToggle = document.querySelector('.comment-edit-toggle[data-comment-id="' + otherId + '"]');
if (otherToggle) otherToggle.textContent = '[Edit]';
const otherBody = document.getElementById('comment-body-' + otherId);
if (otherBody) otherBody.style.display = '';
}
});

const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
replyToggle.textContent = isHidden ? 'Cancel' : 'Reply';
Expand Down Expand Up @@ -51,6 +62,58 @@
return;
}

const editToggle = e.target.closest('.comment-edit-toggle');
if (editToggle) {
e.preventDefault();
const commentId = editToggle.dataset.commentId;
const container = document.getElementById('edit-form-' + commentId);
if (!container) return;

document.querySelectorAll('.edit-form-container').forEach(other => {
if (other.style.display === 'block' && other.id !== container.id) {
other.style.display = 'none';
const otherId = other.id.replace('edit-form-', '');
const otherToggle = document.querySelector('.comment-edit-toggle[data-comment-id="' + otherId + '"]');
if (otherToggle) otherToggle.textContent = '[Edit]';
const otherBody = document.getElementById('comment-body-' + otherId);
if (otherBody) {
otherBody.style.display = '';
const otherEdited = otherBody.nextElementSibling;
if (otherEdited && otherEdited.classList.contains('comment-edited-line')) {
otherEdited.style.display = '';
}
}
}
});

document.querySelectorAll('.reply-form-container').forEach(other => {
if (other.style.display === 'block') {
other.style.display = 'none';
const otherId = other.id.replace('reply-form-', '');
const otherToggle = document.querySelector('.reply-toggle[data-comment-id="' + otherId + '"]');
if (otherToggle) otherToggle.textContent = 'Reply';
}
});

const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
editToggle.textContent = isHidden ? 'Cancel' : '[Edit]';

const bodyText = document.getElementById('comment-body-' + commentId);
if (bodyText) {
bodyText.style.display = isHidden ? 'none' : '';
const editedLine = bodyText.nextElementSibling;
if (editedLine && editedLine.classList.contains('comment-edited-line')) {
editedLine.style.display = isHidden ? 'none' : '';
}
}

if (isHidden && window.initEditEditor) {
window.initEditEditor(commentId);
}
return;
}

const cancelBtn = e.target.closest('.cancel-reply');
if (cancelBtn) {
const container = cancelBtn.closest('.reply-form-container');
Expand All @@ -61,6 +124,28 @@
if (toggle) {
toggle.textContent = 'Reply';
}
return;
}

const cancelEdit = e.target.closest('.cancel-edit');
if (cancelEdit) {
const container = cancelEdit.closest('.edit-form-container');
if (!container) return;
container.style.display = 'none';
const commentId = container.id.replace('edit-form-', '');
const toggle = document.querySelector('.comment-edit-toggle[data-comment-id="' + commentId + '"]');
if (toggle) {
toggle.textContent = '[Edit]';
}
const bodyText = document.getElementById('comment-body-' + commentId);
if (bodyText) {
bodyText.style.display = '';
const editedLine = bodyText.nextElementSibling;
if (editedLine && editedLine.classList.contains('comment-edited-line')) {
editedLine.style.display = '';
}
}
return;
}
});
});
Expand Down
4 changes: 4 additions & 0 deletions frontend/static/scripts/suneditor-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@
initCommentEditor('reply-editor-' + commentId, 'reply-content-' + commentId);
};

window.initEditEditor = function (commentId) {
initCommentEditor('edit-editor-' + commentId, 'edit-content-' + commentId);
};

document.addEventListener('DOMContentLoaded', () => {
initCommentEditor('comment-editor', 'comment-content');
});
Expand Down
48 changes: 42 additions & 6 deletions frontend/templates/article_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ <h1 class="item-title detail-title">{{ article.article_title }}</h1>
{% if article.author_username == 'Anonymous' %}
<span class="meta-author">{{ article.author_username }}</span>
{% else %}
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author">
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author-avatar-link">
{% if article.author_avatar_file_id %}
<img src="{{ url_for('file.serve_file', file_id=article.author_avatar_file_id, filename='avatar') }}" alt="{{ article.author_username }}" class="meta-author-avatar">
{% else %}
<span class="meta-author-initial">{{ article.author_username[0]|upper }}</span>
{% endif %}
</a>
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author">
{{ article.author_username }}
</a>
{% endif %}
Expand Down Expand Up @@ -86,7 +90,9 @@ <h1 class="item-title detail-title">{{ article.article_title }}</h1>

{% macro render_comment(node, depth=0) %}
{% set display_depth = depth if depth < 3 else 3 %}
{% set is_deleted = node.comment.comment_content in ('Comment removed', '[deleted]', '<em>Comment removed</em>') %}
{% set is_deleted = node.comment.is_deleted %}
{% set is_author = current_user and current_user.account_id == node.comment.comment_written_account_id %}
{% set is_admin = current_user and current_user.account_role == 'admin' %}
<div class="comment comment-depth-{{ display_depth }}{% if is_deleted %} comment-deleted{% endif %}">
<div class="comment-avatar">
{% if is_deleted %}
Expand All @@ -112,23 +118,52 @@ <h1 class="item-title detail-title">{{ article.article_title }}</h1>
<a href="{{ url_for('auth.user_profile', username=node.comment.author_username) }}" class="comment-author">{{ node.comment.author_username }}</a>
{% endif %}
<span class="comment-timestamp">{{ node.comment.comment_posted_at_formatted }}</span>
{% if current_user and current_user.account_role == 'admin' %}
{% if current_user and is_author and not is_deleted %}
<button type="button" class="comment-edit-toggle" data-comment-id="{{ node.comment.comment_id }}">[Edit]</button>
{% endif %}
{% if current_user and (is_author or is_admin) and not is_deleted %}
<form id="delete-comment-{{ node.comment.comment_id }}" action="{{ url_for('comment.delete_comment', article_id=article.article_id, comment_id=node.comment.comment_id) }}" method="POST" class="comment-delete-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="comment-delete-btn confirm-trigger" data-confirm-message="Delete this comment permanently ?" data-form-id="delete-comment-{{ node.comment.comment_id }}">[Delete]</button>
<button type="submit" class="comment-delete-btn">[Delete]</button>
</form>
{% endif %}
{% if current_user and is_admin and is_deleted %}
<form action="{{ url_for('comment.hard_delete_comment', article_id=article.article_id, comment_id=node.comment.comment_id) }}" method="POST" class="comment-delete-form" onsubmit="return confirm('Permanently delete this comment? This cannot be undone.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="comment-delete-btn">[Delete]</button>
</form>
{% endif %}
</div>
<div class="comment-body-text">
<div class="comment-body-text" id="comment-body-{{ node.comment.comment_id }}">
{{ node.comment.comment_content | safe }}
</div>
{% if current_user and not is_deleted and depth < 3 %}
{% if node.comment.edited_at and not node.comment.is_deleted %}
<div class="comment-edited-line">edited at {{ node.comment.edited_at_formatted }}</div>
{% endif %}
{% if current_user and is_author and not is_deleted %}
<div class="edit-form-container" id="edit-form-{{ node.comment.comment_id }}" style="display: none;">
<form method="POST" action="{{ url_for('comment.edit_comment', article_id=article.article_id, comment_id=node.comment.comment_id) }}" class="edit-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="content" id="edit-content-{{ node.comment.comment_id }}">
<span class="comment-limit-hint">Comment must be between 1 and 5000 characters</span>
<textarea id="edit-editor-{{ node.comment.comment_id }}" class="edit-editor" maxlength="5000">{{ node.comment.comment_content }}</textarea>
<div class="edit-actions">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
<button type="button" class="btn btn-sm btn-secondary cancel-edit">Cancel</button>
</div>
</form>
</div>
{% endif %}
{% if current_user and not is_deleted %}
<div class="comment-actions">
{% if depth < 3 %}
<button type="button" class="reply-toggle" data-comment-id="{{ node.comment.comment_id }}">Reply</button>
{% endif %}
{% if node.replies %}
<button type="button" class="view-replies-btn">View replies</button>
{% endif %}
</div>
{% if depth < 3 %}
<div class="reply-form-container" id="reply-form-{{ node.comment.comment_id }}" style="display: none;">
<form method="POST" action="{{ url_for('comment.reply_to_comment', article_id=article.article_id, parent_comment_id=node.comment.comment_id) }}" class="reply-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
Expand All @@ -142,6 +177,7 @@ <h1 class="item-title detail-title">{{ article.article_title }}</h1>
</div>
</form>
</div>
{% endif %}
{% elif node.replies %}
<div class="comment-actions">
<button type="button" class="view-replies-btn">View replies</button>
Expand Down
10 changes: 7 additions & 3 deletions frontend/templates/article_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ <h2 class="item-title">
{% if article.author_username == 'Anonymous' %}
<span class="meta-author">{{ article.author_username }}</span>
{% else %}
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author">
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author-avatar-link">
{% if article.author_avatar_file_id %}
<img src="{{ url_for('file.serve_file', file_id=article.author_avatar_file_id, filename='avatar') }}" alt="{{ article.author_username }}" class="meta-author-avatar">
{% else %}
<span class="meta-author-initial">{{ article.author_username[0]|upper }}</span>
{% endif %}
{{ article.author_username }}
</a>
</a>
<a href="{{ url_for('auth.user_profile', username=article.author_username) }}" class="meta-author">
{{ article.author_username }}
</a>
{% endif %}
{% if article.article_published_at %}
<span class="meta-divider">•</span>
Expand Down
4 changes: 4 additions & 0 deletions migrations/V14__add_comment_soft_delete_and_edit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE comments
ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN deleted_at TIMESTAMP,
ADD COLUMN edited_at TIMESTAMP;
Loading
Loading