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, '"') : '') + '" 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.