1// === VANILLA TESTIMONIAL CAROUSEL === 2// HTML is pre-rendered â JS only attaches listeners (no innerHTML, no forced reflow, no LCP impact) 3(function() { 4 const wrapper = document.querySelector(".testimonial-wrapper.vc-init"); 5 if (!wrapper) return; 6 const track = wrapper.querySelector(".vc-track"); 7 if (!track) return; 8 9 const vcSlides = Array.from(track.querySelectorAll(".vc-slide")); 10 const dots = Array.from(wrapper.querySelectorAll(".vc-dot")); 11 const total = vcSlides.length; 12 let currentIndex = 0; 13 14 function goTo(idx) { 15 currentIndex = ((idx % total) + total) % total; 16 update(); 17 } 18 19 function update() { 20 vcSlides.forEach((s, i) => { 21 s.classList.remove("vc-active", "vc-prev-slide", "vc-next-slide", "vc-far"); 22 const diff = i - currentIndex; 23 if (diff === 0) s.classList.add("vc-active"); 24 else if (diff === -1 || diff === total - 1) s.classList.add("vc-prev-slide"); 25 else if (diff === 1 || diff === -(total - 1)) s.classList.add("vc-next-slide"); 26 else s.classList.add("vc-far"); 27 }); 28 dots.forEach((d, i) => d.classList.toggle("vc-dot-active", i === currentIndex)); 29 // Keep track tall enough so the active card (scale 1.08) never gets clipped 30 const active = vcSlides[currentIndex]; 31 if (active) { 32 const h = active.offsetHeight; 33 if (h > 0) track.style.minHeight = Math.ceil(h * 1.15 + 40) + 'px'; 34 } 35 } 36 37 vcSlides.forEach((s, i) => s.addEventListener("click", () => goTo(i))); 38 39 const prevBtn = wrapper.querySelector(".vc-prev"); 40 const nextBtn = wrapper.querySelector(".vc-next"); 41 if (prevBtn) prevBtn.addEventListener("click", () => goTo(currentIndex - 1)); 42 if (nextBtn) nextBtn.addEventListener("click", () => goTo(currentIndex + 1)); 43 44 dots.forEach((d, i) => d.addEventListener("click", () => goTo(i))); 45 46 let touchStartX = 0; 47 wrapper.addEventListener("touchstart", e => { touchStartX = e.touches[0].clientX; }, { passive: true }); 48 wrapper.addEventListener("touchend", e => { 49 const diff = touchStartX - e.changedTouches[0].clientX; 50 if (Math.abs(diff) > 50) goTo(currentIndex + (diff > 0 ? 1 : -1)); 51 }, { passive: true }); 52 53 wrapper.addEventListener("keydown", e => { 54 if (e.key === "ArrowLeft") { e.preventDefault(); goTo(currentIndex - 1); } 55 if (e.key === "ArrowRight") { e.preventDefault(); goTo(currentIndex + 1); } 56 }); 57 58 update(); // set initial track height 59 window.addEventListener('resize', () => update(), { passive: true }); 60}()); 61 62// header: transparent overlay â fixed glass on scroll 63window.addEventListener("scroll", () => { 64 const header = document.querySelector("header"); 65 if (window.scrollY > 100) { 66 header.classList.add("scrolled"); 67 } else { 68 header.classList.remove("scrolled"); 69 } 70}); 71 72// Pause animations during window resize to prevent breakpoint flicker 73(function () { 74 let resizeTimer; 75 window.addEventListener("resize", () => { 76 document.body.classList.add("is-resizing"); 77 clearTimeout(resizeTimer); 78 resizeTimer = setTimeout(() => { 79 document.body.classList.remove("is-resizing"); 80 }, 250); 81 }, { passive: true }); 82})(); 83 84// navigation functionalitys for mobile device start 85(function () { 86 const bar = document.querySelector(".bar"); 87 const closeIcon = document.querySelector(".close-icon"); 88 const navmenu = document.querySelector(".navmenu"); 89 const navLink = document.querySelectorAll(".navmenu a"); 90 if (!navmenu) return; // No nav on this page â bail out before any null-deref 91 const toggleSidebar = () => { 92 navmenu.classList.toggle("shown"); 93 }; 94 if (bar) bar.addEventListener("click", toggleSidebar); 95 if (closeIcon) closeIcon.addEventListener("click", toggleSidebar); 96 navLink.forEach((element) => { 97 element.addEventListener("click", toggleSidebar); 98 }); 99})(); 100// navigation functionalitys for mobile device end 101 102// Auto-scroll to contact form when #contact hash is present 103window.addEventListener("load", () => { 104 if (window.location.hash === "#contact") { 105 const section = document.querySelector(".contact-form"); 106 if (section) { 107 section.scrollIntoView({ behavior: "smooth" }); 108 const nameInput = document.querySelector(".name"); 109 nameInput?.focus(); 110 } 111 } 112}); 113 114// === VISITOR METADATA TRACKING === 115// Capture landing page + UTMs on first visit, persist in sessionStorage 116(function () { 117 try { 118 function parseBrowser(ua) { 119 if (/Edg\//.test(ua)) return 'Edge ' + (ua.match(/Edg\/(\d+)/)?.[1] || ''); 120 if (/OPR\//.test(ua)) return 'Opera ' + (ua.match(/OPR\/(\d+)/)?.[1] || ''); 121 if (/Chrome\//.test(ua)) return 'Chrome ' + (ua.match(/Chrome\/(\d+)/)?.[1] || ''); 122 if (/Firefox\//.test(ua)) return 'Firefox ' + (ua.match(/Firefox\/(\d+)/)?.[1] || ''); 123 if (/Safari\//.test(ua)) return 'Safari ' + (ua.match(/Version\/(\d+)/)?.[1] || ''); 124 return 'Unknown'; 125 } 126 function parseOS(ua) { 127 if (/iPhone/.test(ua)) return 'iOS ' + (ua.match(/iPhone OS ([\d_]+)/)?.[1]?.replace(/_/g, '.') || ''); 128 if (/iPad/.test(ua)) return 'iPadOS ' + (ua.match(/CPU OS ([\d_]+)/)?.[1]?.replace(/_/g, '.') || ''); 129 if (/Android/.test(ua)) return 'Android ' + (ua.match(/Android ([\d.]+)/)?.[1] || ''); 130 if (/Windows NT 10/.test(ua)) return 'Windows 10/11'; 131 if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1'; 132 if (/Windows NT 6\.1/.test(ua)) return 'Windows 7'; 133 if (/Mac OS X/.test(ua)) return 'macOS ' + (ua.match(/Mac OS X ([\d_]+)/)?.[1]?.replace(/_/g, '.') || ''); 134 if (/Linux/.test(ua)) return 'Linux'; 135 return 'Unknown'; 136 } 137 function parseDevice(ua) {
138 if (/iPhone|Android.*Mobile|IEMobile|BlackBerry|Windows Phone/.test(ua)) return 'Mobile'; 139 if (/iPad|Android(?!.*Mobile)|Tablet/.test(ua)) return 'Tablet'; 140 return 'Desktop'; 141 } 142 143 const STORAGE_KEY = 'mawd_visitor_data'; 144 const existing = sessionStorage.getItem(STORAGE_KEY); 145 if (!existing) { 146 const ua = navigator.userAgent; 147 const data = { 148 landing_url: window.location.href, 149 landing_referrer: document.referrer || '(direct)', 150 landing_timestamp: new Date().toISOString(), 151 user_agent: ua, 152 browser: parseBrowser(ua), 153 os: parseOS(ua), 154 device_type: parseDevice(ua), 155 screen_size: window.screen.width + 'x' + window.screen.height, 156 viewport_size: window.innerWidth + 'x' + window.innerHeight, 157 language: navigator.language, 158 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, 159 }; 160 161 // Capture UTM parameters 162 const params = new URLSearchParams(window.location.search); 163 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid', 'msclkid'].forEach(p => { 164 if (params.has(p)) data[p] = params.get(p); 165 }); 166 167 sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); 168 } 169 } catch (e) { 170 // sessionStorage may be blocked; fail silently 171 } 172})(); 173 174// Track scroll depth and first form interaction globally 175window._mawdMaxScroll = 0; 176window._mawdFirstInteraction = null; 177window.addEventListener('scroll', function () { 178 const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100); 179 if (pct > window._mawdMaxScroll) window._mawdMaxScroll = pct; 180}, { passive: true }); 181document.addEventListener('focusin', function (e) { 182 if (!window._mawdFirstInteraction && e.target.closest('form')) { 183 window._mawdFirstInteraction = new Date().toISOString(); 184 } 185}, { passive: true }); 186 187// === FORM SUBMISSION + CONVERSION TRACKING === 188 189(function () { 190 const fireCrossPlatform = function (eventName, params) { 191 try { if (typeof gtag === 'function') gtag('event', eventName, params || {}); } catch (e) {} 192 try { if (typeof fbq === 'function') fbq('track', 'Lead'); } catch (e) {} 193 try { if (typeof lintrk === 'function') lintrk('track', { conversion_id: 17869865 }); } catch (e) {} 194 try { if (typeof uetq !== 'undefined') uetq.push('event', 'submit_lead_form', {}); } catch (e) {} 195 }; 196 197 const fireGoogleAdsConversion = function (label, userData) { 198 try { 199 if (typeof gtag === 'function') { 200 if (userData) gtag('set', 'user_data', userData); 201 gtag('event', 'conversion', { 202 'send_to': 'AW-980331966/' + label, 203 'value': 1.0, 204 'currency': 'USD' 205 }); 206 } 207 } catch (err) {} 208 }; 209 210 const buildUserData = function (form) { 211 const name = (form.querySelector('[name="name"]')?.value || '').trim(); 212 const email = (form.querySelector('[name="email"]')?.value || '').trim().toLowerCase(); 213 const phone = (form.querySelector('[name="phone"]')?.value || '').trim(); 214 const parts = name.split(' '); 215 return { 216 email: email, 217 phone_number: phone, 218 address: { 219 first_name: parts[0] || '', 220 last_name: parts.slice(1).join(' ') || '' 221 } 222 }; 223 }; 224 225 // === VALIDATION HELPERS === 226 const isValidEmail = function (email) { 227 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email); 228 }; 229 230 const isValidPhone = function (phone) { 231 if (!phone) return true; // optional 232 const digits = phone.replace(/\D/g, ''); 233 return digits.length >= 10 && digits.length <= 15; 234 }; 235 236 const showFieldError = function (field, message) { 237 field.classList.add('field-error'); 238 field.setAttribute('aria-invalid', 'true'); 239 let err = field.parentElement.querySelector('.field-error-msg'); 240 if (!err) { 241 err = document.createElement('div'); 242 err.className = 'field-error-msg'; 243 err.setAttribute('role', 'alert'); 244 field.parentElement.appendChild(err); 245 } 246 err.textContent = message; 247 }; 248 249 const clearFieldError = function (field) { 250 field.classList.remove('field-error'); 251 field.removeAttribute('aria-invalid'); 252 const err = field.parentElement.querySelector('.field-error-msg'); 253 if (err) err.remove(); 254 }; 255 256 const validateForm = function (form) { 257 let valid = true; 258 let firstError = null; 259 260 // Clear previous errors 261 form.querySelectorAll('.field-error-msg').forEach(e => e.remove()); 262 form.querySelectorAll('.field-error').forEach(e => e.classList.remove('field-error')); 263 264 // Required name 265 const nameField = form.querySelector('[name="name"]:not([type="hidden"])'); 266 if (nameField) { 267 const val = nameField.value.trim(); 268 if (!val) { 269 showFieldError(nameField, 'Please enter your name'); 270 if (!firstError) firstError = nameField; 271 valid = false;
272 } else if (val.length < 2) { 273 showFieldError(nameField, 'Name is too short'); 274 if (!firstError) firstError = nameField; 275 valid = false; 276 } 277 } 278 279 // Required email 280 const emailField = form.querySelector('[name="email"]'); 281 if (emailField) { 282 const val = emailField.value.trim(); 283 if (!val) { 284 showFieldError(emailField, 'Please enter your email'); 285 if (!firstError) firstError = emailField; 286 valid = false; 287 } else if (!isValidEmail(val)) { 288 showFieldError(emailField, 'Please enter a valid email address'); 289 if (!firstError) firstError = emailField; 290 valid = false; 291 } 292 } 293 294 // Optional phone (validate format if provided) 295 const phoneField = form.querySelector('[name="phone"]'); 296 if (phoneField && phoneField.value.trim()) { 297 if (!isValidPhone(phoneField.value)) { 298 showFieldError(phoneField, 'Please enter a valid phone number'); 299 if (!firstError) firstError = phoneField; 300 valid = false; 301 } 302 } 303 304 if (firstError) firstError.focus(); 305 return valid; 306 }; 307 308 // Generic AJAX submit handler 309 async function submitForm(form, options) { 310 const opts = options || {}; 311 const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]'); 312 const originalBtnText = submitBtn ? submitBtn.textContent : ''; 313 314 // Honeypot bot check â if filled, silently "succeed" but don't send 315 const honeypot = form.querySelector('[name="website"]'); 316 if (honeypot && honeypot.value) { 317 form.innerHTML = `<div style="text-align: center; padding: 30px;"><h2 style="color: #fff;">Thanks!</h2></div>`; 318 return; 319 } 320 321 // Client-side validation 322 if (!validateForm(form)) return; 323 324 if (submitBtn) { 325 submitBtn.disabled = true; 326 submitBtn.textContent = 'Sending...'; 327 } 328 329 try { 330 const formData = new FormData(form); 331 formData.append('source', opts.source || 'Contact Form'); 332 const data = Object.fromEntries(formData.entries()); 333 334 // Attach visitor metadata 335 try { 336 const visitor = JSON.parse(sessionStorage.getItem('mawd_visitor_data') || '{}'); 337 const submittedAt = new Date().toISOString(); 338 const fillSeconds = window._mawdFirstInteraction 339 ? Math.round((new Date(submittedAt) - new Date(window._mawdFirstInteraction)) / 1000) 340 : null; 341 data._meta = { 342 ...visitor, 343 current_url: window.location.href, 344 current_page_title: document.title, 345 submitted_at: submittedAt, 346 scroll_depth_pct: window._mawdMaxScroll || 0, 347 form_fill_seconds: fillSeconds, 348 }; 349 } catch (e) {} 350 351 const response = await fetch(form.action, { 352 method: 'POST', 353 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, 354 body: JSON.stringify(data) 355 }); 356 357 if (response.ok) { 358 fireGoogleAdsConversion(opts.conversionLabel, buildUserData(form)); 359 fireCrossPlatform('generate_lead', { form_id: opts.source, method: opts.method }); 360 361 form.innerHTML = ` 362 <div style="text-align: center; padding: 30px 20px;"> 363 <div style="font-size: 48px; margin-bottom: 12px;">â</div> 364 <h2 style="font-family: var(--outfit); font-size: 24px; color: #fff; margin-bottom: 10px;">Thanks â message received!</h2> 365 <p style="font-family: var(--inter); font-size: 15px; color: rgba(255,255,255,0.7);">We'll reach out within one business day.</p> 366 </div> 367 `; 368 } else { 369 if (submitBtn) { 370 submitBtn.disabled = false; 371 submitBtn.textContent = originalBtnText; 372 } 373 const errData = await response.json().catch(() => ({})); 374 alert(errData.error || 'Sorry, there was an error sending your message. Please try again or call (510) 857-7838.'); 375 } 376 } catch (err) { 377 console.error('Form submit error:', err); 378 if (submitBtn) { 379 submitBtn.disabled = false; 380 submitBtn.textContent = originalBtnText; 381 } 382 alert('Sorry, there was an error sending your message. Please try again or call (510) 857-7838.'); 383 } 384 } 385 386 // Real-time field validation â clear error as user fixes it 387 document.querySelectorAll('form input, form textarea').forEach(function (field) { 388 field.addEventListener('input', function () { 389 if (field.classList.contains('field-error')) { 390 clearFieldError(field); 391 } 392 }); 393 }); 394 395 // Contact form (homepage) 396 const contactForm = document.querySelector('form.contact-form'); 397 if (contactForm) { 398 contactForm.addEventListener('submit', function (e) { 399 e.preventDefault(); 400 submitForm(contactForm, { 401 source: 'Contact Form', 402 method: 'web_form', 403 conversionLabel: '9kUWCMakmZYYEL7butMD' 404 }); 405 }); 406 } 407 408 // Lead wizard form 409 const wizardForm = document.getElementById('wizardContactForm'); 410 if (wizardForm) { 411 wizardForm.addEventListener('submit', function (e) { 412 e.preventDefault(); 413 submitForm(wizardForm, { 414 source: 'Lead Wizard', 415 method: 'lead_wizard', 416 conversionLabel: 'TdpmCOmD25UYEL7butMD' 417 }); 418 }); 419 } 420 421 // PPC questionnaire form 422 const ppcForm = document.querySelector('form.ppc-form'); 423 if (ppcForm) { 424 ppcForm.addEventListener('submit', function (e) { 425 e.preventDefault(); 426 submitForm(ppcForm, { 427 source: 'PPC Questionnaire', 428 method: 'ppc_form', 429 conversionLabel: 'aYa9CKr_hJYYEL7butMD' 430 }); 431 }); 432 } 433 434 // Phone click conversions 435 document.querySelectorAll('a[href^="tel:"]').forEach(function (link) { 436 link.addEventListener('click', function () { 437 fireGoogleAdsConversion('ysJxCO-BiPwDEL7butMD'); 438 fireCrossPlatform('phone_call_click', { phone_number: this.getAttribute('href').replace('tel:', '') }); 439 }); 440 }); 441})(); 442 443// === LEAD WIZARD === 444(function() { 445 const wizard = document.getElementById("leadWizard"); 446 if (!wizard) return; 447 448 let currentStep = 1; 449 const totalSteps = 3; 450 const answers = { service: null, stage: null }; 451 452 const stepEls = wizard.querySelectorAll(".wizard-step"); 453 const progressSteps = wizard.querySelectorAll(".wizard-progress-step"); 454 const currentStepLabel = document.getElementById("wizardCurrentStep"); 455 456 function showStep(n) { 457 currentStep = n;
458 stepEls.forEach(el => el.classList.toggle("active", Number(el.dataset.step) === n)); 459 progressSteps.forEach(el => { 460 const step = Number(el.dataset.step); 461 el.classList.remove("active", "completed"); 462 if (step < n) el.classList.add("completed"); 463 else if (step === n) el.classList.add("active"); 464 }); 465 if (currentStepLabel) currentStepLabel.textContent = n; 466 } 467 468 // Option clicks â auto-advance 469 wizard.querySelectorAll(".wizard-option").forEach(btn => { 470 btn.addEventListener("click", () => { 471 const step = btn.closest(".wizard-step"); 472 const stepNum = Number(step.dataset.step); 473 const value = btn.dataset.value; 474 475 step.querySelectorAll(".wizard-option").forEach(b => b.classList.remove("selected")); 476 btn.classList.add("selected"); 477 478 if (stepNum === 1) answers.service = value; 479 if (stepNum === 2) answers.stage = value; 480 481 // Auto-advance after brief delay 482 setTimeout(() => { 483 if (stepNum < totalSteps) showStep(stepNum + 1); 484 if (stepNum === 2) { 485 // Populate hidden fields before contact step 486 const sEl = document.getElementById("wizardService"); 487 const stEl = document.getElementById("wizardStage"); 488 if (sEl) sEl.value = answers.service || ""; 489 if (stEl) stEl.value = answers.stage || ""; 490 } 491 }, 300); 492 }); 493 }); 494 495 // Back buttons 496 wizard.querySelectorAll("[data-back]").forEach(btn => { 497 btn.addEventListener("click", () => { 498 if (currentStep > 1) showStep(currentStep - 1); 499 }); 500 }); 501 502 // Contact form submit handled by global form handler below 503})(); 504 505// Scrollspy â highlight nav link based on currently visible section 506(function() { 507 const homeLink = document.querySelector('.navmenu a[href="/"]'); 508 const servicesLink = document.querySelector('.navmenu a.services-link'); 509 const industriesLink = document.querySelector('.navmenu a.industries-link'); 510 const reviewsLink = document.querySelector('.navmenu a.reviews-link'); 511 const aboutLink = document.querySelector('.navmenu a.about-us-link'); 512 const faqLink = document.querySelector('.navmenu a.faq-link'); 513 const contactLink = document.querySelector('.navmenu a.contact-link'); 514 515 const sections = [ 516 { id: "services", link: servicesLink }, 517 { id: "industries", link: industriesLink }, 518 { id: "reviews", link: reviewsLink }, 519 { id: "about-us", link: aboutLink }, 520 { id: "faq", link: faqLink }, 521 { id: "contact", link: contactLink }, 522 ]; 523 524 // Sections that have NO nav link 525 const unlinkedSections = []; 526 527 function clearActive() { 528 document.querySelectorAll(".navmenu a").forEach(a => a.classList.remove("active-menu")); 529 } 530 531 function onScroll() { 532 const scrollY = window.scrollY + 200; 533 let active = homeLink; // default to home 534 let inUnlinked = false; 535 536 // Check if we're inside an unlinked section (e.g. contact) 537 for (const id of unlinkedSections) { 538 const el = document.getElementById(id); 539 if (el) { 540 const top = el.offsetTop; 541 const bottom = top + el.offsetHeight; 542 if (scrollY >= top && scrollY < bottom) { 543 inUnlinked = true; 544 break; 545 } 546 } 547 } 548 549 if (inUnlinked) { 550 clearActive(); 551 return; 552 } 553 554 // Walk sections from bottom to top â use the furthest-down one we've passed 555 for (let i = sections.length - 1; i >= 0; i--) { 556 const el = document.getElementById(sections[i].id); 557 if (el && scrollY >= el.offsetTop) { 558 active = sections[i].link; 559 break; 560 } 561 } 562 563 clearActive(); 564 if (active) active.classList.add("active-menu"); 565 } 566 567 window.addEventListener("scroll", onScroll, { passive: true }); 568 document.addEventListener("DOMContentLoaded", onScroll); 569})();
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.