@@ -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