/* ========================================================== SOINBA main.js Parte 1 ========================================================== */ document.addEventListener("DOMContentLoaded", () => { //========================= // Inicializar AOS //========================= AOS.init({ duration: 900, once: true, easing: "ease-in-out" }); //========================= // Variables //========================= const navbar = document.getElementById("mainNav"); const backTop = document.getElementById("backToTop"); const navLinks = document.querySelectorAll(".nav-link"); //========================= // Navbar Scroll //========================= function navbarScroll(){ if(window.scrollY > 50){ navbar.classList.add("scrolled"); }else{ navbar.classList.remove("scrolled"); } } navbarScroll(); //========================= // Botón volver arriba //========================= function backTopButton(){ if(window.scrollY > 500){ backTop.classList.add("show"); }else{ backTop.classList.remove("show"); } } backTopButton(); //========================= // Evento Scroll //========================= window.addEventListener("scroll",()=>{ navbarScroll(); backTopButton(); activeSection(); }); //========================= // Smooth Scroll //========================= navLinks.forEach(link=>{ link.addEventListener("click",(e)=>{ e.preventDefault(); const target=document.querySelector(link.getAttribute("href")); if(target){ window.scrollTo({ top:target.offsetTop-70, behavior:"smooth" }); } //Cerrar menú móvil const navbarCollapse=document.querySelector(".navbar-collapse"); if(navbarCollapse.classList.contains("show")){ bootstrap.Collapse.getInstance(navbarCollapse).hide(); } }); }); //========================= // Scroll del Hero //========================= const heroArrow=document.querySelector(".hero-scroll a"); if(heroArrow){ heroArrow.addEventListener("click",(e)=>{ e.preventDefault(); const section=document.querySelector("#nosotros"); window.scrollTo({ top:section.offsetTop-70, behavior:"smooth" }); }); } //========================= // Back To Top //========================= backTop.addEventListener("click",()=>{ window.scrollTo({ top:0, behavior:"smooth" }); }); //========================= // Scroll Spy //========================= const sections=document.querySelectorAll("section[id]"); function activeSection(){ const scroll=window.scrollY+120; sections.forEach(section=>{ const top=section.offsetTop; const height=section.offsetHeight; const id=section.getAttribute("id"); if(scroll>=top && scroll{ link.classList.remove("active"); }); const active=document.querySelector(`.nav-link[href="#${id}"]`); if(active){ active.classList.add("active"); } } }); } activeSection(); }); /*==================================================== CALIBRADOR DIGITAL ====================================================*/ const caliperValue = document.getElementById("caliperValue"); const caliperFill = document.getElementById("caliperFill"); let calibrationStarted = false; function startCalibrator() { if (calibrationStarted) return; calibrationStarted = true; let progress = 0; const maxProgress = 100; const interval = setInterval(() => { progress++; caliperFill.style.width = progress + "%"; const value = (progress * 0.0005).toFixed(3); caliperValue.textContent = value + " mm"; if (progress >= maxProgress) { clearInterval(interval); caliperValue.textContent = "0.050 mm"; } }, 25); } /*==================================================== CONTADORES ====================================================*/ const counters = document.querySelectorAll(".stat-num"); let counterStarted = false; function animateCounters() { if (counterStarted) return; counterStarted = true; counters.forEach(counter => { const target = Number(counter.dataset.count); let current = 0; const increment = target / 100; const timer = setInterval(() => { current += increment; if (current >= target) { current = target; clearInterval(timer); } counter.textContent = Math.floor(current); }, 20); }); } /*==================================================== OBSERVER HERO ====================================================*/ const heroSection = document.querySelector("#inicio"); const heroObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { startCalibrator(); animateCounters(); } }); }, { threshold: 0.45 }); heroObserver.observe(heroSection); /*==================================================== EFECTO PARPADEO CALIBRADOR ====================================================*/ setInterval(() => { if (!caliperValue) return; caliperValue.style.opacity = ".6"; setTimeout(() => { caliperValue.style.opacity = "1"; }, 120); }, 3000); /*==================================================== EFECTO TARJETAS ESTADÍSTICAS ====================================================*/ const statCards = document.querySelectorAll(".stat-card"); statCards.forEach(card => { card.addEventListener("mouseenter", () => { card.style.transform = "translateY(-6px)"; card.style.transition = ".3s"; }); card.addEventListener("mouseleave", () => { card.style.transform = "translateY(0)"; }); }); /*==================================================== EFECTO HERO ====================================================*/ const hero = document.querySelector(".hero-section"); window.addEventListener("mousemove", (e) => { if (!hero) return; const x = e.clientX / window.innerWidth; const y = e.clientY / window.innerHeight; hero.style.backgroundPosition = `${50 + x * 4}% ${50 + y * 4}%`; }); /*==================================================== GALERÍA ====================================================*/ const filterButtons = document.querySelectorAll(".filter-btn"); const galleryItems = document.querySelectorAll(".gallery-item"); filterButtons.forEach(button => { button.addEventListener("click", () => { filterButtons.forEach(btn => btn.classList.remove("active")); button.classList.add("active"); const filter = button.dataset.filter; galleryItems.forEach(item => { if ( filter === "all" || item.dataset.cat === filter || item.dataset.cat === "all" ) { item.classList.remove("hidden"); } else { item.classList.add("hidden"); } }); }); }); /*==================================================== LIGHTBOX ====================================================*/ const lightboxModal = new bootstrap.Modal( document.getElementById("lightboxModal") ); const lightboxImg = document.getElementById("lightboxImg"); const lightboxTitle = document.getElementById("lightboxTitle"); function activateGalleryCards(){ document.querySelectorAll(".gallery-card").forEach(card=>{ card.onclick = ()=>{ const src = card.dataset.src; const title = card.dataset.title; lightboxImg.src = src; lightboxTitle.textContent = title; lightboxModal.show(); }; }); } activateGalleryCards(); /*==================================================== SUBIR IMÁGENES ====================================================*/ const uploadTrigger = document.getElementById("uploadTrigger"); const photoUpload = document.getElementById("photoUpload"); const galleryGrid = document.getElementById("galleryGrid"); uploadTrigger.addEventListener("click",()=>{ photoUpload.click(); }); photoUpload.addEventListener("change",(event)=>{ const files = event.target.files; if(!files.length) return; Array.from(files).forEach(file=>{ if(!file.type.startsWith("image/")) return; const reader = new FileReader(); reader.onload = function(e){ createGalleryCard( e.target.result, file.name, "all" ); saveImage(e.target.result,file.name); }; reader.readAsDataURL(file); }); }); /*==================================================== CREAR TARJETA ====================================================*/ function createGalleryCard(src,title,category){ const column = document.createElement("div"); column.className = "col-6 col-md-4 col-lg-3 gallery-item"; column.dataset.cat = category; column.innerHTML = ` `; const uploadCard = uploadTrigger.parentElement; galleryGrid.insertBefore(column,uploadCard); activateGalleryCards(); } /*==================================================== LOCAL STORAGE ====================================================*/ function saveImage(src,title){ let images = JSON.parse( localStorage.getItem("galleryImages") ) || []; images.push({ src:src, title:title }); localStorage.setItem( "galleryImages", JSON.stringify(images) ); } function loadImages(){ const images = JSON.parse( localStorage.getItem("galleryImages") ) || []; images.forEach(img=>{ createGalleryCard( img.src, img.title, "all" ); }); } loadImages(); /*==================================================== ANIMACIÓN HOVER ====================================================*/ document.addEventListener("mouseover",(e)=>{ if(e.target.closest(".gallery-card")){ e.target.closest(".gallery-card").style.transform = "scale(1.04)"; } }); document.addEventListener("mouseout",(e)=>{ if(e.target.closest(".gallery-card")){ e.target.closest(".gallery-card").style.transform = "scale(1)"; } }); /*==================================================== EFECTO FADE ====================================================*/ const galleryObserver = new IntersectionObserver(entries=>{ entries.forEach(entry=>{ if(entry.isIntersecting){ entry.target.style.opacity="1"; entry.target.style.transform="translateY(0)"; } }); }); document.querySelectorAll(".gallery-item").forEach(item=>{ item.style.opacity="0"; item.style.transform="translateY(40px)"; item.style.transition=".5s"; galleryObserver.observe(item); }); /*==================================================== FORMULARIO DE COTIZACIÓN ====================================================*/ const quoteForm = document.getElementById("quoteForm"); const submitBtn = document.getElementById("submitBtn"); const btnText = document.getElementById("btnText"); const btnSpinner = document.getElementById("btnSpinner"); const formMsg = document.getElementById("formMsg"); if (quoteForm) { quoteForm.addEventListener("submit", sendQuote); } /*==================================================== ENVIAR FORMULARIO ====================================================*/ function sendQuote(e){ e.preventDefault(); formMsg.innerHTML=""; if(!validateForm()){ return; } loadingButton(true); // Simulación de envío // Aquí posteriormente se reemplaza por fetch() setTimeout(()=>{ loadingButton(false); showSuccess(); quoteForm.reset(); clearValidation(); },2000); } /*==================================================== VALIDACIÓN ====================================================*/ function validateForm(){ let valid=true; const required=quoteForm.querySelectorAll("[required]"); required.forEach(input=>{ removeError(input); if(input.value.trim()==""){ showError(input,"Este campo es obligatorio"); valid=false; } }); // Validar Email const email=quoteForm.email; if(email.value!=""){ const regex=/^[^\s@]+@[^\s@]+\.[^\s@]+$/; if(!regex.test(email.value)){ showError(email,"Correo electrónico inválido"); valid=false; } } // Validar Teléfono const phone=quoteForm.telefono; if(phone.value!=""){ const clean=phone.value.replace(/\D/g,''); if(clean.length<10){ showError(phone,"Teléfono inválido"); valid=false; } } return valid; } /*==================================================== MOSTRAR ERROR ====================================================*/ function showError(input,message){ input.classList.add("is-invalid"); const error=document.createElement("div"); error.className="invalid-feedback"; error.textContent=message; input.parentNode.appendChild(error); } /*==================================================== ELIMINAR ERROR ====================================================*/ function removeError(input){ input.classList.remove("is-invalid"); input.classList.remove("is-valid"); const feedback=input.parentNode.querySelector(".invalid-feedback"); if(feedback){ feedback.remove(); } } /*==================================================== LIMPIAR VALIDACIÓN ====================================================*/ function clearValidation(){ const controls=quoteForm.querySelectorAll( ".form-control,.form-select" ); controls.forEach(control=>{ control.classList.remove("is-valid"); control.classList.remove("is-invalid"); }); } /*==================================================== VALIDACIÓN EN TIEMPO REAL ====================================================*/ const controls=quoteForm.querySelectorAll( ".form-control,.form-select" ); controls.forEach(control=>{ control.addEventListener("input",()=>{ removeError(control); if(control.value.trim()!=""){ control.classList.add("is-valid"); }else{ control.classList.remove("is-valid"); } }); }); /*==================================================== BOTÓN LOADING ====================================================*/ function loadingButton(status){ submitBtn.disabled=status; if(status){ btnText.classList.add("d-none"); btnSpinner.classList.remove("d-none"); }else{ btnSpinner.classList.add("d-none"); btnText.classList.remove("d-none"); } } /*==================================================== MENSAJE ÉXITO ====================================================*/ function showSuccess(){ formMsg.innerHTML=`
¡Solicitud enviada correctamente!
Nos pondremos en contacto contigo en menos de 24 horas.
`; } /*==================================================== MENSAJE ERROR ====================================================*/ function showServerError(){ formMsg.innerHTML=`
No fue posible enviar la solicitud.
Intenta nuevamente en unos minutos.
`; } /*==================================================== FORMATO TELÉFONO ====================================================*/ if (quoteForm) { const phoneInput = quoteForm.telefono; phoneInput.addEventListener("input",(e)=>{ let value=e.target.value.replace(/\D/g,''); value=value.substring(0,10); let formatted=""; if(value.length>0) formatted=value.substring(0,3); if(value.length>=4) formatted+=" "+value.substring(3,6); if(value.length>=7) formatted+=" "+value.substring(6,10); e.target.value=formatted; }); } /*==================================================== EFECTO ENFOQUE ====================================================*/ controls.forEach(control=>{ control.addEventListener("focus",()=>{ control.parentNode.style.transition=".25s"; control.parentNode.style.transform="translateY(-2px)"; }); control.addEventListener("blur",()=>{ control.parentNode.style.transform="translateY(0)"; }); }); /*==================================================== ENTER ====================================================*/ quoteForm.addEventListener("keypress",(e)=>{ if(e.key==="Enter" && e.target.tagName!="TEXTAREA"){ e.preventDefault(); } }); /*==================================================== PARTE 5 EFECTOS FINALES Y OPTIMIZACIÓN ====================================================*/ /*==================================================== REVEAL CARDS ====================================================*/ const revealElements = document.querySelectorAll( ".identity-card, .service-card, .value-pill, .process-step, .contact-info" ); const revealObserver = new IntersectionObserver((entries)=>{ entries.forEach(entry=>{ if(entry.isIntersecting){ entry.target.style.opacity="1"; entry.target.style.transform="translateY(0)"; revealObserver.unobserve(entry.target); } }); },{ threshold:.15 }); revealElements.forEach(el=>{ el.style.opacity="0"; el.style.transform="translateY(40px)"; el.style.transition="all .7s ease"; revealObserver.observe(el); }); /*==================================================== EFECTO PARALLAX HERO ====================================================*/ const heroOverlay=document.querySelector(".hero-overlay"); window.addEventListener("scroll",()=>{ if(!heroOverlay) return; const offset=window.pageYOffset; heroOverlay.style.transform=`translateY(${offset*0.25}px)`; }); /*==================================================== EFECTO BOTONES ====================================================*/ document.querySelectorAll(".btn").forEach(btn=>{ btn.addEventListener("mouseenter",()=>{ btn.style.transition=".25s"; btn.style.transform="translateY(-3px)"; }); btn.addEventListener("mouseleave",()=>{ btn.style.transform="translateY(0)"; }); }); /*==================================================== HOVER TARJETAS SERVICIOS ====================================================*/ document.querySelectorAll(".service-card").forEach(card=>{ card.addEventListener("mousemove",(e)=>{ const rect=card.getBoundingClientRect(); const x=e.clientX-rect.left; const y=e.clientY-rect.top; card.style.background=` radial-gradient(circle at ${x}px ${y}px, rgba(232,100,10,.10), transparent 60%), var(--steel-card) `; }); card.addEventListener("mouseleave",()=>{ card.style.background="var(--steel-card)"; }); }); /*==================================================== LAZY LOADING IMÁGENES ====================================================*/ const lazyImages=document.querySelectorAll("img"); const lazyObserver=new IntersectionObserver(entries=>{ entries.forEach(entry=>{ if(entry.isIntersecting){ const img=entry.target; if(img.dataset.src){ img.src=img.dataset.src; img.removeAttribute("data-src"); } lazyObserver.unobserve(img); } }); }); lazyImages.forEach(img=>{ lazyObserver.observe(img); }); /*==================================================== EFECTO ENLACES FOOTER ====================================================*/ document.querySelectorAll(".footer a").forEach(link=>{ link.addEventListener("mouseenter",()=>{ link.style.paddingLeft="6px"; link.style.transition=".25s"; }); link.addEventListener("mouseleave",()=>{ link.style.paddingLeft="0"; }); }); /*==================================================== TOOLTIPS BOOTSTRAP ====================================================*/ const tooltipTriggerList=[].slice.call( document.querySelectorAll('[title]') ); tooltipTriggerList.forEach(el=>{ new bootstrap.Tooltip(el); }); /*==================================================== PRELOAD HERO ====================================================*/ window.addEventListener("load",()=>{ document.body.classList.add("loaded"); }); /*==================================================== ESC PARA CERRAR MODAL ====================================================*/ document.addEventListener("keydown",(e)=>{ if(e.key==="Escape"){ const modal=document.getElementById("lightboxModal"); const instance=bootstrap.Modal.getInstance(modal); if(instance){ instance.hide(); } } }); /*==================================================== REDIMENSIONAR ====================================================*/ window.addEventListener("resize",()=>{ AOS.refresh(); }); /*==================================================== PERFORMANCE ====================================================*/ let resizeTimer; window.addEventListener("resize",()=>{ clearTimeout(resizeTimer); resizeTimer=setTimeout(()=>{ AOS.refresh(); },250); }); /*==================================================== LOG DESARROLLADOR ====================================================*/ console.log(` ========================================= SOINBA Soluciones Industriales de Celaya Main.js cargado correctamente Versión: 1.0 Desarrollado con JavaScript ES6 ========================================= `);