Add: comment section

This commit is contained in:
hyzen
2026-06-08 15:18:47 +05:30
parent 269f405c8d
commit 343fabeb95
4 changed files with 573 additions and 12 deletions

File diff suppressed because one or more lines are too long

View File

@@ -868,3 +868,178 @@
.brand__auth-user-btn svg {
flex: 0 0 auto;
}
/* ── Comments section ─────────────────────────────────────────────────────── */
.comments {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--background-color1, #ddd);
}
.comments__title {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 1.2rem;
}
.comments__login-msg {
font-size: 0.9rem;
margin-bottom: 1.2rem;
color: var(--foreground-color3, #888);
}
.comments__empty {
font-size: 0.88rem;
color: var(--foreground-color3, #888);
}
.comments__error {
font-size: 0.88rem;
color: var(--error-color, #c0392b);
}
/* ── Comment form ── */
.comment-form {
margin-bottom: 1.5rem;
}
.comment-form__input {
width: 100%;
box-sizing: border-box;
padding: 0.6rem 0.8rem;
border: 1px solid var(--background-color1, #ccc);
border-radius: 4px;
background: var(--background-color);
color: var(--foreground-color);
font: inherit;
font-size: 0.9rem;
resize: vertical;
outline: none;
transition: border-color 0.2s;
}
.comment-form__input:focus {
border-color: var(--accent-color);
}
.comment-form__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.4rem;
}
.comment-form__counter {
font-size: 0.75rem;
color: var(--foreground-color3, #999);
}
.comment-form__submit {
padding: 0.35rem 0.9rem;
background: var(--accent-color);
color: #fff;
border: none;
border-radius: 4px;
font: inherit;
font-size: 0.85rem;
cursor: pointer;
transition: opacity 0.15s;
}
.comment-form__submit:hover {
opacity: 0.85;
}
.comment-form__submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.comment-form__error {
font-size: 0.8rem;
color: var(--error-color, #c0392b);
margin-top: 0.3rem;
min-height: 1rem;
}
/* ── Individual comments ── */
.comment {
margin-bottom: 1.2rem;
padding: 0.75rem 0.9rem;
border: 1px solid var(--background-color1, #e0e0e0);
border-radius: 6px;
background: var(--background-color);
}
.comment--reply {
margin-top: 0.7rem;
margin-left: 1.5rem;
border-left: 3px solid var(--accent-color);
border-radius: 0 6px 6px 0;
}
.comment__meta {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.4rem;
flex-wrap: wrap;
}
.comment__author {
font-weight: 600;
font-size: 0.88rem;
}
.comment__time {
font-size: 0.78rem;
color: var(--foreground-color3, #999);
}
.comment__actions {
margin-left: auto;
display: flex;
gap: 0.5rem;
}
.comment__action {
background: none;
border: none;
padding: 0;
font: inherit;
font-size: 0.78rem;
cursor: pointer;
color: var(--foreground-color3, #888);
text-decoration: underline;
}
.comment__action:hover {
color: var(--accent-color);
}
.comment__action--delete:hover {
color: var(--error-color, #c0392b);
}
.comment__body {
font-size: 0.9rem;
line-height: 1.55;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.comment__deleted {
font-size: 0.85rem;
color: var(--foreground-color3, #aaa);
font-style: italic;
}
.comment__reply-form {
margin-top: 0.7rem;
}
.comment__replies {
margin-top: 0.5rem;
}

View File

@@ -6,13 +6,224 @@
</nav>
<h1>{{ .Title }}</h1>
{{ partial "page/author.html" . }}
{{ partial "main/dates.html" . }}
{{ partial "page/translation_list.html" . }}
{{ partial "page/toc.html" . }}
{{ .Content }}
{{ partial "page/terms.html" (dict "taxonomy" "tags" "page" .) }}
{{ partial "page/terms.html" (dict "taxonomy" "categories" "page" .) }}
{{ partial "page/page_nav.html" . }}
{{ partial "page/page-end.html" . }}
{{ partial "page/author.html" . }} {{ partial "main/dates.html" . }} {{ partial
"page/translation_list.html" . }} {{ partial "page/toc.html" . }} {{ .Content }} {{ partial
"page/terms.html" (dict "taxonomy" "tags" "page" .) }} {{ partial "page/terms.html" (dict "taxonomy"
"categories" "page" .) }} {{ partial "page/page_nav.html" . }} {{ partial "page/page-end.html" . }}
<!-- ── Comments ── -->
<section class="comments" id="comments">
<h2 class="comments__title">Comments</h2>
<div id="comments-status" class="comments__status"></div>
<div id="comments-list" class="comments__list"></div>
</section>
<script>
(function () {
var BACKEND = 'https://backend.freedoms4.org/comments.php';
var POST_ID = {{ .RelPermalink | jsonify }};
var username = localStorage.getItem('f4_username');
var statusEl = document.getElementById('comments-status');
var listEl = document.getElementById('comments-list');
// ── Helpers ──
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function timeAgo(iso) {
var d = new Date(iso);
var diff = Math.floor((Date.now() - d) / 1000);
if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
return Math.floor(diff/86400) + 'd ago';
}
function makeForm(placeholder, submitLabel, onSubmit) {
var wrap = document.createElement('div');
wrap.className = 'comment-form';
wrap.innerHTML =
'<textarea class="comment-form__input" placeholder="' + escHtml(placeholder) + '" rows="3" maxlength="2000"></textarea>' +
'<div class="comment-form__footer">' +
'<span class="comment-form__counter">0 / 2000</span>' +
'<button class="comment-form__submit">' + escHtml(submitLabel) + '</button>' +
'</div>' +
'<div class="comment-form__error"></div>';
var ta = wrap.querySelector('textarea');
var counter = wrap.querySelector('.comment-form__counter');
var errEl = wrap.querySelector('.comment-form__error');
var btn = wrap.querySelector('.comment-form__submit');
ta.addEventListener('input', function () {
counter.textContent = ta.value.length + ' / 2000';
});
btn.addEventListener('click', function () {
var text = ta.value.trim();
if (!text) { errEl.textContent = 'Comment cannot be empty.'; return; }
errEl.textContent = '';
btn.disabled = true;
onSubmit(text, function (err) {
btn.disabled = false;
if (err) { errEl.textContent = err; }
else { ta.value = ''; counter.textContent = '0 / 2000'; }
});
});
return wrap;
}
function renderComment(c, depth) {
var el = document.createElement('div');
el.className = 'comment' + (depth > 0 ? ' comment--reply' : '');
el.dataset.id = c.id;
var bodyHtml = c.body === null
? '<span class="comment__deleted">[deleted]</span>'
: '<p class="comment__body">' + escHtml(c.body) + '</p>';
var actionsHtml = '';
if (c.is_own && c.body !== null) {
actionsHtml += '<button class="comment__action comment__action--delete" data-id="' + c.id + '">Delete</button>';
}
if (username && depth === 0) {
actionsHtml += '<button class="comment__action comment__action--reply" data-id="' + c.id + '">Reply</button>';
}
el.innerHTML =
'<div class="comment__meta">' +
'<span class="comment__author">' + escHtml(c.username) + '</span>' +
'<span class="comment__time">' + timeAgo(c.created_at) + '</span>' +
(actionsHtml ? '<span class="comment__actions">' + actionsHtml + '</span>' : '') +
'</div>' +
bodyHtml +
'<div class="comment__reply-form"></div>' +
'<div class="comment__replies"></div>';
// Delete handler
var delBtn = el.querySelector('.comment__action--delete');
if (delBtn) {
delBtn.addEventListener('click', function () {
if (!confirm('Delete this comment?')) return;
delBtn.disabled = true;
fetch(BACKEND, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action: 'delete', comment_id: c.id }),
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.success) {
el.querySelector('.comment__body').outerHTML = '<span class="comment__deleted">[deleted]</span>';
delBtn.remove();
} else {
delBtn.disabled = false;
alert(d.message || 'Failed to delete.');
}
})
.catch(function () { delBtn.disabled = false; alert('Network error.'); });
});
}
// Reply handler
var replyBtn = el.querySelector('.comment__action--reply');
var replyFormEl = el.querySelector('.comment__reply-form');
if (replyBtn) {
replyBtn.addEventListener('click', function () {
if (replyFormEl.children.length) { replyFormEl.innerHTML = ''; return; }
var form = makeForm('Write a reply…', 'Reply', function (text, done) {
fetch(BACKEND, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action: 'reply', post_id: POST_ID, parent_id: c.id, body: text }),
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.success) {
done(null);
replyFormEl.innerHTML = '';
loadComments();
} else {
done(d.message || 'Failed to post reply.');
}
})
.catch(function () { done('Network error.'); });
});
replyFormEl.appendChild(form);
});
}
// Nested replies
var repliesEl = el.querySelector('.comment__replies');
if (c.replies && c.replies.length) {
c.replies.forEach(function (r) {
repliesEl.appendChild(renderComment(r, depth + 1));
});
}
return el;
}
function loadComments() {
fetch(BACKEND + '?action=get&post_id=' + encodeURIComponent(POST_ID), {
credentials: 'include',
})
.then(function (r) { return r.json(); })
.then(function (data) {
listEl.innerHTML = '';
if (!data.success) {
listEl.innerHTML = '<p class="comments__error">Failed to load comments.</p>';
return;
}
// Comment form for logged-in users
if (data.logged_in) {
var form = makeForm('Write a comment…', 'Post comment', function (text, done) {
fetch(BACKEND, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action: 'post', post_id: POST_ID, body: text }),
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.success) { done(null); loadComments(); }
else { done(d.message || 'Failed to post.'); }
})
.catch(function () { done('Network error.'); });
});
listEl.appendChild(form);
} else {
var msg = document.createElement('p');
msg.className = 'comments__login-msg';
msg.innerHTML = '<a href="/login/">Log in</a> or <a href="/signup/">Sign up</a> to comment.';
listEl.appendChild(msg);
}
if (!data.comments.length) {
var empty = document.createElement('p');
empty.className = 'comments__empty';
empty.textContent = 'No comments yet. Be the first!';
listEl.appendChild(empty);
return;
}
data.comments.forEach(function (c) {
listEl.appendChild(renderComment(c, 0));
});
})
.catch(function () {
listEl.innerHTML = '<p class="comments__error">Failed to load comments.</p>';
});
}
loadComments();
})();
</script>
{{ end }}

View File

@@ -868,3 +868,178 @@
.brand__auth-user-btn svg {
flex: 0 0 auto;
}
/* ── Comments section ─────────────────────────────────────────────────────── */
.comments {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--background-color1, #ddd);
}
.comments__title {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 1.2rem;
}
.comments__login-msg {
font-size: 0.9rem;
margin-bottom: 1.2rem;
color: var(--foreground-color3, #888);
}
.comments__empty {
font-size: 0.88rem;
color: var(--foreground-color3, #888);
}
.comments__error {
font-size: 0.88rem;
color: var(--error-color, #c0392b);
}
/* ── Comment form ── */
.comment-form {
margin-bottom: 1.5rem;
}
.comment-form__input {
width: 100%;
box-sizing: border-box;
padding: 0.6rem 0.8rem;
border: 1px solid var(--background-color1, #ccc);
border-radius: 4px;
background: var(--background-color);
color: var(--foreground-color);
font: inherit;
font-size: 0.9rem;
resize: vertical;
outline: none;
transition: border-color 0.2s;
}
.comment-form__input:focus {
border-color: var(--accent-color);
}
.comment-form__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.4rem;
}
.comment-form__counter {
font-size: 0.75rem;
color: var(--foreground-color3, #999);
}
.comment-form__submit {
padding: 0.35rem 0.9rem;
background: var(--accent-color);
color: #fff;
border: none;
border-radius: 4px;
font: inherit;
font-size: 0.85rem;
cursor: pointer;
transition: opacity 0.15s;
}
.comment-form__submit:hover {
opacity: 0.85;
}
.comment-form__submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.comment-form__error {
font-size: 0.8rem;
color: var(--error-color, #c0392b);
margin-top: 0.3rem;
min-height: 1rem;
}
/* ── Individual comments ── */
.comment {
margin-bottom: 1.2rem;
padding: 0.75rem 0.9rem;
border: 1px solid var(--background-color1, #e0e0e0);
border-radius: 6px;
background: var(--background-color);
}
.comment--reply {
margin-top: 0.7rem;
margin-left: 1.5rem;
border-left: 3px solid var(--accent-color);
border-radius: 0 6px 6px 0;
}
.comment__meta {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.4rem;
flex-wrap: wrap;
}
.comment__author {
font-weight: 600;
font-size: 0.88rem;
}
.comment__time {
font-size: 0.78rem;
color: var(--foreground-color3, #999);
}
.comment__actions {
margin-left: auto;
display: flex;
gap: 0.5rem;
}
.comment__action {
background: none;
border: none;
padding: 0;
font: inherit;
font-size: 0.78rem;
cursor: pointer;
color: var(--foreground-color3, #888);
text-decoration: underline;
}
.comment__action:hover {
color: var(--accent-color);
}
.comment__action--delete:hover {
color: var(--error-color, #c0392b);
}
.comment__body {
font-size: 0.9rem;
line-height: 1.55;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.comment__deleted {
font-size: 0.85rem;
color: var(--foreground-color3, #aaa);
font-style: italic;
}
.comment__reply-form {
margin-top: 0.7rem;
}
.comment__replies {
margin-top: 0.5rem;
}