/** * La Arcadia de Zahora - JavaScript Principal * Funcionalidades de interactividad y navegación */ (function() { 'use strict'; // Estado de la aplicación let isMenuOpen = false; // Elementos DOM const navbar = document.querySelector('.navbar'); const navToggle = document.querySelector('.nav-toggle'); const navMenu = document.querySelector('.nav-menu'); const navLinks = document.querySelectorAll('.nav-link'); const mobileCTA = document.querySelector('.mobile-cta'); /** * Inicialización de la aplicación */ function init() { setupEventListeners(); setupSmoothScrolling(); setupNavbarScrollEffect(); setupMobileCTAVisibility(); setupLazyLoading(); setupAnimationOnScroll(); } /** * Configurar event listeners */ function setupEventListeners() { // Toggle del menú móvil if (navToggle) { navToggle.addEventListener('click', toggleMobileMenu); } // Cerrar menú móvil al hacer click en un enlace navLinks.forEach(link => { link.addEventListener('click', closeMobileMenu); }); // Cerrar menú móvil al hacer click fuera document.addEventListener('click', (e) => { if (isMenuOpen && !navbar.contains(e.target)) { closeMobileMenu(); } }); // Scroll suave para enlaces internos document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', handleSmoothScroll); }); // FAQ toggles document.querySelectorAll('.faq-question').forEach(button => { button.addEventListener('click', toggleFAQ); }); // Eventos de ventana window.addEventListener('scroll', handleScroll); window.addEventListener('resize', handleResize); window.addEventListener('load', handlePageLoad); } /** * Toggle del menú móvil */ function toggleMobileMenu() { isMenuOpen = !isMenuOpen; navMenu.classList.toggle('active', isMenuOpen); navToggle.classList.toggle('active', isMenuOpen); // Cambiar icono const icon = navToggle.querySelector('i'); if (icon) { icon.className = isMenuOpen ? 'fas fa-times' : 'fas fa-bars'; } // Prevenir scroll del body cuando el menú está abierto document.body.style.overflow = isMenuOpen ? 'hidden' : ''; // Accessibility navToggle.setAttribute('aria-expanded', isMenuOpen); } /** * Cerrar menú móvil */ function closeMobileMenu() { if (isMenuOpen) { isMenuOpen = false; navMenu.classList.remove('active'); navToggle.classList.remove('active'); const icon = navToggle.querySelector('i'); if (icon) { icon.className = 'fas fa-bars'; } document.body.style.overflow = ''; navToggle.setAttribute('aria-expanded', false); } } /** * Configurar navegación suave */ function setupSmoothScrolling() { // Asegurar que el scroll suave esté habilitado document.documentElement.style.scrollBehavior = 'smooth'; } /** * Manejar scroll suave para enlaces internos */ function handleSmoothScroll(e) { const targetId = this.getAttribute('href'); if (targetId.startsWith('#') && targetId.length > 1) { e.preventDefault(); const targetElement = document.querySelector(targetId); if (targetElement) { const navbarHeight = navbar.offsetHeight; const targetPosition = targetElement.offsetTop - navbarHeight - 20; window.scrollTo({ top: targetPosition, behavior: 'smooth' }); // Cerrar menú móvil si está abierto closeMobileMenu(); // Actualizar URL history.pushState(null, null, targetId); } } } /** * Efecto de navbar al hacer scroll */ function setupNavbarScrollEffect() { let lastScrollTop = 0; window.addEventListener('scroll', () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; if (scrollTop > 100) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } // Ocultar/mostrar navbar en móvil al hacer scroll if (window.innerWidth <= 768) { if (scrollTop > lastScrollTop && scrollTop > 200) { navbar.style.transform = 'translateY(-100%)'; } else { navbar.style.transform = 'translateY(0)'; } } else { navbar.style.transform = 'translateY(0)'; } lastScrollTop = scrollTop; }); } /** * Configurar visibilidad del CTA móvil */ function setupMobileCTAVisibility() { if (!mobileCTA) return; const observer = new IntersectionObserver((entries) => { const pricingSection = document.querySelector('.pricing-section'); const isVisible = entries.some(entry => entry.isIntersecting); if (window.innerWidth <= 992) { mobileCTA.style.display = isVisible ? 'none' : 'block'; } }, { threshold: 0.1 }); const pricingSection = document.querySelector('.pricing-section'); if (pricingSection) { observer.observe(pricingSection); } } /** * Configurar lazy loading para imágenes */ function setupLazyLoading() { const images = document.querySelectorAll('img[data-src]'); if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); } else { // Fallback para navegadores sin IntersectionObserver images.forEach(img => { img.src = img.dataset.src; img.classList.remove('lazy'); }); } } /** * Configurar animaciones al hacer scroll */ function setupAnimationOnScroll() { const animatedElements = document.querySelectorAll('[data-animate]'); if ('IntersectionObserver' in window) { const animationObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const element = entry.target; const animationType = element.dataset.animate; element.classList.add('animate-' + animationType); animationObserver.unobserve(element); } }); }, { threshold: 0.2 }); animatedElements.forEach(el => animationObserver.observe(el)); } } /** * Manejar eventos de scroll */ function handleScroll() { // Actualizar enlaces activos de navegación updateActiveNavLinks(); // Parallax suave para el hero const hero = document.querySelector('.hero'); if (hero) { const scrolled = window.pageYOffset; const parallax = scrolled * 0.5; hero.style.transform = `translateY(${parallax}px)`; } } /** * Actualizar enlaces activos de navegación */ function updateActiveNavLinks() { const sections = document.querySelectorAll('section[id]'); const navbarHeight = navbar.offsetHeight; const scrollPos = window.scrollY + navbarHeight + 100; sections.forEach(section => { const sectionTop = section.offsetTop; const sectionHeight = section.offsetHeight; const sectionId = section.getAttribute('id'); const navLink = document.querySelector(`.nav-link[href="#${sectionId}"]`); if (scrollPos >= sectionTop && scrollPos < sectionTop + sectionHeight) { // Remover clase activa de todos los enlaces navLinks.forEach(link => link.classList.remove('active')); // Añadir clase activa al enlace actual if (navLink) { navLink.classList.add('active'); } } }); } /** * Manejar redimensionado de ventana */ function handleResize() { // Cerrar menú móvil si la ventana se hace más grande if (window.innerWidth > 992 && isMenuOpen) { closeMobileMenu(); } // Resetear estilos del navbar en desktop if (window.innerWidth > 768) { navbar.style.transform = 'translateY(0)'; } // Actualizar visibilidad del CTA móvil if (window.innerWidth > 992 && mobileCTA) { mobileCTA.style.display = 'none'; } } /** * Manejar carga de página */ function handlePageLoad() { // Añadir clase para indicar que la página ha cargado document.body.classList.add('loaded'); // Si hay un hash en la URL, hacer scroll al elemento if (window.location.hash) { setTimeout(() => { const target = document.querySelector(window.location.hash); if (target) { const navbarHeight = navbar.offsetHeight; const targetPosition = target.offsetTop - navbarHeight - 20; window.scrollTo({ top: targetPosition, behavior: 'smooth' }); } }, 100); } } /** * Utilidad: Throttle function */ function throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } /** * Utilidad: Debounce function */ function debounce(func, wait, immediate) { let timeout; return function() { const context = this, args = arguments; const later = function() { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } // Funcionalidades adicionales específicas /** * Contador animado para las plazas limitadas */ function setupAnimatedCounter() { const counterElement = document.querySelector('.big-number'); if (counterElement) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounter(counterElement, 33, 2000); observer.unobserve(entry.target); } }); }); observer.observe(counterElement); } } /** * Animar contador */ function animateCounter(element, target, duration) { let start = 0; const startTime = performance.now(); function update(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const current = Math.floor(progress * target); element.textContent = current; if (progress < 1) { requestAnimationFrame(update); } } requestAnimationFrame(update); } /** * Toggle FAQ items */ function toggleFAQ(e) { const button = e.currentTarget; const answer = button.nextElementSibling; const isExpanded = button.getAttribute('aria-expanded') === 'true'; // Cerrar todas las otras FAQs document.querySelectorAll('.faq-question').forEach(otherButton => { if (otherButton !== button) { otherButton.setAttribute('aria-expanded', 'false'); otherButton.nextElementSibling.classList.remove('show'); } }); // Toggle la FAQ actual button.setAttribute('aria-expanded', !isExpanded); answer.classList.toggle('show', !isExpanded); } // Optimización de rendimiento con throttle/debounce const throttledHandleScroll = throttle(handleScroll, 16); // 60fps const debouncedHandleResize = debounce(handleResize, 250); // Reemplazar event listeners con versiones optimizadas window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleResize); window.addEventListener('scroll', throttledHandleScroll); window.addEventListener('resize', debouncedHandleResize); // Inicializar cuando el DOM esté listo if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Configurar efectos adicionales después de la carga window.addEventListener('load', () => { setupAnimatedCounter(); }); })();