PageSourceSearch

https://www.wowl.io/assets/js/newsletter-enhancements.js?v=ydNomw74lbIe1PHn

js wowl.io collected 2026-10-04 06:56:51 UTC 13,218 bytes, 354 lines download raw bytes

1/**
2 * Newsletter Enhancement Features
3 * - Exit-intent popup
4 * - Sticky newsletter bar
5 * - Matomo event tracking
6 * - Scroll animations
7 */
8
9(function() {
10    'use strict';
11
12    // Configuration
13    const config = {
14        exitIntentEnabled: true,
15        stickyBarEnabled: true,
16        scrollAnimationsEnabled: true,
17        stickyBarScrollThreshold: 50, // Show sticky bar after 50% scroll
18        exitIntentCooldown: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
19    };
20
21    // Helper: Check if Matomo is available
22    function trackMatomoEvent(category, action, name) {
23        if (typeof _paq !== 'undefined') {
24            _paq.push(['trackEvent', category, action, name || '']);
25        }
26    }
27
28    // Helper: Check localStorage availability
29    function isLocalStorageAvailable() {
30        try {
31            const test = '__test__';
32            localStorage.setItem(test, test);
33            localStorage.removeItem(test);
34            return true;
35        } catch(e) {
36            return false;
37        }
38    }
39
40    // Helper: Check if element is in viewport
41    function isInViewport(element) {
42        const rect = element.getBoundingClientRect();
43        return rect.top < window.innerHeight && rect.bottom >= 0;
44    }
45
46    // Helper: Get scroll percentage
47    function getScrollPercentage() {
48        const windowHeight = window.innerHeight;
49        const documentHeight = document.documentElement.scrollHeight;
50        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
51        return (scrollTop / (documentHeight - windowHeight)) * 100;
52    }
53
54    // Exit Intent Popup
55    class ExitIntentPopup {
56        constructor() {
57            this.shown = false;
58            this.popup = null;
59            this.lastShown = isLocalStorageAvailable() ? localStorage.getItem('wowl_exit_popup_shown') : null;
60
61            // Don't show if recently shown
62            if (this.lastShown && (Date.now() - parseInt(this.lastShown)) < config.exitIntentCooldown) {
63                return;
64            }
65
66            this.createPopup();
67            this.attachListeners();
68        }
69
70        createPopup() {
71            const popupHTML = `
72                <div class="exit-intent-popup" id="exitIntentPopup">
73                    <div class="exit-popup-content">
74                        <button class="exit-popup-close" aria-label="Close popup">&times;</button>
75                        <div class="text-center">
76                            <h3 class="text-3xl md:text-4xl font-bold mb-4">Wait! Don't Miss Out</h3>
77                            <p class="text-lg mb-6">Join 500+ supply chain professionals getting weekly insights and exclusive case studies.</p>
78                            <form data-members-form="subscribe" class="exit-popup-form">
79                                <div class="flex flex-col sm:flex-row gap-3">
80                                    <input
81                                        data-members-email
82                                        class="flex-1 h-[54px] py-3 px-4 rounded-lg bg-white border-2 border-gray-300 focus:border-black focus:outline-none"
83                                        type="email"
84                                        placeholder="[email protected]"
85                                        required
86                                    />
87                                    <button
88                                        class="h-[54px] px-8 rounded-lg bg-black text-white font-medium hover:bg-gray-800 transition-colors whitespace-nowrap"
89                                        type="submit"
90                                    >
91                                        Get Insights
92                                    </button>
93                                </div>
94                                <p class="text-sm mt-3 text-gray-700">No spam. Unsubscribe anytime.</p>
95                                <p data-members-error class="text-red-600 mt-2 text-sm"></p>
96                            </form>
97                        </div>
98                    </div>
99                </div>
100            `;
101
102            document.body.insertAdjacentHTML('beforeend', popupHTML);
103            this.popup = document.getElementById('exitIntentPopup');
104        }
105
106        attachListeners() {
107            // Exit intent detection
108            document.addEventListener('mouseout', (e) => {
109                if (!this.shown && e.clientY <= 0 && e.relatedTarget == null) {
110                    this.show();
111                }
112            });
113
114            // Close button
115            const closeBtn = this.popup.querySelector('.exit-popup-close');
116            closeBtn.addEventListener('click', () => this.hide());
117
118            // Click outside to close
119            this.popup.addEventListener('click', (e) => {
120                if (e.target === this.popup) {
121                    this.hide();
122                }
123            });
124
125            // Track form submission
126            const form = this.popup.querySelector('form[data-members-form]');
127            form.addEventListener('submit', () => {
128                trackMatomoEvent('Newsletter', 'Submit', 'Exit Intent Popup');
129            });
130        }
131
132        show() {
133            if (this.shown) return;
134
135            this.popup.classList.add('show');
136            this.shown = true;
137
138            if (isLocalStorageAvailable()) {
139                localStorage.setItem('wowl_exit_popup_shown', Date.now().toString());
140            }
141
142            trackMatomoEvent('Newsletter', 'View', 'Exit Intent Popup');
143        }
144
145        hide() {
146            this.popup.classList.remove('show');
147            trackMatomoEvent('Newsletter', 'Close', 'Exit Intent Popup');
148        }
149    }
150
151    // Sticky Newsletter Bar
152    class StickyNewsletterBar {
153        constructor() {
154            this.bar = null;
155            this.shown = false;
156            this.dismissed = isLocalStorageAvailable() && localStorage.getItem('wowl_sticky_bar_dismissed') === 'true';
157
158            if (this.dismissed) return;
159
160            this.createBar();
161            this.attachListeners();
162        }
163
164        createBar() {
165            const barHTML = `
166                <div class="sticky-newsletter-bar" id="stickyNewsletterBar">
167                    <div class="container">
168                        <div class="flex items-center justify-between gap-4 flex-wrap">
169                            <div class="flex-1 min-w-[200px]">
170                                <p class="text-lg font-bold mb-1">📬 Get Weekly Supply Chain Insights</p>
171                                <p class="text-sm">Join our newsletter for expert advice and industry news.</p>
172                            </div>
173                            <div class="flex items-center gap-3">
174                                <a href="#newsletter" class="inline-block px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors whitespace-nowrap sticky-bar-subscribe">
175                                    Subscribe Now
176                                </a>
177                                <button class="sticky-bar-close" aria-label="Close bar">&times;</button>
178                            </div>
179                        </div>
180                    </div>
181                </div>
182            `;
183
184            document.body.insertAdjacentHTML('beforeend', barHTML);
185            this.bar = document.getElementById('stickyNewsletterBar');
186        }
187
188        attachListeners() {
189            // Scroll detection
190            let scrollTimeout;
191            window.addEventListener('scroll', () => {
192                clearTimeout(scrollTimeout);
193                scrollTimeout = setTimeout(() => {
194                    const scrollPercent = getScrollPercentage();
195
196                    if (scrollPercent >= config.stickyBarScrollThreshold && !this.shown) {
197                        this.show();
198                    }
199                }, 100);
200            });
201
202            // Close button
203            const closeBtn = this.bar.querySelector('.sticky-bar-close');
204            closeBtn.addEventListener('click', () => this.dismiss());
205
206            // Subscribe button click tracking
207            const subscribeBtn = this.bar.querySelector('.sticky-bar-subscribe');
208            subscribeBtn.addEventListener('click', (e) => {
209                trackMatomoEvent('Newsletter', 'Click', 'Sticky Bar CTA');
210
211                // Smooth scroll to newsletter section
212                e.preventDefault();
213                const newsletterSection = document.querySelector('.bg-\\[\\#FED109\\]') ||
214                                        document.querySelector('[data-newsletter-section]');
215                if (newsletterSection) {
216                    newsletterSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
217                }
218            });
219        }
220
221        show() {
222            if (this.shown) return;
223
224            this.bar.classList.add('show');
225            this.shown = true;
226
227            trackMatomoEvent('Newsletter', 'View', 'Sticky Bar');
228        }
229
230        dismiss() {
231            this.bar.classList.remove('show');
232
233            if (isLocalStorageAvailable()) {
234                localStorage.setItem('wowl_sticky_bar_dismissed', 'true');
235            }
236
237            trackMatomoEvent('Newsletter', 'Dismiss', 'Sticky Bar');
238        }
239    }
240
241    // Newsletter Section Tracking
242    function initNewsletterTracking() {
243        // Track newsletter section visibility
244        const newsletterSection = document.querySelector('.bg-\\[\\#FED109\\]') ||
245                                 document.querySelector('[data-newsletter-section]');
246
247        if (newsletterSection) {
248            let tracked = false;
249
250            const observer = new IntersectionObserver((entries) => {
251                entries.forEach(entry => {
252                    if (entry.isIntersecting && !tracked) {
253                        trackMatomoEvent('Newsletter', 'View', 'Section Visible');
254                        tracked = true;
255                        observer.disconnect();
256                    }
257                });
258            }, { threshold: 0.5 });
259
260            observer.observe(newsletterSection);
261
262            // Track form interactions
263            const form = newsletterSection.querySelector('form[data-members-form]');
264            if (form) {
265                const emailInput = form.querySelector('[data-members-email]');
266
267                // Track email input focus
268                emailInput.addEventListener('focus', () => {
269                    trackMatomoEvent('Newsletter', 'Focus', 'Email Input');
270                }, { once: true });
271
272                // Track form submission
273                form.addEventListener('submit', () => {
274                    trackMatomoEvent('Newsletter', 'Submit', 'Main Footer Form');
275                    trackMatomoEvent('Goal', 'Newsletter Signup', 'Footer');
276                });
277            }
278        }
279    }
280
281    // CTA Button Tracking
282    function initCTATracking() {
283        // Track all CTA clicks
284        document.querySelectorAll('a[href*="/demo-request"], a[href*="/contact"]').forEach(link => {
285            link.addEventListener('click', () => {
286                const linkText = link.textContent.trim();
287                trackMatomoEvent('CTA', 'Click', linkText);
288            });
289        });
290    }
291
292    // Scroll Animations for Services Page
293    function initScrollAnimations() {
294        if (!config.scrollAnimationsEnabled) return;
295
296        const animateElements = document.querySelectorAll('.service-card, .prose > *');
297
298        if (animateElements.length === 0) return;
299
300        const observer = new IntersectionObserver((entries) => {
301            entries.forEach((entry, index) => {
302                if (entry.isIntersecting) {
303                    setTimeout(() => {
304                        entry.target.classList.add('animate-fade-in-up');
305                    }, index * 100);
306                    observer.unobserve(entry.target);
307                }
308            });
309        }, {
310            threshold: 0.1,
311            rootMargin: '0px 0px -100px 0px'
312        });
313
314        animateElements.forEach(el => observer.observe(el));
315    }
316
317    // Initialize everything on DOM ready
318    function init() {
319        // Only initialize on services page or pages with newsletter
320        const isServicesPage = window.location.pathname.includes('/services');
321        const hasNewsletter = document.querySelector('form[data-members-form]');
322
323        if (!isServicesPage && !hasNewsletter) return;
324
325        // Initialize tracking
326        initNewsletterTracking();
327        initCTATracking();
328
329        // Initialize scroll animations
330        if (isServicesPage) {
331            initScrollAnimations();
332        }
333
334        // Initialize exit intent (only on services page)
335        if (config.exitIntentEnabled && isServicesPage) {
336            setTimeout(() => {
337                new ExitIntentPopup();
338            }, 5000); // Wait 5 seconds before enabling exit intent
339        }
340
341        // Initialize sticky bar
342        if (config.stickyBarEnabled) {
343            new StickyNewsletterBar();
344        }
345    }
346
347    // Run on DOM ready
348    if (document.readyState === 'loading') {
349        document.addEventListener('DOMContentLoaded', init);
350    } else {
351        init();
352    }
353
354})();

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.