CoolFace
Apppublic

kws3000/deepsite-project-bvyne

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
script.js517 linesDownload Raw Back to root
1/**2 * ==============================3 ======= ==============================4 * 5 * CONTACT FORM — CRM Integration Ready6 * =====================================7 * The contact form is designed to be plug-and-play with any CRM.8 * 9 * To connect to HubSpot:10 * 1. Create a HubSpot form endpoint11 * 2. Update FORM_CONFIG.webhookUrl below12 * 3. The form data will be sent as JSON to your webhook13 * 14 * The form data structure matches standard CRM fields:15 * - fullName → standard name field16 * - email → standard email field  17 * - phone → standard phone field18 * - budget → can map to a custom property19 * - propertyInterest → can map to a custom property20 * - message → standard message/description field21 * 22 * Additional CRM fields (HubSpot compatible):23 * - hs_context for HubSpot tracking24 * - utm parameters captured automatically25 */26 27// ============================================================28// CONFIGURATION — Update these for your CRM integration29// ============================================================30const FORM_CONFIG = {31  // HubSpot form webhook URL — replace with your actual endpoint32  // Example: 'https://api.hsforms.com/submissions/v3/integration/submit/{portalId}/{formGuid}'33  webhookUrl: '',34 35  // Form portal ID (HubSpot)36  portalId: '',37 38  // Form GUID (HubSpot)  39  formGuid: '',40 41  // Enable/disable webhook submission42  // Set to true when you have a valid webhook URL43  webhookEnabled: false,44 45  // Redirect URL after successful submission (optional)46  redirectUrl: '',47 48  // Success message delay before reset (ms)49  successMessageDelay: 5000,50};51 52// ============================================================53// NAVIGATION — Scroll effects & Mobile menu54// ============================================================55(function initNavigation() {56  const navbar = document.getElementById('navbar');57  const mobileMenuBtn = document.getElementById('mobileMenuBtn');58  const mobileMenu = document.getElementById('mobileMenu');59  const mobileMenuClose = document.getElementById('mobileMenuClose');60  const mobileLinks = mobileMenu.querySelectorAll('a');61 62  // Scroll effect for navbar63  let lastScrollY = 0;64 65  function handleScroll() {66    const scrollY = window.scrollY;67 68    if (scrollY > 50) {69      navbar.classList.add('nav-scrolled');70    } else {71      navbar.classList.remove('nav-scrolled');72    }73 74    lastScrollY = scrollY;75  }76 77  window.addEventListener('scroll', handleScroll, { passive: true });78  handleScroll(); // Initial check79 80  // Mobile menu open81  mobileMenuBtn.addEventListener('click', () => {82    mobileMenu.classList.remove('opacity-0', 'pointer-events-none');83    mobileMenu.classList.add('opacity-100', 'pointer-events-auto');84    document.body.style.overflow = 'hidden';85  });86 87  // Mobile menu close88  function closeMobileMenu() {89    mobileMenu.classList.add('opacity-0', 'pointer-events-none');90    mobileMenu.classList.remove('opacity-100', 'pointer-events-auto');91    document.body.style.overflow = '';92  }93 94  mobileMenuClose.addEventListener('click', closeMobileMenu);95  mobileLinks.forEach(link => {96    link.addEventListener('click', closeMobileMenu);97  });98 99  // Close on escape key100  document.addEventListener('keydown', (e) => {101    if (e.key === 'Escape' && !mobileMenu.classList.contains('opacity-0')) {102      closeMobileMenu();103    }104  });105})();106 107// ============================================================108// SCROLL REVEAL — Intersection Observer animations109// ============================================================110(function initScrollReveal() {111  // Elements to reveal112  const selectors = [113    '.property-card',114    '.testimonial-card',115    '#about .relative',116    '#about h2',117    '#about p',118    '#about .grid',119    '#contact h2',120    '#contact p',121    '#contact form',122    '#hero .max-w-4xl',123  ];124 125  // Add reveal classes126  document.querySelectorAll(selectors.join(',')).forEach((el, i) => {127    el.classList.add('reveal');128  });129 130  // Stagger property and testimonial cards131  document.querySelectorAll('.property-card').forEach((card, i) => {132    card.style.setProperty('--stagger-index', i);133    card.style.transitionDelay = `${i * 0.1}s`;134  });135 136  document.querySelectorAll('.testimonial-card').forEach((card, i) => {137    card.style.setProperty('--stagger-index', i);138    card.style.transitionDelay = `${i * 0.12}s`;139  });140 141  // Intersection Observer142  const observer = new IntersectionObserver(143    (entries) => {144      entries.forEach((entry) => {145        if (entry.isIntersecting) {146          entry.target.classList.add('active');147          observer.unobserve(entry.target); // Only animate once148        }149      });150    },151    {152      threshold: 0.1,153      rootMargin: '0px 0px -40px 0px',154    }155  );156 157  document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));158})();159 160// ============================================================161// CONTACT FORM — Validation & CRM-ready submission162// ============================================================163(function initContactForm() {164  const form = document.getElementById('contactForm');165  const submitBtn = document.getElementById('submitBtn');166  const submitText = document.getElementById('submitText');167  const submitIcon = document.getElementById('submitIcon');168  const submitSpinner = document.getElementById('submitSpinner');169  const formSuccess = document.getElementById('formSuccess');170 171  // Validation rules172  const validators = {173    fullName: {174      validate: (value) => value.trim().length >= 2,175      message: 'Please enter your full name',176    },177    email: {178      validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()),179      message: 'Please enter a valid email address',180    },181    phone: {182      validate: (value) => /^[\d\s\-\(\)\+\.]{7,20}$/.test(value.trim()),183      message: 'Please enter a valid phone number',184    },185  };186 187  // Show error on field188  function showError(fieldName, message) {189    const input = form.querySelector(`[name="${fieldName}"]`);190    const errorSpan = form.querySelector(`[data-error="${fieldName}"]`);191    if (input) input.classList.add('error');192    if (errorSpan) {193      errorSpan.textContent = message;194      errorSpan.classList.remove('hidden');195    }196  }197 198  // Clear error on field199  function clearError(fieldName) {200    const input = form.querySelector(`[name="${fieldName}"]`);201    const errorSpan = form.querySelector(`[data-error="${fieldName}"]`);202    if (input) input.classList.remove('error');203    if (errorSpan) {204      errorSpan.textContent = '';205      errorSpan.classList.add('hidden');206    }207  }208 209  // Clear all errors210  function clearAllErrors() {211    Object.keys(validators).forEach(clearError);212  }213 214  // Validate all required fields215  function validateForm() {216    let isValid = true;217    clearAllErrors();218 219    Object.entries(validators).forEach(([fieldName, rule]) => {220      const input = form.querySelector(`[name="${fieldName}"]`);221      const value = input ? input.value : '';222      if (!rule.validate(value)) {223        showError(fieldName, rule.message);224        isValid = false;225      }226    });227 228    return isValid;229  }230 231  // Real-time validation on blur232  Object.keys(validators).forEach((fieldName) => {233    const input = form.querySelector(`[name="${fieldName}"]`);234    if (input) {235      input.addEventListener('blur', () => {236        const rule = validators[fieldName];237        if (input.value && !rule.validate(input.value)) {238          showError(fieldName, rule.message);239        } else {240          clearError(fieldName);241        }242      });243 244      // Clear error when user starts typing245      input.addEventListener('input', () => {246        if (input.classList.contains('error')) {247          clearError(fieldName);248        }249      });250    }251  });252 253  // Phone number auto-formatting254  const phoneInput = form.querySelector('[name="phone"]');255  if (phoneInput) {256    phoneInput.addEventListener('input', (e) => {257      let value = e.target.value.replace(/[^\d]/g, '');258      if (value.length > 10) value = value.substring(0, 10);259      if (value.length >= 7) {260        e.target.value = `(${value.substring(0, 3)}) ${value.substring(3, 6)}-${value.substring(6)}`;261      } else if (value.length >= 4) {262        e.target.value = `(${value.substring(0, 3)}) ${value.substring(3)}`;263      } else if (value.length >= 1) {264        e.target.value = `(${value}`;265      }266    });267  }268 269  // Collect UTM parameters270  function getUtmParams() {271    const params = {};272    const urlParams = new URLSearchParams(window.location.search);273    const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];274    utmKeys.forEach((key) => {275      if (urlParams.has(key)) {276        params[key] = urlParams.get(key);277      }278    });279    return params;280  }281 282  // Build form payload283  function buildPayload() {284    const formData = new FormData(form);285    const payload = {};286 287    // Standard fields288    payload.fullName = formData.get('fullName')?.trim() || '';289    payload.email = formData.get('email')?.trim() || '';290    payload.phone = formData.get('phone')?.trim() || '';291    payload.budget = formData.get('budget') || '';292    payload.propertyInterest = formData.get('propertyInterest') || '';293    payload.message = formData.get('message')?.trim() || '';294 295    // UTM parameters296    const utmParams = getUtmParams();297    Object.assign(payload, utmParams);298 299    // Metadata300    payload.submittedAt = new Date().toISOString();301    payload.source = window.location.href;302    payload.formId = 'jason-adam-realty-contact';303 304    return payload;305  }306 307  // Build HubSpot-compatible payload308  function buildHubSpotPayload(payload) {309    return {310      fields: [311        { name: 'firstname', value: payload.fullName.split(' ')[0] || '' },312        { name: 'lastname', value: payload.fullName.split(' ').slice(1).join(' ') || '' },313        { name: 'email', value: payload.email },314        { name: 'phone', value: payload.phone },315        { name: 'budget_range', value: payload.budget },316        { name: 'property_interest', value: payload.propertyInterest },317        { name: 'message', value: payload.message },318      ],319      context: {320        pageUri: window.location.href,321        pageName: document.title,322        ...getUtmParams(),323      },324    };325  }326 327  // Submit to webhook328  async function submitToWebhook(payload) {329    if (!FORM_CONFIG.webhookEnabled || !FORM_CONFIG.webhookUrl) {330      // Simulate success for demo331      console.log('📝 Form submission payload (connect webhook to send):', payload);332      console.log('🏢 HubSpot-compatible payload:', buildHubSpotPayload(payload));333      await new Promise(resolve => setTimeout(resolve, 1500));334      return { success: true, demo: true };335    }336 337    // HubSpot form submission338    if (FORM_CONFIG.portalId && FORM_CONFIG.formGuid) {339      const hsPayload = buildHubSpotPayload(payload);340      const response = await fetch(FORM_CONFIG.webhookUrl, {341        method: 'POST',342        headers: { 'Content-Type': 'application/json' },343        body: JSON.stringify(hsPayload),344      });345      return { success: response.ok, status: response.status };346    }347 348    // Generic webhook submission349    const response = await fetch(FORM_CONFIG.webhookUrl, {350      method: 'POST',351      headers: { 'Content-Type': 'application/json' },352      body: JSON.stringify(payload),353    });354 355    return { success: response.ok, status: response.status };356  }357 358  // Set loading state359  function setLoading(isLoading) {360    if (isLoading) {361      submitBtn.disabled = true;362      submitBtn.classList.add('opacity-70', 'cursor-not-allowed');363      submitText.textContent = 'Sending…';364      submitIcon.classList.add('hidden');365      submitSpinner.classList.remove('hidden');366    } else {367      submitBtn.disabled = false;368      submitBtn.classList.remove('opacity-70', 'cursor-not-allowed');369      submitText.textContent = 'Send Inquiry';370      submitIcon.classList.remove('hidden');371      submitSpinner.classList.add('hidden');372    }373  }374 375  // Form submission handler376  form.addEventListener('submit', async (e) => {377    e.preventDefault();378 379    // Validate380    if (!validateForm()) {381      // Scroll to first error382      const firstError = form.querySelector('.error');383      if (firstError) {384        firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });385        firstError.focus();386      }387      return;388    }389 390    // Build payload391    const payload = buildPayload();392 393    // Set loading394    setLoading(true);395 396    try {397      // Submit398      const result = await submitToWebhook(payload);399 400      if (result.success) {401        // Show success402        form.style.display = 'none';403        formSuccess.classList.remove('hidden');404        lucide.createIcons();405 406        // Log if demo mode407        if (result.demo) {408          console.log('✅ Form submission successful (demo mode — connect webhook at FORM_CONFIG.webhookUrl)');409        }410 411        // Reset form after delay412        setTimeout(() => {413          form.reset();414          form.style.display = '';415          formSuccess.classList.add('hidden');416          clearAllErrors();417          setLoading(false);418 419          // Optional redirect420          if (FORM_CONFIG.redirectUrl) {421            window.location.href = FORM_CONFIG.redirectUrl;422          }423        }, FORM_CONFIG.successMessageDelay);424      } else {425        throw new Error(`Submission failed with status: ${result.status}`);426      }427    } catch (error) {428      console.error('❌ Form submission error:', error);429 430      // Show inline error431      const errorNotice = document.createElement('div');432      errorNotice.className = 'text-center py-3 text-red-400 text-sm';433      errorNotice.innerHTML = `434        <span>Something went wrong. Please try again or contact us directly at </span>435        <a href="tel:+13107707087" class="text-gold-400 hover:underline">310-770-7087</a>436      `;437 438      const submitRow = form.querySelector('button[type="submit"]').parentElement;439      submitRow.insertBefore(errorNotice, submitRow);440 441      setTimeout(() => errorNotice.remove(), 8000);442      setLoading(false);443    }444  });445})();446 447// ============================================================448// LAZY LOADING — Enhanced image loading449// ============================================================450(function initLazyLoad() {451  // Native lazy loading with fallback452  document.querySelectorAll('img').forEach((img) => {453    if (!img.hasAttribute('loading')) {454      img.setAttribute('loading', 'lazy');455    }456 457    // Smooth image reveal458    if (img.complete) {459      img.style.opacity = '1';460    } else {461      img.style.opacity = '0';462      img.style.transition = 'opacity 0.5s ease';463      img.addEventListener('load', () => {464        img.style.opacity = '1';465      });466    }467  });468})();469 470// ============================================================471// ACTIVE NAV TRACKING — Highlight current section472// ============================================================473(function initActiveNav() {474  const sections = document.querySelectorAll('section[id]');475  const navLinks = document.querySelectorAll('.nav-link');476 477  const observer = new IntersectionObserver(478    (entries) => {479      entries.forEach((entry) => {480        if (entry.isIntersecting) {481          const id = entry.target.id;482          navLinks.forEach((link) => {483            const href = link.getAttribute('href')?.substring(1);484            if (href === id) {485              link.classList.add('text-white');486              link.classList.remove('text-noir-300');487            } else {488              link.classList.remove('text-white');489              link.classList.add('text-noir-300');490            }491          });492        }493      });494    },495    {496      threshold: 0.2,497      rootMargin: '-80px 0px -50% 0px',498    }499  );500 501  sections.forEach((section) => observer.observe(section));502})();503 504// ============================================================505// INITIALIZE LUCIDE ICONS506// ============================================================507lucide.createIcons();508 509// ============================================================510// PERFORMANCE — Reduce animations for users who prefer it511// ============================================================512if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {513  document.querySelectorAll('.reveal, .reveal-left, .reveal-right').forEach((el) => {514    el.classList.add('active');515    el.style.transition = 'none';516  });517}