PageSourceSearch

https://sprunkygame.co/template/default/js/comments_ajax.js?v=2

js sprunkygame.co collected 2026-09-24 11:48:51 UTC 22,296 bytes, 488 lines download raw bytes

1window.clearCommentsListCache = function () {
2    try {
3        var keys = [];
4        for (var i = 0; i < sessionStorage.length; i++) {
5            var k = sessionStorage.key(i);
6            if (k && (k.indexOf('cmt:') === 0 || k.indexOf('cmt2:') === 0)) keys.push(k);
7        }
8        keys.forEach(function (k) { sessionStorage.removeItem(k); });
9    } catch (e) {}
10};
11
12class CommentSystem {
13    constructor(options = {}) {
14        this.config = {
15            container: '.comments__list',
16            typeid: 0,
17            type: 'news',
18			source_title: document.title || 'Friday Night Funkin',
19            source_uri: window.location.pathname || '/',
20			loginNeeded: 1,
21            ...options
22        };
23        this.init();
24    }
25
26    init() {
27        this.container = document.querySelector(this.config.container);
28		this.paginationContainer = document.querySelector(this.config.pagination);
29        if (!this.container) return;
30
31        this.loadComments();
32        this.setupEventListeners();
33    }
34
35    commentsCacheKey(page) {
36        return 'cmt2:' + this.config.typeid + ':' + this.config.type + ':' + page;
37    }
38
39    applyCommentsPayload(data) {
40        this.renderComments(data.data);
41        this.renderPagination(data.page, data.per_page, data.total || data.data.length);
42        this.config.currentPage = data.page;
43    }
44
45    async loadComments(page = this.config.currentPage) {
46        const pageNum = page || 1;
47        const cacheKey = this.commentsCacheKey(pageNum);
48
49        if (this._inflightKey === cacheKey && this._inflight) {
50            return this._inflight;
51        }
52
53        try {
54            const raw = sessionStorage.getItem(cacheKey);
55            if (raw) {
56                const parsed = JSON.parse(raw);
57                if (parsed && parsed.exp > Date.now() && parsed.payload && parsed.payload.success) {
58                    this.applyCommentsPayload(parsed.payload);
59                    return;
60                }
61            }
62        } catch (e) {}
63
64        const params = new URLSearchParams({
65            typeid: this.config.typeid,
66            type: this.config.type,
67            page: String(pageNum),
68            cv: '2'
69        });
70
71        this._inflightKey = cacheKey;
72        this._inflight = (async () => {
73            try {
74                const response = await fetch('/api/comments?' + params.toString(), {
75                    credentials: 'same-origin'
76                });
77                const data = await response.json();
78
79                if (data.success) {
80                    this.applyCommentsPayload(data);
81                    try {
82                        sessionStorage.setItem(cacheKey, JSON.stringify({
83                            exp: Date.now() + 180000,
84                            payload: data
85                        }));
86                    } catch (e) {}
87                } else {
88                    console.error('No comments:', data.message);
89                }
90            } catch (error) {
91                console.error('Comments loading error:', error);
92            } finally {
93                this._inflight = null;
94                this._inflightKey = null;
95            }
96        })();
97
98        return this._inflight;
99    }
100
101    renderComments(comments) {
102        let html = '';
103        comments.forEach(comment => {
104            html += this.renderComment(comment);
105        });
106        this.container.innerHTML = html || '';
107    }
108	
109	renderPagination(currentPage, perPage, totalItems) {
110        if (!this.paginationContainer) return;
111        
112        const totalPages = Math.ceil(totalItems / perPage);
113        if (totalPages <= 1) {
114            this.paginationContainer.innerHTML = '';
115            return;
116        }
117
118        let html = '<div class="comments__pagination">';
119        
120        const startPage = Math.max(1, currentPage - 2);
121        const endPage = Math.min(totalPages, currentPage + 2);
122        
123        if (startPage > 1) {
124            html += `<button class="comments__page-btn" data-page="1">1</button>`;
125            if (startPage > 2) {
126                html += '<span class="pagination__ellipsis">...</span>';
127            }
128        }
129        
130        for (let i = startPage; i <= endPage; i++) {
131            html += `<button class="comments__page-btn ${i === currentPage ? 'comments__page-btn--active' : ''}" data-page="${i}">${i}</button>`;
132        }
133        
134        if (endPage < totalPages) {
135            if (endPage < totalPages - 1) {
136                html += '<span class="pagination__ellipsis">...</span>';
137            }
138            html += `<button class="comments__page-btn" data-page="${totalPages}">${totalPages}</button>`;
139        }
140
141
142        html += '</div>';
143        this.paginationContainer.innerHTML = html;
144    }
145
146    renderComment(comment) {
147        const avatar = comment.face 
148            ? `<img src="${comment.face}" alt="User Avatar" loading="lazy" decoding="async">` 
149            : comment.cname.charAt(0);
150        
151        const time = new Date(comment.pubtime * 1000).toLocaleStr
151ing();
152        
153        let repliesHtml = '';
154        if (comment.reply && comment.reply.length) {
155            comment.reply.forEach(reply => {
156                repliesHtml += this.renderReply(reply);
157            });
158        }
159
160        return `
161        <div class="comments__item" data-comment-id="${comment.id}">
162            <div class="comments__avatar">${avatar}</div>
163            <div class="comments__content">
164                <div class="comments__header">
165                    <span class="comments__author">${comment.cname}</span>
166                    <span class="comments__date">${time}</span>
167                </div>
168                <p class="comments__text">${comment.content}</p>
169                
170                <div class="comments__actions">
171                    <button class="comments__vote-btn" data-action="upvote" data-comment-id="${comment.id}">
172                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--up">
173                            <path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/>
174                        </svg>
175                        <span class="comments__vote-count">${comment.up || 0}</span>
176                    </button>
177                    <button class="comments__vote-btn" data-action="downvote" data-comment-id="${comment.id}">
178                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--down">
179                            <path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v1.91l.01.01L1 14c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.58-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/>
180                        </svg>
181                        <span class="comments__vote-count">${comment.down || 0}</span>
182                    </button>
183                    <button class="comments__reply-btn" data-comment-id="${comment.id}">
184                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__reply-icon">
185                            <path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/>
186                        </svg>
187                        ${_LANG.reply}
188                    </button>
189                </div>
190                
191                <div class="comments__reply-list">
192                    <div class="comments__reply-form" style="display:none;">
193                        <form class="comments__form">
194                            <input type="hidden" name="parentid" value="${comment.id}">
195                            <input type="hidden" name="typeid" value="${this.config.typeid}">
196                            <input type="hidden" name="type" value="${this.config.type}">
197							<input type="hidden" name="source_title" value="${this.config.source_title}">
198                            <input type="hidden" name="source_uri" value="${this.config.source_uri}">
199                            <div class="comments__form-group">
200                                <input placeholder="${_LANG.yourname}" type="text" name="comment_name" class="comments__form-input" required>
201                            </div>
202                            <div class="comments__form-group">
203                                <textarea name="comment_message" class="comments__form-input" placeholder="${_LANG.yourcomment}..." required></textarea>
204                            </div>
205                            <div class="comments__form-actions">
206                                <button type="submit" class="comments__form-submit">${_LANG.post}</button>
207                                <button type="button" class="comments__form-cancel">${_LANG.cancel}</button>
208                            </div>
209                        </form>
210                    </div>
211                    ${repliesHtml}
212                </div>
213            </div>
214        </div>`;
215    }
216
217    renderReply(reply, isNew = false) {
218        const avatar = reply.face 
219            ? `<img src="${reply.face}" alt="Reply Avatar" loading="lazy" decoding="async">` 
220            : reply.cname.charAt(0);
221        
222        const time = new Date(reply.pubtime * 1000).toLocaleString();
223        
224        return `
225        <div class="comments__reply-item" data-reply-id="${reply.id}">
226            <div class="comments__admin-reply">
227                <div class="comments__admin-header">
228                    <span class="comments__admin-name">
229                        <div class="comments__reply-avatar">${avatar}</div>
230                        ${reply.cname}
231                        ${isNew ? '<span class="comments__under-review">'+_LANG.under_review+'</span>' : ''}
232                    </span>
233                    <span class="comments__reply-date">${time}</span>
234                </div>
235                <p class="comments__admin-text">${reply.content}</p>
236                <div class="comments__actions">
237                    <button class="comments__vote-btn" data-action="upvote" data-comment-id="${reply.id}">
238                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--up">
239                            <path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/>
240                        </svg>
241                        <span class="comments__vote-count">${reply.up || 0}</span>
242                    </button>
243                    <button class="comments__vote-btn" data-action="downvote" data-comment-id="${reply.id}">
244                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="comments__vote-icon comments__vote-icon--down">
245                            <path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v1.91l.01.01L1 14c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.58-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/>
246                        </svg>
247                        <span class="comments__vote-count">${reply.down || 0}</span>
248                    </button>
249                </div>
250            </div>
251        </div>`;
252    }
253
254    setupEventListeners() {
255        document.addEventListener('click', async (e) => {
256			if (e.target.closest('.comments__page-btn')) {
257                e.preventDefault();
258                const btn = e.target.closest('.comments__page-btn');
259                const page = parseInt(btn.dataset.page);
260                if (!isNaN(page)) {
261                    this.loadComments(page);
262                    //this.container.scrollIntoView({ behavior: 'smooth' });
263					this.container.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
264window.scrollBy(0, -100);
265                }
266            }
267            // Vote handling
268            if (e.target.closest('.comments__vote-btn')) {
269                const btn = e.target.closest('.comments__vote-btn');
270                const action = btn.dataset.action;
271                const commentId = btn.dataset.commentId;
272                const countEl = btn.querySelector('.comments__vote-count');
273                
274                // Immediate UI update
275                const oldCount = parseInt(countEl.textContent);
276                countEl.textContent = oldCount + 1;
277                
278                try {
279                    const response = await fetch('/api/submitComment', {
280                        method: 'POST',
281                        headers: {
282                            'Content-Type': 'application/x-www-form-urlencoded',
283                        },
284                        body: `vote_action=${action}&comment_id=${commentId}`
285                    });
286                    
287                    const result = await response.json();
288                    if (!result.success) {
289                        countEl.textContent = oldCount; // Rollback
290                        console.error('Failed:', result.message);
291                        showToast(_LANG.try_again_later+': ' + result.message, false);
292                    } else {
293                        const scope = countEl.closest('.comments__item, .comments__reply-item') || countEl.parentElement;
294                        if (scope && typeof result.up === 'number') {
295                            const upCount = scope.querySelector('[data-action="upvote"] .comments__vote-count');
296                            if (upCount) upCount.textContent = result.up;
297                        }
298                        if (scope && typeof result.down === 'number') {
299                            const downCount = scope.querySelector('[data-action="downvote"] .comments__vote-count');
300                            if (downCount) downCount.textContent = result.down;
301                        }
302                        if (typeof window.clearCommentsListCache === 'function') {
303                            window.clearCommentsListCache();
304                        }
305                        showToast(_LANG.thankyou, true);
306                    }
307                } catch (error) {
308                    countEl.textContent = oldCount; // Rollback
309                    console.error('Error:', error);
310                    showToast(_LANG.try_again_later+': ' + error.message, false);
311                }
312            }
313            
314            // Reply button
315            if (e.target.closest('.comments__reply-btn')) {
316                const btn = e.target.closest('.comments__reply-btn');
317                const commentId = btn.dataset.commentId;
318                const commentItem = btn.closest('.comments__item');
319                const replyList = commentItem.querySelector('.comments__reply-list');
320                
321                // Hide other forms
322                document.querySelectorAll('.comments__reply-form').forEach(f => {
323                    if (f !== replyList.querySelector('.comments__reply-form')) {
324                        f.style.display = 'none';
325                    }
326                });
327                
328                // Show current form
329				var isLogin = (typeof window.iflogin !== 'undefined' ? window.iflogin : 0);
330				if(isLogin==1 || this.config.loginNeeded !=1){
331					let form = replyList.querySelector('.comments__reply-form');
332					if (!form) {
333						form = document.createElement('div');
334						form.className = 'comments__reply-form';
335						form.innerHTML = `
336							<form class="comments__form">
337								<input type="hidden" name="parentid" value="${commentId}">
338								<input type="hidden" name="typeid" value="${this.config.typeid}">
339								<input type="hidden" name="type" value="${this.config.type}">
340								<div class="comments__form-group">
341									<input placeholder="Your Name" type="text" name="comment_name" class="comments__form-input" required>
342								</div>
343								<div class="comments__form-group">
344									<textarea name="comment_message" class="comments__form-input" placeholder="Write your reply..." required></textarea>
345								</div>
346								<div class="comments__form-actions">
347									<button type="submit" class="comments__form-submit">${_LANG.post}</button>
348									<button type="button" class="comments__form-cancel">${_LANG.cancel}</button>
349								</div>
350							</form>
351						`;
352						replyList.prepend(form);
353						// removed recaptcha render for reply form per request
354					}
355					form.style.display = 'block';
356                	form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
357				}else{
358					if (typeof showConfirm === 'function') {
359						showConfirm(_LANG.continue_by_sign||'continue by sign');
360					}
361				}
362                
363            }
364            
365            // Cancel reply
366            if (e.target.closest('.comments__form-cancel')) {
367                e.target.closest('.comments__reply-form').style.display = 'none';
368            }
369            
370            // Submit reply
371            if (e.target.closest('.comments__form-submit')) {
372                e.preventDefault();
373                const form = e.target.closest('form');
374				const formContainer = form.closest('.comments__reply-form'); // 获取表单容器
375                const submitBtn = e.target.closest('button');
376                const commentId = form.querySelector('[name="parentid"]').value;
377                const commentItem = document.querySelector(`[data-comment-id="${commentId}"]`);
378                const replyList = commentItem.querySelector('.comments__reply-list');
379                
380                // Get form values
381                const nameInput = form.querySelector('[name="comment_name"]');
382                const contentInput = form.querySelector('[name="comment_message"]');
383                const name = nameInput.value.trim();
384                const content = contentInput.value.trim();
385
386                // 验证逻辑
387                let validationPassed = true;
388                let validationMessage = '';
389                
390                // 如果有外部验证函数
391                if (typeof validateCommentForm === 'function') {
392                    const validationResult = validateCommentForm(name, content);
393                    if (!validationResult) {
394                        validationMessage = validationResult.message || _LANG.try_again_later;
395                        validationPassed = false;
396                    }
397                } 
398                // 备用验证
399                else {
400                    if (!name) {
401                        validationMessage = 'Please enter your name';
402                        validationPassed = false;
403                    } 
404                    else if (!content) {
405                        validationMessage = 'Please enter your comment';
406                        validationPassed = false;
407                    }
408                }
409
410                // 如果验证失败
411                if (!validationPassed) {
412                    showToast(validationMessage, false);
413                    if (!name) nameInput.focus();
414                    else if (!content) contentInput.focus();
415                    return false;
416                }
417
418                submitBtn.disabled = true;
419                submitBtn.innerHTML = '<span class="spinner"></span> '+_LANG.submitting;
420                
421                try {
422                    // 立即显示回复(带"审核中"标签)
423                    replyList.insertAdjacentHTML('afterbegin', this.renderReply({
424                        id: 'new-' + Date.now(),
425                        cname: name,
426                        content: content,
427                        pubtime: Math.floor(Date.now() / 1000),
428                        up: 0,
429                        down: 0,
430                        face: ''
431                    }, true));
432
433                    // 准备提交数据 - 强制使用 URLSearchParams 格式
434                    const formData = new FormData(form);
435                    const urlParams = new URLSearchParams();
436                    
437                    // 将 FormData 转换为 URLSearchParams
438                    for (const [key, value] of formData.entries()) {
439                        urlParams.append(key, value);
440                    }
441                    
442                    console.log('Sending comment data:', Object.fromEntries(urlParams.entries()));
443                    
444                    // 提交到服务器 - 明确指定内容类型
445                    const response = await fetch('/api/submitComment', {
446                        method: 'POST',
447                        headers: {
448                            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
449                        },
450                        body: urlParams.toString()
451                    });
452                    
453                    const result = await response.json();
454                    
455                    if (!result.success) {
456                        throw new Error(result.message || _LANG.try_again_later);
457                    }
458
459                    if (typeof window.clearCommentsListCache === 'function') {
460                        window.clearCommentsListCache();
461                    }
462                    
463                    // 成功提交后重置表单
464                    form.reset();
465                    formContainer.style.display = 'none';
466                    showToast(_LANG.submitted, true);
467                    
468                } catch (error) {
469                    console.error('Error:', error);
470                    showToast(_LANG.try_again_later, false);
471                    
472                    // 可以在这里添加回滚UI的代码
473                    // 例如移除刚添加的回复
474                    const newReply = replyList.querySelector('[data-reply-id^="new-"]');
475                    if (newReply) {
476                        newReply.remove();
477                    }
478                    
479                } finally {
480                    submitBtn.disabled = false;
481                    submitBtn.textContent = _LANG.post;
482                }
483            }
484        });
485    }
486}
487
488window.CommentSystem = CommentSystem;

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.