feat: add comments feature with nested replies and recursive rendering
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
Implement full comments system: domain entities (Comment, CommentLike), value objects (CommentContent), use cases (CRUD, like toggle), SQLAlchemy repository, API v1 endpoints, web UI with comment form and nested replies, i18n translations (EN/RU/FR/DE), and E2E tests. Fix nested reply (reply-to-reply) not displaying — the flat reply_comments dict was only queried for top-level comment IDs, so deeply nested replies were saved to DB (incrementing comment count) but never rendered. Switch to a recursive Jinja2 macro that renders any nesting depth.
This commit is contained in:
@@ -362,3 +362,179 @@ input[type="checkbox"] {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Comment section */
|
||||
.comments-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.comments-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comments-count {
|
||||
font-weight: 400;
|
||||
color: var(--color-text-light-3);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.comment-form-wrapper {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-secondary-bg);
|
||||
}
|
||||
|
||||
.comment-form .form-group {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
min-height: 5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-alpha-30);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-light-3);
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-error {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: var(--color-error-bg);
|
||||
border: 1px solid var(--color-error-border);
|
||||
border-radius: 4px;
|
||||
color: var(--color-error-text);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Individual comment */
|
||||
.comment {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.comment + .comment {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
flex-shrink: 0;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-light-3);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.375rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-comment-reply,
|
||||
.btn-comment-like {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
margin-top: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.comment-reply {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.comment-reply + .comment-reply {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.comments-empty {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--color-text-light-3);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.btn-cancel-reply {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-cancel-reply:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user