PageSourceSearch

https://www.makeawebsitedesign.com/js/index.js

js makeawebsitedesign.com collected 2026-10-05 07:43:27 UTC 20,653 bytes, 569 lines download raw bytes

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.