PageSourceSearch

https://www.thespurs.news/wp-content/plugins/talkingpoints-wp/asse…/js/talkingpoints-archive.js?ver=1788449912

js thespurs.news collected 2026-10-09 03:31:53 UTC 46,126 bytes, 978 lines download raw bytes

1// The Vue app handles URL parameters on page load via initFromUrlParams()
2// No need to intercept clicks - just let the browser navigate normally
3
4// Function to update likes and comments text
5function updateLikesAndComments() {
6    // Replace "0" with "Like" for like buttons
7    document.querySelectorAll('.like-wrapper').forEach(function(likeBtn) {
8        var likeSpan = likeBtn.querySelector('span');
9        if (likeSpan && likeSpan.textContent.trim() === '0') {
10            likeSpan.textContent = 'Like';
11        }
12    });
13
14    // Replace "0" with "Reply" for reply buttons
15    document.querySelectorAll('.reply-wrapper').forEach(function(replyBtn) {
16        var replySpan = replyBtn.querySelector('span');
17        if (replySpan && replySpan.textContent.trim() === '0') {
18            replySpan.textContent = 'Reply';
19        }
20    });
21}
22
23// Function to align hero image and comments heights
24function alignHeroHeights() {
25    var hero = document.querySelector('.tp-hero');
26    if (!hero) return;
27
28    var heroContent = hero.querySelector('.tp-hero-content');
29    var heroImage = hero.querySelector('.tp-hero-image');
30
31    if (!heroContent || !heroImage) return;
32
33    // Only apply on desktop (larger than 768px)
34    if (window.innerWidth > 768) {
35        // Reset heights first to get natural dimensions
36        heroContent.style.minHeight = '';
37        heroImage.style.minHeight = '';
38
39        // Force reflow to ensure accurate measurements
40        void(heroContent.offsetHeight);
41        void(heroImage.offsetHeight);
42
43        // Use requestAnimationFrame to ensure DOM is fully rendered
44        requestAnimationFrame(function() {
45            // Get natural heights including all content
46            var imageHeight = heroImage.offsetHeight;
47            var contentHeight = heroContent.scrollHeight; // Use scrollHeight to get full content height
48            var maxHeight = Math.max(imageHeight, contentHeight);
49
50            // Apply min-height to both to prevent cutoff
51            heroContent.style.minHeight = maxHeight + 'px';
52            heroImage.style.minHeight = maxHeight + 'px';
53        });
54    } else {
55        // Reset on mobile/tablet
56        heroContent.style.minHeight = '';
57        heroImage.style.minHeight = '';
58    }
59}
60
61function initHeroResizeObserver() {
62    var hero = document.querySelector('.tp-hero');
63    if (!hero) return;
64    
65    var commentsContainer = hero.querySelector('.tp-comments-ajax-container');
66    if (!commentsContainer) return;
67
68    if (window.ResizeObserver) {
69        var ro = new ResizeObserver(function() {
70            requestAnimationFrame(alignHeroHeights);
71        });
72        ro.observe(commentsContainer);
73    }
74}
75
76// Function to align trending grid comments
77function alignTrendingComments() {
78    var cards = document.querySelectorAll('.tp-trending-grid .tp-card:not(.hidden)');
79    if (cards.length === 0) return;
80
81    // Reset any previous heights
82    cards.forEach(function(card) {
83        var comments = card.querySelectorAll('.comment-container');
84        comments.forEach(function(c) {
85            c.style.minHeight = '';
86        });
87    });
88
89    // Determine columns per row based on screen width
90    var columnsPerRow = 3; // Default desktop
91    if (window.innerWidth <= 768) {
92        columnsPerRow = 1; // Mobile
93    } else if (window.innerWidth <= 1200) {
94        columnsPerRow = 2; // Tablet
95    }
96
97    // Group cards by row
98    var rows = [];
99    for (var i = 0; i < cards.length; i += columnsPerRow) {
100        rows.push(Array.from(cards).slice(i, i + columnsPerRow));
101    }
102
103    // Align comments within each row
104    rows.forEach(function(rowCards) {
105        var firstComments = [];
106        var secondComments = [];
107
108        rowCards.forEach(function(card) {
109            var comments = card.querySelectorAll('.comment-container');
110            if (comments[0]) firstComments.push(comments[0]);
111            if (comments[1]) secondComments.push(comments[1]);
112        });
113
114        // Find max heights for this row
115        var maxFirstHeight = 0;
116        var maxSecondHeight = 0;
117
118        firstComments.forEach(function(c) {
119            var height = c.offsetHeight;
120            if (height > maxFirstHeight) maxFirstHeight = height;
121        });
122
123        secondComments.forEach(function(c) {
124            var height = c.offsetHeight;
125            if (height > maxSecondHeight) maxSecondHeight = height;
126        });
127
128        // Apply max heights to this row only
129        firstComments.forEach(function(c) {
130            c.style.minHeight = maxFirstHeight + 'px';
131        });
132
133        secondComments.forEach(function(c) {
134            c.style.minHeight = maxSecondHeight + 'px';
135        });
136    });
137}
138
139// Topics "See all" + client-side search were removed; the Topics grid now uses
140// API-backed Load more + search wired in talkingpoints-landing.js.
141
142// Function to render comments HTML from Coral API data (matches server-side structure)
143function renderCommentsHTML(comments, storySlug, isHero, commentSort, debugInfo) {
144    // Helper function to check if a comment is from the system user
145    function isSystemComment(comment) {
146        if (!comment || !comment.author) {
147            return false;
148        }
149
150        // Check username field
151        if (comment.author.username) {
152            var username = comment.author.username.toLowerCase();
153            return username === 'system';
154        }
155
156        // Also check authorUsername field (alternative structure)
157        if (comment.authorUsername) {
158            var authorUsername = comment.authorUsername.toLowerCase();
159            return authorUsername === 'system';
160        }
161
162        return false;
163    }
164
165    // Helper function to count all nested replies (including deeply nested)
166    function countAllReplies(comment) {
167        var count = 0;
168        if (comment.replies && comment.replies.edges && Array.isArray(comment.replies.edges)) {
169            comment.replies.edges.forEach(function(edge) {
170                if (edge.node && !isSystemComment(edge.node)) {
171                    count++; // Count this reply
172                    count += countAllReplies(edge.node); // Count its nested replies
173                }
174            });
175        }
176        return count;
177    }
178
179    // Helper function to recursively flatten nested replies from Coral API structure
180    // Excludes system comments themselves but includes their replies
181    function flattenComments(commentsList) {
182        var flattened = [];
183
184        commentsList.forEach(function(comment) {
185            // Only add non-system comments
186            if (!isSystemComment(comment)) {
187                // Before flattening, calculate the true reply count including all nested replies
188                var totalReplies = countAllReplies(comment);
189                // Store this in a field that will be used for display
190                comment._calculatedReplyCount = totalReplies;
191
192                flattened.push(comment);
193            }
194
195            // Always process replies, even for system comments (to get user replies to system comments)
196            // Coral API returns replies as: replies.edges[].node
197            if (comment.replies && comment.replies.edges && Array.isArray(comment.replies.edges)) {
198                comment.replies.edges.forEach(function(edge) {
199                    if (edge.node) {
200                        // Recursively flatten the reply and its nested replies
201                        var nestedReplies = flattenComments([edge.node]);
202                        flattened = flattened.concat(nestedReplies);
203                    }
204                });
205            }
206        });
207
208        return flattened;
209    }
210
211    // Flatten ALL comments including deeply nested replies, excluding system comments
212    var validComments = flattenComments(comments);
213
214    // Apply sorting based on the comment_sort parameter
215    commentSort = commentSort || 'recent'; // Default to recent if not specified
216    if (commentSort === 'recent') {
217        // Sort by creation date (newest first)
218        validComments.sort(function(a, b) {
219            if (a._is_featured && !b._is_featured) return -1;
220            if (!a._is_featured && b._is_featured) return 1;
221            if (a._is_featured && b._is_featured) return (a._featured_order || 0) - (b._featured_order || 0);
222
223            var timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
224            var timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
225            return timeB - timeA;
226        });
227    } else if (commentSort === 'activity_all_time' || commentSort === 'activity_24h') {
228        // Sort by activity score (likes + replies, highest first)
229        // Note: For activity_24h, the server has already filtered to only include
230        // comments from the last 24 hours, so we just sort by activity score
231        validComments.sort(function(a, b) {
232            if (a._is_featured && !b._is_featured) return -1;
233            if (!a._is_featured && b._is_featured) return 1;
234            if (a._is_featured && b._is_featured) return (a._featured_order || 0) - (b._featured_order || 0);
235
236            var getActivityScore = function(comment) {
237                var replyCount = 0;
238                // Use calculated reply count if available (includes all nested replies)
239                if (comment._calculatedReplyCount !== undefined) {
240                    replyCount = parseInt(comment._calculatedReplyCount, 10);
241                } else if (comment.replyCount) {
242                    replyCount = parseInt(comment.replyCount, 10);
243                } else if (comment.actionCounts && comment.actionCounts.reply && comment.actionCounts.reply.total) {
244                    replyCount = parseInt(comment.actionCounts.reply.total, 10);
245                } else if (comment.replies && comment.replies.edges) {
246                    replyCount = comment.replies.edges.length;
247                }
248
249                var reactionCount = 0;
250                if (comment.reactionCount) {
251                    reactionCount = parseInt(comment.reactionCount, 10);
252                } else if (comment.actionCounts && comment.actionCounts.reaction && comment.actionCounts.reaction.total) {
253                    reactionCount = parseInt(comment.actionCounts.reaction.total, 10);
254                }
255
256                return replyCount + reactionCount;
257            };
258
259            var scoreA = getActivityScore(a);
260            var scoreB = getActivityScore(b);
261            return scoreB - scoreA;
262        });
263    }
264
265    // Limit to 2 comments total (including all types of replies)
266    validComments = validComments.slice(0, 2);
267
268    var storyUrl = '/talkingpoints/' + storySlug;
269
270    if (validComments.length === 0) {
271        if (isHero) {
272            return '<div class="tp-no-comments-message"><p>No comments yet</p><a href="' + storyUrl + '" class="tp-see-all-comments">Add a comment</a></div>';
273        }
274        return '<div class="tp-comments-section tp-no-comments"><h4 class="tp-comments-title">Comments</h4><div class="tp-no-comments-message"><p>No comments yet</p><a href="' + storyUrl + '" class="tp-see-all-comments">Add a comment</a></div></div>';
275    }
276
277    var html = '';
278    if (!isHero) {
279        html += '<div class="tp-comments-section">';
280        html += '<h4 class="tp-comments-title">Comments</h4>';
281    }
282    html += '<div class="tp-comments-list">';
283
284    validComments.forEach(function(comment, index) {
285        if (index > 0) {
286            html += '<div data-v-c4e1ea10="" class="divider"></div>';
287        }
288
289        var avatar = comment.author && comment.author.avatar ? comment.author.avatar : '/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png';
290        var username = comment.author && comment.author.username ? comment.author.username : 'Anonymous';
291        var body = comment.body || '';
292
293        // Truncate comment body to 40 words and add ellipsis if truncated
294        var bodyWords = body.split(' ');
295        var truncatedBody = bodyWords.slice(0, 40).join(' ');
296        if (bodyWords.length > 40) {
297            truncatedBody = truncatedBody.trimEnd() + '...';
298        }
299
300        // Calculate time ago
301        var timeStr = '';
302        if (comment.createdAt) {
303            var diff = Math.floor((Date.now() - new Date(comment.createdAt).getTime()) / 1000);
304            if (diff < 60) timeStr = 'Just now';
305            else if (diff < 3600) timeStr = Math.floor(diff / 60) + 'm';
306            else if (diff < 86400) timeStr = Math.floor(diff / 3600) + 'h';
307            else if (diff < 172800) timeStr = 'Yesterday';
308            else timeStr = Math.floor(diff / 86400) + 'd';
309        }
310
311        var likes = comment.reactionCount || (comment.actionCounts && comment.actionCounts.reaction && comment.actionCounts.reaction.total) || 0;
312        // Use calculated reply count if available (includes all nested replies), otherwise fall back to API values
313        var replies = comment._calculatedReplyCount !== undefined ? comment._calculatedReplyCount : (comment.replyCount || (comment.actionCounts && comment.actionCounts.reply && comment.actionCounts.reply.total) || 0);
314
315        var discussionUrl = '/talkingpoints/' + storySlug + '#commentID=' + encodeURIComponent(comment.id);
316
317        html += '<a href="' + discussionUrl + '" class="comment-link" aria-label="View discussion for comment by ' + username + '">';
318        html += '<div class="comment-container">';
319        html += '<div class="comment-user-info">';
320        html += '<div class="avatar-wrapper" style="width: 30px; height: 30px;">';
321        html += '<img src="' + avatar + '" alt="User avatar" onerror="this.onerror=null; this.src=\'/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png\';">';
322        html += '</div>';
323        html += '<span class="user-name">' + username + '</span>';
324        if (comment._is_featured) {
325            html += '<span class="tp-featured-tag" style="background: #e5e7eb; color: #333; font-size: 0.7em; padding: 1px 5px; border-radius: 3px; margin-left: 6px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Featured</span>';
326        }
327        html += '<span class="time-label">' + timeStr + '</span>';
328        html += '</div>';
329        if (truncatedBody) {
330            html += '<span class="paragraph-text">' + truncatedBody + '</span>';
331        }
332
333        // Render GIF if present (matches sidebar Comment.vue logic)
334        var media = comment.revision && comment.revision.media ? comment.revision.media : null;
335        var mediaUrl = media && media.url ? media.url : null;
336        var isGiphy = mediaUrl && mediaUrl.indexOf('youtube.com') === -1 && mediaUrl.indexOf('youtu.be') === -1;
337        var gifVideo = media && media.video ? media.video : null;
338        if (isGiphy && gifVideo) {
339            html += '<div class="vue-tp-gif-wrapper" style="margin-top: 8px;">';
340            html += '<video src="' + gifVideo + '" title="' + (media.title ? media.title.replace(/"/g, '&quot;') : '') + '" autoplay loop muted playsinline class="vue-tp-gif-video" style="max-width: 100%; border-radius: 4px;"></video>';
341            html += '</div>';
342        }
343        html += '<div class="user-actions comment-actions">';
344        html += '<div class="left-side">';
345        html += '<span class="like-wrapper action cursor-pointer">';
346        html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M19.105 4.448c0 1.129-.47 2.258-1.411 3.125l-1.195 1.1L9.622 15l-6.96-6.252-1.208-1.085c-1.922-1.726-1.94-4.542-.043-6.29C2.37.493 3.63.051 4.891.051c1.243 0 2.485.428 3.439 1.284L9.538 2.42l1.194-1.098c1.9-1.747 4.997-1.765 6.92-.038.968.87 1.453 2.017 1.453 3.164"></path></svg>';
347        html += '<span>' + (likes > 0 ? likes : 'Like') + '</span>';
348        html += '</span>';
349        html += '<span class="reply-wrapper action">';
350        html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
350ox="0 0 16 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M15.921 6.307c0 3.374-3.056 6.129-6.9 6.298a10 10 0 0 1-.373.008V15C3.872 15 0 11.642 0 7.5 0 3.466 3.672.176 8.274.008A10 10 0 0 1 8.648 0c4.017 0 7.273 2.824 7.273 6.307"></path></svg>';
351        html += '<span>' + (replies > 0 ? replies : 'Reply') + '</span>';
352        html += '</span>';
353        html += '</div>';
354        html += '</div>';
355        html += '</div>';
356        html += '</a>';
357    });
358
359    html += '</div>'; // close tp-comments-list
360    html += '<a href="' + storyUrl + '" class="tp-see-all-comments">See all comments</a>';
361    if (!isHero) {
362        html += '</div>'; // close tp-comments-section
363    }
364    return html;
365}
366
367// Function to load comments for a card via AJAX (WordPress acts as proxy to avoid CORS)
368function loadCommentsForCard(card) {
369    var container = card.querySelector('.tp-comments-ajax-container');
370    var storyId = card.getAttribute('data-story-id');
371    var postId = card.getAttribute('data-post-id');
372
373    if (container && storyId && container.getAttribute('data-loading') === 'true') {
374        container.setAttribute('data-loading', 'false'); // Prevent duplicate loads
375
376        // Check if TalkingPointsApi is defined
377        // Wait for TalkingPointsApi to be initialized if not ready
378        var apiWaitAttempts = 0;
379        var maxApiWaitAttempts = 300; // 30 seconds (300 * 100ms)
380        
381        function waitForTalkingPointsApi() {
382            apiWaitAttempts++;
383            
384            if (typeof TalkingPointsApi !== 'undefined' && TalkingPointsApi.ajaxUrl) {
385                loadCommentsNow();
386            } else {
387                if (apiWaitAttempts >= maxApiWaitAttempts) {
388                    // Timeout reached - log detailed error information
389                    console.error('[Archive JS] ERROR: TalkingPointsApi timeout after ' + (apiWaitAttempts * 100) + 'ms');
390                    console.error('[Archive JS] TalkingPointsApi state at timeout:');
391                    console.error('[Archive JS] - typeof TalkingPointsApi:', typeof TalkingPointsApi);
392                    console.error('[Archive JS] - TalkingPointsApi defined:', typeof TalkingPointsApi !== 'undefined');
393                    
394                    if (typeof TalkingPointsApi !== 'undefined') {
395                        console.error('[Archive JS] - TalkingPointsApi object:', TalkingPointsApi);
396                        console.error('[Archive JS] - TalkingPointsApi keys:', Object.keys(TalkingPointsApi));
397                        console.error('[Archive JS] - TalkingPointsApi.ajaxUrl:', TalkingPointsApi.ajaxUrl);
398                        console.error('[Archive JS] - TalkingPointsApi.publicUrl:', TalkingPointsApi.publicUrl);
399                        console.error('[Archive JS] - TalkingPointsApi properties:');
400                        for (var prop in TalkingPointsApi) {
401                            if (TalkingPointsApi.hasOwnProperty(prop)) {
402                                console.error('[Archive JS]   - ' + prop + ':', TalkingPointsApi[prop]);
403                            }
404                        }
405                    } else {
406                        console.error('[Archive JS] - window.TalkingPointsApi is undefined');
407                        console.error('[Archive JS] - window object keys (first 20):', Object.keys(window).slice(0, 20));
408                    }
409                    
410                    // Show error in the container
411                    container.innerHTML = '<div style="padding:1em; text-align:center; color:#d32f2f;">Error: Unable to initialize TalkingPointsApi. Please refresh the page.</div>';
412                    return;
413                }
414
415                setTimeout(waitForTalkingPointsApi, 100);
416            }
417        }
418
419        function loadCommentsNow() {
420            // Check if this card has featured comments
421            var featuredCommentsContainer = card.querySelector('.tp-featured-comments-container');
422            
423            if (featuredCommentsContainer && postId) {
424                // Load featured comments first
425                loadFeaturedCommentsForCard(featuredCommentsContainer, storyId, postId, function() {
426                    // After featured comments are loaded, load regular comments
427                    loadRegularComments();
428                });
429            } else {
430                // No featured comments, load regular comments directly
431                loadRegularComments();
432            }
433        }
434
435        function loadRegularComments() {
436            // Fetch comments via WordPress AJAX (which proxies to Coral API)
437            var data = new FormData();
438            data.append('action', 'tpwp_load_comments_json');
439            data.append('story_id', storyId);
440            if (postId) data.append('post_id', postId);
441
442            // Determine comment sort preference and card type
443            var isHero = card.classList.contains('tp-hero');
444            var isTrendingCard = card.classList.contains('tp-card') && card.closest('.tp-trending-grid');
445
446            // Pass context to determine sorting
447            if (isHero) {
448                data.append('card_type', 'hero');
449            } else if (isTrendingCard) {
450                data.append('card_type', 'trending');
451                // Pass the comment sort preference for Top 3 cards
452                if (typeof TalkingPointsApi !== 'undefined' && TalkingPointsApi.top3CommentSort) {
453                    data.append('comment_sort', TalkingPointsApi.top3CommentSort);
454                }
455                // Pass manual top 3 IDs to determine if this is a manually selected card
456                if (typeof TalkingPointsApi !== 'undefined' && TalkingPointsApi.manualTop3Ids) {
457                    data.append('manual_top3_ids', JSON.stringify(TalkingPointsApi.manualTop3Ids));
458                }
459            }
460
461            fetch(TalkingPointsApi.ajaxUrl, {
462                method: 'POST',
463                body: data,
464                credentials: 'same-origin'
465            })
466            .then(res => {
467                if (!res.ok) throw new Error('AJAX response was not ok: ' + res.status);
468                return res.json();
469            })
470            .then(data => {
471                // wp_send_json_success wraps the response in {success: true, data: {...}}
472                var responseData = data.success ? data.data : data;
473                var comments = (responseData && responseData.comments) ? responseData.comments : [];
474                var debugInfo = responseData._debug || null;
475
476                // Render comments client-side
477                var isHero = card.classList.contains('tp-hero');
478                var isTrendingCard = card.classList.contains('tp-card') && card.closest('.tp-trending-grid');
479
480                // Determine comment sort preference
481                var commentSort = 'recent'; // Default
482                if (isTrendingCard && typeof TalkingPointsApi !== 'undefined' && TalkingPointsApi.top3CommentSort) {
483                    commentSort = TalkingPointsApi.top3CommentSort;
484                }
485
486                var html = renderCommentsHTML(comments, card.getAttribute('data-story-slug'), isHero, commentSort, debugInfo);
487                container.innerHTML = html;
488                // Update likes and comments display after loading
489                updateLikesAndComments();
490                // Re-align comments after loading
491                setTimeout(alignTrendingComments, 100);
492                if (card.classList.contains('tp-hero')) {
493                    // Multiple attempts to ensure height is correct after content loads
494                    setTimeout(alignHeroHeights, 50);
495                    setTimeout(alignHeroHeights, 150);
496                    setTimeout(alignHeroHeights, 300);
497                }
498            })
499            .catch(err => {
500                console.error('[Archive JS] Error loading comments:', err);
501                container.innerHTML = '<div style="padding:1em; text-align:center; color:#999;">Unable to load comments.</div>';
502            });
503        }
504
505        // Start waiting for TalkingPointsApi
506        waitForTalkingPointsApi();
507    }
508}
509
510// Function to load featured comments for a specific card
511function loadFeaturedCommentsForCard(container, storyId, postId, callback) {
512    // Fetch featured comment IDs for this story
513    var data = new FormData();
514    data.append('action', 'tpwp_load_featured_comments_by_story');
515    data.append('story_id', storyId);
516    data.append('post_id', postId);
517
518    fetch(TalkingPointsApi.ajaxUrl, {
519        method: 'POST',
520        body: data,
521        credentials: 'same-origin'
522    })
523    .then(res => {
524        if (!res.ok) throw new Error('AJAX response was not ok: ' + res.status);
525        return res.json();
526    })
527    .then(data => {
528        if (data.success && data.data && data.data.comments && data.data.comments.length > 0) {
529            // Render featured comments
530            var html = '<div class="tp-featured-comments-list">';
531            data.data.comments.forEach(function(comment, index) {
532                if (index > 0) {
533                    html += '<div class="divider"></div>';
534                }
535
536                var avatar = comment.author && comment.author.avatar ? comment.author.avatar : '/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png';
537                var username = comment.author && comment.author.username ? comment.author.username : 'Anonymous';
538                var body = comment.body || '';
539
540                // Calculate time ago
541                var timeStr = '';
542                if (comment.createdAt) {
543                    var diff = Math.floor((Date.now() - new Date(comment.createdAt).getTime()) / 1000);
544                    if (diff < 60) timeStr = 'Just now';
545                    else if (diff < 3600) timeStr = Math.floor(diff / 60) + 'm';
546                    else if (diff < 86400) timeStr = Math.floor(diff / 3600) + 'h';
547                    else if (diff < 172800) timeStr = 'Yesterday';
548                    else timeStr = Math.floor(diff / 86400) + 'd';
549                }
550
551                var likes = comment.actionCounts && comment.actionCounts.reaction && comment.actionCounts.reaction.total ? comment.actionCounts.reaction.total : 0;
552                var replies = comment.actionCounts && comment.actionCounts.reply && comment.actionCounts.reply.total ? comment.actionCounts.reply.total : 0;
553
554                // Get story info from comment metadata or URL
555                var storySlug = comment.story && comment.story.metadata && comment.story.metadata.slug ? comment.story.metadata.slug : 'unknown';
556                var discussionUrl = '/talkingpoints/' + storySlug + '#commentID=' + encodeURIComponent(comment.id);
557
558                html += '<div class="tp-featured-comment-item" data-comment-id="' + comment.id + '">';
559                html += '<span class="tp-featured-badge">Featured</span>';
560                html += '<div class="comment-user-info">';
561                html += '<div class="avatar-wrapper" style="width: 40px; height: 40px;">';
562                html += '<img src="' + avatar + '" alt="User avatar" onerror="this.onerror=null; this.src=\'/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png\';">';
563                html += '</div>';
564                html += '<div>';
565                html += '<span class="user-name">' + username + '</span>';
566                html += '<span class="time-label">' + timeStr + '</span>';
567                html += '</div>';
568                html += '</div>';
569                html += '<span class="paragraph-text">' + body + '</span>';
570                html += '<div class="user-actions comment-actions">';
571                html += '<div class="left-side">';
572                html += '<span class="like-wrapper action cursor-pointer">';
573                html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M19.105 4.448c0 1.129-.47 2.258-1.411 3.125l-1.195 1.1L9.622 15l-6.96-6.252-1.208-1.085c-1.922-1.726-1.94-4.542-.043-6.29C2.37.493 3.63.051 4.891.051c1.243 0 2.485.428 3.439 1.284L9.538 2.42l1.194-1.098c1.9-1.747 4.997-1.765 6.92-.038.968.87 1.453 2.017 1.453 3.164"></path></svg>';
574                html += '<span>' + (likes > 0 ? likes : 'Like') + '</span>';
575                html += '</span>';
576                html += '<span class="reply-wrapper action">';
577                html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M15.921 6.307c0 3.374-3.056 6.129-6.9 6.298a10 10 0 0 1-.373.008V15C3.872 15 0 11.642 0 7.5 0 3.466 3.672.176 8.274.008A10 10 0 0 1 8.648 0c4.017 0 7.273 2.824 7.273 6.307"></path></svg>';
578                html += '<span>' + (replies > 0 ? replies : 'Reply') + '</span>';
579                html += '</span>';
580                html += '</div>';
581                html += '</div>';
582                html += '<a href="' + discussionUrl + '" class="tp-see-comment-link">View discussion</a>';
583                html += '</div>';
584            });
585            html += '</div>';
586
587            container.innerHTML = html;
588        } else {
589            // No featured comments or error loading them
590            container.innerHTML = '';
591        }
592
593        // Call the callback to continue with regular comments loading
594        if (callback) {
595            callback();
596        }
597    })
598    .catch(err => {
599        console.error('[Archive JS] Error loading featured comments:', err);
600        container.innerHTML = '';
601        // Continue with regular comments loading even if featured comments fail
602        if (callback) {
603            callback();
604        }
605    });
606}
607
608// Function to load featured comments via AJAX
609function loadFeaturedComments(container) {
610    var commentIdsAttr = container.getAttribute('data-featured-comment-ids');
611    console.log('[Featured Comments] Raw attribute value:', commentIdsAttr);
612
613    if (!commentIdsAttr) {
614        console.error('[Featured Comments] No data-featured-comment-ids attribute found');
615        return;
616    }
617
618    try {
619        var commentIds = JSON.parse(commentIdsAttr);
620        console.log('[Featured Comments] Parsed comment IDs:', commentIds);
621
622        if (!Array.isArray(commentIds) || commentIds.length === 0) {
623            console.warn('[Featured Comments] Empty or invalid comment IDs array');
624            container.innerHTML = '<p style="padding: 2em; text-align: center;">No featured comments selected.</p>';
625            return;
626        }
627    } catch (e) {
628        console.error('[Featured Comments] Error parsing comment IDs:', e, 'Raw value:', commentIdsAttr);
629        container.innerHTML = '<p style="padding: 2em; text-align: center;">Error loading featured comments.</p>';
630        return;
631    }
632
633    // Wait for TalkingPointsApi
634    function waitForApi(callback) {
635        if (typeof TalkingPointsApi !== 'undefined' && TalkingPointsApi.ajaxUrl) {
636            callback();
637        } else {
638            setTimeout(function() { waitForApi(callback); }, 100);
639        }
640    }
641
642    waitForApi(function() {
643        // Fetch featured comments via AJAX
644        // Use URLSearchParams instead of FormData for better WordPress compatibility
645        var params = new URLSearchParams();
646        params.append('action', 'tpwp_load_featured_comments');
647        params.append('comment_ids', JSON.stringify(commentIds));
648
649        console.log('[Featured Comments] Sending to:', TalkingPointsApi.ajaxUrl);
650        console.log('[Featured Comments] comment_ids param:', JSON.stringify(commentIds));
651        console.log('[Featured Comments] Params:', params.toString());
652
653        fetch(TalkingPointsApi.ajaxUrl, {
654            method: 'POST',
655            headers: {
656                'Content-Type': 'application/x-www-form-urlencoded',
657            },
658            body: params.toString(),
659            credentials: 'same-origin'
660        })
661        .then(function(res) {
662            if (!res.ok) throw new Error('AJAX response was not ok: ' + res.status);
663            return res.json();
664        })
665        .then(function(response) {
666            if (!response.success) {
667                console.error('[Featured Comments] AJAX error:', response);
668                throw new Error(response.data || 'Failed to load featured comments');
669            }
670
671            var comments = response.data.comments || [];
672            var errors = response.data.errors || [];
673
674            // Log any errors
675            if (errors.length > 0) {
676                console.error('[Featured Comments] Failed to load some comments:', errors);
677            }
678
679            if (comments.length === 0) {
680                var errorMessage = '<p style="padding: 2em; text-align: center;">No featured comments found.</p>';
681                if (errors.length > 0) {
682                    errorMessage = '<p style="padding: 2em; text-align: center; color: #d32f2f;">Failed to load comments:<br>' +
683                        errors.map(function(e) { return e.comment_id + ': ' + e.error; }).join('<br>') + '</p>';
684                }
685                container.innerHTML = errorMessage;
686                return;
687            }
688
689            // Render featured comments
690            var html = '<div class="tp-featured-comments-list">';
691            comments.forEach(function(comment, index) {
692                if (index > 0) {
693                    html += '<div class="divider"></div>';
694                }
695
696                var avatar = comment.author && comment.author.avatar ? comment.author.avatar : '/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png';
697                var username = comment.author && comment.author.username ? comment.author.username : 'Anonymous';
698                var body = comment.body || '';
699
700                // Calculate time ago
701                var timeStr = '';
702                if (comment.createdAt) {
703                    var diff = Math.floor((Date.now() - new Date(comment.createdAt).getTime()) / 1000);
704                    if (diff < 60) timeStr = 'Just now';
705                    else if (diff < 3600) timeStr = Math.floor(diff / 60) + 'm';
706                    else if (diff < 86400) timeStr = Math.floor(diff / 3600) + 'h';
707                    else if (diff < 172800) timeStr = 'Yesterday';
708                    else timeStr = Math.floor(diff / 86400) + 'd';
709                }
710
711                var likes = comment.actionCounts && comment.actionCounts.reaction && comment.actionCounts.reaction.total ? comment.actionCounts.reaction.total : 0;
712                var replies = comment.actionCounts && comment.actionCounts.reply && comment.actionCounts.reply.total ? comment.actionCounts.reply.total : 0;
713
714                // Get story info from comment metadata or URL
715                var storySlug = comment.story && comment.story.metadata && comment.story.metadata.slug ? comment.story.metadata.slug : 'unknown';
716                var discussionUrl = '/talkingpoints/' + storySlug + '#commentID=' + encodeURIComponent(comment.id);
717
718                html += '<div class="tp-featured-comment-item" data-comment-id="' + comment.id + '">';
719                html += '<span class="tp-featured-badge">Featured</span>';
720                html += '<div class="comment-user-info">';
721                html += '<div class="avatar-wrapper" style="width: 40px; height: 40px;">';
722                html += '<img src="' + avatar + '" alt="User avatar" onerror="this.onerror=null; this.src=\'/wp-content/plugins/talking-points/vue-src/dist/images/fallback-avatar.png\';">';
723                html += '</div>';
724                html += '<div>';
725                html += '<span class="user-name">' + username + '</span>';
726                html += '<span class="time-label">' + timeStr + '</span>';
727                html += '</div>';
728                html += '</div>';
729                html += '<span class="paragraph-text">' + body + '</span>';
730                html += '<div class="user-actions comment-actions">';
731                html += '<div class="left-side">';
732                html += '<span class="like-wrapper action cursor-pointer">';
733                html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
733ox="0 0 20 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M19.105 4.448c0 1.129-.47 2.258-1.411 3.125l-1.195 1.1L9.622 15l-6.96-6.252-1.208-1.085c-1.922-1.726-1.94-4.542-.043-6.29C2.37.493 3.63.051 4.891.051c1.243 0 2.485.428 3.439 1.284L9.538 2.42l1.194-1.098c1.9-1.747 4.997-1.765 6.92-.038.968.87 1.453 2.017 1.453 3.164"></path></svg>';
734                html += '<span>' + (likes > 0 ? likes : 'Like') + '</span>';
735                html += '</span>';
736                html += '<span class="reply-wrapper action">';
737                html += '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 15" class="icon" fill="#666"><path fill-rule="evenodd" d="M15.921 6.307c0 3.374-3.056 6.129-6.9 6.298a10 10 0 0 1-.373.008V15C3.872 15 0 11.642 0 7.5 0 3.466 3.672.176 8.274.008A10 10 0 0 1 8.648 0c4.017 0 7.273 2.824 7.273 6.307"></path></svg>';
738                html += '<span>' + (replies > 0 ? replies : 'Reply') + '</span>';
739                html += '</span>';
740                html += '</div>';
741                html += '</div>';
742                html += '<a href="' + discussionUrl + '" class="tp-see-comment-link">View discussion</a>';
743                html += '</div>';
744            });
745            html += '</div>';
746
747            container.innerHTML = html;
748        })
749        .catch(function(err) {
750            console.error('[Featured Comments] Error:', err);
751            container.innerHTML = '<p style="padding: 2em; text-align: center; color: #d32f2f;">Error loading featured comments.</p>';
752        });
753    });
754}
755
756// Load comments + re-align for hero/trending cards injected after the initial
757// render (cold-start async featured path in talkingpoints-landing.js). Safe to
758// call repeatedly — loadCommentsForCard() guards against duplicate loads.
759window.tpArchiveLoadFeaturedComments = function () {
760    var hero = document.querySelector('.tp-hero');
761    if (hero) {
762        loadCommentsForCard(hero);
763    }
764    document.querySelectorAll('.tp-trending-grid .tp-card:not(.hidden)').forEach(function (card) {
765        loadCommentsForCard(card);
766    });
767    initHeroResizeObserver();
768    setTimeout(alignHeroHeights, 100);
769    setTimeout(alignTrendingComments, 100);
770};
771
772function initArchive() {
773    if (window.tpArchiveInitialized) return;
774    window.tpArchiveInitialized = true;
775
776    // Run on initial page load
777    updateLikesAndComments();
778    
779    // Initialize hero resize observer
780    initHeroResizeObserver();
781
782    // Load comments for all visible trending cards on page load
783    var visibleTrendingCards = document.querySelectorAll('.tp-trending-grid .tp-card:not(.hidden)');
784    visibleTrendingCards.forEach(function(card) {
785        loadCommentsForCard(card);
786    });
787
788    // Load comments for hero
789    var hero = document.querySelector('.tp-hero');
790    if (hero) {
791        loadCommentsForCard(hero);
792    }
793
794    // Load featured comments if they exist
795    var featuredCommentsContainer = document.querySelector('.tp-featured-comments-container');
796    if (featuredCommentsContainer) {
797        loadFeaturedComments(featuredCommentsContainer);
798    }
799
800    // Align hero heights after content loads - multiple attempts with increasing delays
801    setTimeout(alignHeroHeights, 200);
802    setTimeout(alignHeroHeights, 500);
803    setTimeout(alignHeroHeights, 1000);
804    window.addEventListener('load', function() {
805        setTimeout(alignHeroHeights, 100);
806        setTimeout(alignHeroHeights, 500);
807    });
808
809    // Align comments after images load
810    setTimeout(alignTrendingComments, 100);
811    window.addEventListener('load', alignTrendingComments);
812
813    // Re-align on window resize (debounced)
814    var resizeTimer;
815    window.addEventListener('resize', function() {
816        clearTimeout(resizeTimer);
817        resizeTimer = setTimeout(function() {
818            alignHeroHeights();
819            alignTrendingComments();
820        }, 250);
821    });
822    // Trending section "See all" button
823    var trendingSeeAllBtn = document.querySelector('#tp-trending-see-all');
824    if (trendingSeeAllBtn) {
825        trendingSeeAllBtn.addEventListener('click', function() {
826            var trendingCards = document.querySelectorAll('.tp-trending-grid .tp-card.hidden');
827            trendingCards.forEach(function(card) {
828                card.classList.remove('hidden');
829                // Load comments for newly visible cards
830                loadCommentsForCard(card);
831            });
832            // Hide the button after showing all
833            this.style.display = 'none';
834            // Re-align after showing all cards
835            setTimeout(alignTrendingComments, 50);
836        });
837    }
838
839    // Mobile Carousel Logic for Topics
840    if (window.innerWidth <= 768) {
841        var topicsGrid = document.getElementById('tp-topics-grid');
842        var prevButton = document.querySelector('.tp-carousel-prev');
843        var nextButton = document.querySelector('.tp-carousel-next');
844
845        if (topicsGrid && prevButton && nextButton) {
846            // Select ALL cards for mobile carousel (including hidden ones on desktop)
847            var cards = Array.from(topicsGrid.querySelectorAll('.tp-card'));
848
849            if (cards.length > 0) {
850                var currentIndex = 0;
851                var timer = setInterval(nextSlide, 3000);
852
853                function scrollToSlide(index) {
854                    currentIndex = index;
855                    var width = topicsGrid.offsetWidth;
856                    topicsGrid.scrollTo({
857                        left: width * index,
858                        behavior: 'smooth'
859                    });
860                    updateArrowStates();
861                }
862
863                function nextSlide() {
864                    var next = currentIndex + 1;
865                    if (next >= cards.length) next = 0;
866                    scrollToSlide(next);
867                }
868
869                function prevSlide() {
870                    var prev = currentIndex - 1;
871                    if (prev < 0) prev = cards.length - 1;
872                    scrollToSlide(prev);
873                }
874
875                function updateArrowStates() {
876                    // Optionally disable arrows at boundaries (or keep them for infinite loop)
877                    // For now, we'll keep them always enabled for infinite scrolling
878                }
879
880                function resetTimer() {
881                    clearInterval(timer);
882                    timer = setInterval(nextSlide, 3000);
883                }
884
885                // Arrow click handlers
886                prevButton.addEventListener('click', function() {
887                    prevSlide();
888                    resetTimer();
889                });
890
891                nextButton.addEventListener('click', function() {
892                    nextSlide();
893                    resetTimer();
894                });
895
896                // Handle scroll (swipe) updates
897                topicsGrid.addEventListener('scroll', function() {
898                    var width = topicsGrid.offsetWidth;
899                    var newIndex = Math.round(topicsGrid.scrollLeft / width);
900                    if (newIndex !== currentIndex && newIndex >= 0 && newIndex < cards.length) {
901                        currentIndex = newIndex;
902                        updateArrowStates();
903                    }
904                });
905
906                // Touch interactions to pause/reset
907                topicsGrid.addEventListener('touchstart', function() { clearInterval(timer); });
908                topicsGrid.addEventListener('touchend', function() { resetTimer(); });
909            }
910        }
911    }
912
913    // Intercept clicks on TalkingPoints links to navigate to story pages
914    document.addEventListener('click', function(e) {
915        var link = e.target.closest('a');
916        if (!link) return;
917
918        var href = link.getAttribute('href');
919
920        // Check if this is a hash fragment link (comment link from trending section)
921        if (href && href.indexOf('#tp=') !== -1) {
922            e.preventDefault();
923
924            var url = new URL(link.href);
925            var hashParams = new URLSearchParams(url.hash.substring(1));
926            var tpSlug = hashParams.get('tp');
927            var commentId = hashParams.get('commentID') || hashParams.get('commentId');
928
929            if (tpSlug) {
930                // Navigate to story page with comment ID as hash parameter
931                var storyUrl = window.location.origin + '/talkingpoints/' + tpSlug;
932                if (commentId) {
933                    storyUrl += '#tpopen=true&tp=' + tpSlug + '&commentID=' + commentId;
934                } else {
935                    storyUrl += '#tpopen=true&tp=' + tpSlug;
936                }
937                window.location.href = storyUrl;
938            }
939            return;
940        }
941
942        // Check if this is a TalkingPoints story card/hero
943        var container = link.closest('.tp-card, .tp-hero');
944        if (container) {
945            var storySlug = container.getAttribute('data-story-slug');
946            if (storySlug) {
947                // Navigate to the story page - let browser handle it naturally
948                // The story page will auto-open the sidebar
949                return;
950            }
951        }
952    });
953
954    // Handle browser back/forward buttons
955    window.addEventListener('popstate', function(e) {
956        // When using back/forward on /talkingpoints landing page, just reload
957        window.location.reload();
958    });
959
960    // Listen for sidebar close event - navigate back to landing page
961    window.addEventListener('tp-sidebar-closed', function() {
962        // If we're on a story page, navigate back to landing page
963        if (window.location.pathname !== '/talkingpoints' && window.location.pathname.startsWith('/talkingpoints/')) {
964            window.location.href = window.location.origin + '/talkingpoints';
965        }
966    });
967
968}
969
970if (document.readyState === 'loading') {
971    document.addEventListener('DOMContentLoaded', initArchive);
972} else {
973    initArchive();
974}
975
976// Listen for TalkingPointsApi ready event from template
977document.addEventListener('talkingpoints-api-ready', function(e) {
978});

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.