Skip to content

Commit 75f3bfc

Browse files
committed
fix: improved comments
- fixed edit and remove available globally - improved date format - added avatar
1 parent 01aa16f commit 75f3bfc

1 file changed

Lines changed: 87 additions & 97 deletions

File tree

_includes/comments.html

Lines changed: 87 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
1212
<div id="comments-container" class="space-y-4"></div>
1313

1414
<form id="comment-form" class="mt-6 space-y-3">
15+
<input
16+
type="text"
17+
id="comment-name"
18+
placeholder="Your name (optional)"
19+
class="w-full bg-white/50 dark:bg-neutral-800/50 outline-none rounded-lg border border-neutral-300 dark:border-neutral-700 p-2 text-neutral-900 dark:text-neutral-100"
20+
/>
1521
<textarea
1622
id="comment-input"
1723
rows="3"
@@ -39,19 +45,31 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
3945
</form>
4046
</section>
4147

48+
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
49+
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script>
50+
<script>
51+
dayjs.extend(window.dayjs_plugin_relativeTime);
52+
</script>
53+
4254
<script type="module">
4355
const supabase = window.supabase;
4456
const POST_SLUG = "{{ page.slug }}";
4557

58+
// ---------- Anonymous ID (persistent per browser) ----------
59+
let anonId = localStorage.getItem("user_id");
60+
if (!anonId) {
61+
anonId = crypto.randomUUID();
62+
localStorage.setItem("user_id", anonId);
63+
}
64+
4665
// ---------- Local reaction state ----------
4766
const REACT_KEY = "comment_reactions_v3";
4867
const reacted = JSON.parse(localStorage.getItem(REACT_KEY) || "{}");
4968
const setReacted = (id, emoji, on) => {
5069
reacted[id] = reacted[id] || {};
5170
if (on) reacted[id][emoji] = true;
5271
else delete reacted[id][emoji];
53-
if (reacted[id] && Object.keys(reacted[id]).length === 0)
54-
delete reacted[id];
72+
if (reacted[id] && Object.keys(reacted[id]).length === 0) delete reacted[id];
5573
localStorage.setItem(REACT_KEY, JSON.stringify(reacted));
5674
};
5775
const hasReacted = (id, emoji) => reacted[id]?.[emoji] === true;
@@ -77,6 +95,17 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
7795
const countReplies = (n) =>
7896
n.children.reduce((a, ch) => a + 1 + countReplies(ch), 0);
7997

98+
// ---------- Avatar ----------
99+
function generateAvatar(name) {
100+
const initials = (name || "Anonymous")
101+
.split(" ")
102+
.map((w) => w[0])
103+
.join("")
104+
.toUpperCase()
105+
.slice(0, 2);
106+
return `<div class="w-8 h-8 flex items-center justify-center rounded-full bg-amber-500 text-white font-bold">${initials}</div>`;
107+
}
108+
80109
// ---------- Render ----------
81110
function reactionChipHTML(id, emoji, count) {
82111
const pressed = hasReacted(id, emoji);
@@ -86,9 +115,7 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
86115
: `${base} bg-neutral-100 text-neutral-800 hover:bg-neutral-200 dark:bg-neutral-700 dark:text-neutral-100`;
87116
return `
88117
<button class="${cls}" data-action="react" data-id="${id}" data-emoji="${emoji}" aria-pressed="${pressed}">
89-
<span>${emoji}</span><span class="min-w-3 text-[11px]">${
90-
count || 0
91-
}</span>
118+
<span>${emoji}</span><span class="min-w-3 text-[11px]">${count || 0}</span>
92119
</button>
93120
`;
94121
}
@@ -98,61 +125,46 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
98125
const emojis = ["👍", "❤️", "😂"];
99126
const repliesCount = countReplies(node);
100127
const indent = Math.min(depth, 6) * 4;
128+
const canModify = anonId === node.user_id;
101129

102130
return `
103131
<div id="comment-${node.id}" class="relative">
104-
${
105-
depth > 0
106-
? `<div class="absolute -left-3 top-0 bottom-0 border-l border-neutral-200 dark:border-neutral-700"></div>`
107-
: ""
108-
}
132+
${depth > 0
133+
? `<div class="absolute -left-3 top-0 bottom-0 border-l border-neutral-200 dark:border-neutral-700"></div>`
134+
: ""}
109135
<div class="p-3 ml-${indent} rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white/50 dark:bg-neutral-800/50">
110-
<div class="flex justify-between items-center mb-1">
111-
<span class="text-xs text-neutral-500">${new Date(
112-
node.created_at
113-
).toLocaleString()}</span>
114-
<div class="space-x-3">
115-
<button class="text-xs text-amber-600 hover:underline" data-action="edit" data-id="${
116-
node.id
117-
}" data-content="${encodeURIComponent(
118-
node.content
119-
)}">Edit</button>
120-
<button class="text-xs text-red-500 hover:underline" data-action="delete" data-id="${
121-
node.id
122-
}">Delete</button>
136+
<div class="flex items-start gap-3">
137+
${generateAvatar(node.name)}
138+
<div class="flex-1">
139+
<div class="flex justify-between items-center mb-1">
140+
<span class="text-sm font-medium">${node.name || "Anonymous"}</span>
141+
<span class="text-xs text-neutral-500">${dayjs(node.created_at).fromNow()}</span>
142+
</div>
143+
<p class="text-neutral-900 dark:text-neutral-100 mb-2" id="content-${node.id}">${node.content}</p>
144+
<div class="flex flex-wrap items-center gap-2 mb-2">
145+
${emojis.map((e) => reactionChipHTML(node.id, e, reactions[e])).join("")}
146+
<button class="text-xs text-amber-600 dark:text-amber-400 hover:underline ml-1" data-action="reply" data-id="${node.id}">Reply</button>
147+
${node.children.length
148+
? `<button class="text-xs text-neutral-600 dark:text-neutral-300 hover:underline ml-2" data-action="toggle-replies" data-id="${node.id}" data-open="1">Hide replies (${repliesCount})</button>`
149+
: ""}
150+
${canModify
151+
? `
152+
<button class="text-xs text-amber-600 hover:underline" data-action="edit" data-id="${node.id}" data-content="${encodeURIComponent(node.content)}">Edit</button>
153+
<button class="text-xs text-red-500 hover:underline" data-action="delete" data-id="${node.id}">Delete</button>
154+
`
155+
: ""}
156+
</div>
157+
<div id="replies-${node.id}" class="space-y-2">
158+
${node.children.map((ch) => renderNode(ch, depth + 1)).join("")}
159+
</div>
123160
</div>
124161
</div>
125-
126-
<p class="text-neutral-900 dark:text-neutral-100 mb-2" id="content-${
127-
node.id
128-
}">${node.content}</p>
129-
130-
<div class="flex flex-wrap items-center gap-2 mb-2">
131-
${emojis
132-
.map((e) => reactionChipHTML(node.id, e, reactions[e]))
133-
.join("")}
134-
<button class="text-xs text-amber-600 dark:text-amber-400 hover:underline ml-1" data-action="reply" data-id="${
135-
node.id
136-
}">Reply</button>
137-
${
138-
node.children.length
139-
? `
140-
<button class="text-xs text-neutral-600 dark:text-neutral-300 hover:underline ml-2"
141-
data-action="toggle-replies" data-id="${node.id}" data-open="1">
142-
Hide replies (${repliesCount})
143-
</button>`
144-
: ""
145-
}
146-
</div>
147-
148-
<div id="replies-${node.id}" class="space-y-2">
149-
${node.children.map((ch) => renderNode(ch, depth + 1)).join("")}
150-
</div>
151162
</div>
152163
</div>
153164
`;
154165
}
155166

167+
// ---------- Load comments ----------
156168
async function loadComments() {
157169
const { data, error } = await supabase
158170
.from("comments")
@@ -161,40 +173,37 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
161173
.order("created_at", { ascending: true });
162174
if (error) return console.error(error);
163175

164-
document.getElementById("comments-count").innerText = `${
165-
data.length
166-
} Comment${data.length === 1 ? "" : "s"}`;
167-
document.getElementById("comments-link").innerText = `${
168-
data.length
169-
} Comment${data.length === 1 ? "" : "s"}`;
176+
document.getElementById("comments-count").innerText = `${data.length} Comment${data.length === 1 ? "" : "s"}`;
177+
document.getElementById("comments-link").innerText = `${data.length} Comment${data.length === 1 ? "" : "s"}`;
170178
const tree = buildTree(data);
171-
document.getElementById("comments-container").innerHTML = tree
172-
.map((n) => renderNode(n, 0))
173-
.join("");
179+
document.getElementById("comments-container").innerHTML = tree.map((n) => renderNode(n, 0)).join("");
174180
}
175181

176-
// ---------- Actions ----------
177-
async function postOrUpdate(content, editId, parentId) {
182+
// ---------- Post / Update ----------
183+
async function postOrUpdate(content, editId, parentId, name) {
178184
if (editId) {
179185
await supabase.from("comments").update({ content }).eq("id", editId);
180186
} else {
181-
await supabase
187+
const { data, error } = await supabase
182188
.from("comments")
183189
.insert([
184-
{ post_slug: POST_SLUG, content, parent_id: parentId || null },
190+
{
191+
post_slug: POST_SLUG,
192+
content,
193+
parent_id: parentId || null,
194+
name: name || "Anonymous",
195+
user_id: anonId,
196+
},
185197
])
186198
.select()
187199
.single();
188200
}
189201
await loadComments();
190202
}
191203

204+
// ---------- Toggle reactions ----------
192205
async function toggleReaction(id, emoji) {
193-
const { data } = await supabase
194-
.from("comments")
195-
.select("reactions")
196-
.eq("id", id)
197-
.single();
206+
const { data } = await supabase.from("comments").select("reactions").eq("id", id).single();
198207
const reactions = data?.reactions || {};
199208
const active = hasReacted(id, emoji);
200209
const delta = active ? -1 : 1;
@@ -204,12 +213,14 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
204213
await loadComments();
205214
}
206215

216+
// ---------- Delete ----------
207217
async function removeComment(id) {
208218
if (!confirm("Delete this comment?")) return;
209219
await supabase.from("comments").delete().eq("id", id);
210220
await loadComments();
211221
}
212222

223+
// ---------- Edit / Reply ----------
213224
function startEdit(id, encoded) {
214225
const content = decodeURIComponent(encoded);
215226
document.getElementById("comment-input").value = content;
@@ -228,9 +239,7 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
228239
}
229240

230241
function toggleReplies(id) {
231-
const btn = document.querySelector(
232-
`[data-action="toggle-replies"][data-id="${id}"]`
233-
);
242+
const btn = document.querySelector(`[data-action="toggle-replies"][data-id="${id}"]`);
234243
const box = document.getElementById("replies-" + id);
235244
const open = btn.dataset.open === "1";
236245
if (open) {
@@ -246,17 +255,6 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
246255

247256
// ---------- Event delegation ----------
248257
document.addEventListener("DOMContentLoaded", () => {
249-
const link = document.getElementById("comments-link");
250-
link?.addEventListener("click", (e) => {
251-
e.preventDefault();
252-
const target = document.getElementById("comments");
253-
if (!target) return;
254-
target.scrollIntoView({ behavior: "smooth", block: "start" });
255-
setTimeout(() => {
256-
document.getElementById("comment-input")?.focus();
257-
}, 300);
258-
});
259-
260258
const form = document.getElementById("comment-form");
261259
const cancelBtn = document.getElementById("cancel-edit");
262260
const container = document.getElementById("comments-container");
@@ -266,22 +264,21 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
266264
return;
267265
}
268266

269-
// Form submit
270267
form.addEventListener("submit", async (e) => {
271268
e.preventDefault();
272269
const val = document.getElementById("comment-input").value.trim();
273270
const editId = document.getElementById("edit-id").value;
274271
const parentId = document.getElementById("parent-id").value;
272+
const name = document.getElementById("comment-name").value.trim() || "Anonymous";
275273
if (!val) return;
276-
await postOrUpdate(val, editId, parentId);
274+
await postOrUpdate(val, editId, parentId, name);
277275
form.reset();
278276
document.getElementById("edit-id").value = "";
279277
document.getElementById("parent-id").value = "";
280278
document.getElementById("comment-submit").innerText = "Post Comment";
281279
cancelBtn.classList.add("hidden");
282280
});
283281

284-
// Cancel edit
285282
cancelBtn?.addEventListener("click", () => {
286283
form.reset();
287284
document.getElementById("edit-id").value = "";
@@ -290,7 +287,6 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
290287
cancelBtn.classList.add("hidden");
291288
});
292289

293-
// Event delegation for actions
294290
container.addEventListener("click", (e) => {
295291
const btn = e.target.closest("button[data-action]");
296292
if (!btn) return;
@@ -314,25 +310,19 @@ <h2 class="text-xl font-semibold dark:text-neutral-100">Comments</h2>
314310
}
315311
});
316312

317-
// Initial load
318313
loadComments();
319314
});
320315

321316
// ---------- Realtime sync ----------
322317
supabase
323318
.channel("comments-" + POST_SLUG)
324-
.on(
325-
"postgres_changes",
326-
{
327-
event: "*",
328-
schema: "public",
329-
table: "comments",
330-
filter: `post_slug=eq.${POST_SLUG}`,
331-
},
332-
() => loadComments()
333-
)
319+
.on("postgres_changes", {
320+
event: "*",
321+
schema: "public",
322+
table: "comments",
323+
filter: `post_slug=eq.${POST_SLUG}`,
324+
}, () => loadComments())
334325
.subscribe();
335326

336-
// Initial load
337327
loadComments();
338328
</script>

0 commit comments

Comments
 (0)