kws3000/deepsite-project-bvyne
0
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}