/* ---- raiz del diseno (ver preludio en subir_js.py) ---- */ ( function () { var raiz = document.getElementById( 'ah-refocus-root' ); if ( raiz ) { raiz.classList.add( 'ah-page-reparaciones', 'ah-reparaciones' ); } } )(); ; /* ---- assets/js/reviews.js ---- */ /** * Carrusel de reseñas: conecta las flechas al scroll horizontal de las tarjetas. * El markup vive en el contenido de la página, así que este script no asume * cuántas reseñas hay: mide el ancho real de una tarjeta en cada movimiento. */ ( function () { 'use strict'; function initCarousel( carousel ) { var track = carousel.querySelector( '.refocus-reviews-cards' ); var arrows = carousel.querySelectorAll( '.refocus-reviews-arrow' ); if ( ! track || arrows.length < 2 ) { return; } var prev = arrows[ 0 ]; var next = arrows[ 1 ]; function step() { var card = track.querySelector( '.refocus-review-card' ); if ( ! card ) { return track.clientWidth; } var styles = window.getComputedStyle( track ); var gap = parseFloat( styles.columnGap || styles.gap ) || 0; return card.getBoundingClientRect().width + gap; } function syncArrows() { var maxScroll = track.scrollWidth - track.clientWidth; prev.disabled = track.scrollLeft <= 1; next.disabled = track.scrollLeft >= maxScroll - 1; } prev.addEventListener( 'click', function () { track.scrollBy( { left: -step(), behavior: 'smooth' } ); } ); next.addEventListener( 'click', function () { track.scrollBy( { left: step(), behavior: 'smooth' } ); } ); track.addEventListener( 'scroll', syncArrows, { passive: true } ); window.addEventListener( 'resize', syncArrows ); syncArrows(); } function init() { var carousels = document.querySelectorAll( '.refocus-reviews-carousel' ); Array.prototype.forEach.call( carousels, initCarousel ); } if ( 'loading' === document.readyState ) { document.addEventListener( 'DOMContentLoaded', init ); } else { init(); } }() ); ; /* ---- assets/js/galerias.js ---- */ /** * Galerías de la página Nosotros. * * Ganchos (clases derivadas de los nombres de capa del diseño): * .ah-galeria-section -> galería del local * .ah-equipo-section -> carrusel de equipo * En ambos: .ah-slide-active (foto en foco), laterales, .ah-prev/.ah-next, * .ah-counter y, si existen, .ah-thumb-strip y .ah-progress. * * Tres cosas del markup exportado condicionan el código: * 1. La foto de cada miniatura NO está en la miniatura sino en su hijo * .ah-image; leerla del contenedor devuelve vacío. * 2. Los slides usan background-image de DOS capas (degradado + foto): al * cambiar hay que reemplazar solo la parte url() y conservar el * degradado, o el texto encima queda ilegible. * 3. Los laterales tienen nombres distintos en cada bloque. */ ( function () { 'use strict'; var RE_URL = /url\((["']?)(.*?)\1\)/; // Los dos valores del mockup para la miniatura resaltada. var ORO_BORDE = '2px solid #E8C593'; var ORO_TEXTO = '#F0D7AC'; function elImagen( el ) { return el ? ( el.querySelector( '.ah-image' ) || el ) : null; } function urlDe( el ) { var origen = elImagen( el ); if ( ! origen ) { return ''; } var bg = origen.style.backgroundImage || window.getComputedStyle( origen ).backgroundImage || ''; var m = RE_URL.exec( bg ); return m ? m[ 2 ] : ''; } function setUrl( el, url ) { if ( ! el || ! url ) { return; } var bg = el.style.backgroundImage || window.getComputedStyle( el ).backgroundImage || ''; if ( RE_URL.test( bg ) ) { el.style.backgroundImage = bg.replace( RE_URL, 'url("' + url + '")' ); } else { el.style.backgroundImage = ( bg && 'none' !== bg ? bg + ', ' : '' ) + 'url("' + url + '")'; } } function dosDigitos( n ) { return ( n < 10 ? '0' : '' ) + n; } function activarBoton( boton, etiqueta, alClic ) { if ( ! boton ) { return; } boton.style.cursor = 'pointer'; boton.setAttribute( 'role', 'button' ); boton.setAttribute( 'tabindex', '0' ); boton.setAttribute( 'aria-label', etiqueta ); boton.addEventListener( 'click', alClic ); boton.addEventListener( 'keydown', function ( e ) { if ( 'Enter' === e.key || ' ' === e.key ) { e.preventDefault(); alClic(); } } ); } /** * Arrastre con mouse/trackpad y deslizamiento horizontal de rueda. * Se usa Pointer Events para cubrir mouse y touch con un solo camino. */ function activarArrastre( zona, alAvanzar, alRetroceder ) { if ( ! zona ) { return; } var UMBRAL = 45; var inicioX = null; var arrastrando = false; zona.style.cursor = 'grab'; zona.style.touchAction = 'pan-y'; zona.addEventListener( 'pointerdown', function ( e ) { if ( e.button && 0 !== e.button ) { return; } inicioX = e.clientX; arrastrando = true; zona.style.cursor = 'grabbing'; } ); function terminar( e ) { if ( ! arrastrando || null === inicioX ) { return; } var delta = e.clientX - inicioX; arrastrando = false; inicioX = null; zona.style.cursor = 'grab'; if ( Math.abs( delta ) < UMBRAL ) { return; } // Arrastrar hacia la izquierda avanza, como en cualquier carrusel. if ( delta < 0 ) { alAvanzar(); } else { alRetroceder(); } } zona.addEventListener( 'pointerup', terminar ); zona.addEventListener( 'pointercancel', function () { arrastrando = false; inicioX = null; zona.style.cursor = 'grab'; } ); zona.addEventListener( 'pointerleave', function ( e ) { if ( arrastrando ) { terminar( e ); } } ); // Un clic no debe arrastrar la imagen de fondo del navegador. zona.addEventListener( 'dragstart', function ( e ) { e.preventDefault(); } ); // Deslizamiento horizontal de trackpad. Se acumula para no saltar // varias fotos con un solo gesto. var acumulado = 0; var ultimo = 0; zona.addEventListener( 'wheel', function ( e ) { if ( Math.abs( e.deltaX ) <= Math.abs( e.deltaY ) ) { return; // gesto vertical: es scroll de la página } e.preventDefault(); var ahora = Date.now(); if ( ahora - ultimo > 400 ) { acumulado = 0; } ultimo = ahora; acumulado += e.deltaX; if ( acumulado > 60 ) { acumulado = 0; alAvanzar(); } else if ( acumulado < -60 ) { acumulado = 0; alRetroceder(); } }, { passive: false } ); } function initCarrusel( seccionSel ) { var seccion = document.querySelector( seccionSel ); if ( ! seccion ) { return; } var stage = seccion.querySelector( '.ah-slide-active' ); if ( ! stage ) { return; } var peekPrev = seccion.querySelector( '.ah-peek-prev, .ah-slide-peek-03' ); var peekNext = seccion.querySelector( '.ah-peek-next, .ah-slide-peek-02' ); var thumbs = Array.prototype.slice.call( seccion.querySelectorAll( '.ah-thumb-strip > [class*="ah-thumb-"]' ) ); var fotos = thumbs.length ? thumbs.map( urlDe ).filter( Boolean ) : [ stage, peekPrev, peekNext ].map( urlDe ).filter( Boolean ); if ( fotos.length < 2 ) { return; } // Texto por foto: viene de los data-* que pone la plantilla. var textos = thumbs.map( function ( t ) { return { titulo: t.getAttribute( 'data-ah-title' ) || '', texto: t.getAttribute( 'data-ah-text' ) || '', }; } ); var elNum = seccion.querySelector( '.ah-num' ); var elTitulo = seccion.querySelector( '.ah-caption-text .ah-title' ); var elTexto = seccion.querySelector( '.ah-caption-text .ah-text' ); var elContador = seccion.querySelector( '.ah-counter' ); var segmentos = Array.prototype.slice.call( seccion.querySelectorAll( '.ah-progress > [class*="ah-seg-"]' ) ); var actual = 0; function pintarProgreso() { if ( ! segmentos.length ) { return; } // Un segmento por foto: los que sobran se ocultan. Antes se // repartían proporcionalmente (4 barras para 3 fotos) y el // resultado no significaba nada: pasaba de 1 a 3 barras marcadas. segmentos.forEach( function ( s, i ) { if ( i >= fotos.length ) { s.style.display = 'none'; return; } s.style.display = ''; s.style.opacity = i === actual ? '1' : '0.25'; } ); } function mostrar( i ) { actual = ( ( i % fotos.length ) + fotos.length ) % fotos.length; setUrl( stage, fotos[ actual ] ); setUrl( peekNext, fotos[ ( actual + 1 ) % fotos.length ] ); setUrl( peekPrev, fotos[ ( actual - 1 + fotos.length ) % fotos.length ] ); if ( elNum ) { elNum.textContent = dosDigitos( actual + 1 ); } if ( textos[ actual ] ) { if ( elTitulo && textos[ actual ].titulo ) { elTitulo.textContent = textos[ actual ].titulo; } if ( elTexto && textos[ actual ].texto ) { elTexto.textContent = textos[ actual ].texto; } } if ( elContador ) { var hoja = elContador.querySelector( '.ah-value' ) || elContador; hoja.textContent = dosDigitos( actual + 1 ) + ' / ' + dosDigitos( fotos.length ); } // Se fija el estado de TODAS, activa y no activas, con valores // explícitos. Poner '' en las no activas devolvía el estilo inline // del mockup, y la miniatura 2 —que el diseño trae resaltada— // quedaba marcada para siempre además de la real. // // El resaltado va en la FOTO y en el rótulo, no en el contenedor: // así lo hace el diseño (la miniatura 2 del mockup trae el borde // dorado sobre .ah-image). Poniéndolo en el contenedor salía un // rectángulo que envolvía foto + texto, y encima en azul. thumbs.forEach( function ( t, k ) { var activo = k === actual; var foto = t.querySelector( '.ah-image' ); var rotulo = t.querySelector( '.ah-label' ); if ( foto ) { foto.style.outline = activo ? ORO_BORDE : '1px solid #FFFFFF1F'; foto.style.outlineOffset = activo ? '-1px' : '-0.5px'; foto.style.opacity = activo ? '1' : '0.42'; } if ( rotulo ) { rotulo.style.color = activo ? ORO_TEXTO : '#FFFFFF6B'; } t.style.outline = ''; t.style.outlineOffset = ''; t.style.opacity = ''; t.setAttribute( 'aria-current', activo ? 'true' : 'false' ); } ); pintarProgreso(); } function avanzar() { mostrar( actual + 1 ); } function retroceder() { mostrar( actual - 1 ); } thumbs.forEach( function ( t, i ) { activarBoton( t, ( textos[ i ] && textos[ i ].titulo ) || 'Ver foto ' + ( i + 1 ), function () { mostrar( i ); } ); } ); activarBoton( seccion.querySelector( '.ah-prev' ), 'Foto anterior', retroceder ); activarBoton( seccion.querySelector( '.ah-next' ), 'Foto siguiente', avanzar ); activarArrastre( seccion.querySelector( '.ah-carousel-stage, .ah-equipo-carousel, .ah-track' ) || stage, avanzar, retroceder ); // Arranca siempre en la primera foto. mostrar( 0 ); } function init() { initCarrusel( '.ah-galeria-section' ); initCarrusel( '.ah-equipo-section' ); } if ( 'loading' === document.readyState ) { document.addEventListener( 'DOMContentLoaded', init ); } else { init(); } }() ); ; /* ---- assets/js/reparaciones.js ---- */ /** * Página Reparaciones. * * Dos cosas independientes: * * 1. El carrusel de equipos. Según la nota del diseño, el texto de la * izquierda es FIJO: lo único que rota es la foto de fondo, sola. Las * fotos son 2:1 y dejan libre la mitad izquierda a propósito, que es * donde se apoya el texto. * * 2. El selector que arma el mensaje de WhatsApp. El visitante elige equipo * (obligatorio), falla (opcional) y escribe la situación (opcional); con * eso se arma el texto y el enlace abre WhatsApp con el mensaje escrito. * No hay envío al servidor: es un que se recalcula. */ ( function () { 'use strict'; var raiz = document.querySelector( '.ah-page-reparaciones' ); if ( ! raiz ) { return; } /* ---------------------------------------------------------------- */ /* 1. Carrusel */ /* ---------------------------------------------------------------- */ function carrusel() { var slide = raiz.querySelector( '.ah-carrusel-slide' ); if ( ! slide ) { return; } // La ruta sale del propio background del slide, así no hay que repetir // acá la URL del tema: se cambia sólo el número de archivo. // // El separador y la extensión se toman de la URL en vez de darlos por // fijos. En el tema los archivos son `.../pages/3.webp`, pero servidos // desde la biblioteca de medios de WordPress quedan como // `.../ah-dev-carrusel-3.png` (el nombre lleva guion, y la extensión // real es png: los cinco archivos se llaman .webp pero por dentro son // PNG). Con el patrón viejo no había coincidencia y la función salía // antes de crear las capas, así que el carrusel no arrancaba. // Se lee el estilo CALCULADO, no el atributo style. La foto puede venir // de una regla CSS y no del marcado: en la versión de Elementor el fondo // lo pone el propio editor, y leyendo el atributo salía vacío, el patrón // no coincidía y el carrusel no llegaba a inicializarse. var match = getComputedStyle( slide ).backgroundImage.match( /^url\((["']?)(.*[\/-])(\d)\.(webp|png|jpe?g)\1\)$/ ); if ( ! match ) { return; } var comilla = match[ 1 ]; var base = match[ 2 ]; var extension = '.' + match[ 4 ]; var total = 5; var i = parseInt( match[ 3 ], 10 ); function fondo( n ) { return 'url(' + comilla + base + n + extension + comilla + ')'; } for ( var n = 1; n <= total; n++ ) { var img = new Image(); img.src = base + n + extension; } if ( window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) { return; } // La foto pasa a una capa propia: el fondo del slide se usaría igual, // pero background-image no se puede animar, y el cambio quedaba seco. // Con dos capas una entra desde abajo mientras la otra sale por arriba. var actual = document.createElement( 'div' ); actual.className = 'ah-slide-capa ah-slide-capa--visible'; actual.style.backgroundImage = fondo( i ); slide.insertBefore( actual, slide.firstChild ); slide.style.backgroundImage = 'none'; var animando = false; function pasarA( siguiente ) { if ( animando ) { return; } animando = true; var nueva = document.createElement( 'div' ); nueva.className = 'ah-slide-capa ah-slide-capa--entra'; nueva.style.backgroundImage = fondo( siguiente ); slide.insertBefore( nueva, slide.firstChild ); // Un reflow antes de sacarle la clase de entrada, si no el // navegador junta los dos estados y no hay transición. void nueva.offsetWidth; nueva.className = 'ah-slide-capa ah-slide-capa--visible'; actual.className = 'ah-slide-capa ah-slide-capa--sale'; var vieja = actual; actual = nueva; window.setTimeout( function () { if ( vieja.parentNode ) { vieja.parentNode.removeChild( vieja ); } animando = false; }, 700 ); } window.setInterval( function () { i = ( i % total ) + 1; pasarA( i ); }, 3500 ); } /* ---------------------------------------------------------------- */ /* 2. Selector de consulta */ /* ---------------------------------------------------------------- */ var TELEFONO = '5492254414873'; function seleccionado( grupo ) { var marcado = raiz.querySelector( 'input[name="' + grupo + '"]:checked' ); return marcado ? marcado.value : ''; } function selector() { var enlace = raiz.querySelector( '#ah-wa-link' ); var situacion = raiz.querySelector( '#ah-situacion' ); var burbuja = raiz.querySelector( '.ah-burbuja' ); if ( ! enlace || ! burbuja ) { return; } var lineas = burbuja.children; var lineaSaludo = lineas[ 0 ]; var lineaFalla = raiz.querySelector( '.ah-burbuja .ah-falla' ); var lineaSituacion = raiz.querySelector( '.ah-burbuja .ah-situacion' ); function actualizar() { var equipo = seleccionado( 'ah-equipo' ); var falla = seleccionado( 'ah-falla' ); var texto = situacion ? situacion.value.trim() : ''; var partes = [ 'Hola, quiero reparar mi ' + equipo + '.' ]; if ( falla ) { partes.push( 'Falla: ' + falla ); } if ( texto ) { partes.push( 'Situación: ' + texto ); } // Vista previa: las líneas sin dato se ocultan, no se vacían, para // que la burbuja no quede con huecos. if ( lineaSaludo ) { lineaSaludo.textContent = partes[ 0 ]; } if ( lineaFalla ) { lineaFalla.textContent = falla ? 'Falla: ' + falla : ''; lineaFalla.hidden = ! falla; } if ( lineaSituacion ) { lineaSituacion.textContent = texto ? 'Situación: ' + texto : ''; lineaSituacion.hidden = ! texto; } // encodeURIComponent deja los saltos como %0A; no hay que armar // el texto con + a mano. var url = 'https://wa.me/' + TELEFONO + '?text=' + encodeURIComponent( partes.join( '\n' ) ); if ( equipo ) { enlace.setAttribute( 'href', url ); enlace.removeAttribute( 'aria-disabled' ); enlace.removeAttribute( 'tabindex' ); raiz.classList.remove( 'ah-consulta-incompleta' ); } else { // Sin equipo el mensaje no aporta nada sobre el botón de // WhatsApp que ya tiene la página. enlace.setAttribute( 'href', '#' ); enlace.setAttribute( 'aria-disabled', 'true' ); enlace.setAttribute( 'tabindex', '-1' ); raiz.classList.add( 'ah-consulta-incompleta' ); } } raiz.addEventListener( 'change', function ( e ) { if ( e.target.name === 'ah-equipo' || e.target.name === 'ah-falla' ) { actualizar(); } } ); if ( situacion ) { situacion.addEventListener( 'input', actualizar ); } enlace.addEventListener( 'click', function ( e ) { if ( enlace.getAttribute( 'aria-disabled' ) === 'true' ) { e.preventDefault(); } } ); actualizar(); } carrusel(); selector(); } )();