PageSourceSearch

https://sportsreflector.com/exit-intent.js

js sportsreflector.com collected 2026-10-02 00:18:45 UTC 16,917 bytes, 349 lines download raw bytes

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.