PageSourceSearch

https://www.sellerapp.com/blog/wp-content/plugins/plugins-widgets/…/plugins-widgets-frontend.js?ver=1788176605

js sellerapp.com collected 2026-09-24 08:33:53 UTC 22,343 bytes, 585 lines download raw bytes

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.