vueloenglobo.mx/public/assets/js/main.js

576 lines
25 KiB
JavaScript

// ==================== 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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
})
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>')
.replace(/\$(\d+(?:,\d+)*(?:\.\d+)?)/g, '<span style="color: #10b981; font-weight: bold;">$$$1</span>');
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 <audio> del DOM
// Si prefieres usar el elemento existente:
const beepElement = document.getElementById('chatbotBeep');
if (beepElement) this.beepAudio = beepElement;
}
this.beepAudio?.play().catch(() => {});
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (!dom.elements.chatbotContainer) return;
dom.elements.chatbotContainer.classList.add('active');
this.isOpen = true;
setTimeout(() => dom.elements.chatbotInput?.focus(), 300);
}
close() {
if (!dom.elements.chatbotContainer) return;
dom.elements.chatbotContainer.classList.remove('active');
this.isOpen = false;
}
addMessage(text, type) {
const msgDiv = document.createElement('div');
msgDiv.className = `chatbot-message ${type}`;
let finalText = text;
try {
const parsed = JSON.parse(text);
if (parsed.message) finalText = parsed.message;
} catch (e) {}
msgDiv.innerHTML = utils.formatMessage(finalText);
dom.elements.chatbotMessages?.appendChild(msgDiv);
dom.elements.chatbotMessages.scrollTop = dom.elements.chatbotMessages.scrollHeight;
}
showLoading() {
if (this.isLoading) return;
this.isLoading = true;
const loadingDiv = document.createElement('div');
loadingDiv.className = 'chatbot-loading';
loadingDiv.id = 'chatbotLoading';
loadingDiv.innerHTML = '<span></span><span></span><span></span>';
dom.elements.chatbotMessages?.appendChild(loadingDiv);
dom.elements.chatbotMessages.scrollTop = dom.elements.chatbotMessages.scrollHeight;
dom.setText(dom.elements.chatbotStatus, '✍️ Escribiendo...');
if (dom.elements.chatbotSendBtn) dom.elements.chatbotSendBtn.disabled = true;
if (dom.elements.chatbotInput) dom.elements.chatbotInput.disabled = true;
}
hideLoading() {
const loading = document.getElementById('chatbotLoading');
if (loading) loading.remove();
this.isLoading = false;
dom.setText(dom.elements.chatbotStatus, '🟢 Conectado');
if (dom.elements.chatbotSendBtn) dom.elements.chatbotSendBtn.disabled = false;
if (dom.elements.chatbotInput) dom.elements.chatbotInput.disabled = false;
if (this.isOpen) dom.elements.chatbotInput?.focus();
}
async sendMessage() {
const input = dom.elements.chatbotInput;
if (!input) return;
const userText = input.value.trim();
if (!userText || this.isLoading) return;
this.addMessage(userText, 'user');
input.value = '';
this.showLoading();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
chatInput: userText,
sessionId: this.sessionId,
userData: {
userAgent: navigator.userAgent,
language: navigator.language,
timestamp: new Date().toISOString()
}
}),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
this.hideLoading();
let botReply = '';
if (data.action && data.message) {
botReply = data.message;
if (data.data) localStorage.setItem('chatbotUserData', JSON.stringify(data.data));
} else if (typeof data === 'string') botReply = data;
else if (data.output) botReply = data.output;
else if (data.response) botReply = data.response;
else botReply = 'Lo siento, no pude procesar tu solicitud. Intenta de nuevo.';
this.addMessage(botReply, 'bot');
} catch (error) {
console.error('Chatbot error:', error);
this.hideLoading();
this.addMessage('⚠️ Error de conexión. Por favor, intenta más tarde.', 'error');
}
}
sendPredefined(message) {
if (!dom.elements.chatbotInput) return;
if (!this.isOpen) this.open();
dom.elements.chatbotInput.value = message;
this.sendMessage();
}
}
// -------------------- MÓDULOS DE UI --------------------
const ui = {
init() {
this.setupPreloader();
this.setupTheme();
this.setupMobileMenu();
this.setupSearchModal();
this.setupAnimations();
this.setupVideo();
this.setupBookingButtons();
this.setupResponsive();
},
setupPreloader() {
const preloader = dom.elements.preloader;
const progressBar = dom.elements.progressBar;
if (!preloader) return;
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
this.finishPreloader(preloader);
}
if (progressBar) progressBar.style.width = `${Math.min(progress, 100)}%`;
}, 100);
window.addEventListener('load', () => {
clearInterval(interval);
this.finishPreloader(preloader);
});
},
finishPreloader(preloader) {
if (!state.isPreloaderActive) return;
state.isPreloaderActive = false;
preloader.classList.add('loaded');
setTimeout(() => {
if (preloader) preloader.style.display = 'none';
}, 600);
},
setupTheme() {
if (state.isLightMode) document.body.classList.add('light-mode');
dom.elements.themeToggles?.forEach(toggle => {
toggle.addEventListener('click', () => {
state.isLightMode = !state.isLightMode;
document.body.classList.toggle('light-mode');
localStorage.setItem('theme', state.isLightMode ? 'light' : 'dark');
});
});
},
setupMobileMenu() {
const { hamburger, mobileMenu, closeMenu, menuOverlay, mobileNavLinks } = dom.elements;
const toggle = () => {
state.menuOpen = !state.menuOpen;
hamburger?.classList.toggle('active');
mobileMenu?.classList.toggle('active');
menuOverlay?.classList.toggle('active');
document.body.style.overflow = state.menuOpen ? 'hidden' : 'auto';
};
hamburger?.addEventListener('click', toggle);
closeMenu?.addEventListener('click', toggle);
menuOverlay?.addEventListener('click', toggle);
mobileNavLinks?.forEach(link => link.addEventListener('click', toggle));
},
setupSearchModal() {
const { searchBtn, searchModal, closeSearch, searchInput, searchResults, searchSuggestions } = dom.elements;
if (!searchBtn || !searchModal) return;
const open = () => {
state.searchOpen = true;
searchModal.classList.add('active');
document.body.style.overflow = 'hidden';
setTimeout(() => searchInput?.focus(), 300);
};
const close = () => {
state.searchOpen = false;
searchModal.classList.remove('active');
document.body.style.overflow = 'auto';
if (searchInput) searchInput.value = '';
if (searchResults) {
searchResults.innerHTML = '';
searchResults.classList.remove('active');
}
};
searchBtn.addEventListener('click', open);
closeSearch?.addEventListener('click', close);
document.addEventListener('keydown', (e) => e.key === 'Escape' && state.searchOpen && close());
const performSearch = utils.debounce(() => {
const query = searchInput?.value.toLowerCase().trim() || '';
if (query.length === 0) {
if (searchResults) {
searchResults.innerHTML = '';
searchResults.classList.remove('active');
}
return;
}
const vuelos = utils.getVuelosFromDOM();
const filtered = vuelos.filter(v =>
v.nombre.toLowerCase().includes(query) ||
v.ubicacion.toLowerCase().includes(query) ||
v.duracion.toLowerCase().includes(query) ||
(v.precio && v.precio.toString().includes(query))
);
this.displaySearchResults(filtered, query);
}, 300);
searchInput?.addEventListener('input', performSearch);
searchSuggestions?.forEach(sugg => {
sugg.addEventListener('click', () => {
const text = sugg.getAttribute('data-search');
if (searchInput && text) {
searchInput.value = text;
performSearch();
searchInput.focus();
}
});
});
},
displaySearchResults(results, query) {
const container = dom.elements.searchResults;
if (!container) return;
if (results.length === 0) {
container.innerHTML = `
<div class="text-center p-8">
<i class="fas fa-search fa-3x text-gray-400 mb-4"></i>
<p class="text-gray-600">No se encontraron vuelos para "${escapeHtml(query)}"</p>
<p class="text-gray-400 text-sm mt-2">Prueba con: amanecer, familiar, premium, teotihuacán</p>
</div>
`;
} else {
container.innerHTML = results.map(vuelo => {
let imgUrl = vuelo.imagen;
if (imgUrl && !imgUrl.startsWith('http') && !imgUrl.startsWith('//')) {
imgUrl = window.location.origin + '/' + imgUrl.replace(/^\.?\//, '');
}
return `
<div class="search-result-item" style="display: flex; gap: 1rem; padding: 1rem; cursor: pointer; background: white; border-radius: 16px; margin-bottom: 0.75rem; border: 1px solid #e5e7eb;"
onclick="window.handleSearchResultClick('${vuelo.id}', '${escapeHtml(vuelo.enlace)}')">
<div style="flex:0 0 100px; height:100px; border-radius:12px; overflow:hidden; background:#f3f4f6;">
<img src="${imgUrl}" alt="${escapeHtml(vuelo.imagenAlt)}" style="width:100%; height:100%; object-fit:cover;"
onerror="this.src='https://via.placeholder.com/100x100?text=Vuelo'">
</div>
<div style="flex:1">
<h4 style="font-size:1.1rem; font-weight:700; margin-bottom:0.5rem;">${escapeHtml(vuelo.nombre)}</h4>
<div class="flex gap-3 text-sm text-gray-500 mb-2">
<span><i class="far fa-clock"></i> ${escapeHtml(vuelo.duracion)}</span>
<span><i class="far fa-map"></i> ${escapeHtml(vuelo.ubicacion)}</span>
</div>
<div>
${vuelo.precioOriginal ? `<span class="line-through text-gray-400 mr-2">$${vuelo.precioOriginal.toLocaleString()}</span>` : ''}
<span class="bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-3 py-1 rounded-full font-bold">$${vuelo.precio.toLocaleString()}</span>
${vuelo.unidad ? `<span class="text-gray-400 ml-2">/ ${escapeHtml(vuelo.unidad)}</span>` : ''}
</div>
</div>
</div>
`;
}).join('');
}
container.classList.add('active');
},
setupAnimations() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.animation = 'fadeInUp 0.8s ease-out forwards';
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -100px 0px' });
document.querySelectorAll('.feature-card, .tour-one__single').forEach(el => observer.observe(el));
},
setupVideo() {
const video = dom.elements.heroVideo;
if (video) {
video.play().catch(() => {
video.style.display = 'none';
const container = document.querySelector('.video-container');
if (container) {
container.style.backgroundImage = 'url("assets/img/banner/globopuertoteotihuacan-video.jpg")';
container.style.backgroundSize = 'cover';
container.style.backgroundPosition = 'center';
}
});
}
},
setupResponsive() {
const handler = utils.throttle(() => {
const w = window.innerWidth;
document.body.classList.remove('mobile-small', 'mobile-large', 'tablet', 'desktop');
if (w <= 480) document.body.classList.add('mobile-small');
else if (w <= 768) document.body.classList.add('mobile-large');
else if (w <= 1024) document.body.classList.add('tablet');
else document.body.classList.add('desktop');
}, 200);
window.addEventListener('resize', handler);
handler();
},
setupBookingButtons() {
const { notificationBtn, reservarBtn } = dom.elements;
notificationBtn?.addEventListener('click', () => {
alert('Notificaciones:\n\n• Tu reserva para el 15 de mayo está confirmada\n• Nuevo destino disponible: Valle de la Luna\n• Oferta especial: 20% de descuento en vuelos nocturnos');
});
reservarBtn?.addEventListener('click', (e) => {
e.preventDefault();
chatbot.sendPredefined('Quiero reservar un vuelo en globo');
});
// Agregar botón "Consultar" a cada tarjeta de vuelo
document.querySelectorAll('.tour-one__single').forEach(card => {
const nombre = card.getAttribute('data-vuelo-nombre');
if (!nombre) return;
const content = card.querySelector('.tour-one__content');
if (content && !content.querySelector('.consulta-btn')) {
const btn = document.createElement('button');
btn.innerHTML = '<i class="fas fa-comment"></i> Consultar';
btn.className = 'consulta-btn';
btn.style.cssText = 'background:linear-gradient(135deg,#04cdff,#0099cc);color:white;border:none;padding:8px 16px;border-radius:25px;font-size:0.85rem;cursor:pointer;margin-top:10px;width:100%;transition:transform 0.2s';
btn.onmouseover = () => btn.style.transform = 'translateY(-2px)';
btn.onmouseout = () => btn.style.transform = 'translateY(0)';
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
chatbot.sendPredefined(`Quiero información sobre el vuelo "${nombre}"`);
};
content.appendChild(btn);
}
});
}
};
// Helper para escapar HTML
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
// Exponer función global para el clic en resultados de búsqueda (por compatibilidad)
window.handleSearchResultClick = function(id, enlace) {
const modal = dom.elements.searchModal;
if (modal) modal.classList.remove('active');
document.body.style.overflow = 'auto';
state.searchOpen = false;
if (enlace) window.location.href = enlace;
};
// -------------------- INICIALIZACIÓN --------------------
let chatbot;
document.addEventListener('DOMContentLoaded', () => {
dom.cacheSelectors();
ui.init();
chatbot = new ChatbotManager();
// Exponer chatbot en window solo si es necesario (para depuración)
window.__chatbot = chatbot; // opcional
});
})();