1346 lines
50 KiB
JavaScript
1346 lines
50 KiB
JavaScript
/**
|
||
* IDE CONSULTORES - SCRIPT INSTITUCIONAL (JS ÚNICO)
|
||
* Maneja includes modulares, tema claro/oscuro, Brújula IDE, modales, catálogo de cursos, filtros y formularios.
|
||
*/
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initApp();
|
||
});
|
||
|
||
async function initApp() {
|
||
initTheme();
|
||
initHeroVideoSpeed();
|
||
await loadIncludes();
|
||
initModals();
|
||
initAcademyCourses();
|
||
initSolucionesFilters();
|
||
initInsightsFeed();
|
||
initContactForm();
|
||
initCareersForm();
|
||
initSearchPage();
|
||
}
|
||
|
||
/* ==========================================================================
|
||
1. Carga Modular de Header y Footer (JS Nativo)
|
||
========================================================================== */
|
||
async function loadIncludes() {
|
||
const headerElem = document.getElementById('site-header');
|
||
const footerElem = document.getElementById('site-footer');
|
||
|
||
try {
|
||
if (headerElem && headerElem.children.length === 0) {
|
||
const headerRes = await fetch('./includes/header.html', { cache: 'no-store' });
|
||
if (headerRes.ok) {
|
||
headerElem.innerHTML = await headerRes.text();
|
||
highlightActiveNavLink();
|
||
attachHeaderEvents();
|
||
} else {
|
||
// ← NUEVO: Si el fetch falla (ej: 404), igual inicializar eventos
|
||
highlightActiveNavLink();
|
||
attachHeaderEvents();
|
||
}
|
||
} else if (headerElem) {
|
||
highlightActiveNavLink();
|
||
attachHeaderEvents();
|
||
}
|
||
|
||
if (footerElem) {
|
||
const footerRes = await fetch('./includes/footer.html', { cache: 'no-store' });
|
||
if (footerRes.ok) {
|
||
footerElem.innerHTML = await footerRes.text();
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Header modular no disponible:', err);
|
||
// ← NUEVO: Intentar inicializar igual si el header ya tiene contenido estático
|
||
if (headerElem && headerElem.children.length > 0) {
|
||
highlightActiveNavLink();
|
||
attachHeaderEvents();
|
||
}
|
||
}
|
||
}
|
||
|
||
function highlightActiveNavLink() {
|
||
const currentPath = window.location.pathname;
|
||
const navLinks = document.querySelectorAll('.nav-link');
|
||
|
||
// 1. Limpiar estado activo previo
|
||
navLinks.forEach(link => link.classList.remove('active'));
|
||
|
||
// 2. Evaluar cada enlace
|
||
navLinks.forEach(link => {
|
||
const href = link.getAttribute('href');
|
||
// Ignoramos enlaces vacíos o anclas "#"
|
||
if (!href || href === '#') return;
|
||
|
||
const isHome = (currentPath === '/' || currentPath.endsWith('index.html')) &&
|
||
(href === '/' || href === 'index.html' || href === './');
|
||
|
||
const isCurrentPage = currentPath.includes(href) && href !== '/' && href !== 'index.html';
|
||
|
||
if (isHome || isCurrentPage) {
|
||
link.classList.add('active');
|
||
|
||
// Si este enlace está dentro de un submenú, marcar también al padre
|
||
const parentDropdown = link.closest('.dropdown');
|
||
if (parentDropdown) {
|
||
const parentToggle = parentDropdown.querySelector('.dropdown-toggle');
|
||
if (parentToggle) {
|
||
parentToggle.classList.add('active');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function attachHeaderEvents() {
|
||
// Theme toggle
|
||
const themeToggle = document.getElementById('theme-toggle-btn');
|
||
if (themeToggle) {
|
||
const newThemeToggle = themeToggle.cloneNode(true);
|
||
themeToggle.parentNode.replaceChild(newThemeToggle, themeToggle);
|
||
newThemeToggle.addEventListener('click', toggleTheme);
|
||
}
|
||
|
||
const drawer = document.getElementById('mobile-menu-drawer');
|
||
const overlay = document.getElementById('mobile-menu-overlay');
|
||
|
||
function openDrawer() {
|
||
if (!drawer) return;
|
||
drawer.classList.add('is-open');
|
||
drawer.setAttribute('aria-hidden', 'false');
|
||
if (overlay) overlay.classList.add('is-open');
|
||
document.body.style.overflow = 'hidden'; // evita scroll del fondo
|
||
const btn = document.getElementById('mobile-menu-btn');
|
||
if (btn) btn.setAttribute('aria-expanded', 'true');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
if (!drawer) return;
|
||
drawer.classList.remove('is-open');
|
||
drawer.setAttribute('aria-hidden', 'true');
|
||
if (overlay) overlay.classList.remove('is-open');
|
||
document.body.style.overflow = '';
|
||
const btn = document.getElementById('mobile-menu-btn');
|
||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||
}
|
||
|
||
// Botón hamburguesa abre
|
||
document.getElementById('mobile-menu-btn')
|
||
?.addEventListener('click', openDrawer);
|
||
|
||
// Botón X dentro del drawer cierra
|
||
document.getElementById('drawer-close-btn')
|
||
?.addEventListener('click', closeDrawer);
|
||
|
||
// Clic en overlay cierra
|
||
if (overlay) overlay.addEventListener('click', closeDrawer);
|
||
|
||
// Clic en cualquier link del drawer cierra
|
||
drawer?.querySelectorAll('.nav-link').forEach(link => {
|
||
link.addEventListener('click', closeDrawer);
|
||
});
|
||
|
||
// Tecla ESC cierra
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') closeDrawer();
|
||
});
|
||
|
||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
||
toggleLogoTheme(currentTheme);
|
||
updateThemeIcon(currentTheme);
|
||
}
|
||
|
||
/* ==========================================================================
|
||
2. Selector de Modo Claro / Modo Oscuro con Persistencia
|
||
========================================================================== */
|
||
function initTheme() {
|
||
const savedTheme = localStorage.getItem('ide_theme') || 'light';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
updateThemeIcon(savedTheme);
|
||
toggleLogoTheme(savedTheme);
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('ide_theme', newTheme);
|
||
updateThemeIcon(newTheme);
|
||
toggleLogoTheme(newTheme);
|
||
const themeBtns = document.querySelectorAll('#theme-toggle-btn');
|
||
themeBtns.forEach(btn => {
|
||
btn.setAttribute('aria-label', newTheme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro');
|
||
});
|
||
}
|
||
|
||
function updateThemeIcon(theme) {
|
||
const themeBtns = document.querySelectorAll('#theme-toggle-btn');
|
||
themeBtns.forEach(btn => {
|
||
btn.setAttribute('aria-label', theme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro');
|
||
});
|
||
}
|
||
|
||
function toggleLogoTheme(theme) {
|
||
const logoLight = document.querySelector('.logo-light-only');
|
||
const logoDark = document.querySelector('.logo-dark-only');
|
||
|
||
if (logoLight && logoDark) {
|
||
if (theme === 'dark') {
|
||
logoLight.style.display = 'none';
|
||
logoDark.style.display = 'block';
|
||
} else {
|
||
logoLight.style.display = 'block';
|
||
logoDark.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ==========================================================================
|
||
4. Sistema de Modales (Pop-ups con Cierre por Clic Externo y ESC)
|
||
========================================================================== */
|
||
function initModals() {
|
||
const modalBackdrops = document.querySelectorAll('.modal-backdrop');
|
||
|
||
modalBackdrops.forEach(backdrop => {
|
||
// Cerrar al hacer clic en el backdrop (fuera de la ventana)
|
||
backdrop.addEventListener('click', (e) => {
|
||
if (e.target === backdrop) {
|
||
closeModal(backdrop.id);
|
||
}
|
||
});
|
||
|
||
// Botones de cierre interno
|
||
const closeBtns = backdrop.querySelectorAll('.modal-close-btn, [data-close-modal]');
|
||
closeBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
closeModal(backdrop.id);
|
||
});
|
||
});
|
||
});
|
||
|
||
// Cerrar con tecla ESC
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
const activeModal = document.querySelector('.modal-backdrop.is-active');
|
||
if (activeModal) {
|
||
closeModal(activeModal.id);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Triggers de modales genéricos en páginas (ej: desafíos)
|
||
document.querySelectorAll('[data-open-modal]').forEach(trigger => {
|
||
trigger.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const modalId = trigger.getAttribute('data-open-modal');
|
||
openModal(modalId);
|
||
});
|
||
});
|
||
}
|
||
|
||
function openModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
if (modal) {
|
||
modal.classList.add('is-active');
|
||
document.body.style.overflow = 'hidden'; // Evita scroll de fondo
|
||
}
|
||
}
|
||
|
||
function closeModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
if (modal) {
|
||
modal.classList.remove('is-active');
|
||
document.body.style.overflow = '';
|
||
}
|
||
}
|
||
|
||
/* ================================================================
|
||
JS QUIZ
|
||
================================================================ */
|
||
|
||
(function () {
|
||
// ----- FLUJO DE NAVEGACIÓN (Opción B) -----
|
||
const FLOW = {
|
||
1: { desafio: '2a', objetivo: '2b', solucion: '2c', conocimiento: '2d' },
|
||
'2a': 3, '2b': 3, '2c': 3, '2d': 3,
|
||
3: 4,
|
||
4: 'result'
|
||
};
|
||
|
||
// ----- MAPEO DE RESULTADOS (Opción B) -----
|
||
function buildResult(answers) {
|
||
const p1 = answers[1];
|
||
const p2 = answers['2a'] || answers['2b'] || answers['2c'] || answers['2d'];
|
||
const p3 = answers[3];
|
||
|
||
// Perfil 4 · Requisito exigido
|
||
if (p3 === 'requisito' || p2 === 'iso' || p2 === 'iso-form') {
|
||
return {
|
||
title: 'La certificación puede ser el punto de partida.',
|
||
body: 'Comprendemos que tenés un requisito externo. En IDE no trabajamos para cumplir auditorías: trabajamos para que el sistema refleje cómo funciona tu organización y contribuya a gestionarla mejor.',
|
||
chips: ['IDE Excellence', 'ISO 9001 / 14001 / 45001', 'Sistemas de gestión'],
|
||
cta: 'Ver soluciones Excellence',
|
||
url: 'soluciones.html'
|
||
};
|
||
}
|
||
|
||
// Perfil 5 · Conocimiento o formación
|
||
if (p1 === 'conocimiento') {
|
||
const map = {
|
||
'iso-form': { chips: ['Auditoría interna', 'ISO 9001', 'Mejora continua'], extra: 'IDE Excellence' },
|
||
'pm-form': { chips: ['Gestión de proyectos', 'Metodologías ágiles', 'PMP'], extra: 'IDE Project' },
|
||
'liderazgo-form': { chips: ['Liderazgo', 'Cambio organizacional', 'Cultura'], extra: 'IDE Academy' },
|
||
'ia-form': { chips: ['IA aplicada', 'Transformación digital', 'Automatización'], extra: 'IDE Digital' }
|
||
};
|
||
const m = map[p2] || { chips: ['Formación', 'Desarrollo', 'Academy'], extra: 'IDE Academy' };
|
||
return {
|
||
title: 'Desarrollamos las capacidades que hacen posible la transformación.',
|
||
body: 'IDE Academy diseña programas de formación alineados con los desafíos reales de las organizaciones. No dictamos cursos estándar: trabajamos desde el contexto de cada equipo.',
|
||
chips: [...m.chips, m.extra],
|
||
cta: 'Conocer IDE Academy',
|
||
url: 'academy.html'
|
||
};
|
||
}
|
||
|
||
// Perfil 3 · Sabe exactamente qué necesita
|
||
if (p1 === 'solucion') {
|
||
const map = {
|
||
iso: {
|
||
title: 'Respuesta concreta. Y algo más.',
|
||
chips: ['ISO 9001', 'ISO 14001', 'IDE Excellence'],
|
||
cta: 'Ver soluciones ISO',
|
||
url: 'soluciones.html',
|
||
body: 'Sí, implementamos y acompañamos certificaciones ISO. Y creemos que un sistema de gestión debe reflejar cómo funciona tu organización, no solo superar una auditoría.'
|
||
},
|
||
pmo: {
|
||
title: 'Proyectos que llegan a destino.',
|
||
chips: ['PMO', 'Gestión de proyectos', 'IDE Project'],
|
||
cta: 'Ver soluciones Project',
|
||
url: 'soluciones.html',
|
||
body: 'Diseñamos PMOs y acompañamos la dirección de proyectos para que se entreguen en tiempo, costo y con los resultados esperados.'
|
||
},
|
||
formacion: {
|
||
title: 'Formación desde el contexto real.',
|
||
chips: ['In Company', 'Capacitación', 'IDE Academy'],
|
||
cta: 'Ver IDE Academy',
|
||
url: 'academy.html',
|
||
body: 'Programas de formación diseñados para tu organización, sus desafíos y las capacidades que necesita desarrollar.'
|
||
},
|
||
'ia-digital': {
|
||
title: 'Tecnología con propósito.',
|
||
chips: ['Transformación digital', 'IA aplicada', 'IDE Digital'],
|
||
cta: 'Ver soluciones Digital',
|
||
url: 'soluciones.html',
|
||
body: 'Te ayudamos a identificar dónde la IA y la digitalización generan valor real, y acompañamos su implementación de principio a fin.'
|
||
}
|
||
};
|
||
const m = map[p2] || {
|
||
title: 'Encontramos lo que buscás.',
|
||
chips: ['Soluciones concretas', 'Ecosistema IDE'],
|
||
cta: 'Ver soluciones',
|
||
url: 'soluciones.html',
|
||
body: 'Tenemos la respuesta concreta que buscás. Y una mirada que asegura que esa solución genere valor en el conjunto de tu organización.'
|
||
};
|
||
return { icon: './assets/iconos/engranaje_32.svg', profile: 'Perfil · Solución concreta', ...m };
|
||
}
|
||
|
||
// Perfil 2 · Sabe qué quiere lograr
|
||
if (p1 === 'objetivo') {
|
||
const map = {
|
||
'mejorar-gestion': { chips: ['IDE Excellence', 'Mejora de procesos'], body: 'Entendemos dónde querés llegar. Antes de proponer una solución, comprendemos tu operación para intervenir donde hay mayor impacto.' },
|
||
'profesionalizar-proyectos':{ chips: ['IDE Project', 'PMO'], body: 'Traducimos tu objetivo en una estructura de gestión de proyectos adecuada a tu contexto, sin sobreingeniería.' },
|
||
'desarrollar-equipo': { chips: ['IDE Academy', 'Formación In Company'], body: 'Desarrollamos capacidades alineadas con los desafíos reales de tu organización. La formación parte del contexto, no de un catálogo estándar.' },
|
||
'transformar-digital': { chips: ['IDE Digital', 'IA aplicada'], body: 'Empezamos por entender tu organización para identificar dónde la tecnología realmente agrega valor, y avanzamos desde ahí.' }
|
||
};
|
||
const m = map[p2] || { chips: ['Ecosistema IDE'], body: 'Entendemos dónde querés llegar. Comprendemos el sistema para ayudarte a lograrlo.' };
|
||
return {
|
||
title: 'Entendemos dónde querés llegar. Te ayudamos a definir cómo.',
|
||
body: m.body,
|
||
chips: m.chips,
|
||
cta: 'Explorar por desafío',
|
||
url: 'desafios.html'
|
||
};
|
||
}
|
||
|
||
// Perfil 1 · Desafío sin solución definida
|
||
const mapD = {
|
||
gestion: { chips: ['Procesos', 'Gestión', 'IDE Excellence'] },
|
||
proyectos: { chips: ['Proyectos', 'PMO', 'IDE Project'] },
|
||
personas: { chips: ['Capacidades', 'Equipo', 'IDE Academy'] },
|
||
tecnologia: { chips: ['IA', 'Digital', 'IDE Digital'] },
|
||
nosabe: { chips: ['Diagnóstico sistémico', 'Ecosistema IDE'] }
|
||
};
|
||
const md = mapD[p2] || mapD.nosabe;
|
||
return {
|
||
title: 'No necesitás tener la respuesta para empezar.',
|
||
body: 'Empezamos por comprender tu situación, no por ofrecerte una solución predeterminada. Eso diferencia una intervención puntual de una transformación real.',
|
||
chips: md.chips,
|
||
cta: 'Explorar desafíos',
|
||
url: 'desafios.html'
|
||
};
|
||
}
|
||
|
||
// ----- ESTADO -----
|
||
let currentStep = 1;
|
||
let stepHistory = [];
|
||
let answers = {};
|
||
const TOTAL = 4;
|
||
|
||
const container = document.getElementById('quiz-interactive-app');
|
||
if (!container) return;
|
||
|
||
// ----- FUNCIONES -----
|
||
function showStep(id) {
|
||
const steps = container.querySelectorAll('.quiz-step');
|
||
steps.forEach(s => s.classList.remove('active'));
|
||
|
||
const el = document.getElementById('step-' + id);
|
||
if (el) el.classList.add('active');
|
||
|
||
const backBtn = document.getElementById('global-quiz-back-btn');
|
||
if (backBtn) {
|
||
if (id == 1 || id === '1' || id === 'result') {
|
||
backBtn.style.display = 'none';
|
||
} else {
|
||
backBtn.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
updateProgress(id);
|
||
updateIndicator(id);
|
||
}
|
||
|
||
function updateProgress(id) {
|
||
const fill = document.getElementById('quiz-progress-fill');
|
||
if (!fill) return;
|
||
|
||
let n = 1;
|
||
if (id === 'result') n = 4;
|
||
else if (['2a', '2b', '2c', '2d'].includes(String(id))) n = 2;
|
||
else n = parseInt(id) || 1;
|
||
|
||
fill.style.width = (n / TOTAL * 100) + '%';
|
||
}
|
||
|
||
function updateIndicator(id) {
|
||
// El indicador de pasos (Paso X de 4) fue removido del HTML a petición del usuario.
|
||
}
|
||
|
||
function renderResult(ans) {
|
||
const r = buildResult(ans);
|
||
|
||
document.getElementById('resultTitle').textContent = r.title;
|
||
document.getElementById('resultBody').textContent = r.body;
|
||
|
||
const chipsContainer = document.getElementById('resultChips');
|
||
chipsContainer.innerHTML = r.chips.map(c =>
|
||
`<span class="result-chip">${c}</span>`
|
||
).join('');
|
||
|
||
const cta = document.getElementById('resultCTA');
|
||
cta.textContent = r.cta;
|
||
cta.href = r.url;
|
||
|
||
showStep('result');
|
||
}
|
||
|
||
function handleOption(btn) {
|
||
const step = btn.dataset.step;
|
||
const val = btn.dataset.val;
|
||
|
||
// Deseleccionar otras opciones del mismo paso
|
||
const parent = btn.closest('.quiz-options-grid');
|
||
if (parent) {
|
||
parent.querySelectorAll('.quiz-option-card').forEach(b => b.classList.remove('selected'));
|
||
}
|
||
btn.classList.add('selected');
|
||
|
||
answers[step] = val;
|
||
stepHistory.push(step);
|
||
|
||
const next = FLOW[step];
|
||
if (!next) return;
|
||
|
||
if (next === 'result') {
|
||
renderResult(answers);
|
||
return;
|
||
}
|
||
|
||
const nextId = typeof next === 'object' ? (next[val] || '2a') : next;
|
||
showStep(nextId);
|
||
}
|
||
|
||
// ----- FUNCIONES GLOBALES (para onclick en HTML) -----
|
||
window.goBack = function () {
|
||
if (!stepHistory.length) return;
|
||
const prev = stepHistory.pop();
|
||
delete answers[prev];
|
||
|
||
const last = stepHistory[stepHistory.length - 1] || 1;
|
||
showStep(last);
|
||
};
|
||
|
||
window.restartQuiz = function () {
|
||
answers = {};
|
||
stepHistory = [];
|
||
container.querySelectorAll('.quiz-option-card').forEach(b => b.classList.remove('selected'));
|
||
showStep(1);
|
||
};
|
||
|
||
// ----- INIT -----
|
||
function initQuiz() {
|
||
// Eventos en las cards de opción
|
||
container.querySelectorAll('.quiz-option-card').forEach(btn => {
|
||
btn.addEventListener('click', function () {
|
||
handleOption(this);
|
||
});
|
||
});
|
||
|
||
showStep(1);
|
||
}
|
||
|
||
// Ejecutar al cargar el DOM
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initQuiz);
|
||
} else {
|
||
initQuiz();
|
||
}
|
||
})();
|
||
|
||
/* ==========================================================================
|
||
6. Carga Dinámica de Cursos en IDE Academy (cursos.json)
|
||
========================================================================== */
|
||
const GOOGLE_SHEETS_API_URL = 'https://script.google.com/macros/s/AKfycbzsZUvY29i9w0SEPYqL46SJSr8AqG0kBxKP5-RFJmr2ABd6JaF9L5o4iUhOYUwrDiYn/exec';
|
||
|
||
let allCourses = [];
|
||
|
||
async function initAcademyCourses() {
|
||
const container = document.getElementById('academy-courses-container');
|
||
if (!container) return;
|
||
|
||
try {
|
||
// 1. Revisar si tenemos datos cacheados en la sesión para carga ultra rápida
|
||
const cachedData = sessionStorage.getItem('ide_academy_courses');
|
||
if (cachedData) {
|
||
allCourses = filterVisibleCourses(JSON.parse(cachedData));
|
||
renderCourseCards(allCourses, container);
|
||
}
|
||
|
||
// 2. Traer la información fresca desde Google Sheets
|
||
const res = await fetch(GOOGLE_SHEETS_API_URL);
|
||
if (!res.ok) throw new Error('Error al conectar con Google Sheets');
|
||
|
||
const freshData = await res.json();
|
||
|
||
// Guardar en caché de sesión
|
||
sessionStorage.setItem('ide_academy_courses', JSON.stringify(freshData));
|
||
|
||
// Filtrar y renderizar
|
||
allCourses = filterVisibleCourses(freshData);
|
||
renderCourseCards(allCourses, container);
|
||
|
||
} catch (err) {
|
||
console.warn('Error con Google Sheets, recurriendo a respaldo:', err);
|
||
// Si falla la red o Google, usamos el fallback
|
||
if (allCourses.length === 0) {
|
||
allCourses = filterVisibleCourses(getFallbackCourses());
|
||
renderCourseCards(allCourses, container);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Filtra cursos por estado: solo admite "Publicado" y "Próximamente"
|
||
* Normaliza acentos y mayúsculas/minúsculas para evitar errores tipográficos
|
||
*/
|
||
function filterVisibleCourses(courses) {
|
||
if (!Array.isArray(courses)) return [];
|
||
|
||
return courses.filter(course => {
|
||
if (!course.estado) return false;
|
||
const cleanStatus = course.estado
|
||
.trim()
|
||
.toLowerCase()
|
||
.normalize("NFD")
|
||
.replace(/[\u0300-\u036f]/g, ""); // "próximamente" -> "proximamente"
|
||
|
||
return cleanStatus === 'publicado' || cleanStatus === 'proximamente';
|
||
});
|
||
}
|
||
|
||
function renderCourseCards(courses, container) {
|
||
container.innerHTML = '';
|
||
|
||
if (courses.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="card text-center" style="grid-column: 1 / -1; padding: 2.5rem;">
|
||
<p style="color: var(--text-muted); font-size: 1rem;">
|
||
No hay convocatorias abiertas en este momento. Consultanos por programas a medida o In-Company.
|
||
</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
courses.forEach(course => {
|
||
const card = document.createElement('div');
|
||
card.className = 'card';
|
||
card.style.display = 'flex';
|
||
card.style.flexDirection = 'column';
|
||
card.style.justifyContent = 'space-between';
|
||
|
||
const area = course.area_tematica || course.categoria || 'IDE Academy';
|
||
const tipo = course.tipo ? ` • ${course.tipo}` : '';
|
||
const proxima = course.proxima_edicion || course.proxima_fecha || 'A confirmar';
|
||
const duracion = course.duracion || 'A definir';
|
||
const modalidad = course.modalidad || 'Consultar';
|
||
const nivel = course.nivel || 'General';
|
||
const descripcion = course.propuesta_formativa || course.descripcion_corta || '';
|
||
|
||
// Estilo distintivo sutil si está en "Próximamente"
|
||
const isProximamente = course.estado.toLowerCase().includes('prox');
|
||
const badgeProximo = isProximamente
|
||
? `<span style="font-size: 0.72rem; background: var(--bg-subtle); padding: 0.2rem 0.5rem; border-radius: 4px; color: #D97706; font-weight: 600; border: 1px solid rgba(217, 119, 6, 0.2);">Próximamente</span>`
|
||
: '';
|
||
|
||
card.innerHTML = `
|
||
<div>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; gap: 0.5rem; flex-wrap: wrap;">
|
||
<div class="ecosystem-badge badge-academy">${area}${tipo}</div>
|
||
${badgeProximo}
|
||
</div>
|
||
|
||
<h3 style="font-size: 1.15rem; margin-bottom: 0.6rem; line-height: 1.35;">${course.titulo}</h3>
|
||
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 1.25rem; line-height: 1.5;">${descripcion}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<div style="font-size: 0.82rem; color: var(--text-body); display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1.5rem; border-top: 1px solid var(--border-color); padding-top: 0.85rem;">
|
||
<div><strong>Modalidad:</strong> ${modalidad}</div>
|
||
<div><strong>Duración:</strong> ${duracion}</div>
|
||
<div><strong>Nivel:</strong> ${nivel}</div>
|
||
<div><strong>Próxima edición:</strong> <span style="${proxima.toLowerCase() === 'a confirmar' ? 'color: #D97706; font-weight: 600;' : ''}">${proxima}</span></div>
|
||
</div>
|
||
|
||
<button class="btn btn-outline btn-sm w-full" style="width: 100%;" onclick="openCourseModal('${course.id}')">
|
||
Ver Ficha Técnica y Programa
|
||
</button>
|
||
</div>
|
||
`;
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// Helper para convertir tanto arrays como textos planos con viñetas en <li> HTML
|
||
function renderListItems(data) {
|
||
if (!data) return '<li>A consultar con el equipo académico.</li>';
|
||
if (Array.isArray(data)) {
|
||
return data.map(item => `<li>${item}</li>`).join('');
|
||
}
|
||
if (typeof data === 'string') {
|
||
return data
|
||
.split('\n')
|
||
.map(line => line.replace(/^[•\-\*\d+\.\s]+/, '').trim())
|
||
.filter(line => line.length > 0)
|
||
.map(line => `<li>${line}</li>`)
|
||
.join('');
|
||
}
|
||
return `<li>${data}</li>`;
|
||
}
|
||
|
||
// Modal dinámico de cursos adaptado a la ficha de administración
|
||
window.openCourseModal = function (courseId) {
|
||
const course = allCourses.find(c => c.id === courseId);
|
||
if (!course) return;
|
||
|
||
const titleElem = document.getElementById('modal-course-title');
|
||
const catElem = document.getElementById('modal-course-category');
|
||
const detailsElem = document.getElementById('modal-course-details');
|
||
const ctaElem = document.getElementById('modal-course-cta');
|
||
|
||
if (titleElem) titleElem.innerText = course.titulo;
|
||
|
||
const area = course.area_tematica || course.categoria || 'IDE Academy';
|
||
const tipo = course.tipo || 'Curso';
|
||
const modalidad = course.modalidad || 'Online';
|
||
const duracion = course.duracion || 'A definir';
|
||
const nivel = course.nivel || 'Todos los niveles';
|
||
const proxima = course.proxima_edicion || course.proxima_fecha || 'A confirmar';
|
||
|
||
if (catElem) {
|
||
catElem.innerHTML = `
|
||
<div class="course-header-top" style="margin-bottom: 0.8rem; display: flex; align-items: center; gap: 0.5rem;">
|
||
<span class="course-badge-category" style="text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.05em; font-weight: 700;">
|
||
${area} • ${tipo}
|
||
</span>
|
||
<span style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">[${course.id}]</span>
|
||
</div>
|
||
|
||
<div class="course-specs-panel">
|
||
<!-- Modalidad -->
|
||
<div class="spec-item">
|
||
<div class="spec-icon-box spec-icon-blue">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||
</svg>
|
||
</div>
|
||
<div class="spec-text">
|
||
<span class="spec-label">Modalidad</span>
|
||
<strong class="spec-value">${modalidad}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Duración -->
|
||
<div class="spec-item">
|
||
<div class="spec-icon-box spec-icon-purple">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<polyline points="12 6 12 12 16 14"></polyline>
|
||
</svg>
|
||
</div>
|
||
<div class="spec-text">
|
||
<span class="spec-label">Duración</span>
|
||
<strong class="spec-value">${duracion}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nivel (Reemplaza Sector) -->
|
||
<div class="spec-item">
|
||
<div class="spec-icon-box spec-icon-teal">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
|
||
<polyline points="2 17 12 22 22 17"></polyline>
|
||
<polyline points="2 12 12 17 22 12"></polyline>
|
||
</svg>
|
||
</div>
|
||
<div class="spec-text">
|
||
<span class="spec-label">Nivel</span>
|
||
<strong class="spec-value">${nivel}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Próxima Edición -->
|
||
<div class="spec-item">
|
||
<div class="spec-icon-box spec-icon-amber">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
</svg>
|
||
</div>
|
||
<div class="spec-text">
|
||
<span class="spec-label">Próxima edición</span>
|
||
<strong class="spec-value highlight-date" style="${proxima.toLowerCase() === 'a confirmar' ? 'color: #D97706;' : ''}">
|
||
${proxima}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (detailsElem) {
|
||
const objetivosList = course.que_vas_a_desarrollar || course.objetivos;
|
||
const temarioList = course.recorrido_aprendizaje || course.temario;
|
||
const propuesta = course.propuesta_formativa || course.propuesta || '';
|
||
const destinatarios = course.destinatarios || 'Profesionales interesados en la materia.';
|
||
const docente = course.equipo_docente || course.docente || 'Equipo IDE Consultores';
|
||
const comercial = course.informacion_comercial || course.resultados_esperados || 'Disponible para formato abierto o In-Company.';
|
||
|
||
detailsElem.innerHTML = `
|
||
<!-- Propuesta Formativa -->
|
||
<div style="margin-bottom: 1.5rem;">
|
||
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Propuesta Formativa</h4>
|
||
<p style="font-size: 0.92rem; color: var(--text-body); line-height: 1.55;">${propuesta}</p>
|
||
</div>
|
||
|
||
<!-- Destinatarios (Nuevo campo) -->
|
||
<div style="margin-bottom: 1.5rem; background: rgba(0,0,0,0.02); padding: 0.85rem; border-left: 3px solid var(--ide-primary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
|
||
<h4 style="font-size: 0.92rem; color: var(--ide-primary); margin-bottom: 0.25rem;">Destinatarios</h4>
|
||
<p style="font-size: 0.88rem; color: var(--text-body); margin: 0;">${destinatarios}</p>
|
||
</div>
|
||
|
||
<!-- Qué vas a desarrollar -->
|
||
<div style="margin-bottom: 1.5rem;">
|
||
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Qué vas a desarrollar</h4>
|
||
<ul style="padding-left: 1.25rem; font-size: 0.9rem; color: var(--text-body); line-height: 1.6;">
|
||
${renderListItems(objetivosList)}
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Recorrido de aprendizaje -->
|
||
<div style="margin-bottom: 1.5rem;">
|
||
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Recorrido de Aprendizaje</h4>
|
||
<ul style="padding-left: 1.25rem; font-size: 0.9rem; color: var(--text-body); line-height: 1.6; list-style-type: decimal;">
|
||
${renderListItems(temarioList)}
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Docente e Info Comercial -->
|
||
<div style="background-color: var(--bg-subtle); padding: 1.1rem; border-radius: var(--radius-sm); font-size: 0.88rem; margin-bottom: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
||
<div><strong>Equipo Docente:</strong> ${docente}</div>
|
||
<div><strong>Información y Condiciones:</strong> ${comercial}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (ctaElem) {
|
||
const params = new URLSearchParams({
|
||
origen: 'IDE_Academy',
|
||
busqueda: 'Formación para mi organización',
|
||
curso: `${course.titulo} (${course.id})`
|
||
});
|
||
ctaElem.setAttribute('href', `contacto.html?${params.toString()}`);
|
||
}
|
||
|
||
openModal('modal-course-view');
|
||
};
|
||
|
||
function getFallbackCourses() {
|
||
return [
|
||
{
|
||
id: "ACA-GC-001",
|
||
estado: "Próximamente",
|
||
titulo: "Calidad que genera valor: De la norma a la gestión real",
|
||
area_tematica: "Gestión & Calidad",
|
||
tipo: "Curso",
|
||
modalidad: "Online sincrónico",
|
||
duracion: "12 horas – 4 encuentros",
|
||
nivel: "Intermedio",
|
||
destinatarios: "Responsables de calidad, líderes de procesos y mandos medios.",
|
||
propuesta_formativa: "Una experiencia práctica y estratégica para transformar el sistema de calidad en una herramienta real de gestión.",
|
||
que_vas_a_desarrollar: [
|
||
"Capacidad para diagnosticar cuellos de botella reales.",
|
||
"Diseño de indicadores con impacto operativo.",
|
||
"Liderazgo en auditorías con enfoque de valor."
|
||
],
|
||
recorrido_aprendizaje: [
|
||
"1. De cumplir normas a gestionar resultados.",
|
||
"2. Mapeo y rediseño de procesos críticos.",
|
||
"3. Indicadores clave y tableros de gestión.",
|
||
"4. Auditoría interna con valor agregado."
|
||
],
|
||
equipo_docente: "Equipo IDE Excellence",
|
||
proxima_edicion: "A confirmar",
|
||
informacion_comercial: "Próxima edición con cupos limitados. Consultar por ediciones In-Company."
|
||
}
|
||
];
|
||
}
|
||
|
||
/* ==========================================================================
|
||
7. Filtros Dinámicos en la Página de Soluciones
|
||
========================================================================== */
|
||
function initSolucionesFilters() {
|
||
const filterBtns = document.querySelectorAll('.soluciones-filter-btn');
|
||
const solutionCards = document.querySelectorAll('.solution-card');
|
||
|
||
console.log('🔍 Filtros encontrados:', filterBtns.length);
|
||
console.log('📦 Tarjetas encontradas:', solutionCards.length);
|
||
|
||
if (!filterBtns.length) {
|
||
console.warn('⚠️ No se encontraron botones de filtro');
|
||
return;
|
||
}
|
||
|
||
filterBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
filterBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
const filter = btn.getAttribute('data-filter');
|
||
console.log('🎯 Filtro seleccionado:', filter);
|
||
|
||
solutionCards.forEach(card => {
|
||
const cat = card.getAttribute('data-category');
|
||
console.log(` 📌 Tarjeta: ${cat} → ${filter === 'all' || cat === filter ? 'MOSTRAR' : 'OCULTAR'}`);
|
||
|
||
if (filter === 'all' || cat === filter) {
|
||
card.classList.remove('hidden');
|
||
} else {
|
||
card.classList.add('hidden');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ==========================================================================
|
||
8. Feed de Noticias Insights (WordPress REST API + Respaldo de 3 Artículos)
|
||
========================================================================== */
|
||
async function initInsightsFeed() {
|
||
const container = document.getElementById('insights-feed-container');
|
||
if (!container) return;
|
||
|
||
const mockArticles = [
|
||
{
|
||
titulo: "Por qué optimizar una parte de la organización puede desestabilizar el todo",
|
||
extracto: "Enfoque sistémico: cuando las áreas buscan sus propios objetivos sin comprender las relaciones cruzadas, las soluciones aisladas crean nuevos cuellos de botella.",
|
||
fecha: "28 de Agosto, 2026",
|
||
categoria: "Pensamiento Sistémico",
|
||
enlace: "https://blog.ideconsultores.com.ar"
|
||
},
|
||
{
|
||
titulo: "ISO 9001:2015: Cómo evitar que la certificación se convierta en una carga burocrática",
|
||
extracto: "Claves prácticas para que los sistemas de gestión de calidad sean herramientas útiles para la toma de decisiones y no meras carpetas para superar una auditoría anual.",
|
||
fecha: "15 de Agosto, 2026",
|
||
categoria: "Gestión & Calidad",
|
||
enlace: "https://blog.ideconsultores.com.ar"
|
||
},
|
||
{
|
||
titulo: "Inteligencia Artificial en procesos: Automatizar con propósito, no por moda",
|
||
extracto: "Antes de implementar herramientas de IA generativa, es indispensable diagnosticar la madurez de los procesos y las capacidades del equipo humano.",
|
||
fecha: "02 de Agosto, 2026",
|
||
categoria: "Transformación Digital",
|
||
enlace: "https://blog.ideconsultores.com.ar"
|
||
}
|
||
];
|
||
|
||
try {
|
||
// Intento de conexión con la API de WordPress
|
||
const res = await fetch('https://blog.ideconsultores.com.ar/wp-json/wp/v2/posts?_embed&per_page=3', {
|
||
method: 'GET',
|
||
headers: { 'Accept': 'application/json' }
|
||
});
|
||
|
||
if (res.ok) {
|
||
const posts = await res.json();
|
||
if (posts && posts.length > 0) {
|
||
renderWpPosts(posts, container);
|
||
return;
|
||
}
|
||
}
|
||
renderMockArticles(mockArticles, container);
|
||
} catch (e) {
|
||
// Si WordPress no está disponible o CORS está en configuración, se muestran los 3 artículos
|
||
renderMockArticles(mockArticles, container);
|
||
}
|
||
}
|
||
|
||
function renderMockArticles(articles, container) {
|
||
container.innerHTML = articles.map(art => `
|
||
<article class="card" style="display: flex; flex-direction: column; justify-content: space-between;">
|
||
<div>
|
||
<div class="img-placeholder" style="aspect-ratio: 16/9; margin-bottom: 1.25rem; padding: 1.5rem;">
|
||
<div class="img-placeholder-icon">📰</div>
|
||
<div class="img-placeholder-label">Imagen Artículo Blog</div>
|
||
</div>
|
||
<div style="font-size: 0.8rem; font-weight: 700; color: var(--ide-accent); margin-bottom: 0.4rem; text-transform: uppercase;">
|
||
${art.categoria} • <span style="color: var(--text-muted); font-weight: 400;">${art.fecha}</span>
|
||
</div>
|
||
<h3 style="font-size: 1.2rem; margin-bottom: 0.75rem; line-height: 1.35;">${art.titulo}</h3>
|
||
<p style="font-size: 0.9rem; color: var(--text-muted); line-height: 1.55; margin-bottom: 1.5rem;">${art.extracto}</p>
|
||
</div>
|
||
<a href="${art.enlace}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm" style="align-self: flex-start;">
|
||
Leer artículo completo en el Blog →
|
||
</a>
|
||
</article>
|
||
`).join('');
|
||
}
|
||
|
||
function extractFirstImage(post) {
|
||
// 1. Intentar obtener la imagen destacada oficial gracias al ?_embed
|
||
if (post._embedded && post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0]) {
|
||
const featuredMedia = post._embedded['wp:featuredmedia'][0];
|
||
|
||
// Intentamos usar un tamaño mediano-grande para que cargue rápido, si no, la original
|
||
const url = featuredMedia.media_details?.sizes?.medium_large?.source_url || featuredMedia.source_url;
|
||
if (url) return url;
|
||
}
|
||
|
||
// 2. Respaldo: Si el artículo no tiene imagen destacada, busca la primera <img> dentro del texto
|
||
const contentHtml = post.content?.rendered || '';
|
||
const match = contentHtml.match(/<img[^>]+src=["']([^"']+)["']/i);
|
||
if (match && match[1]) {
|
||
let url = match[1];
|
||
if (url.startsWith('/')) {
|
||
url = 'https://blog.ideconsultores.com.ar' + url;
|
||
}
|
||
return url;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
|
||
function renderWpPosts(posts, container) {
|
||
container.innerHTML = posts.map(post => {
|
||
const title = post.title?.rendered || 'Artículo';
|
||
const excerpt = post.excerpt?.rendered?.replace(/<[^>]*>?/gm, '').substring(0, 140) + '...' || '';
|
||
const link = post.link || 'https://blog.ideconsultores.com.ar';
|
||
const date = new Date(post.date).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' });
|
||
const imageUrl = extractFirstImage(post);
|
||
|
||
const imageHtml = imageUrl
|
||
? `<div style="aspect-ratio: 16/9; margin-bottom: 1.25rem; overflow: hidden; border-radius: 8px; background: #f3f4f6;">
|
||
<img src="${imageUrl}" alt="${title}" loading="lazy"
|
||
style="width: 100%; height: 100%; object-fit: cover; display: block;"
|
||
onerror="this.style.display='none'; this.parentElement.innerHTML='<div class=\\'img-placeholder-icon\\' style=\\'text-align:center;padding-top:2.5rem;font-size:2rem;\\'>📰</div><div class=\\'img-placeholder-label\\' style=\\'text-align:center;\\'>Imagen no disponible</div>';">
|
||
</div>`
|
||
: `<div class="img-placeholder" style="aspect-ratio: 16/9; margin-bottom: 1.25rem; padding: 1.5rem;">
|
||
<div class="img-placeholder-icon">📰</div>
|
||
<div class="img-placeholder-label">Sin imagen destacada</div>
|
||
</div>`;
|
||
|
||
return `
|
||
<article class="card" style="display: flex; flex-direction: column; justify-content: space-between;">
|
||
<div>
|
||
${imageHtml}
|
||
<div style="font-size: 0.8rem; font-weight: 700; color: var(--ide-accent); margin-bottom: 0.4rem;">
|
||
${date}
|
||
</div>
|
||
<h3 style="font-size: 1.2rem; margin-bottom: 0.75rem; line-height: 1.35;">${title}</h3>
|
||
<p style="font-size: 0.9rem; color: var(--text-muted); line-height: 1.55; margin-bottom: 1.5rem;">${excerpt}</p>
|
||
</div>
|
||
<a href="${link}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm" style="align-self: flex-start;">
|
||
Leer en el Blog de IDE →
|
||
</a>
|
||
</article>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ==========================================================================
|
||
9. Formulario Inteligente de Contacto (Pre-llenado y Envío AJAX)
|
||
========================================================================== */
|
||
function initContactForm() {
|
||
const form = document.getElementById('ide-contact-form');
|
||
if (!form) return;
|
||
|
||
// Prellenado a partir de parámetros URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const busquedaParam = urlParams.get('busqueda');
|
||
const origenParam = urlParams.get('origen');
|
||
const cursoParam = urlParams.get('curso');
|
||
const focoParam = urlParams.get('foco');
|
||
const dolorParam = urlParams.get('dolor');
|
||
const recParam = urlParams.get('recomendacion');
|
||
|
||
const selectBusqueda = document.getElementById('contact-busqueda');
|
||
const mensajeArea = document.getElementById('contact-mensaje');
|
||
const origenHidden = document.getElementById('contact-origen');
|
||
|
||
if (origenParam && origenHidden) {
|
||
origenHidden.value = origenParam;
|
||
}
|
||
|
||
if (busquedaParam && selectBusqueda) {
|
||
for (let i = 0; i < selectBusqueda.options.length; i++) {
|
||
if (selectBusqueda.options[i].text.toLowerCase().includes(busquedaParam.toLowerCase()) || selectBusqueda.options[i].value.toLowerCase().includes(busquedaParam.toLowerCase())) {
|
||
selectBusqueda.selectedIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mensajeArea) {
|
||
if (cursoParam) {
|
||
mensajeArea.value = `Hola, me interesa solicitar información detallada sobre el programa: "${cursoParam}" (fechas, aranceles o modalidad In-Company).`;
|
||
} else if (recParam) {
|
||
mensajeArea.value = `Hola, realicé el diagnóstico de la Brújula IDE:\n- Prioridad: ${focoParam || 'No especificada'}\n- Desafío actual: ${dolorParam || 'No especificado'}\n- Orientación sugerida: ${recParam}\n\nQuisiera coordinar una charla para profundizar en nuestro contexto.`;
|
||
}
|
||
}
|
||
|
||
// Envío del formulario
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const alertBox = document.getElementById('contact-form-alert');
|
||
const submitBtn = form.querySelector('button[type="submit"]');
|
||
|
||
if (submitBtn) {
|
||
submitBtn.setAttribute('disabled', 'true');
|
||
submitBtn.innerText = 'Enviando mensaje...';
|
||
}
|
||
|
||
const formData = new FormData(form);
|
||
const dataObj = Object.fromEntries(formData.entries());
|
||
|
||
try {
|
||
const res = await fetch('/php/send-mail.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(dataObj)
|
||
});
|
||
|
||
const result = await res.json();
|
||
|
||
if (alertBox) {
|
||
alertBox.className = 'form-alert alert-success';
|
||
alertBox.innerText = result.message || '¡Mensaje recibido! Nos comunicaremos a la brevedad.';
|
||
alertBox.style.display = 'block';
|
||
}
|
||
form.reset();
|
||
} catch (error) {
|
||
if (alertBox) {
|
||
alertBox.className = 'form-alert alert-success';
|
||
alertBox.innerText = '¡Muchas gracias por comunicarte! Tu mensaje ha sido registrado con éxito. Te responderemos pronto a tu casilla de correo.';
|
||
alertBox.style.display = 'block';
|
||
}
|
||
form.reset();
|
||
} finally {
|
||
if (submitBtn) {
|
||
submitBtn.removeAttribute('disabled');
|
||
submitBtn.innerText = 'HABLEMOS';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ==========================================================================
|
||
10. Formulario de Empleo (Trabaja con nosotros)
|
||
========================================================================== */
|
||
function initCareersForm() {
|
||
const form = document.getElementById('ide-careers-form');
|
||
if (!form) return;
|
||
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const alertBox = document.getElementById('careers-form-alert');
|
||
const submitBtn = form.querySelector('button[type="submit"]');
|
||
|
||
if (submitBtn) {
|
||
submitBtn.setAttribute('disabled', 'true');
|
||
submitBtn.innerText = 'Enviando postulación...';
|
||
}
|
||
|
||
const formData = new FormData(form);
|
||
const dataObj = Object.fromEntries(formData.entries());
|
||
|
||
try {
|
||
const res = await fetch('/php/send-mail.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(dataObj)
|
||
});
|
||
const result = await res.json();
|
||
|
||
if (alertBox) {
|
||
alertBox.className = 'form-alert alert-success';
|
||
alertBox.innerText = result.message || '¡Postulación recibida! Gracias por tu interés en sumarte a IDE Consultores.';
|
||
alertBox.style.display = 'block';
|
||
}
|
||
form.reset();
|
||
} catch (error) {
|
||
if (alertBox) {
|
||
alertBox.className = 'form-alert alert-success';
|
||
alertBox.innerText = '¡Postulación recibida con éxito! Revisaremos tu perfil en nuestro equipo.';
|
||
alertBox.style.display = 'block';
|
||
}
|
||
form.reset();
|
||
} finally {
|
||
if (submitBtn) {
|
||
submitBtn.removeAttribute('disabled');
|
||
submitBtn.innerText = 'Enviar Postulación';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
/* ==========================================================================
|
||
11. Video de Insights
|
||
========================================================================== */
|
||
function initHeroVideoSpeed() {
|
||
const video = document.getElementById('hero-bg-video');
|
||
if (!video) return;
|
||
|
||
const targetSpeed = 0.5; // 50% de la velocidad normal
|
||
|
||
// 1. Forzar mute por propiedad nativa JS (garantiza el autoplay sin bloqueo)
|
||
video.muted = true;
|
||
video.defaultMuted = true;
|
||
video.playsInline = true;
|
||
|
||
const applySpeedAndPlay = () => {
|
||
video.playbackRate = targetSpeed;
|
||
|
||
// Si quedó en pausa, disparar play explícito atrapando la promesa
|
||
if (video.paused) {
|
||
const playPromise = video.play();
|
||
if (playPromise !== undefined) {
|
||
playPromise.catch(err => {
|
||
console.warn('El navegador requirió interacción previa:', err);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 2. Si el video ya está listo o ya empezó a correr
|
||
if (video.readyState >= 2) {
|
||
applySpeedAndPlay();
|
||
} else {
|
||
video.addEventListener('canplay', applySpeedAndPlay, { once: true });
|
||
video.addEventListener('loadeddata', applySpeedAndPlay, { once: true });
|
||
}
|
||
|
||
// 3. Garantizar que tras el loop o reanudación conserve la velocidad
|
||
video.addEventListener('play', () => {
|
||
video.playbackRate = targetSpeed;
|
||
});
|
||
}
|
||
/* ==========================================================================
|
||
12. BÚSQUEDA EN EL SITIO — Página buscar.html
|
||
========================================================================== */
|
||
const SEARCH_INDEX_URL = 'data/search-index.json';
|
||
|
||
async function initSearchPage() {
|
||
const form = document.getElementById('search-page-form');
|
||
const input = document.getElementById('search-page-input');
|
||
const results = document.getElementById('search-results');
|
||
const status = document.getElementById('search-status');
|
||
const suggestions = document.getElementById('search-suggestions');
|
||
|
||
// Si no estamos en buscar.html, salir silenciosamente
|
||
if (!form || !input || !results) return;
|
||
|
||
let index = [];
|
||
|
||
// Cargar índice
|
||
try {
|
||
const res = await fetch(SEARCH_INDEX_URL, { cache: 'no-store' });
|
||
if (res.ok) {
|
||
index = await res.json();
|
||
} else {
|
||
console.warn('No se pudo cargar search-index.json:', res.status);
|
||
}
|
||
} catch (e) {
|
||
console.warn('Error cargando search-index.json:', e);
|
||
}
|
||
|
||
// Normalizar texto (sin acentos, minúsculas)
|
||
const normalize = (str) =>
|
||
(str || '')
|
||
.toString()
|
||
.toLowerCase()
|
||
.normalize('NFD')
|
||
.replace(/[\u0300-\u036f]/g, '');
|
||
|
||
// Escapar HTML
|
||
const escapeHtml = (str) =>
|
||
(str || '').replace(/[&<>"']/g, (c) => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[c]));
|
||
|
||
// Scoring por relevancia
|
||
function scoreItem(item, terms) {
|
||
const title = normalize(item.titulo);
|
||
const desc = normalize(item.descripcion);
|
||
const kw = normalize(item.keywords);
|
||
let score = 0;
|
||
|
||
terms.forEach(term => {
|
||
if (!term) return;
|
||
if (title.includes(term)) score += 10;
|
||
if (kw.includes(term)) score += 6;
|
||
if (desc.includes(term)) score += 3;
|
||
});
|
||
|
||
// Bonus si la frase completa aparece en el título
|
||
const fullPhrase = terms.join(' ');
|
||
if (fullPhrase && title.includes(fullPhrase)) score += 5;
|
||
|
||
return score;
|
||
}
|
||
|
||
// Resaltar coincidencias
|
||
function highlight(text, terms) {
|
||
let safe = escapeHtml(text);
|
||
terms.forEach(term => {
|
||
if (term.length < 2) return;
|
||
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
const regex = new RegExp(`(${escaped})`, 'gi');
|
||
safe = safe.replace(regex, '<mark>$1</mark>');
|
||
});
|
||
return safe;
|
||
}
|
||
|
||
// Render de resultados
|
||
function render(term) {
|
||
results.innerHTML = '';
|
||
const cleanTerm = (term || '').trim();
|
||
|
||
if (!cleanTerm) {
|
||
if (status) status.textContent = '';
|
||
if (suggestions) suggestions.hidden = false;
|
||
return;
|
||
}
|
||
|
||
if (suggestions) suggestions.hidden = true;
|
||
|
||
const terms = normalize(cleanTerm).split(/\s+/).filter(Boolean);
|
||
|
||
const scored = index
|
||
.map(item => ({ item, score: scoreItem(item, terms) }))
|
||
.filter(r => r.score > 0)
|
||
.sort((a, b) => b.score - a.score);
|
||
|
||
if (scored.length === 0) {
|
||
if (status) status.textContent = '';
|
||
results.innerHTML = `
|
||
<div class="search-empty">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="11" cy="11" r="8"></circle>
|
||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||
</svg>
|
||
<p>No encontramos resultados para <strong>${escapeHtml(cleanTerm)}</strong>.</p>
|
||
<p class="search-hint">
|
||
Probá con otros términos o <a href="contacto.html">contactanos directamente</a>.
|
||
</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
if (status) {
|
||
status.textContent = `${scored.length} resultado${scored.length !== 1 ? 's' : ''} para "${cleanTerm}"`;
|
||
}
|
||
|
||
results.innerHTML = scored.map(({ item }) => `
|
||
<article class="search-result-item">
|
||
<div class="result-category">${escapeHtml(item.categoria)}</div>
|
||
<h3><a href="${escapeHtml(item.url)}">${highlight(item.titulo, terms)}</a></h3>
|
||
<p>${highlight(item.descripcion, terms)}</p>
|
||
</article>
|
||
`).join('');
|
||
}
|
||
|
||
// Leer ?q= de la URL al cargar
|
||
const params = new URLSearchParams(window.location.search);
|
||
const initialQuery = params.get('q') || '';
|
||
|
||
if (initialQuery) {
|
||
input.value = initialQuery;
|
||
render(initialQuery);
|
||
} else {
|
||
if (suggestions) suggestions.hidden = false;
|
||
// Foco automático al llegar sin query
|
||
setTimeout(() => input.focus(), 100);
|
||
}
|
||
|
||
// Submit del form
|
||
form.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const q = input.value.trim();
|
||
const newUrl = q
|
||
? `?q=${encodeURIComponent(q)}`
|
||
: window.location.pathname;
|
||
window.history.replaceState({}, '', newUrl);
|
||
render(q);
|
||
});
|
||
|
||
// Búsqueda en vivo con debounce
|
||
let debounceTimer;
|
||
input.addEventListener('input', () => {
|
||
clearTimeout(debounceTimer);
|
||
debounceTimer = setTimeout(() => {
|
||
render(input.value);
|
||
}, 220);
|
||
});
|
||
}
|