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">×</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">×</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.