// ==================== MÓDULO PRINCIPAL (IIFE) ==================== (function() { 'use strict'; // -------------------- ESTADO PRIVADO -------------------- const state = { menuOpen: false, searchOpen: false, isLightMode: localStorage.getItem('theme') === 'light', isPreloaderActive: true, chatbot: { isOpen: false, isLoading: false, sessionId: localStorage.getItem('chatbotSession') || crypto.randomUUID(), apiUrl: 'https://n8npix.pixelteo.com/webhook/d9de4096-c21a-41e8-aa2c-30dd1de5c3ba/chat' } }; // Guardar session ID localStorage.setItem('chatbotSession', state.chatbot.sessionId); // -------------------- UTILIDADES -------------------- const utils = { // Throttle simple para eventos de alta frecuencia throttle(fn, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn.apply(this, args); } }; }, // Debounce para búsqueda debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, // Formateo seguro de mensajes (evita XSS) formatMessage(text) { if (!text) return ''; let formatted = text .replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }) .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1') .replace(/\n/g, '
') .replace(/\$(\d+(?:,\d+)*(?:\.\d+)?)/g, '$$$1'); return formatted; }, // Obtener vuelos desde el DOM (una sola vez con caché) getVuelosFromDOM() { if (this._vuelosCache) return this._vuelosCache; const vuelos = []; document.querySelectorAll('.tour-one__single').forEach((card, index) => { const img = card.querySelector('.tour-one__image img'); const imgSrc = img?.getAttribute('src') || img?.getAttribute('data-src') || ''; const link = card.querySelector('.tour-one__content h3 a'); const precioEspecial = parseFloat(card.getAttribute('data-precio-especial') || 0); const precioPublico = parseFloat(card.getAttribute('data-precio-publico') || 0); const precio = precioEspecial > 0 ? precioEspecial : precioPublico; const meta = card.querySelectorAll('.tour-one__meta li a'); vuelos.push({ id: card.getAttribute('data-vuelo-id') || index, nombre: link?.textContent.trim() || '', enlace: link?.getAttribute('href') || '', precio, precioOriginal: precioEspecial > 0 ? precioPublico : null, unidad: card.getAttribute('data-unidad') || '', duracion: meta[0]?.textContent.trim() || '', ubicacion: meta[1]?.textContent.trim() || '', imagen: imgSrc, imagenAlt: img?.getAttribute('alt') || 'Vuelo en globo' }); }); this._vuelosCache = vuelos; return vuelos; }, _vuelosCache: null }; // -------------------- MANIPULACIÓN DEL DOM -------------------- const dom = { elements: {}, cacheSelectors() { this.elements = { preloader: document.getElementById('preloader'), progressBar: document.getElementById('preloaderProgressBar'), themeToggles: document.querySelectorAll('.theme-toggle'), hamburger: document.getElementById('hamburgerMenu'), mobileMenu: document.getElementById('mobileMenu'), closeMenu: document.getElementById('closeMenu'), menuOverlay: document.getElementById('menuOverlay'), mobileNavLinks: document.querySelectorAll('.mobile-nav-link'), searchBtn: document.getElementById('searchBtn'), searchModal: document.getElementById('searchModal'), closeSearch: document.getElementById('closeSearch'), searchInput: document.getElementById('searchInput'), searchResults: document.getElementById('searchResults'), searchSuggestions: document.querySelectorAll('.search-suggestion'), chatbotToggle: document.getElementById('chatbotToggle'), chatbotContainer: document.getElementById('chatbotContainer'), chatbotClose: document.getElementById('chatbotClose'), chatbotMessages: document.getElementById('chatbotMessages'), chatbotInput: document.getElementById('chatbotInput'), chatbotSendBtn: document.getElementById('chatbotSendBtn'), chatbotStatus: document.getElementById('chatbotStatus'), notificationBtn: document.getElementById('notificationBtn'), reservarBtn: document.getElementById('reservarBtn'), heroVideo: document.getElementById('heroVideo') }; }, show(element) { if (element) element.classList.add('active'); }, hide(element) { if (element) element.classList.remove('active'); }, setHtml(element, html) { if (element) element.innerHTML = html; }, setText(element, text) { if (element) element.textContent = text; } }; // -------------------- CHATBOT (CLASE) -------------------- class ChatbotManager { constructor() { this.isOpen = false; this.isLoading = false; this.sessionId = state.chatbot.sessionId; this.apiUrl = state.chatbot.apiUrl; this.beepAudio = null; this.init(); } init() { if (!dom.elements.chatbotToggle) return; dom.elements.chatbotToggle.addEventListener('click', (e) => { e.stopPropagation(); this.playBeep(); this.toggle(); }); dom.elements.chatbotClose?.addEventListener('click', () => this.close()); dom.elements.chatbotSendBtn?.addEventListener('click', () => this.sendMessage()); dom.elements.chatbotInput?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.sendMessage(); } }); // Cerrar al hacer clic fuera document.addEventListener('click', (e) => { if (this.isOpen && dom.elements.chatbotContainer && dom.elements.chatbotToggle && !dom.elements.chatbotContainer.contains(e.target) && !dom.elements.chatbotToggle.contains(e.target)) { this.close(); } }); } playBeep() { if (!this.beepAudio) { this.beepAudio = new Audio(); this.beepAudio.src = 'data:audio/wav;base64,U3RlYWx0aCBibG9i...'; // Opcional: puedes mantener el elemento