1/** 2 * Luck & Fortune Finance - Comments System 3 * Client-side comments integration 4 * 5 * Usage: Include this script and call initComments('article-slug') 6 */ 7 8(function() { 9 'use strict'; 10 11 // Configuration 12 const CONFIG = { 13 // Your Cloudflare Worker URL 14 API_URL: 'https://comments.myluckfortune.com', 15 16 // Number of comments to show initially 17 DEFAULT_LIMIT: 50, 18 19 // Gravatar URL (using hashed email) 20 GRAVATAR_URL: 'https://www.gravatar.com/avatar/', 21 22 // Default avatar if no Gravatar 23 DEFAULT_AVATAR: '/assets/images/default-avatar.png' 24 }; 25 26 // State 27 let currentSlug = null; 28 let comments = []; 29 30 /** 31 * Initialize comments for a page 32 */ 33 window.initComments = function(slug) { 34 currentSlug = slug; 35 const container = document.getElementById('comments-container'); 36 37 if (!container) { 38 console.error('Comments container not found. Add: <div id="comments-container"></div>'); 39 return; 40 } 41 42 renderCommentsInterface(container); 43 loadComments(); 44 }; 45 46 /** 47 * Render the comments interface 48 */ 49 function renderCommentsInterface(container) { 50 container.innerHTML = ` 51 <section class="comments-section"> 52 <h2 class="comments-title"> 53 ð¬ Comments 54 <span class="comment-count-badge" id="comment-count">0</span> 55 </h2> 56 57 <div id="comments-list" class="comments-list"> 58 <p class="comments-loading">Loading comments...</p> 59 </div> 60 61 <div class="comment-form-wrapper"> 62 <h3>Leave a Comment</h3> 63 <form id="comment-form" class="comment-form"> 64 <div class="form-row"> 65 <div class="form-group"> 66 <label for="comment-name">Name *</label> 67 <input type="text" id="comment-name" name="author_name" 68 required maxlength="100" placeholder="Your name"> 69 </div> 70 <div class="form-group"> 71 <label for="comment-email">Email * (not published)</label> 72 <input type="email" id="comment-email" name="author_email" 73 required placeholder="[email protected]"> 74 </div> 75 </div> 76 77 <!-- Honeypot field - hidden from real users --> 78 <div class="form-group honey-pot"> 79 <input type="text" name="website" tabindex="-1" autocomplete="off"> 80 </div> 81 82 <div class="form-group"> 83 <label for="comment-content">Comment *</label> 84 <textarea id="comment-content" name="content" 85 required maxlength="5000" rows="5" 86 placeholder="Share your thoughts..."></textarea> 87 <small class="help-text"> 88 You can use: **bold**, *italic*, `code` 89 </small> 90 </div> 91 92 <input type="hidden" id="comment-parent-id" name="parent_id" value=""> 93 94 <div id="replying-to" class="replying-to d-none"> 95 <span>Replying to <strong id="reply-author"></strong></span> 96 <button type="button" class="btn-cancel-reply" id="btn-cancel-reply">â Cancel</button> 97 </div> 98 99 <button type="submit" class="btn-submit-comment">
100 <span class="btn-text">Post Comment</span> 101 <span class="btn-loading d-none">Submitting...</span> 102 </button> 103 </form> 104 105 <div id="comment-message" class="comment-message d-none"></div> 106 </div> 107 </section> 108 `; 109 110 // Bind form submission 111 const form = document.getElementById('comment-form'); 112 form.addEventListener('submit', handleSubmit); 113 114 // Bind reply buttons (event delegation) 115 document.getElementById('comments-list').addEventListener('click', function(e) { 116 if (e.target.classList.contains('btn-reply')) { 117 const commentId = e.target.dataset.commentId; 118 const authorName = e.target.dataset.authorName; 119 setReplyTo(commentId, authorName); 120 } 121 }); 122 123 // Bind cancel reply button 124 document.getElementById('btn-cancel-reply').addEventListener('click', cancelReply); 125 126 // Reset form state (in case browser restored it) 127 resetSubmitButton(); 128 document.getElementById('replying-to').classList.add('d-none'); 129 document.getElementById('comment-parent-id').value = ''; 130 } 131 132 /** 133 * Reset submit button to default state 134 */ 135 function resetSubmitButton() { 136 const submitBtn = document.querySelector('.btn-submit-comment'); 137 const btnText = submitBtn.querySelector('.btn-text'); 138 const btnLoading = submitBtn.querySelector('.btn-loading'); 139 140 submitBtn.disabled = false; 141 btnText.classList.remove('d-none'); 142 btnLoading.classList.add('d-none'); 143 btnLoading.textContent = 'Submitting...'; 144 } 145 146 /** 147 * Load comments from API 148 */ 149 async function loadComments() { 150 const listContainer = document.getElementById('comments-list'); 151 152 try { 153 const response = await fetch( 154 `${CONFIG.API_URL}/comments?slug=${encodeURIComponent(currentSlug)}&limit=${CONFIG.DEFAULT_LIMIT}` 155 ); 156 157 if (!response.ok) { 158 throw new Error('Failed to load comments'); 159 } 160 161 const data = await response.json(); 162 comments = data.comments || []; 163 164 // Update comment count 165 const countEl = document.getElementById('comment-count'); 166 if (countEl) { 167 countEl.textContent = data.count || 0; 168 } 169 170 if (comments.length === 0) { 171 listContainer.innerHTML = ` 172 <p class="no-comments">No comments yet. Be the first to share your thoughts!</p> 173 `; 174 } else { 175 listContainer.innerHTML = renderCommentList(comments); 176 } 177 178 } catch (error) { 179 console.error('Error loading comments:', error); 180 listContainer.innerHTML = ` 181 <p class="comments-error"> 182 Unable to load comments. Please try again later. 183 </p> 184 `; 185 } 186 } 187 188 /** 189 * Render list of comments 190 */ 191 function renderCommentList(comments, level = 0) { 192 if (!comments || comments.length === 0) { 193 return ''; 194 } 195 196 return comments.map(comment => renderComment(comment, level)).join(''); 197 } 198 199 /** 200 * Render a single comment 201 */ 202 function renderComment(comment, level = 0) { 203 const avatarUrl = comment.author_email_hash 204 ? `${CONFIG.GRAVATAR_URL}${comment.author_email_hash}?d=mp&s=80` 205 : CONFIG.DEFAULT_AVATAR; 206 207 const date = new Date(comment.created_at).toLocaleDateString('en-US', { 208 year: 'numeric', 209 month: 'long', 210 day: 'numeric' 211 }); 212 213 const indentClass = level > 0 ? `comment-reply comment-level-${Math.min(level, 3)}` : ''; 214 215 return ` 216 <article class="comment ${indentClass}" data-comment-id="${comment.id}"> 217 <div class="comment-avatar"> 218 <img src="${avatarUrl}" alt="${escapeHtml(comment.author_name)}" loading="lazy"> 219 </div> 220 <div class="comment-body"> 221 <header class="comment-header">
222 <span class="comment-author">${escapeHtml(comment.author_name)}</span> 223 <time class="comment-date" datetime="${comment.created_at}">${date}</time> 224 </header> 225 <div class="comment-content"> 226 ${formatCommentContent(comment.content)} 227 </div> 228 <footer class="comment-footer"> 229 <button type="button" class="btn-reply" 230 data-comment-id="${comment.id}" 231 data-author-name="${escapeHtml(comment.author_name)}"> 232 â© Reply 233 </button> 234 </footer> 235 236 ${comment.replies && comment.replies.length > 0 ? ` 237 <div class="comment-replies"> 238 ${renderCommentList(comment.replies, level + 1)} 239 </div> 240 ` : ''} 241 </div> 242 </article> 243 `; 244 } 245 246 /** 247 * Format comment content (simple markdown-like parsing) 248 */ 249 function formatCommentContent(content) { 250 if (!content) return ''; 251 252 // Convert line breaks to <br> 253 let formatted = escapeHtml(content).replace(/\n/g, '<br>'); 254 255 // Bold: **text** or __text__ 256 formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); 257 formatted = formatted.replace(/__(.+?)__/g, '<strong>$1</strong>'); 258 259 // Italic: *text* or _text_ 260 formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>'); 261 formatted = formatted.replace(/_(.+?)_/g, '<em>$1</em>'); 262 263 // Code: `text` 264 formatted = formatted.replace(/`(.+?)`/g, '<code>$1</code>'); 265 266 return formatted; 267 } 268 269 /** 270 * Handle form submission 271 */ 272 async function handleSubmit(event) { 273 event.preventDefault(); 274 275 const form = event.target; 276 const submitBtn = form.querySelector('.btn-submit-comment'); 277 const btnText = submitBtn.querySelector('.btn-text'); 278 const btnLoading = submitBtn.querySelector('.btn-loading'); 279 const messageEl = document.getElementById('comment-message'); 280 281 // Show loading state 282 submitBtn.disabled = true; 283 btnText.classList.add('d-none'); 284 btnLoading.classList.remove('d-none'); 285 286 const formData = { 287 slug: currentSlug, 288 author_name: form.author_name.value.trim(), 289 author_email: form.author_email.value.trim(), 290 content: form.content.value.trim(), 291 parent_id: form.parent_id.value || null, 292 website: form.website.value // Honeypot 293 }; 294 295 try { 296 const response = await fetch(`${CONFIG.API_URL}/comment`, { 297 method: 'POST', 298 headers: { 299 'Content-Type': 'application/json' 300 }, 301 body: JSON.stringify(formData) 302 }); 303 304 const data = await response.json(); 305 306 if (response.ok) { 307 // Success 308 if (data.status === 'approved') { 309 btnLoading.textContent = 'Posted!'; 310 showMessage(messageEl, data.message, 'success'); 311 setTimeout(() => loadComments(), 500); 312 } else { 313 // Pending moderation 314 btnLoading.textContent = 'Submitted!'; 315 showMessage(messageEl, data.message || 'Comment submitted for moderation', 'success'); 316 } 317 form.reset(); 318 cancelReply(); 319 return; // Exit early - success state persists 320 } else { 321 // Error 322 showMessage(messageEl, data.error || 'Failed to post comment', 'error'); 323 } 324 325 } catch (error) { 326 console.error('Error submitting comment:', error); 327 showMessage(messageEl, 'Network error. Please try again.', 'error'); 328 } 329 330 // Reset button state on error (or if not successful) 331 submitBtn.disabled = false; 332 btnText.classList.remove('d-none'); 333 btnLoading.classList.add('d-none'); 334 } 335 336 /** 337 * Set reply target 338 */ 339 window.setReplyTo = function(commentId, authorName) { 340 const parentIdInput = document.getElementById('comment-parent-id'); 341 const replyingTo = document.getElementById('replying-to'); 342 const replyAuthor = document.getElementById('reply-author'); 343 344 parentIdInput.value = commentId; 345 replyAuthor.textContent = authorName; 346 replyingTo.classList.remove('d-none'); 347 348 // Scroll to form 349 document.getElementById('comment-form').scrollIntoView({ behavior: 'smooth' });
350 }; 351 352 /** 353 * Cancel reply 354 */ 355 window.cancelReply = function() { 356 const parentIdInput = document.getElementById('comment-parent-id'); 357 const replyingTo = document.getElementById('replying-to'); 358 359 parentIdInput.value = ''; 360 replyingTo.classList.add('d-none'); 361 }; 362 363 /** 364 * Show message 365 */ 366 function showMessage(element, text, type) { 367 element.textContent = text; 368 element.className = `comment-message comment-message-${type}`; 369 element.classList.remove('d-none'); 370 371 setTimeout(() => { 372 element.classList.add('d-none'); 373 }, 5000); 374 } 375 376 /** 377 * Escape HTML to prevent XSS 378 */ 379 function escapeHtml(text) { 380 const div = document.createElement('div'); 381 div.textContent = text; 382 return div.innerHTML; 383 } 384 385 // Auto-initialize if data-slug is present 386 document.addEventListener('DOMContentLoaded', function() { 387 const container = document.getElementById('comments-container'); 388 if (container && container.dataset.slug) { 389 initComments(container.dataset.slug); 390 } 391 }); 392 393})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.