Files
hyzendust.github.io/layouts/blog/single.html
2026-06-26 16:55:23 +05:30

247 lines
8.4 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ define "main" }}
<nav class="uninotes-breadcrumbs breadcrumbs">
<a href="/blog/">Blog</a>
<span>{{ .Title }}</span>
</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" . }}
<!-- ── Comments ── -->
<section class="comments" id="comments">
<h2 class="comments__title">Comments</h2>
<div id="comments-list" class="comments__list"></div>
</section>
<script>
(function () {
var BACKEND = 'https://backend.freedoms4.org/comments.php';
var POST_ID = {{ .RelPermalink | jsonify | safeJS }};
var username = localStorage.getItem('f4_username');
var isAdmin = false;
var listEl = document.getElementById('comments-list');
// ── Helpers ──
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
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="4" maxlength="2000" style="resize:none;width:100%;"></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 isDeleted = c.body === null;
var deletedLabel = c.deleted_label || 'deleted by user';
var bodyHtml = isDeleted
? '<span class="comment__deleted">[' + escHtml(deletedLabel) + ']</span>'
: '<p class="comment__body">' + escHtml(c.body) + '</p>';
var actionsHtml = '';
// Delete button: own comment OR admin, only if not already deleted
if (!isDeleted && (c.is_own || isAdmin)) {
actionsHtml += '<button class="comment__action comment__action--delete" data-id="' + c.id + '">Delete</button>';
}
// Reply button: any logged-in user can reply to any non-deleted comment at any depth
if (username && !isDeleted) {
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, deleted_by_owner: c.username === username }),
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.success) {
loadComments();
} 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 — suppressed entirely when parent is deleted
var repliesEl = el.querySelector('.comment__replies');
if (!isDeleted) {
var replyList = Array.isArray(c.replies) ? c.replies : Object.values(c.replies || {});
if (replyList.length) {
replyList.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;
}
isAdmin = !!data.is_admin;
username = data.logged_in ? (data.username || null) : null;
// Keep localStorage in sync with what the server says
if (!data.logged_in && localStorage.getItem('f4_username')) {
localStorage.removeItem('f4_username');
localStorage.removeItem('f4_login_time');
localStorage.removeItem('f4_session_fails');
}
// ── Render comments first ──
if (!data.comments.length) {
var empty = document.createElement('p');
empty.className = 'comments__empty';
empty.textContent = 'No comments yet. Be the first!';
listEl.appendChild(empty);
} else {
data.comments.forEach(function (c) {
listEl.appendChild(renderComment(c, 0));
});
}
// ── Then: form (logged in) or login prompt (logged out) ──
if (data.logged_in) {
var form = makeForm('Comment as ' + data.username + '…', '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);
}
})
.catch(function () {
listEl.innerHTML = '<p class="comments__error">Failed to load comments.</p>';
});
}
loadComments();
})();
</script>
{{ end }}