1document.addEventListener('DOMContentLoaded', function() { 2 // 1. Topic/Category Filter for "Latest Insights" Section 3 const tabContainer = document.querySelector('.sellerapp-section-latest-insights'); 4 if (tabContainer) { 5 const tabs = tabContainer.querySelectorAll('button'); 6 const articles = document.querySelectorAll('.sellerapp-section-latest-insights .article'); 7 8 tabs.forEach(tab => { 9 tab.addEventListener('click', function(e) { 10 e.preventDefault(); 11 12 // Toggle active classes on tabs 13 tabs.forEach(t => { 14 t.className = 'rounded-full px-3.5 py-1.5 transition-colors hairline text-ink-soft hover:bg-surface'; 15 }); 16 tab.className = 'rounded-full px-3.5 py-1.5 transition-colors bg-ink text-background'; 17 18 const category = tab.textContent.trim().toLowerCase(); 19 20 articles.forEach(art => { 21 if (category === 'all') { 22 art.style.display = ''; 23 return; 24 } 25 26 // Extract the category text from the article (e.g. "Amazon Ads", "TikTok Shop", "AI Tools") 27 const categoryEl = art.querySelector('.text-brand') || art.querySelector('span'); 28 const artCatText = categoryEl ? categoryEl.textContent.trim().toLowerCase() : ''; 29 30 let match = false; 31 32 // Simple category mapping matching the Lovable tabs to actual post categories 33 if (category === 'ppc' && (artCatText.includes('ppc') || artCatText.includes('ads'))) { 34 match = true; 35 } else if (category === 'seo' && (artCatText.includes('seo') || artCatText.includes('sem') || artCatText.includes('listings') || artCatText.includes('search'))) { 36 match = true; 37 } else if (category === 'walmart' && artCatText.includes('walmart')) { 38 match = true; 39 } else if (category === 'inventory' && artCatText.includes('inventory')) { 40 match = true; 41 } else if (category === 'ai search' && (artCatText.includes('ai') || artCatText.includes('rufus') || artCatText.includes('cosmo'))) { 42 match = true; 43 } else if (category === 'case studies' && (artCatText.includes('case') || artCatText.includes('study') || artCatText.includes('affiliate') || artCatText.includes('flywheel'))) { 44 match = true; 45 } 46 47 if (match) { 48 art.style.display = ''; 49 } else { 50 art.style.display = 'none'; 51 } 52 }); 53 }); 54 }); 55 } 56 57 // 2. Logo Slider â draggable, infinite 58 document.querySelectorAll('.sellerapp-logo-slider').forEach(function(slider) { 59 var wrapper = slider.querySelector('.sellerapp-logo-track-wrapper'); 60 var track = slider.querySelector('.sellerapp-logo-track'); 61 var pages = slider.querySelectorAll('.sellerapp-logo-page'); 62 var autoplay = parseInt(slider.getAttribute('data-autoplay')) || 0; 63 if (!track || pages.length < 2) return; 64 65 var currentIndex = 0; 66 var intervalId = null; 67 var isDragging = false; 68 var startX = 0; 69 var currentX = 0; 70 var dragOffset = 0; 71 72 function getPageWidth() { 73 return wrapper ? wrapper.offsetWidth : 0; 74 } 75 76 function updateSlider(animate) { 77 var offset = currentIndex * getPageWidth(); 78 track.style.transition = animate !== false ? 'transform 0.4s ease' : 'none'; 79 track.style.transform = 'translateX(-' + offset + 'px)'; 80 } 81 82 function slideNext() { 83 currentIndex = (currentIndex + 1) % pages.length; 84 updateSlider(); 85 } 86 87 function slidePrev() { 88 currentIndex = (currentIndex - 1 + pages.length) % pages.length; 89 updateSlider(); 90 } 91 92 function startAutoplay() { 93 if (autoplay && pages.length > 1) { 94 stopAutoplay(); 95 intervalId = setInterval(slideNext, autoplay); 96 } 97 } 98 99 function stopAutoplay() { 100 if (intervalId) { 101 clearInterval(intervalId); 102 intervalId = null; 103 } 104 } 105
106 function onDragStart(clientX) { 107 stopAutoplay(); 108 isDragging = true; 109 startX = clientX; 110 currentX = clientX; 111 dragOffset = 0; 112 track.style.transition = 'none'; 113 track.style.cursor = 'grabbing'; 114 } 115 116 function onDragMove(clientX) { 117 if (!isDragging) return; 118 currentX = clientX; 119 dragOffset = currentX - startX; 120 var baseOffset = currentIndex * getPageWidth(); 121 track.style.transform = 'translateX(-' + (baseOffset - dragOffset) + 'px)'; 122 } 123 124 function onDragEnd() { 125 if (!isDragging) return; 126 isDragging = false; 127 track.style.cursor = ''; 128 var threshold = getPageWidth() * 0.2; 129 if (dragOffset > threshold) { 130 slidePrev(); 131 } else if (dragOffset < -threshold) { 132 slideNext(); 133 } else { 134 updateSlider(); 135 } 136 startAutoplay(); 137 } 138 139 // Mouse events 140 slider.addEventListener('mousedown', function(e) { e.preventDefault(); onDragStart(e.clientX); }); 141 document.addEventListener('mousemove', function(e) { onDragMove(e.clientX); }); 142 document.addEventListener('mouseup', onDragEnd); 143 144 // Touch events 145 slider.addEventListener('touchstart', function(e) { onDragStart(e.touches[0].clientX); }, { passive: true }); 146 slider.addEventListener('touchmove', function(e) { onDragMove(e.touches[0].clientX); }, { passive: true }); 147 slider.addEventListener('touchend', onDragEnd); 148 149 slider.addEventListener('mouseenter', stopAutoplay); 150 slider.addEventListener('mouseleave', startAutoplay); 151 152 // Arrow buttons 153 var prevBtn = slider.querySelector('.sellerapp-logo-arrow-prev'); 154 var nextBtn = slider.querySelector('.sellerapp-logo-arrow-next'); 155 if (prevBtn) { 156 prevBtn.addEventListener('click', function(e) { e.stopPropagation(); slidePrev(); }); 157 } 158 if (nextBtn) { 159 nextBtn.addEventListener('click', function(e) { e.stopPropagation(); slideNext(); }); 160 } 161 162 var resizeTimer; 163 window.addEventListener('resize', function() { 164 clearTimeout(resizeTimer); 165 resizeTimer = setTimeout(function() { 166 if (currentIndex > pages.length - 1) currentIndex = 0; 167 updateSlider(); 168 }, 150); 169 }); 170 171 updateSlider(); 172 startAutoplay(); 173 }); 174 175 // const badge = document.getElementById('lovable-badge'); 176 // if (closeBtn && badge) { 177 // closeBtn.addEventListener('click', function(e) { 178 // e.preventDefault(); 179 // badge.style.display = 'none'; 180 // }); 181 // } 182 183 184 // 4. List Number Start Persistence â fixes FOIC (Flash of Incorrect Numbers) 185 // on initial page load by forcing the CSS counter to the correct value immediately 186 document.addEventListener('DOMContentLoaded', function() { 187 var lists = document.querySelectorAll('.prose-article ol.numbered[style*="--list-counter-reset"]'); 188 lists.forEach(function(list) { 189 // Force the counter to recalculate immediately 190 list.style.transition = 'none'; 191 /* Trigger re-render by temporarily removing and re-adding the style */ 192 var currentStyle = list.getAttribute('style'); 193 list.setAttribute('style', ''); 194 // Small delay to allow render, then restore 195 setTimeout(function() { 196 list.setAttribute('style', currentStyle); 197 }, 10); 198 }); 199 }); 200 201 // 3. Header Interactivity (SellerApp blog header â plugin-owned) 202 const headerRoot = document.getElementById('sellerapp-header-root'); 203 204 const config = typeof pluginsWidgets !== 'undefined' ? pluginsWidgets : {}; 205 const homeUrl = config.homeUrl || '/'; 206 const restPostsBase = config.restPosts || (window.location.origin + '/wp-json/wp/v2/posts'); 207 208 const blogHeader = headerRoot ? headerRoot.querySelector('.sellerapp-blog-header') : null; 209 const headerStack = headerRoot ? headerRoot.querySelector('.sellerapp-header-stack') : null; 210 const heroBar = headerRoot ? headerRoot.querySelector('.sellerapp-hero-bar-wrap') : null; 211 const headerSpacer = document.getElementById('sellerapp-header-spacer'); 212 const searchOverlay = document.getElementById('search-overlay'); 213 const triggerSearchModal = document.getElementById('trigger-search-modal'); 214 const closeSearchModal = document.getElementById('close-search-modal'); 215 const modalSearchInput = document.getElementById('modal-search-input'); 216 const searchEmptyState = document.getElementById('search-empty-state'); 217 const searchResultsList = document.getElementById('search-results-list'); 218 219 let heroOpenHeight = 0; 220 let heroHiddenState = null; 221 222 // Measure the topbar height in its fully-open state without triggering a 223 // transition â used as the animation target when showing/hiding it. 224 function measureHeroOpenHeight() { 225 if (!heroBar) { 226 return 0; 227 } 228 const wasHidden = heroBar.classList.contains('is-scroll-hidden'); 229 const prevTransition = heroBar.style.transition;
230 heroBar.style.transition = 'none'; 231 if (wasHidden) { 232 heroBar.classList.remove('is-scroll-hidden'); 233 } 234 const h = heroBar.offsetHeight; 235 if (wasHidden) { 236 heroBar.classList.add('is-scroll-hidden'); 237 } 238 heroBar.style.transition = prevTransition; 239 return h; 240 } 241 242 function refreshHeroMetrics() { 243 if (!heroBar || !headerRoot.classList.contains('has-hero-bar')) { 244 return; 245 } 246 heroOpenHeight = measureHeroOpenHeight(); 247 // Set the animation target without triggering a transition (e.g. on the 248 // initial page load, or while the bar is mid-collapse). 249 const prevTransition = heroBar.style.transition; 250 heroBar.style.transition = 'none'; 251 heroBar.style.setProperty('--sellerapp-hero-max-height', heroOpenHeight + 'px'); 252 heroBar.style.transition = prevTransition; 253 headerRoot.style.setProperty( 254 '--sellerapp-hero-height', 255 (heroBar.classList.contains('is-scroll-hidden') ? 0 : heroOpenHeight) + 'px' 256 ); 257 } 258 259 // Toggle the topbar only when its hidden state actually changes. No 260 // measurement here â that would cancel the running transition and cache a 261 // partial height, which makes the bar snap/flicker. 262 function handleHeroBarScroll() { 263 if (!heroBar) { 264 return; 265 } 266 const scrollY = window.scrollY || window.pageYOffset; 267 const shouldHide = scrollY > 0; 268 if (shouldHide === heroHiddenState) { 269 return; 270 } 271 heroHiddenState = shouldHide; 272 if (shouldHide) { 273 heroBar.classList.add('is-scroll-hidden'); 274 } else { 275 heroBar.classList.remove('is-scroll-hidden'); 276 } 277 updateHeaderSpacer(); 278 } 279 280 // When the WP admin bar is showing it is fixed to the top and (on the 281 // frontend) WordPress already pushes the document down via 282 // `html { margin-top }`. Only add the admin bar height when that document 283 // offset is NOT already applied, otherwise the spacer double-counts it and 284 // leaves a visible gap under the menu. 285 let adminBarSpacerOffset = 0; 286 function computeAdminBarSpacerOffset() { 287 if (!document.body.classList.contains('admin-bar')) { 288 adminBarSpacerOffset = 0; 289 return; 290 } 291 const bar = document.getElementById('wpadminbar'); 292 const barH = bar ? bar.offsetHeight : (window.matchMedia('(max-width: 782px)').matches ? 46 : 32); 293 const htmlMargin = parseInt(window.getComputedStyle(document.documentElement).marginTop, 10) || 0; 294 const needed = barH - htmlMargin; 295 adminBarSpacerOffset = needed > 0 ? needed : 0; 296 } 297 298 function updateHeaderSpacer() { 299 if (!headerSpacer) { 300 return; 301 } 302 let height = blogHeader ? blogHeader.offsetHeight : 64; 303 if (heroBar && !heroBar.classList.contains('is-scroll-hidden')) { 304 height += heroOpenHeight; 305 } 306 headerSpacer.style.height = (height + adminBarSpacerOffset) + 'px'; 307 } 308 309 function refreshHeaderLayout() { 310 computeAdminBarSpacerOffset(); 311 refreshHeroMetrics(); 312 updateHeaderSpacer(); 313 } 314 315 function handleHeaderScroll() { 316 if (!headerStack) { 317 return; 318 } 319 const scrollY = window.scrollY || window.pageYOffset; 320 if (scrollY > 0) { 321 headerStack.classList.add('is-scrolled'); 322 if (blogHeader) { 323 blogHeader.classList.add('is-stuck'); 324 } 325 } else { 326 headerStack.classList.remove('is-scrolled'); 327 if (blogHeader) { 328 blogHeader.classList.remove('is-stuck'); 329 } 330 } 331 handleHeroBarScroll(); 332 updateHeaderSpacer(); 333 } 334 335 // Enable spacer/hero transitions only once scrolling actually starts, so 336 // the initial layout on page load stays instant. 337 let headerAnimationsOn = false; 338 function enableHeaderAnimations() { 339 if (headerAnimationsOn || !headerRoot) { 340 return; 341 } 342 headerAnimationsOn = true; 343 headerRoot.classList.add('sellerapp-header-animate'); 344 } 345 346 refreshHeaderLayout(); 347 handleHeaderScroll(); 348 handleHeroBarScroll(); 349 window.addEventListener('scroll', function () { 350 enableHeaderAnimations(); 351 handleHeaderScroll(); 352 handleHeroBarScroll(); 353 }, { passive: true }); 354 window.addEventListener('resize', function () { 355 refreshHeaderLayout(); 356 handleHeroBarScroll(); 357 }); 358 window.addEventListener('load', function () { 359 refreshHeaderLayout(); 360 }); 361 362 function stripHtml(html) { 363 const tmp = document.createElement('div'); 364 tmp.innerHTML = html; 365 return tmp.textContent || tmp.innerText || ''; 366 } 367 368 function buildSearchUrl(query) { 369 const url = new URL(homeUrl, window.location.origin); 370 url.searchParams.set('s', query); 371 return url.toString(); 372 } 373 374 function openSearch() { 375 if (!searchOverlay) { 376 return; 377 } 378 searchOverlay.classList.add('active'); 379 document.body.style.overflow = 'hidden'; 380 setTimeout(function() { 381 if (modalSearchInput) { 382 modalSearchInput.focus(); 383 } 384 if (searchEmptyState) { 385 searchEmptyState.textContent = 'Type a query to search the blog...'; 386 searchEmptyState.classList.remove('hidden'); 387 } 388 if (searchResultsList) { 389 searchResultsList.classList.add('hidden'); 390 searchResultsList.innerHTML = ''; 391 } 392 }, 50); 393 } 394 395 function closeSearch() { 396 if (!searchOverlay) { 397 return; 398 } 399 searchOverlay.classList.remove('active'); 400 document.body.style.overflow = ''; 401 } 402 403 let debounceTimer; 404
405 function renderSearchResults(posts, query) { 406 if (!searchEmptyState || !searchResultsList) { 407 return; 408 } 409 410 if (!posts.length) { 411 searchEmptyState.textContent = 'No articles found matching "' + query + '"'; 412 searchEmptyState.classList.remove('hidden'); 413 searchResultsList.classList.add('hidden'); 414 searchResultsList.innerHTML = ''; 415 return; 416 } 417 418 searchEmptyState.classList.add('hidden'); 419 searchResultsList.innerHTML = ''; 420 searchResultsList.classList.remove('hidden'); 421 422 posts.forEach(function(post, index) { 423 let categoryName = 'Article'; 424 if (post._embedded && post._embedded['wp:term'] && post._embedded['wp:term'][0] && post._embedded['wp:term'][0][0]) { 425 categoryName = post._embedded['wp:term'][0][0].name; 426 } 427 428 const date = new Date(post.date).toLocaleDateString('en-US', { 429 month: 'short', 430 day: 'numeric', 431 year: 'numeric' 432 }); 433 434 const item = document.createElement('a'); 435 item.href = post.link; 436 item.className = 'search-result-item'; 437 if (index === 0) { 438 item.classList.add('selected'); 439 } 440 441 const title = document.createElement('div'); 442 title.className = 'item-title'; 443 title.textContent = stripHtml(post.title.rendered); 444 445 const meta = document.createElement('div'); 446 meta.className = 'item-meta'; 447 meta.textContent = categoryName + ' · ' + date; 448 449 item.appendChild(title); 450 item.appendChild(meta); 451 searchResultsList.appendChild(item); 452 }); 453 } 454 455 function runLiveSearch(query) { 456 if (!query) { 457 if (searchEmptyState) { 458 searchEmptyState.textContent = 'Type a query to search the blog...'; 459 searchEmptyState.classList.remove('hidden'); 460 } 461 if (searchResultsList) { 462 searchResultsList.classList.add('hidden'); 463 searchResultsList.innerHTML = ''; 464 } 465 return; 466 } 467 468 if (searchEmptyState) { 469 searchEmptyState.textContent = 'Searching articles...'; 470 searchEmptyState.classList.remove('hidden'); 471 } 472 if (searchResultsList) { 473 searchResultsList.classList.add('hidden'); 474 } 475 476 const apiUrl = restPostsBase + '?_embed&per_page=6&search=' + encodeURIComponent(query); 477 478 fetch(apiUrl) 479 .then(function(response) { 480 if (!response.ok) { 481 throw new Error('Search failed'); 482 } 483 return response.json(); 484 }) 485 .then(function(posts) { 486 renderSearchResults(posts, query); 487 }) 488 .catch(function() { 489 if (searchEmptyState) { 490 searchEmptyState.textContent = 'Press Enter to search for "' + query + '"'; 491 searchEmptyState.classList.remove('hidden'); 492 } 493 if (searchResultsList) { 494 searchResultsList.classList.add('hidden'); 495 } 496 }); 497 } 498 499 if (triggerSearchModal) { 500 triggerSearchModal.addEventListener('click', function(e) { 501 e.preventDefault(); 502 openSearch(); 503 }); 504 triggerSearchModal.addEventListener('keydown', function(e) { 505 if (e.key === 'Enter' || e.key === ' ') { 506 e.preventDefault(); 507 openSearch(); 508 } 509 }); 510 } 511 512 if (closeSearchModal) { 513 closeSearchModal.addEventListener('click', function(e) { 514 e.preventDefault(); 515 closeSearch(); 516 }); 517 } 518 519 if (searchOverlay) { 520 searchOverlay.addEventListener('click', function(e) { 521 if (e.target === searchOverlay) { 522 closeSearch(); 523 } 524 }); 525 } 526 527 if (modalSearchInput) { 528 modalSearchInput.addEventListener('input', function() { 529 clearTimeout(debounceTimer); 530 const query = modalSearchInput.value.trim(); 531 debounceTimer = setTimeout(function() { 532 runLiveSearch(query); 533 }, 280); 534 }); 535 536 modalSearchInput.addEventListener('keydown', function(e) { 537 const items = searchResultsList ? searchResultsList.querySelectorAll('.search-result-item') : []; 538 let selectedIndex = -1; 539
540 items.forEach(function(item, idx) { 541 if (item.classList.contains('selected')) { 542 selectedIndex = idx; 543 } 544 }); 545 546 if (e.key === 'ArrowDown' && items.length) { 547 e.preventDefault(); 548 items.forEach(function(item) { item.classList.remove('selected'); }); 549 selectedIndex = (selectedIndex + 1) % items.length; 550 items[selectedIndex].classList.add('selected'); 551 items[selectedIndex].scrollIntoView({ block: 'nearest' }); 552 } else if (e.key === 'ArrowUp' && items.length) { 553 e.preventDefault(); 554 items.forEach(function(item) { item.classList.remove('selected'); }); 555 selectedIndex = (selectedIndex - 1 + items.length) % items.length; 556 items[selectedIndex].classList.add('selected'); 557 items[selectedIndex].scrollIntoView({ block: 'nearest' }); 558 } else if (e.key === 'Enter') { 559 if (selectedIndex >= 0 && items.length) { 560 e.preventDefault(); 561 items[selectedIndex].click(); 562 } else { 563 const query = modalSearchInput.value.trim(); 564 if (query) { 565 e.preventDefault(); 566 window.location.href = buildSearchUrl(query); 567 } 568 } 569 } 570 }); 571 } 572 573 document.addEventListener('keydown', function(e) { 574 if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 575 e.preventDefault(); 576 openSearch(); 577 return; 578 } 579 if (e.key === 'Escape') { 580 if (searchOverlay && searchOverlay.classList.contains('active')) { 581 closeSearch(); 582 } 583 } 584 }); 585});
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.