PageSourceSearch

https://pivotsports.io/assets/js/site.js?v=9

js pivotsports.io collected 2026-10-02 14:23:24 UTC 8,979 bytes, 215 lines download raw bytes

1// ============================================================
2// Pivot Sports — Global JS
3// ============================================================
4(function () {
5  const toggle = document.querySelector('.nav-toggle');
6  const nav = document.querySelector('.nav');
7  const navLinks = document.querySelector('.nav-links');
8  const navLogin = document.querySelector('.nav-login');
9
10  // Mobile: clone Login into the nav-links dropdown so it's in the same panel
11  if (navLinks && navLogin) {
12    const cloned = navLogin.cloneNode(true);
13    cloned.classList.add('mobile-login-link');
14    cloned.removeAttribute('class');
15    cloned.className = 'mobile-login-link';
16    cloned.textContent = navLogin.textContent.trim() + ' →';
17    cloned.href = navLogin.getAttribute('href') || '#';
18    navLinks.appendChild(cloned);
19  }
20
21  if (toggle && nav) {
22    toggle.setAttribute('aria-expanded', 'false');
23    toggle.setAttribute('aria-controls', 'primary-nav');
24    if (navLinks) navLinks.id = 'primary-nav';
25
26    // Belt-and-braces: drive the menu open/close via INLINE styles too,
27    // so even a stale cached CSS file can't hide the dropdown on mobile.
28    const isMobile = () => window.innerWidth <= 880;
29    const applyOpenStyles = () => {
30      if (!navLinks) return;
31      if (isMobile()) {
32        navLinks.style.cssText = [
33          'display:flex',
34          'flex-direction:column',
35          'gap:0',
36          'position:absolute',
37          'top:100%',
38          'left:0',
39          'right:0',
40          'background:#0a1f5c',
41          'padding:8px 24px 24px',
42          'border-top:1px solid rgba(255,255,255,0.08)',
43          'box-shadow:0 12px 30px rgba(0,0,0,0.35)',
44          'z-index:1000'
45        ].join(';');
46        // Style child links so they're readable even without CSS
47        navLinks.querySelectorAll('a').forEach(a => {
48          a.style.cssText = 'display:block;padding:16px 0;color:#fff;font-size:18px;font-weight:500;border-bottom:1px solid rgba(255,255,255,0.1);text-decoration:none;';
49          if (a.classList.contains('mobile-login-link')) {
50            a.style.color = '#e11d48';
51            a.style.borderBottom = 'none';
52            a.style.paddingTop = '18px';
53          }
54        });
55      }
56    };
57    const clearInlineStyles = () => {
58      if (!navLinks) return;
59      navLinks.style.cssText = '';
60      navLinks.querySelectorAll('a').forEach(a => { a.style.cssText = ''; });
61    };
62    const closeNav = () => {
63      nav.classList.remove('nav-mobile-open');
64      toggle.setAttribute('aria-expanded', 'false');
65      clearInlineStyles();
66    };
67    const openNav = () => {
68      nav.classList.add('nav-mobile-open');
69      toggle.setAttribute('aria-expanded', 'true');
70      applyOpenStyles();
71    };
72    // Track when the menu was just opened so the SAME tap (which bubbles
73    // up to document) doesn't immediately close it via the outside-click
74    // listener. iOS sometimes fires synthetic events with small delays.
75    let openedAt = 0;
76    toggle.addEventListener('click', (e) => {
77      e.stopPropagation();
78      e.preventDefault();
79      if (nav.classList.contains('nav-mobile-open')) {
80        closeNav();
81      } else {
82        openNav();
83        openedAt = Date.now();
84      }
85    });
86
87    // Close mobile nav when clicking a link inside it
88    if (navLinks) {
89      navLinks.addEventListener('click', (e) => {
90        if (e.target.tagName === 'A') closeNav();
91      });
92    }
93
94    // Close on outside click (use 'click' not 'pointerdown' — pointerdown
95    // can fire on the toggle's child SVG and sneak past contains() checks
96    // in some browsers, and we want to ignore the very tap that opened us).
97    document.addEventListener('click', (e) => {
98      if (!nav.classList.contains('nav-mobile-open')) return;
99      if (Date.now() - openedAt < 400) return;        // grace period for the opening tap
100      if (toggle.contains(e.target)) return;          // toggle handles itself
101      if (nav.contains(e.target)) return;             // tap inside the open menu
102      closeNav();
103    });
104
105    // Close on Escape
106    document.addEventListener('keydown', (e) => {
107      if (e.key === 'Escape') closeNav();
108    });
109
110    // Close when resizing back to desktop
111    window.addEventListener('resize', () => {
112      if (window.innerWidth > 880) closeNav();
113    });
114  }
115
116  // Smooth scroll for anchor links
117  document.querySelectorAll('a[href^="#"]').forEach(a => {
118    a.addEventListener('click', (e) => {
119      const id = a.getAttribute('href').slice(1);
120      if (!id) return;
121      const el = document.getElementById(id);
122      if (el) {
123        e.preventDefault();
124        window.scrollTo({ top: el.offsetTop - 70, behavior: 'smooth' });
125        nav && nav.classList.remove('nav-mobile-open');
126      }
127    });
128  });
129
130  // Highlight active nav link by pathname
131  const path = location.pathname.split('/').pop() || 'index.html';
132  document.querySelectorAll('.nav-links a').forEach(a => {
133    const href = a.getAttribute('href');
134    if (href === path || (path === '' && href === 'index.html')) {
135      a.classList.add('active');
136    }
137  });
138
139  // Glass nav: darken on scroll past hero
140  if (nav) {
141    const onScroll = () => {
142      if (window.scrollY > 80) nav.classList.add('scrolled');
143      else nav.classList.remove('scrolled');
144    };
145    window.addEventListener('scroll', onScroll, { passive: true });
146    onScroll();
147  }
148
149  // Demo form handler
150  // -------------------------------------------------------------------------
151  // CONFIG: where do demo requests go?
152  //  - 'mailto'    => opens user's email app addressed to [email protected]
153  //                   (works today, zero setup, but user must click "send" in their mail app)
154  //  - 'formspree' => POSTs to Formspree which forwards to [email protected]
155  //                   (sign up at formspree.io, paste your form ID below, then change MODE to 'formspree')
156  // -------------------------------------------------------------------------
157  const DEMO_FORM_MODE = 'mailto'; // change to 'formspree' once you have an ID
158  const DEMO_EMAIL_TO = '[email protected]';
159  const FORMSPREE_ID = 'YOUR_FORMSPREE_ID_HERE'; // replace with id from formspree.io
160
161  const forms = document.querySelectorAll('form[data-demo-form]');
162  forms.forEach(f => {
163    f.addEventListener('submit', (e) => {
164      e.preventDefault();
165      const name = f.querySelector('[name=name]')?.value || 'there';
166      const btn = f.querySelector('button[type=submit]');
167      const showThanks = () => {
168        f.innerHTML = '<div style="text-align:center;padding:20px 0;"><div style="font-family:Manrope,sans-serif;font-size:30px;font-weight:700;letter-spacing:-0.5px;margin-bottom:10px;">Thanks, ' + name.split(' ')[0] + '!</div><div style="font-size:14px;color:rgba(255,255,255,0.8);line-height:1.6;">We\'ve received your request and a member of our team will reach out within 24 hours to set up your demo.</div></div>';
169      };
170
171      // Collect all form fields
172      const data = {};
173      f.querySelectorAll('input,textarea,select').forEach(el => {
174        if (el.name) data[el.name] = el.value;
175      });
176
177      if (btn) { btn.disabled = true; btn.textContent = 'Sending...'; }
178
179      if (DEMO_FORM_MODE === 'formspree') {
180        // Real submission via Formspree
181        fetch('https://formspree.io/f/' + FORMSPREE_ID, {
182          method: 'POST',
183          headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
184          body: JSON.stringify({ ...data, _replyto: data.email, _subject: 'New demo request from ' + (data.name || 'website') })
185        }).then(r => {
186          if (r.ok) showThanks();
187          else { if (btn) { btn.disabled = false; btn.textContent = 'Request demo →'; } alert('Something went wrong. Please email ' + DEMO_EMAIL_TO + ' directly.'); }
188        }).catch(() => {
189          if (btn) { btn.disabled = false; btn.textContent = 'Request demo →'; }
190          alert('Something went wrong. Please email ' + DEMO_EMAIL_TO + ' directly.');
191        });
192      } else {
193        // mailto: fallback — opens user's email client with pre-filled body
194        const subject = encodeURIComponent('Demo request from ' + (data.name || 'website'));
195        const lines = [
196          'Name: ' + (data.name || ''),
197          'Email: ' + (data.email || ''),
198          'Phone: ' + (data.phone || ''),
199          'Venue / Club: ' + (data.venue || data.club || ''),
200          'Role: ' + (data.role || ''),
201          'Sport(s): ' + (data.sport || ''),
202          'Number of courts: ' + (data.courts || ''),
203          '',
204          'Message:',
205          (data.message || '')
206        ];
207        const body = encodeURIComponent(lines.join('\n'));
208        // Open user's email client
209        window.location.href = 'mailto:' + DEMO_EMAIL_TO + '?subject=' + subject + '&body=' + body;
210        // Show thanks message after a brief delay (user has clicked through to their mail app)
211        setTimeout(showThanks, 700);
212      }
213    });
214  });
215})();

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.