/** * VIRAXIS — main.js * Orquesta todas las interacciones del sitio. Vanilla JS, sin * dependencias de jQuery. Cada bloque está aislado y comentado * para poder activarse/desactivarse de forma independiente. */ (function () { 'use strict'; var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ========================================================= 1. LOADER INICIAL ========================================================= */ function initLoader() { var loader = document.getElementById('viraxis-loader'); if (!loader) return; function hideLoader() { loader.classList.add('is-hidden'); document.body.classList.remove('no-scroll'); setTimeout(function () { loader.remove(); }, 600); } document.body.classList.add('no-scroll'); window.addEventListener('load', function () { setTimeout(hideLoader, 350); }); // Salvaguarda: si "load" tarda demasiado, ocultamos el loader igual setTimeout(hideLoader, 4000); } /* ========================================================= 2. HEADER: sombra/blur al hacer scroll ========================================================= */ function initHeaderScroll() { var header = document.getElementById('site-header'); if (!header) return; function onScroll() { if (window.scrollY > 12) { header.classList.add('is-scrolled'); } else { header.classList.remove('is-scrolled'); } } onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); } /* ========================================================= 3. MENÚ MÓVIL ========================================================= */ function initMobileNav() { var toggle = document.getElementById('nav-toggle'); var nav = document.getElementById('mobile-nav'); var backdrop = document.getElementById('mobile-nav-backdrop'); if (!toggle || !nav || !backdrop) return; function open() { nav.classList.add('is-open'); backdrop.classList.add('is-visible'); toggle.setAttribute('aria-expanded', 'true'); document.body.classList.add('no-scroll'); } function close() { nav.classList.remove('is-open'); backdrop.classList.remove('is-visible'); toggle.setAttribute('aria-expanded', 'false'); document.body.classList.remove('no-scroll'); } toggle.addEventListener('click', function () { nav.classList.contains('is-open') ? close() : open(); }); backdrop.addEventListener('click', close); nav.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', close); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); } /* ========================================================= 4. MODO OSCURO ========================================================= */ function initThemeToggle() { var toggle = document.getElementById('theme-toggle'); var root = document.documentElement; if (!toggle) return; var stored = null; try { stored = localStorage.getItem('viraxis-theme'); } catch (e) { /* almacenamiento no disponible */ } var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; var isDark = stored ? stored === 'dark' : prefersDark; applyTheme(isDark); toggle.addEventListener('click', function () { isDark = !root.classList.contains('dark-mode'); applyTheme(isDark); try { localStorage.setItem('viraxis-theme', isDark ? 'dark' : 'light'); } catch (e) { /* noop */ } }); function applyTheme(dark) { root.classList.toggle('dark-mode', dark); toggle.setAttribute('aria-pressed', String(dark)); toggle.setAttribute('aria-label', dark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'); } } /* ========================================================= 5. CURSOR PERSONALIZADO (solo puntero fino) ========================================================= */ function initCustomCursor() { if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches || prefersReducedMotion) return; var dot = document.querySelector('.cursor-dot'); var ring = document.querySelector('.cursor-ring'); if (!dot || !ring) return; var ringX = 0, ringY = 0, mouseX = 0, mouseY = 0; window.addEventListener('mousemove', function (e) { mouseX = e.clientX; mouseY = e.clientY; dot.style.transform = 'translate(' + mouseX + 'px,' + mouseY + 'px) translate(-50%,-50%)'; }); function loop() { ringX += (mouseX - ringX) * 0.18; ringY += (mouseY - ringY) * 0.18; ring.style.transform = 'translate(' + ringX + 'px,' + ringY + 'px) translate(-50%,-50%)'; requestAnimationFrame(loop); } loop(); document.querySelectorAll('a, button, [role="button"], input, textarea, select').forEach(function (el) { el.addEventListener('mouseenter', function () { ring.classList.add('is-active'); }); el.addEventListener('mouseleave', function () { ring.classList.remove('is-active'); }); }); } /* ========================================================= 6. EFECTO DE ESCRITURA (typewriter) EN EL HERO ========================================================= */ function initTypewriter() { var el = document.getElementById('hero-typewriter'); if (!el) return; var words = (el.getAttribute('data-words') || '').split(',').filter(Boolean); if (!words.length) return; if (prefersReducedMotion) { el.textContent = words[0]; return; } var wordIndex = 0, charIndex = 0, deleting = false; var TYPE_SPEED = 38, DELETE_SPEED = 22, HOLD_TIME = 2200; function tick() { var current = words[wordIndex]; if (!deleting) { charIndex++; el.textContent = current.slice(0, charIndex); if (charIndex === current.length) { deleting = true; setTimeout(tick, HOLD_TIME); return; } setTimeout(tick, TYPE_SPEED); } else { charIndex--; el.textContent = current.slice(0, charIndex); if (charIndex === 0) { deleting = false; wordIndex = (wordIndex + 1) % words.length; setTimeout(tick, TYPE_SPEED * 2); return; } setTimeout(tick, DELETE_SPEED); } } tick(); } /* ========================================================= 7. CONTADORES ANIMADOS (sección Quiénes Somos) ========================================================= */ function initCounters() { var counters = document.querySelectorAll('.stat-value[data-count]'); if (!counters.length) return; function animateCounter(el) { var target = parseInt(el.getAttribute('data-count'), 10) || 0; if (prefersReducedMotion) { el.textContent = target; return; } var duration = 1200; var start = null; function step(timestamp) { if (!start) start = timestamp; var progress = Math.min((timestamp - start) / duration, 1); el.textContent = Math.floor(progress * target); if (progress < 1) { requestAnimationFrame(step); } else { el.textContent = target; } } requestAnimationFrame(step); } var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { animateCounter(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.5 }); counters.forEach(function (el) { observer.observe(el); }); } /* ========================================================= 8. TABS DE SERVICIOS (implementación propia, sin depender del comportamiento interno de Materialize Tabs) ========================================================= */ function initServiceTabs() { var tabsWrap = document.getElementById('services-tabs'); if (!tabsWrap) return; var links = tabsWrap.querySelectorAll('a'); var panels = document.querySelectorAll('.services-panel'); function activate(targetId) { links.forEach(function (link) { link.classList.toggle('active', link.getAttribute('href') === targetId); }); panels.forEach(function (panel) { panel.classList.toggle('is-active', '#' + panel.id === targetId); }); } links.forEach(function (link) { link.addEventListener('click', function (e) { e.preventDefault(); activate(link.getAttribute('href')); }); }); // Activa el primer panel al cargar if (panels.length) { panels[0].classList.add('is-active'); } } /* ========================================================= 9. ACORDEÓN DE PREGUNTAS FRECUENTES ========================================================= */ function initFaqAccordion() { var questions = document.querySelectorAll('.faq-question'); if (!questions.length) return; questions.forEach(function (question) { var answer = document.getElementById(question.getAttribute('aria-controls')); function toggle() { var isOpen = question.getAttribute('aria-expanded') === 'true'; question.setAttribute('aria-expanded', String(!isOpen)); if (answer) { answer.style.maxHeight = isOpen ? '0px' : answer.scrollHeight + 'px'; } } question.addEventListener('click', toggle); question.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } }); }); } /* ========================================================= 10. SELECTOR "TIPO DE SOLICITUD" EN EL FORMULARIO ========================================================= */ function initRequestType() { var buttons = document.querySelectorAll('.request-type-btn'); var input = document.getElementById('request_type'); if (!buttons.length || !input) return; buttons.forEach(function (btn) { btn.addEventListener('click', function () { buttons.forEach(function (b) { b.classList.remove('is-active'); b.setAttribute('aria-checked', 'false'); }); btn.classList.add('is-active'); btn.setAttribute('aria-checked', 'true'); input.value = btn.getAttribute('data-value'); }); }); } /* ========================================================= 11. ENLACES "SOLICITAR ESTE SERVICIO" → preselecciona el select del formulario de contacto ========================================================= */ function initServiceLinks() { document.querySelectorAll('.service-link').forEach(function (link) { link.addEventListener('click', function () { var select = document.getElementById('service'); var value = link.getAttribute('data-service'); if (select && value) { var match = Array.from(select.options).find(function (opt) { return opt.value === value; }); select.value = match ? value : 'Otro / No estoy seguro'; } }); }); } /* ========================================================= 12. BOTÓN VOLVER ARRIBA ========================================================= */ function initBackToTop() { var btn = document.getElementById('back-to-top'); if (!btn) return; window.addEventListener('scroll', function () { btn.classList.toggle('is-visible', window.scrollY > 480); }, { passive: true }); btn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' }); }); } /* ========================================================= 13. INDICADOR PARA BAJAR (hero) ========================================================= */ function initScrollCue() { var cue = document.getElementById('scroll-cue'); if (!cue) return; cue.addEventListener('click', function () { var next = document.getElementById('nosotros'); if (next) next.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' }); }); } /* ========================================================= 14. LIBRERÍAS EXTERNAS: AOS y Swiper ========================================================= */ function initLibraries() { if (window.AOS) { window.AOS.init({ duration: 650, once: true, offset: 60, disable: prefersReducedMotion, }); } if (window.Swiper) { new window.Swiper('.testimonials-swiper', { loop: false, spaceBetween: 24, slidesPerView: 1, pagination: { el: '.swiper-pagination', clickable: true }, breakpoints: { 768: { slidesPerView: 2 }, 1100: { slidesPerView: 3 }, }, }); } } /* ========================================================= INICIALIZACIÓN ========================================================= */ document.addEventListener('DOMContentLoaded', function () { initLoader(); initHeaderScroll(); initMobileNav(); initThemeToggle(); initCustomCursor(); initTypewriter(); initCounters(); initServiceTabs(); initFaqAccordion(); initRequestType(); initServiceLinks(); initBackToTop(); initScrollCue(); initLibraries(); }); })();