[NIVEL 2 AVANCE] Issues #49-#53 - Componentes Principales Verificados
Todos los componentes del NIVEL 2 ya están implementados correctamente: - ✅ Notification Bar (#49) - ✅ Navbar (#50) - ✅ Hero Section (#51) - ✅ Sidebar (#52) - ✅ Footer (#53) Solo se actualizó notification-bar.css para usar variables CSS. Próximo paso: NIVEL 3 (Refinamientos visuales) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
365
_planeacion/theme-template/js/main.js
Normal file
365
_planeacion/theme-template/js/main.js
Normal file
@@ -0,0 +1,365 @@
|
||||
// Navbar scroll effect
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll para TOC links
|
||||
document.querySelectorAll('.toc-container a').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
// Altura del navbar sticky + margen adicional
|
||||
const navbarHeight = document.querySelector('.navbar').offsetHeight;
|
||||
const offsetTop = targetElement.offsetTop - navbarHeight - 40;
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ScrollSpy mejorado para TOC
|
||||
function updateActiveSection() {
|
||||
const tocLinks = document.querySelectorAll('.toc-container a');
|
||||
const navbar = document.querySelector('.navbar');
|
||||
const navbarHeight = navbar ? navbar.offsetHeight : 0;
|
||||
|
||||
// Obtener todas las secciones referenciadas en el TOC
|
||||
const sectionIds = Array.from(tocLinks).map(link => {
|
||||
const href = link.getAttribute('href');
|
||||
return href ? href.substring(1) : null;
|
||||
}).filter(id => id !== null);
|
||||
|
||||
const sections = sectionIds.map(id => document.getElementById(id)).filter(el => el !== null);
|
||||
|
||||
// Calcular qué sección está visible
|
||||
// Una sección está "activa" cuando su parte superior está por encima del punto de activación
|
||||
const scrollPosition = window.scrollY + navbarHeight + 100; // Punto de activación
|
||||
|
||||
let activeSection = null;
|
||||
|
||||
// Recorrer las secciones de arriba hacia abajo
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionTop = section.offsetTop;
|
||||
|
||||
// Si hemos pasado esta sección, es la activa
|
||||
if (scrollPosition >= sectionTop) {
|
||||
activeSection = section.getAttribute('id');
|
||||
} else {
|
||||
// Ya no hay más secciones visibles
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay sección activa y estamos al inicio, activar la primera
|
||||
if (!activeSection && sections.length > 0 && window.scrollY < sections[0].offsetTop) {
|
||||
activeSection = sections[0].getAttribute('id');
|
||||
}
|
||||
|
||||
// Actualizar clases active en los links del TOC
|
||||
tocLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
const href = link.getAttribute('href');
|
||||
if (href === '#' + activeSection) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ejecutar al hacer scroll
|
||||
window.addEventListener('scroll', updateActiveSection);
|
||||
|
||||
// Ejecutar al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', updateActiveSection);
|
||||
|
||||
// A/B Test para CTA - Rotación en cada carga de página
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Selección aleatoria 50/50 entre A y B en cada carga
|
||||
const ctaVariant = Math.random() < 0.5 ? 'A' : 'B';
|
||||
|
||||
// Mostrar la variante seleccionada
|
||||
if (ctaVariant === 'A') {
|
||||
document.querySelector('.cta-variant-a').style.display = 'block';
|
||||
} else {
|
||||
document.querySelector('.cta-variant-b').style.display = 'block';
|
||||
}
|
||||
|
||||
// Tracking de clicks en el CTA (opcional, para Google Analytics)
|
||||
document.querySelectorAll('.cta-button').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
const variant = this.getAttribute('data-cta-variant');
|
||||
|
||||
// Si tienes Google Analytics instalado:
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'cta_click', {
|
||||
'event_category': 'CTA',
|
||||
'event_label': 'Variant_' + variant,
|
||||
'value': variant
|
||||
});
|
||||
}
|
||||
|
||||
// Log en consola para verificar
|
||||
console.log('CTA clicked - Variant: ' + variant);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// CARGAR MODAL DE CONTACTO DINÁMICAMENTE
|
||||
// ==========================================
|
||||
loadContactModal();
|
||||
|
||||
// ==========================================
|
||||
// INICIALIZAR FORMULARIO DE FOOTER
|
||||
// ==========================================
|
||||
initFooterContactForm();
|
||||
});
|
||||
|
||||
/**
|
||||
* Carga el modal de contacto desde un archivo externo
|
||||
*/
|
||||
function loadContactModal() {
|
||||
fetch('modal-contact.html')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('No se pudo cargar el modal de contacto');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
// Insertar el HTML del modal en el contenedor
|
||||
document.getElementById('modalContainer').innerHTML = html;
|
||||
|
||||
// Inicializar el formulario de contacto
|
||||
initContactForm();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error cargando el modal:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el formulario de contacto con validación y envío a webhook
|
||||
*/
|
||||
function initContactForm() {
|
||||
const contactForm = document.getElementById('contactForm');
|
||||
|
||||
if (!contactForm) {
|
||||
console.error('Formulario de contacto no encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
contactForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Obtener datos del formulario
|
||||
const formData = {
|
||||
fullName: document.getElementById('fullName').value.trim(),
|
||||
company: document.getElementById('company').value.trim(),
|
||||
whatsapp: document.getElementById('whatsapp').value.trim(),
|
||||
email: document.getElementById('email').value.trim(),
|
||||
comments: document.getElementById('comments').value.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'APU Website - Let\'s Talk Button'
|
||||
};
|
||||
|
||||
// Validación básica
|
||||
if (!formData.fullName || !formData.whatsapp || !formData.email) {
|
||||
showFormMessage('Por favor completa todos los campos obligatorios', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
showFormMessage('Por favor ingresa un correo electrónico válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar WhatsApp (básico)
|
||||
const whatsappRegex = /^\+?[0-9]{10,15}$/;
|
||||
if (!whatsappRegex.test(formData.whatsapp.replace(/\s/g, ''))) {
|
||||
showFormMessage('Por favor ingresa un número de WhatsApp válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deshabilitar botón mientras se envía
|
||||
const submitButton = contactForm.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
|
||||
|
||||
try {
|
||||
// IMPORTANTE: Reemplaza esta URL con tu webhook real
|
||||
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
|
||||
|
||||
const response = await fetch(WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showFormMessage('¡Mensaje enviado exitosamente! Nos pondremos en contacto pronto.', 'success');
|
||||
contactForm.reset();
|
||||
|
||||
// Cerrar modal después de 2 segundos
|
||||
setTimeout(() => {
|
||||
const modalElement = document.getElementById('contactModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Tracking de conversión (si tienes Google Analytics)
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'form_submission', {
|
||||
'event_category': 'Contact Form',
|
||||
'event_label': 'Contact Form Submitted',
|
||||
'value': 1
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Error al enviar el formulario');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showFormMessage('Hubo un error al enviar el mensaje. Por favor intenta nuevamente.', 'danger');
|
||||
} finally {
|
||||
// Rehabilitar botón
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra mensajes de éxito o error en el formulario
|
||||
*/
|
||||
function showFormMessage(message, type) {
|
||||
const messageDiv = document.getElementById('formMessage');
|
||||
messageDiv.className = `mt-3 alert alert-${type}`;
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
// Ocultar mensaje después de 5 segundos
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el formulario de contacto del footer
|
||||
*/
|
||||
function initFooterContactForm() {
|
||||
const footerForm = document.getElementById('footerContactForm');
|
||||
|
||||
if (!footerForm) {
|
||||
console.error('Formulario de footer no encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
footerForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Obtener datos del formulario
|
||||
const formData = {
|
||||
fullName: document.getElementById('footerFullName').value.trim(),
|
||||
company: document.getElementById('footerCompany').value.trim(),
|
||||
whatsapp: document.getElementById('footerWhatsapp').value.trim(),
|
||||
email: document.getElementById('footerEmail').value.trim(),
|
||||
comments: document.getElementById('footerComments').value.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'APU Website - Footer Contact Form'
|
||||
};
|
||||
|
||||
// Validación básica
|
||||
if (!formData.fullName || !formData.whatsapp || !formData.email) {
|
||||
showFooterFormMessage('Por favor completa todos los campos obligatorios', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
showFooterFormMessage('Por favor ingresa un correo electrónico válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar WhatsApp (básico)
|
||||
const whatsappRegex = /^\+?[0-9]{10,15}$/;
|
||||
if (!whatsappRegex.test(formData.whatsapp.replace(/\s/g, ''))) {
|
||||
showFooterFormMessage('Por favor ingresa un número de WhatsApp válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deshabilitar botón mientras se envía
|
||||
const submitButton = footerForm.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
|
||||
|
||||
try {
|
||||
// IMPORTANTE: Reemplaza esta URL con tu webhook real
|
||||
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
|
||||
|
||||
const response = await fetch(WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showFooterFormMessage('¡Mensaje enviado exitosamente! Nos pondremos en contacto pronto.', 'success');
|
||||
footerForm.reset();
|
||||
|
||||
// Tracking de conversión (si tienes Google Analytics)
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'form_submission', {
|
||||
'event_category': 'Footer Contact Form',
|
||||
'event_label': 'Footer Form Submitted',
|
||||
'value': 1
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Error al enviar el formulario');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showFooterFormMessage('Hubo un error al enviar el mensaje. Por favor intenta nuevamente.', 'danger');
|
||||
} finally {
|
||||
// Rehabilitar botón
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra mensajes de éxito o error en el formulario del footer
|
||||
*/
|
||||
function showFooterFormMessage(message, type) {
|
||||
const messageDiv = document.getElementById('footerFormMessage');
|
||||
messageDiv.className = `mt-2 alert alert-${type}`;
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
// Ocultar mensaje después de 5 segundos
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
Reference in New Issue
Block a user