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.