PageSourceSearch

https://www.myluckfortune.com/assets/js/comments.js?v=20260320-113658

js myluckfortune.com collected 2026-10-02 00:53:13 UTC 15,106 bytes, 393 lines download raw bytes

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*, &#96;code&#96;
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.