1(function () { 2 'use strict'; 3 4 // âââ Config ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5 var APP_STORE_URL = 'https://apps.apple.com/us/app/sportsreflector/id6759809796'; 6 var SESSION_KEY = 'sr_exit_shown'; 7 var CONVERT_KEY = 'sr_converted'; 8 var DISMISS_COOKIE = 'sr_exit_dismissed'; 9 var MIN_TIME_MS = 8000; // 8 seconds before any trigger fires 10 var HOME_MIN_MS = 5000; // homepage-specific guard (spec: 5s on homepage) 11 var SCROLL_THRESHOLD = 0.6; // 60% scroll depth for mobile fallback 12 13 // âââ Guards ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 14 function isHomepage() { 15 return window.location.pathname === '/' || window.location.pathname === ''; 16 } 17 18 function isAppStorePage() { 19 return window.location.pathname.indexOf('app-store') !== -1 || 20 window.location.pathname.indexOf('download') !== -1; 21 } 22 23 function arrivedFromAppStore() { 24 return document.referrer && document.referrer.indexOf('apps.apple.com') !== -1; 25 } 26 27 function getCookie(name) { 28 var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); 29 return match ? decodeURIComponent(match[1]) : null; 30 } 31 32 function setCookie(name, value, days) { 33 var expires = new Date(Date.now() + days * 864e5).toUTCString(); 34 document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/; SameSite=Lax'; 35 } 36 37 function shouldShow() { 38 if (sessionStorage.getItem(SESSION_KEY)) return false; 39 if (getCookie(DISMISS_COOKIE)) return false; 40 if (arrivedFromAppStore()) return false; 41 if (isAppStorePage()) return false; 42 return true; 43 } 44 45 // âââ Sport / exercise name lookup ââââââââââââââââââââââââââââââââââââââââââ 46 var SPORT_NAMES = { 47 // Ball sports 48 'basketball': 'basketball players', 'soccer': 'soccer players', 'football': 'football players', 49 'baseball': 'baseball players', 'volleyball': 'volleyball players', 'hockey': 'hockey players', 50 'lacrosse': 'lacrosse players', 'cricket': 'cricket players', 'softball': 'softball players', 51 'rugby': 'rugby players', 'handball': 'handball players', 'waterpolo': 'water polo players', 52 // Racquet 53 'tennis': 'tennis players', 'badminton': 'badminton players', 'table-tennis': 'table tennis players', 54 'pickleball': 'pickleball players', 'squash': 'squash players', 'racquetball': 'racquetball players', 55 // Combat 56 'boxing': 'boxers', 'mma': 'MMA fighters', 'muay-thai': 'Muay Thai athletes', 57 'wrestling': 'wrestlers', 'judo': 'judo athletes', 'bjj': 'BJJ athletes', 58 'karate': 'karate athletes', 'taekwondo': 'taekwondo athletes', 'kickboxing': 'kickboxers', 59 // Gym & fitness 60 'weightlifting': 'weightlifters', 'powerlifting': 'powerlifters', 'crossfit': 'CrossFit athletes', 61 'yoga': 'yoga practitioners', 'pilates': 'Pilates athletes', 'gymnastics': 'gymnasts', 62 'calisthenics': 'calisthenics athletes', 'bodybuilding': 'bodybuilders', 63 // Other sports 64 'swimming': 'swimmers', 'golf': 'golfers', 'running': 'runners', 'cycling': 'cyclists', 65 'skateboarding': 'skateboarders', 'rowing': 'rowers', 'skiing': 'skiers', 66 'snowboarding': 'snowboarders', 'surfing': 'surfers', 'climbing': 'climbers', 67 'archery': 'archers', 'fencing': 'fencers', 'diving': 'divers', 68 'track': 'track athletes', 'field': 'field athletes', 'triathlon': 'triathletes', 69 // Gym exercises 70 'bench-press': 'bench press athletes', 'squat': 'squatters', 'deadlift': 'deadlifters', 71 'overhead-press': 'overhead press athletes', 'pull-up': 'pull-up athletes', 72 'push-up': 'push-up athletes', 'bicep-curl': 'gym athletes', 'lat-pulldown': 'gym athletes', 73 'cable': 'gym athletes', 'dumbbell': 'gym athletes', 'barbell': 'gym athletes', 74 'leg-press': 'gym athletes', 'lunge': 'gym athletes', 'plank': 'gym athletes', 75 'burpee': 'gym athletes', 'dip': 'gym athletes', 'row': 'gym athletes', 76 }; 77 78 function getAudienceLabel() { 79 var parts = window.location.pathname.toLowerCase().replace(/^\/|\/$/g, '').split('/'); 80 // Check each path segment against the lookup table 81 for (var i = 0; i < parts.length; i++) { 82 var seg = parts[i]; 83 if (SPORT_NAMES[seg]) return SPORT_NAMES[seg]; 84 // Also try partial match for compound slugs like "bench-press-form" 85 for (var key in SPORT_NAMES) { 86 if (seg.indexOf(key) !== -1) return SPORT_NAMES[key]; 87 } 88 } 89 return null; 90 } 91 92 // âââ UTM URL builder âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 93 function buildAppStoreUrl() { 94 var slug = window.location.pathname.replace(/^\//, '').replace(/\/$/, '') || 'home'; 95 // Sanitise slug: keep only alphanumeric, hyphens, slashes â replace slashes with dashes 96 slug = slug.replace(/\//g, '-').replace(/[^a-z0-9\-]/gi, '').toLowerCase().slice(0, 60); 97 return APP_STORE_URL + 98 '?utm_source=website' + 99 '&utm_medium=exit_intent' + 100 '&utm_campaign=page_exit' + 101 '&utm_content=' + encodeURIComponent(slug); 102 } 103 104 // âââ QR code URL (desktop only, no JS dependency) âââââââââââââââââââââââââ 105 function buildQrUrl(targetUrl) { 106 return 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&color=ffffff&bgcolor=1a1a2e&data=' + 107 encodeURIComponent(targetUrl); 108 } 109 110 // âââ CSS injection âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 111 function injectStyles() { 112 var css = [ 113 '.sr-exit-overlay{position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:999999;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .25s ease;pointer-events:none}', 114 '.sr-exit-overlay.sr-exit-visible{opacity:1;pointer-events:auto}', 115 '.sr-exit-card{background:#0f1117;border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:32px 28px 24px;max-width:480px;width:90%;text-align:center;transform:scale(.92) translateY(24px);transition:transform .3s cubic-bezier(.34,1.56,.64,1),opacity .25s ease;opacity:0;position:relative}', 116 '.sr-exit-overlay.sr-exit-visible .sr-exit-card{transform:scale(1) translateY(0);opacity:1}', 117 '@media(max-width:600px){.sr-exit-card{transform:translateY(60px);border-radius:20px 20px 0 0;position:fixed;bottom:0;left:0;right:0;max-width:100%;width:100%;margin:0}}', 118 '@media(max-width:600px){.sr-exit-overlay.sr-exit-visible .sr-exit-card{transform:translateY(0)}}', 119 '.sr-exit-close{position:absolute;top:12px;right:16px;background:none;border:none;color:rgba(255,255,255,.4);font-size:22px;
119cursor:pointer;line-height:1;padding:4px 8px}', 120 '.sr-exit-close:hover{color:rgba(255,255,255,.8)}', 121 '.sr-exit-icon{width:72px;height:72px;border-radius:16px;margin:0 auto 16px;display:block;box-shadow:0 4px 20px rgba(0,0,0,.4)}', 122 '.sr-exit-headline{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:22px;font-weight:700;color:#fff;margin:0 0 8px;line-height:1.25}', 123 '.sr-exit-sub{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:15px;color:rgba(255,255,255,.65);margin:0 0 14px;line-height:1.4}', 124 '.sr-exit-stars{font-size:14px;color:rgba(255,255,255,.55);margin:0 0 20px}', 125 '.sr-exit-stars span{color:#fbbf24}', 126 '.sr-exit-btn{display:inline-block;background:linear-gradient(135deg,#268CF2,#1a6fd4);color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:16px;font-weight:600;text-decoration:none;padding:14px 28px;border-radius:12px;width:100%;box-sizing:border-box;transition:opacity .15s,transform .15s;cursor:pointer;border:none}', 127 '.sr-exit-btn:hover{opacity:.9;transform:translateY(-1px)}', 128 '.sr-exit-dismiss{display:block;margin-top:12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;color:rgba(255,255,255,.35);cursor:pointer;background:none;border:none;text-decoration:underline;text-underline-offset:2px}', 129 '.sr-exit-dismiss:hover{color:rgba(255,255,255,.6)}', 130 '.sr-exit-qr{margin-top:18px;padding-top:16px;border-top:1px solid rgba(255,255,255,.08)}', 131 '.sr-exit-qr img{width:80px;height:80px;border-radius:8px;display:block;margin:0 auto 6px}', 132 '.sr-exit-qr p{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:12px;color:rgba(255,255,255,.35);margin:0}', 133 '@media(max-width:600px){.sr-exit-qr{display:none}}', 134 ].join(''); 135 var style = document.createElement('style'); 136 style.textContent = css; 137 document.head.appendChild(style); 138 } 139 140 // âââ Modal DOM builder âââââââââââââââââââââââââââââââââââââââââââââââââââââ 141 function buildModal() { 142 var appStoreUrl = buildAppStoreUrl(); 143 var isMobile = window.innerWidth <= 600; 144 145 var overlay = document.createElement('div'); 146 overlay.className = 'sr-exit-overlay'; 147 overlay.setAttribute('role', 'dialog'); 148 overlay.setAttribute('aria-modal', 'true'); 149 overlay.setAttribute('aria-label', 'Download SportsReflector'); 150 151 var card = document.createElement('div'); 152 card.className = 'sr-exit-card'; 153 154 // Close X button 155 var closeBtn = document.createElement('button'); 156 closeBtn.className = 'sr-exit-close'; 157 closeBtn.setAttribute('aria-label', 'Close'); 158 closeBtn.textContent = 'Ã'; 159 160 // App icon 161 var icon = document.createElement('img'); 162 icon.className = 'sr-exit-icon'; 163 icon.src = 'https://d2xsxph8kpxj0f.cloudfront.net/310519663369183448/T6imAG7YCjWfvLQrRA7RDY/sportsreflector-app-icon_ee28771a.jpg'; 164 icon.alt = 'SportsReflector App Icon'; 165 166 // Headline 167 var headline = document.createElement('h2'); 168 headline.className = 'sr-exit-headline'; 169 headline.textContent = 'Analyze Your Form Free â Before You Go'; 170 171 // Subheadline â personalized by sport/exercise 172 var audience = getAudienceLabel(); 173 var sub = document.createElement('p'); 174 sub.className = 'sr-exit-sub'; 175 sub.textContent = audience 176 ? 'Join 50,000+ ' + audience + ' already scoring their technique with AI' 177 : 'Join 50,000+ athletes already scoring their technique with AI'; 178 179 // Stars 180 var stars = document.createElement('p'); 181 stars.className = 'sr-exit-stars'; 182 stars.innerHTML = '<span>â â â â â </span> 4.8 stars · App Store'; 183 184 // CTA button 185 var btn = document.createElement('a'); 186 btn.className = 'sr-exit-btn'; 187 btn.href = appStoreUrl; 188 btn.target = '_blank'; 189 btn.rel = 'noopener noreferrer'; 190 btn.textContent = 'Download Free on iOS'; 191 192 // Dismiss 193 var dismiss = document.createElement('button');
194 dismiss.className = 'sr-exit-dismiss'; 195 dismiss.textContent = 'No thanks'; 196 197 card.appendChild(closeBtn); 198 card.appendChild(icon); 199 card.appendChild(headline); 200 card.appendChild(sub); 201 card.appendChild(stars); 202 card.appendChild(btn); 203 card.appendChild(dismiss); 204 205 // QR code (desktop only) 206 if (!isMobile) { 207 var qrWrap = document.createElement('div'); 208 qrWrap.className = 'sr-exit-qr'; 209 var qrImg = document.createElement('img'); 210 qrImg.src = buildQrUrl(appStoreUrl); 211 qrImg.alt = 'Scan to download'; 212 qrImg.width = 80; 213 qrImg.height = 80; 214 var qrText = document.createElement('p'); 215 qrText.textContent = 'Scan with your phone to download'; 216 qrWrap.appendChild(qrImg); 217 qrWrap.appendChild(qrText); 218 card.appendChild(qrWrap); 219 } 220 221 overlay.appendChild(card); 222 document.body.appendChild(overlay); 223 return { overlay: overlay, btn: btn, dismiss: dismiss, closeBtn: closeBtn }; 224 } 225 226 // âââ Show / hide logic âââââââââââââââââââââââââââââââââââââââââââââââââââââ 227 var shown = false; 228 229 function showModal(els) { 230 if (shown) return; 231 shown = true; 232 sessionStorage.setItem(SESSION_KEY, '1'); 233 234 // Force reflow before adding visible class so transition fires 235 els.overlay.offsetHeight; // eslint-disable-line no-unused-expressions 236 els.overlay.classList.add('sr-exit-visible'); 237 238 // Trap focus inside modal 239 els.btn.focus(); 240 } 241 242 function hideModal(els, dismissed) { 243 els.overlay.classList.remove('sr-exit-visible'); 244 if (dismissed) { 245 setCookie(DISMISS_COOKIE, '1', 1); // 24-hour cookie 246 } 247 setTimeout(function () { 248 if (els.overlay.parentNode) els.overlay.parentNode.removeChild(els.overlay); 249 }, 300); 250 } 251 252 // âââ Trigger setup âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 253 function setupTriggers(els) { 254 var startTime = Date.now(); 255 var minMs = isHomepage() ? HOME_MIN_MS : MIN_TIME_MS; 256 var triggered = false; 257 258 function tryShow() { 259 if (triggered) return; 260 if (Date.now() - startTime < minMs) return; 261 triggered = true; 262 showModal(els); 263 } 264 265 // ââ Desktop: mouseleave at top 10% ââââââââââââââââââââââââââââââââââââââ 266 var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); 267 268 if (!isMobile) { 269 document.addEventListener('mouseleave', function (e) { 270 if (e.clientY <= window.innerHeight * 0.10) { 271 tryShow(); 272 } 273 }); 274 } 275 276 // ââ Mobile: popstate (back button) âââââââââââââââââââââââââââââââââââââ 277 if (isMobile) { 278 // Push a dummy history entry so we can intercept the back press 279 history.pushState({ srExitGuard: true }, ''); 280 window.addEventListener('popstate', function (e) { 281 if (e.state && e.state.srExitGuard) { 282 // Re-push so the page doesn't actually navigate away 283 history.pushState({ srExitGuard: true }, ''); 284 tryShow(); 285 } 286 }); 287 } 288 289 // ââ Mobile fallback: scroll depth 60% ââââââââââââââââââââââââââââââââââ 290 if (isMobile) { 291 var scrollFired = false; 292 window.addEventListener('scroll', function () { 293 if (scrollFired) return; 294 var scrolled = window.scrollY + window.innerHeight; 295 var total = document.documentElement.scrollHeight; 296 if (scrolled / total >= SCROLL_THRESHOLD) { 297 scrollFired = true; 298 // Only fire scroll trigger if user has been on page long enough 299 if (Date.now() - startTime >= minMs) { 300 tryShow(); 301 } 302 } 303 }, { passive: true }); 304 } 305 306 // ââ Dismiss / close handlers ââââââââââââââââââââââââââââââââââââââââââââ 307 els.closeBtn.addEventListener('click', function () { hideModal(els, true); }); 308 els.dismiss.addEventListener('click', function () { hideModal(els, true); }); 309 310 // Click outside card closes 311 els.overlay.addEventListener('click', function (e) { 312 if (e.target === els.overlay) hideModal(els, true); 313 }); 314 315 // Escape key 316 document.addEventListener('keydown', function (e) { 317 if ((e.key === 'Escape' || e.keyCode === 27) && shown) { 318 hideModal(els, true); 319 } 320 }); 321 322 // CTA click tracking 323 els.btn.addEventListener('click', function () { 324 sessionStorage.setItem(CONVERT_KEY, '1');
325 try { 326 var ev = new CustomEvent('exit_cta_clicked', { 327 detail: { url: els.btn.href, page: window.location.pathname } 328 }); 329 window.dispatchEvent(ev); 330 } catch (e) { /* IE fallback â silent */ } 331 }); 332 } 333 334 // âââ Init ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 335 function init() { 336 if (!shouldShow()) return; 337 injectStyles(); 338 var els = buildModal(); 339 setupTriggers(els); 340 } 341 342 // Run after DOM is ready 343 if (document.readyState === 'loading') { 344 document.addEventListener('DOMContentLoaded', init); 345 } else { 346 init(); 347 } 348 349})();
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.