Help
RSS
API
Feed
Maltego
Contact
Domain > afabperu.org.pe
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2024-10-15
195.35.60.180
(
ClassC
)
2025-12-01
82.180.153.106
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyConnection: Keep-AliveKeep-Alive: timeout5, max100Content-Type: text/htmlContent-Length: 795Date: Mon, 01 Dec 2025 15:17:32 GMTServer: LiteSpeedLocation: https://afabperu.org.pe/platform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requests !DOCTYPE html>html styleheight:100%>head>meta nameviewport contentwidthdevice-width, initial-scale1, shrink-to-fitno />title> 301 Moved Permanently/title>style>@media (prefers-color-scheme:dark){body{background-color:#000!important}}/style>/head>body stylecolor: #444; margin:0;font: normal 14px/20px Arial, Helvetica, sans-serif; height:100%; background-color: #fff;>div styleheight:auto; min-height:100%; > div styletext-align: center; width:800px; margin-left: -400px; position:absolute; top: 30%; left:50%;> h1 stylemargin:0; font-size:150px; line-height:150px; font-weight:bold;>301/h1>h2 stylemargin-top:20px;font-size: 30px;>Moved Permanently/h2>p>The document has been permanently moved./p>/div>/div>/body>/html>
Port 443
HTTP/1.1 200 OKConnection: Keep-AliveKeep-Alive: timeout5, max100X-Powered-By: PHP/8.2.29Content-Type: text/html; charsetUTF-8Transfer-Encoding: chunkedDate: Mon, 01 Dec 2025 15:17:33 GMTServer: LiteSpeedplatform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requests !DOCTYPE html>html langes>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>AFABPERU/title> link hrefhttps://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css relstylesheet> link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css> link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css> link hrefhttps://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css relstylesheet> link relicon href../assets/images/uploads/1754341217_FB_IMG_1754341069279-modified.png> link relstylesheet href../assets/css/style.css> link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css/> script srchttps://unpkg.com/scrollreveal>/script> style> /* CONFIGURACIÓN DINÁMICA DEL HEADER */ :root { --header-text-color: #ffffff; --header-hover-color: #cccccc; --page-bg-opacity: 73%; } /* Header principal */ .navbar { background: linear-gradient(135deg, #570e00, #ff001e) !important; /* Transición para ocultar/mostrar y cambiar estilos de scroll */ transition: transform 0.3s ease-in-out, background-color 0.3s ease, box-shadow 0.3s ease; backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } /* Clase para ocultar el navbar */ .navbar-hidden { transform: translateY(-100%); /* Mueve el navbar hacia arriba para ocultarlo */ } /* Header en scroll */ .navbar.navbar-scrolled { background: linear-gradient(135deg, #f21000, #ff7300) !important; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1); backdrop-filter: blur(15px); } /* Textos del navbar */ .navbar-brand, .navbar-nav .nav-link, .navbar-toggler { color: var(--header-text-color) !important; font-weight: 500; transition: all 0.3s ease; } .navbar-brand { font-size: 1.5rem; font-weight: 700; display: flex; align-items: center; gap: 0.75rem; } .navbar-brand img { transition: transform 0.3s ease; } .navbar-brand:hover img { transform: scale(1.05); } .navbar-nav .nav-link { position: relative; padding: 0.75rem 1rem !important; border-radius: 8px; margin: 0 0.25rem; font-weight: 500; } .navbar-nav .nav-link:hover, .navbar-nav .nav-link.active { color: var(--header-hover-color) !important; background-color: rgba(255, 255, 255, 0.1); transform: translateY(-2px); } .navbar-nav .nav-link::before { content: ; position: absolute; bottom: 0; left: 50%; width: 0; height: 2px; background-color: var(--header-hover-color); transition: all 0.3s ease; transform: translateX(-50%); } .navbar-nav .nav-link:hover::before, .navbar-nav .nav-link.active::before { width: 80%; } /* Responsive navbar toggler */ .navbar-toggler { border: none; padding: 0.5rem; border-radius: 8px; } .navbar-toggler:focus { box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.25); } .navbar-toggler-icon { background-image: url(data:image/svg+xml,%3csvg xmlnshttp://www.w3.org/2000/svg viewBox0 0 30 30%3e%3cpath stroke%23ffffff stroke-linecapround stroke-miterlimit10 stroke-width2 dm4 7h22M4 15h22M4 23h22/%3e%3c/svg%3e); } /* CONFIGURACIÓN DINÁMICA DEL FONDO DE PÁGINA */ body { margin: 0; padding: 0; min-height: 100vh; background-image: url(../assets/images/uploads/1749448154_IMG-20250128-WA0014.jpg); background-size: cover; background-position: center; background-repeat: no-repeat; background-attachment: fixed; position: relative; } /* Overlay de opacidad para imagen de fondo */ body::before { content: ; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.27); pointer-events: none; z-index: -1; } /* Contenido principal */ .main-content-public { position: relative; z-index: 1; padding-top: 90px; /* Espacio para navbar fijo */ } /* Mejoras responsivas */ @media (max-width: 991.98px) { .navbar-collapse { background-color: rgba(0, 0, 0, 0.9); border-radius: 12px; margin-top: 1rem; padding: 1rem; backdrop-filter: blur(15px); max-height: 0; overflow: hidden; /* Oculta el contenido que exceda el alto */ transition: max-height 0.4s ease-in-out, opacity 0.4s ease-in-out; /* Transición suave para alto y opacidad */ opacity: 0; /* Inicialmente transparente */ } .navbar-collapse.show { max-height: 500px; /* Un valor lo suficientemente grande para que quepa todo el contenido */ opacity: 1; /* Completamente visible */ } .navbar-nav .nav-link { text-align: center; margin: 0.25rem 0; } .main-content-public { padding-top: 90px; } } @media (max-width: 576px) { .navbar-brand { font-size: 1.25rem; } .navbar-brand img { height: 40px !important; } .main-content-public { padding-top: 70px; } } /* Eliminar o ser muy específico con esta regla si causa problemas */ /* * { transition: color 0.3s ease, background-color 0.3s ease, transform 0.3s ease; } */ /* Mejoras de accesibilidad */ .navbar-nav .nav-link:focus, .navbar-brand:focus, .navbar-toggler:focus { outline: 2px solid var(--header-hover-color); outline-offset: 2px; } /* Loader para imágenes */ .navbar-brand img { max-height: 60px; width: auto; object-fit: contain; } /* Scrollspy activo */ .navbar-nav .nav-link.active { background-color: rgba(255, 255, 255, 0.15) !important; } /* Fuerza la barra de desplazamiento para evitar saltos de diseño */ html { overflow-y: scroll; } @media (min-width: 769px) { .navbar-container-custom { padding-left: 1rem; padding-right: 1rem; } } @media (max-width: 768px) { .navbar-container-custom { padding-left: 5px; padding-right: 5px; } } /style>/head>body> nav classnavbar navbar-expand-lg fixed-top rolenavigation aria-labelNavegación principal> div classcontainer-fluid navbar-container-custom> a classnavbar-brand href/ aria-labelIr al inicio> img src../assets/images/uploads/1754341202_FB_IMG_1754341069279-modified.png altAFABPERU height60 loadinglazy> span>AFABPERU/span> /a> button classnavbar-toggler typebutton aria-controlsnavbarNav aria-expandedfalse aria-labelAlternar navegación> span classnavbar-toggler-icon>/span> /button> div classcollapse navbar-collapse idnavbarNav> ul classnavbar-nav ms-auto> li classnav-item> a classnav-link href/ aria-labelIr a inicio> i classbi bi-house-door me-1>/i> Inicio /a> /li> li classnav-item> a classnav-link href/eventos.php aria-labelVer eventos> i classbi bi-calendar-event me-1>/i> Eventos /a> /li> li classnav-item> a classnav-link href/Nosotros/ aria-labelConocer más sobre nosotros> i classbi bi-people me-1>/i> Nosotros /a> /li> li classnav-item> a classnav-link href#Contacto aria-labelInformación de contacto> i classbi bi-telephone me-1>/i> Contacto /a> /li> /ul> /div> /div> /nav> !-- div styleheight: 2000px; background-color: #f0f0f0; padding: 20px;> Contenido de ejemplo para hacer scroll. Desplázate hacia abajo para ver cómo el navbar se oculta y hacia arriba para que reaparezca solo al llegar al tope. /div> --> script srchttps://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js>/script> script srchttps://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js>/script> script srchttps://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js>/script> script> document.addEventListener(DOMContentLoaded, function() { const navbar document.querySelector(.navbar); const navbarToggler document.querySelector(.navbar-toggler); const navbarCollapse document.getElementById(navbarNav); const navLinks document.querySelectorAll(.navbar-nav .nav-link); // --- Lógica de Scroll para Navbar (COMBINADA) --- let lastScrollY window.scrollY; // Guarda la última posición de scroll if (navbar) { window.addEventListener(scroll, () > { const currentScrollY window.scrollY; // Lógica para cambiar el color del navbar al hacer scroll // (Esta parte ya la tenías y la mantenemos si la necesitas para otro efecto) if (currentScrollY > 50) { navbar.classList.add(navbar-scrolled); } else { navbar.classList.remove(navbar-scrolled); } // Lógica para OCULTAR/MOSTRAR el navbar dinámicamente // Ocultar si el scroll va hacia abajo y ya no estamos en el tope if (currentScrollY > lastScrollY && currentScrollY > 0) { navbar.classList.add(navbar-hidden); } // Mostrar solo si el scroll llega AL TOPE (0 píxeles) else if (currentScrollY 90) { navbar.classList.remove(navbar-hidden); } lastScrollY currentScrollY; // Actualiza la última posición de scroll }); } else { console.warn(⚠️ No se encontró el elemento .navbar. Verifica tu HTML.); } // --- Lógica de Enlaces Activos --- function updateActiveLink() { const currentPath window.location.pathname; navLinks.forEach(link > { link.classList.remove(active); // Comprueba si la ruta del enlace coincide con la ruta actual o si es la raíz if (link.getAttribute(href) currentPath || (currentPath / && link.getAttribute(href) /)) { link.classList.add(active); } }); } updateActiveLink(); // Llama la función al cargar // --- Lógica PERSONALIZADA del Toggle del Navbar --- // Importante: Si estás usando los atributos data-bs-toggle y data-bs-target en tu botón // button classnavbar-toggler typebutton data-bs-togglecollapse data-bs-target#navbarNav ...> // Bootstrap ya maneja esto. Si quieres tu animación personalizada con max-height, // DEBES QUITAR `data-bs-togglecollapse` y `data-bs-target#navbarNav` de tu botón. // Si los dejas, Bootstrap se encargará del toggle y tu JS puede entrar en conflicto. if (navbarToggler && navbarCollapse) { navbarToggler.addEventListener(click, function() { navbarCollapse.classList.toggle(show); const isExpanded navbarCollapse.classList.contains(show); navbarCollapse.setAttribute(aria-expanded, isExpanded); navbarToggler.setAttribute(aria-expanded, isExpanded); }); console.log(✅ Listener personalizado para navbar toggle cargado (animación CSS).); document.addEventListener(click, function(event) { if (navbarCollapse.classList.contains(show) && !navbarCollapse.contains(event.target) && !navbarToggler.contains(event.target)) { navbarCollapse.classList.remove(show); navbarCollapse.setAttribute(aria-expanded, false); navbarToggler.setAttribute(aria-expanded, false); } }); console.log(✔ Listener para cerrar navbar al clic fuera cargado.); navLinks.forEach(link > { link.addEventListener(click, function() { if (navbarCollapse.classList.contains(show)) { navbarCollapse.classList.remove(show); navbarCollapse.setAttribute(aria-expanded, false); navbarToggler.setAttribute(aria-expanded, false); } }); }); console.log(✔ Listener para cerrar navbar al hacer clic en enlace cargado.); } else { console.warn(⚠️ No se encontraron elementos navbarToggler o navbarCollapse. Verifica tus selectores IDs/clases.); } // --- Smooth scroll para enlaces internos --- document.querySelectorAll(ahref^#).forEach(anchor > { anchor.addEventListener(click, function (e) { e.preventDefault(); const target document.querySelector(this.getAttribute(href)); if (target) { target.scrollIntoView({ behavior: smooth, block: start }); // Cierra el navbar si estaba abierto al hacer smooth scroll if (navbarCollapse && navbarCollapse.classList.contains(show)) { navbarCollapse.classList.remove(show); navbarCollapse.setAttribute(aria-expanded, false); navbarToggler.setAttribute(aria-expanded, false); } } }); }); console.log(🎨 Header dinámico (JS) cargado correctamente); }); /script> !-- Contenido principal wrapper --> div classmain-content-public> !-- Aquí va el contenido de cada página -->main> section classcarrusel> div idcarouselEventos classcarousel slide data-bs-ridecarousel> div classcarousel-indicators> button typebutton data-bs-target#carouselEventos data-bs-slide-to0 classactive aria-currenttrue aria-labelSlide 1>/button> button typebutton data-bs-target#carouselEventos data-bs-slide-to1 class aria-currentfalse aria-labelSlide 2>/button> button typebutton data-bs-target#carouselEventos data-bs-slide-to2 class aria-currentfalse aria-labelSlide 3>/button> button typebutton data-bs-target#carouselEventos data-bs-slide-to3 class aria-currentfalse aria-labelSlide 4>/button> button typebutton data-bs-target#carouselEventos data-bs-slide-to4 class aria-currentfalse aria-labelSlide 5>/button> /div> div classcarousel-inner> div classcarousel-item active> a hrefevent.php?id16 classcarousel-link> div classcarousel-media-container> video classcarousel-media-item media-video-local autoplay loop muted playsinline>source src../assets/media/eventos/landscape_videos/6914f58ff0ee4.mp4 typevideo/mp4>Tu navegador no soporta la etiqueta de video./video> /div> div classcarousel-event-title-overlay> h3 classanimacion-cargar-texto>4to Desafío Selva Alegre/h3> /div> div classcarousel-caption carousel-concluido-badge animacion-cargar-badge-evento> h5 classanimacion-cargar-texto>EVENTO CONCLUIDO/h5> /div> /a> /div> div classcarousel-item > a hrefevent.php?id15 classcarousel-link> div classcarousel-media-container> img srcassets/images/eventos/landscape/evento_1754341420_dcb4b7b3.jpg classcarousel-media-item media-image alt3er Desafío Socabaya > /div> div classcarousel-event-title-overlay> h3 classanimacion-cargar-texto>3er Desafío Socabaya /h3> /div> div classcarousel-caption carousel-concluido-badge animacion-cargar-badge-evento> h5 classanimacion-cargar-texto>EVENTO CONCLUIDO/h5> /div> /a> /div> div classcarousel-item > a hrefevent.php?id7 classcarousel-link> div classcarousel-media-container> img srcassets/images/eventos/landscape/evento_1748972156_1cb8adce.jpg classcarousel-media-item media-image alt2do DESAFIO QUEQUEÑA> /div> div classcarousel-event-title-overlay> h3 classanimacion-cargar-texto>2do DESAFIO QUEQUEÑA/h3> /div> div classcarousel-caption carousel-concluido-badge animacion-cargar-badge-evento> h5 classanimacion-cargar-texto>EVENTO CONCLUIDO/h5> /div> /a> /div> div classcarousel-item > a hrefevent.php?id9 classcarousel-link> div classcarousel-media-container> img srcassets/images/eventos/landscape/evento_1751400392_ff8da44a.jpg classcarousel-media-item media-image alt🪼 GRAN CARRERA PEDESTRE de SOLIDARIDAD> /div> div classcarousel-event-title-overlay> h3 classanimacion-cargar-texto>🪼 GRAN CARRERA PEDESTRE de SOLIDARIDAD/h3> /div> div classcarousel-caption carousel-concluido-badge animacion-cargar-badge-evento> h5 classanimacion-cargar-texto>EVENTO CONCLUIDO/h5> /div> /a> /div> div classcarousel-item > a hrefevent.php?id8 classcarousel-link> div classcarousel-media-container> img srcassets/images/eventos/landscape/evento_1749783676_dfac03ad.jpg classcarousel-media-item media-image altGRANDE PAPÁ> /div> div classcarousel-event-title-overlay> h3 classanimacion-cargar-texto>GRANDE PAPÁ/h3> /div> div classcarousel-caption carousel-concluido-badge animacion-cargar-badge-evento> h5 classanimacion-cargar-texto>EVENTO CONCLUIDO/h5> /div> /a> /div> /div> button classcarousel-control-prev typebutton data-bs-target#carouselEventos data-bs-slideprev> span classcarousel-control-prev-icon aria-hiddentrue>/span> span classvisually-hidden>Anterior/span> /button> button classcarousel-control-next typebutton data-bs-target#carouselEventos data-bs-slidenext> span classcarousel-control-next-icon aria-hiddentrue>/span> span classvisually-hidden>Siguiente/span> /button> /div> /section> div idav-isolated-component> section classav-avisos-importantes> div classcontainer> div idavCarouselAvisos classav-carousel> div classav-carousel-inner> div classav-carousel-item active> div classav-aviso-item mx-2 styleborder-left: 5px solid #005c0f; background-color: #f4f8e3;> div classav-aviso-content> div classav-aviso-header d-flex align-items-center> div classav-aviso-icon me-3> i classfas fa-bell fa-2x stylecolor: #005c0f;>/i> /div> div classav-aviso-title summernote-content> p>ENTRENAMIENTO TRAIL AL AIRE LIBRE/p> /div> /div> div classav-aviso-body> div classav-aviso-short summernote-content> !-- Fecha principal --> div stylebackground: #fff; padding: 15px; text-align: center; border-radius: 8px; margin: 15px 0; font-weight: bold; font-size: 1.2em; color: #FF6B6B;> 🔷 08 de OCTUBRE 2025 /div> /div> button typebutton classbtn btn-link p-0 av-aviso-more-link stylecolor: #005c0f; font-weight: bold; data-title<p>ENTRENAMIENTO TRAIL AL AIRE LIBRE</p> data-full-content<div style"font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 600px; margin: auto; padding: 20px; background: #f9f9f9; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); color: #333; line-height: 1.6;"> <!-- Encabezado --> <div style"text-align: center; background: linear-gradient(135deg, #FF6B6B, #FFA07A); padding: 15px; border-radius: 10px 10px 0 0; color: white; font-weight: bold; font-size: 1.4em;"> 🌄 ENTRENAMIENTO TRAIL AL AIRE LIBRE 🗻 </div> <!-- Organizador --> <div style"text-align: center; margin: 15px 0; font-size: 0.9em; color: #666;"> <strong>Organiza:</strong> <span style"color: #007BFF;">AFABPERÚ</span> </div> <!-- Frase motivacional --> <div style"text-align: center; font-weight: bold; font-size: 1.1em; margin: 10px 0; color: #2E8B57;"> 💥 Un gran entrenamiento para un gran día. </div> <!-- Fechas importantes --> <div style"background: #fff; padding: 15px; border-left: 4px solid #FF6B6B; margin: 15px 0; border-radius: 6px;"> <strong>📅 FECHA CÉLEBRE:</strong><br> • Día de la Dignidad Nacional del Perú<br> • Día de la Educación Física a nivel nacional </div> <!-- Fecha principal --> <div style"background: #fff; padding: 15px; text-align: center; border-radius: 8px; margin: 15px 0; font-weight: bold; font-size: 1.2em; color: #FF6B6B;"> 🔷 08 de OCTUBRE 2025 </div> <!-- Gratuito --> <div style"text-align: center; font-weight: bold; color: #FF8C00; font-size: 1.1em; margin: 10px 0;"> 🔥 TOTALMENTE GRATUITO </div> <!-- Ruta --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #4CAF50;"> <strong>🌎 RUTA:</strong> Pampa de Bateones<br> <em style"font-size: 0.9em; color: #666;">Ruta de Reto Volcánico – 4.º Desafío en Alto Selva Alegre, Sector de San Luis</em><br> <small style"color: #888;">(Próximo evento: 30 de noviembre 2025)</small> </div> <!-- Distancias --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #2196F3;"> <strong>💫 DISTANCIAS DISPONIBLES:</strong><br> • 5 km • 10 km • 21 km </div> <!-- Lugar y hora --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #9C27B0;"> <strong>📍 LUGAR DE ENCUENTRO:</strong><br> Complejo Deportivo “Bella Esperanza”<br> <em>Referencia:</em> Por el Cementerio de San Luis, Terminal de los buses “D” <br><br> <strong>⏰ HORARIO:</strong><br> • Encuentro: 7:00 a.m.<br> • Partida: 7:30 a.m. </div> <!-- Recomendaciones --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #FF9800;"> <strong>🧏🏻 RECOMENDACIONES:</strong><br> • Polo de color fuerte o llamativo<br> • Silbato<br> • Visera o gorro<br> • Bloqueador solar<br> • Agua suficiente </div> <!-- Servicios --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; text-align: center; border-left: 4px solid #673AB7;"> 💎 <strong>Servicios incluidos:</strong> Guardarropa y fruta de recuperación. </div> <!-- Cierre motivador --> <div style"text-align: center; font-weight: bold; font-size: 1.1em; margin: 20px 0; color: #007BFF;"> 🙋🏻 ¡Nos vemos el <strong>miércoles 08 de octubre</strong> en San Luis – Alto Selva Alegre! </div> <!-- Transporte --> <div style"background: #fff; padding: 15px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #E91E63;"> <strong>🚨 ATENCIÓN - TRANSPORTE:</strong><br> Los omnibuses de la línea “D” (color anaranjado) pasan por:<br> • Lambramani<br> • Av. Venezuela (por la Feria)<br> • Av. Progreso<br> <strong>Bajarse en el Terminal de los buses “D”, cerca del Cementerio de San Luis.</strong> </div> <!-- Pie de página --> <div style"text-align: center; margin-top: 20px; font-size: 0.8em; color: #999; padding-top: 10px; border-top: 1px solid #ddd;"> 📍 AFABPERÚ — Promoviendo el deporte y la salud en la comunidad </div></div> data-icon-classfa-bell data-icon-color#005c0f data-modal-background#f4f8e3 data-modal-border#005c0f> Leer comunicado completo /button> /div> /div> /div> /div> /div> button classav-carousel-control-prev typebutton> span classav-carousel-control-prev-icon aria-hiddentrue>/span> span classvisually-hidden>Anterior/span> /button> button classav-carousel-control-next typebutton> span classav-carousel-control-next-icon aria-hiddentrue>/span> span classvisually-hidden>Siguiente/span> /button> /div> /div> /section> style> /* --- ESTILOS TOTALMENTE AISLADOS DENTRO DEL COMPONENTE --- */ #av-isolated-component { all: initial; /* Reseteo inicial para aislar de herencia */ font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, Liberation Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; } #av-isolated-component *, #av-isolated-component *::before, #av-isolated-component *::after { box-sizing: border-box; } /* Estilos base del carrusel (recreados para ser independientes) */ #av-isolated-component .av-carousel { position: relative; padding: 0 40px; border-radius: 10px; box-shadow: 0 2px 20px rgba(0,0,0,0.55); } #av-isolated-component .av-carousel-inner { position: relative; width: 100%; overflow: hidden; padding: 20px 0; } #av-isolated-component .av-carousel-item { position: relative; display: none; float: left; width: 100%; margin-right: -100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; transition: transform .6s ease-in-out; padding: 0 10px; } @media (prefers-reduced-motion: reduce) { #av-isolated-component .av-carousel-item { transition: none; } } #av-isolated-component .av-carousel-item.active, #av-isolated-component .av-carousel-item-next, #av-isolated-component .av-carousel-item-prev { display: block; } #av-isolated-component .av-carousel-item-next:not(.av-carousel-item-start), #av-isolated-component .active.av-carousel-item-end { transform: translateX(100%); } #av-isolated-component .av-carousel-item-prev:not(.av-carousel-item-end), #av-isolated-component .active.av-carousel-item-start { transform: translateX(-100%); } /* Controles del carrusel */ #av-isolated-component .av-carousel-control-prev, #av-isolated-component .av-carousel-control-next { position: absolute; top: 0; bottom: 0; z-index: 1; display: flex; align-items: center; justify-content: center; width: 5%; padding: 0; color: #fff; text-align: center; background: 0 0; border: 0; opacity: .5; transition: opacity .15s ease; } #av-isolated-component .av-carousel-control-prev { left: -10px; } #av-isolated-component .av-carousel-control-next { right: -10px; } #av-isolated-component .av-carousel-control-prev:hover, #av-isolated-component .av-carousel-control-next:hover { opacity: .9; } #av-isolated-component .av-carousel-control-prev-icon, #av-isolated-component .av-carousel-control-next-icon { display: inline-block; width: 2rem; height: 2rem; background-repeat: no-repeat; background-position: 50%; background-size: 100% 100%; background-image: none; font-size: 2rem; color: #333; } #av-isolated-component .av-carousel-control-prev-icon::before { content: \f053; font-family: Font Awesome 5 Free; font-weight: 900; } #av-isolated-component .av-carousel-control-next-icon::before { content: \f054; font-family: Font Awesome 5 Free; font-weight: 900; } #av-isolated-component .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } /* Estilos específicos de tu componente (avisos) */ #av-isolated-component .av-avisos-importantes { background-color: #3355135c; } #av-isolated-component .av-aviso-item { border-radius: 21px; box-shadow: 0 2px 40px rgba(0,0,0,0.75); overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; height: 100%; display: flex; flex-direction: column; justify-content: space-between; } #av-isolated-component .av-aviso-item:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } #av-isolated-component .av-aviso-content { padding: 1.5rem; flex-grow: 1; } #av-isolated-component .av-aviso-header { margin-bottom: 1rem; } #av-isolated-component .av-aviso-title { font-size: 1.25rem; font-weight: 600; flex: 1; } #av-isolated-component .av-aviso-icon { flex-shrink: 0; } #av-isolated-component .av-aviso-short { line-height: 1.6; margin-bottom: 1rem; } #av-isolated-component .av-aviso-more-link { text-decoration: none; transition: opacity 0.2s; cursor: pointer; background: none; border: none; padding: 0; } #av-isolated-component .av-aviso-more-link:hover { opacity: 0.8; text-decoration: underline; } /* Clases de utilidad de Bootstrap recreadas y aisladas */ #av-isolated-component .container { width: 100%; padding-right: .75rem; padding-left: .75rem; margin-right: auto; margin-left: auto; } #av-isolated-component .d-flex { display: flex !important; } #av-isolated-component .align-items-center { align-items: center !important; } #av-isolated-component .me-3 { margin-right: 1rem !important; } #av-isolated-component .mx-2 { margin-right: .5rem !important; margin-left: .5rem !important; } #av-isolated-component .p-0 { padding: 0 !important; } /* Estilos SweetAlert2 (estos son globales, pero con clases muy específicas para evitar conflictos) */ .av-swal2-custom-popup.swal2-popup { border-radius: 15px !important; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2) !important; font-family: Inter, sans-serif; width: 90% !important; max-width: 900px !important; } .av-swal2-custom-popup .swal2-title { color: #333 !important; font-size: 1.8rem !important; font-weight: 700 !important; } .av-swal2-custom-popup .swal2-html-container { font-size: 1.1rem !important; line-height: 1.6 !important; color: #555 !important; text-align: left !important; } .av-swal2-custom-popup .av-swal-title-icon { font-size: 1.5rem; margin-right: 0.8rem; } /style> script srchttps://cdn.jsdelivr.net/npm/sweetalert2@11>/script> script> // --- SCRIPT AISLADO Y SIN JQUERY --- document.addEventListener(DOMContentLoaded, function() { const container document.getElementById(av-isolated-component); if (!container) return; // --- INICIALIZACIÓN MANUAL DEL CARRUSEL --- const carouselElement container.querySelector(#avCarouselAvisos); if (carouselElement && typeof bootstrap ! undefined) { const carouselInstance new bootstrap.Carousel(carouselElement, { interval: 5000, wrap: true, ride: false // Importante: la inicialización es manual }); // Controladores manuales para los botones const prevButton container.querySelector(.av-carousel-control-prev); const nextButton container.querySelector(.av-carousel-control-next); if(prevButton) { prevButton.addEventListener(click, function() { carouselInstance.prev(); }); } if(nextButton) { nextButton.addEventListener(click, function() { carouselInstance.next(); }); } } // --- MANEJADOR DE EVENTOS PARA SWEETALERT2 --- container.querySelectorAll(.av-aviso-more-link).forEach(link > { link.addEventListener(click, function() { const title this.dataset.title; const fullContent this.dataset.fullContent; const iconClass this.dataset.iconClass; const iconColor this.dataset.iconColor; const modalBackground this.dataset.modalBackground; const modalBorder this.dataset.modalBorder; const swalTitleHtml `div styledisplay: flex; align-items: center; justify-content: center;>i classfas ${iconClass} av-swal-title-icon stylecolor: ${iconColor};>/i>span>${title}/span>/div>`; Swal.fire({ title: swalTitleHtml, html: fullContent, showCloseButton: true, confirmButtonText: Cerrar, customClass: { popup: av-swal2-custom-popup, }, background: modalBackground, didOpen: (popup) > { popup.style.border `1px solid ${modalBorder}`; } }); }); }); }); /script> /div> section classcountdown-section animacion-cargar> div classcountdown-media-background> img srcassets/images/eventos/principales/evento_1759787638_acd5fb32.jpg classcountdown-media-item media-image alt4to Desafío Selva Alegre> /div> div classcountdown-overlay> div classcontainer text-center py-5> h2 classtext-white animacion-cargar-titulo> Último Evento: 4to Desafío Selva Alegre /h2> div classdisplay-4 my-4 text-white animacion-cargar-mensaje> Este evento ya ha concluido /div> a hrefevent.php?id16 classbtn btn-outline-light btn-lg animacion-cargar-boton animacion-cargar-texto-boton>Ver Resumen/a> /div> /div> /section> script>document.addEventListener(DOMContentLoaded, function() { console.log(Script de animaciones cargado.); const elementosAnimacionCarga document.querySelectorAll(.animacion-cargar, .animacion-cargar-imagen, .animacion-cargar-titulo, .animacion-cargar-badge, .animacion-cargar-badge-evento, .animacion-cargar-badge-evento-agotado, .animacion-cargar-texto, .animacion-cargar-texto-boton, .animacion-cargar-contador, .animacion-cargar-numero, .animacion-cargar-boton, .animacion-cargar-mensaje, .animacion-cargar-titulo-pdf, .animacion-cargar-pdf-viewer, .animacion-cargar-botones-pdf, .animacion-cargar-texto-boton-pdf, .animacion-cargar-boton-descargar-pdf, .animacion-cargar-boton-fullscreen-pdf, .animacion-cargar-texto-boton-fullscreen-pdf, .animacion-cargar-titulo-seccion, .animacion-cargar-boton-ver-todos); setTimeout(() > { console.log(Aplicando animaciones de carga a, elementosAnimacionCarga.length, elementos.); elementosAnimacionCarga.forEach(elemento > { elemento.classList.add(visible); }); }, 300); const elementosAnimacionScroll document.querySelectorAll(.animacion-scroll, .tarjeta-evento-contenedor); console.log(Detectados, elementosAnimacionScroll.length, elementos para animación de scroll, hover y focus repetida.); const carouselElement document.getElementById(carouselEventos); const carousel new bootstrap.Carousel(carouselElement, { interval: 5000, pause: hover }); const carouselVideos carouselElement.querySelectorAll(.carousel-media-item.media-video-iframe, .carousel-media-item.media-video-local); function handleCarouselSlide() { const activeItem carouselElement.querySelector(.carousel-item.active); const activeVideo activeItem ? activeItem.querySelector(.carousel-media-item.media-video-iframe, .carousel-media-item.media-video-local) : null; carouselVideos.forEach(video > { if (video ! activeVideo) { if (video.tagName VIDEO) { video.pause(); video.currentTime 0; } else if (video.tagName IFRAME) { } } }); if (activeVideo) { carousel.pause(); console.log(Carrusel pausado debido a video.); if (activeVideo.tagName VIDEO) { activeVideo.play().catch(error > console.error(Error al intentar reproducir video:, error)); activeVideo.onended () > { console.log(Video local terminado. Avanzando carrusel.); carousel.next(); carousel.cycle(); }; } else if (activeVideo.tagName IFRAME) { } } else { carousel.cycle(); console.log(No hay video en el slide activo. Carrusel reanudado.); } } carouselElement.addEventListener(slid.bs.carousel, handleCarouselSlide); handleCarouselSlide(); if (elementosAnimacionScroll.length > 0) { const aplicarAnimacion (elemento) > { elemento.classList.remove(visible); void elemento.offsetWidth; elemento.classList.add(visible); console.log(Animación aplicada a:, elemento); }; const iniciarCicloAnimacion (elemento) > { if (elemento.dataset.animationInterval) { clearInterval(parseInt(elemento.dataset.animationInterval)); delete elemento.dataset.animationInterval; } const intervalId setInterval(() > { aplicarAnimacion(elemento); }, 10000); elemento.dataset.animationInterval intervalId.toString(); console.log(`Ciclo de animación iniciado para: ${elemento.tagName} con ID ${intervalId}`); }; const observer new IntersectionObserver(entries > { entries.forEach(entry > { console.log(Elemento:, entry.target, isIntersecting:, entry.isIntersecting); if (entry.isIntersecting) { aplicarAnimacion(entry.target); iniciarCicloAnimacion(entry.target); } else { if (entry.target.dataset.animationInterval) { clearInterval(parseInt(entry.target.dataset.animationInterval)); delete entry.target.dataset.animationInterval; entry.target.classList.remove(visible); console.log(Intervalo detenido y clase visible removida de:, entry.target); } } }); }, { threshold: 0.1, rootMargin: 0px 0px -80px 0px }); elementosAnimacionScroll.forEach(elemento > { observer.observe(elemento); elemento.addEventListener(mouseover, function() { console.log(Mouseover en:, elemento); aplicarAnimacion(elemento); iniciarCicloAnimacion(elemento); }); elemento.addEventListener(focusin, function() { console.log(Focusin en:, elemento); aplicarAnimacion(elemento); iniciarCicloAnimacion(elemento); }); elemento.addEventListener(mouseout, function() { console.log(Mouseout en:, elemento); }); elemento.addEventListener(focusout, function() { console.log(Focusout en:, elemento); }); }); } else { console.log(No se encontraron elementos con las clases .animacion-scroll o .tarjeta-evento-contenedor para observar y repetir.); } const fullscreenBtn document.getElementById(fullscreenBtn); const pdfViewer document.getElementById(pdfViewer); if (fullscreenBtn && pdfViewer) { fullscreenBtn.addEventListener(click, function() { if (pdfViewer.requestFullscreen) { pdfViewer.requestFullscreen(); } else if (pdfViewer.webkitRequestFullscreen) { pdfViewer.webkitRequestFullscreen(); } else if (pdfViewer.msRequestFullscreen) { pdfViewer.msRequestFullscreen(); } }); document.addEventListener(fullscreenchange, function() { if (document.fullscreenElement) { fullscreenBtn.innerHTML i classfas fa-compress>/i> Salir de pantalla completa; } else { fullscreenBtn.innerHTML i classfas fa-expand>/i> Pantalla completa; } }); }});/script> section classeventos py-5> div classcontainer> h2 classtext-center mb-5 animacion-cargar-titulo-seccion>Eventos Destacados/h2> div classrow> div classcol-md-3 mb-4 animacion-scroll tarjeta-evento-contenedor> a hrefevent.php?id16> div classcard h-100 styleposition: relative; overflow: hidden; min-height: 280px;> img srcassets/images/eventos/principales/evento_1759787638_acd5fb32.jpg classcard-img tarjeta-evento-imagen alt4to Desafío Selva Alegre styleposition: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;>/a> div classevento-pasado-badge animacion-cargar-badge-evento styleposition: absolute; top: 10px; right: 10px; z-index: 2; background-color: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 3px;>EVENTO CONCLUIDO/div> div classcard-content-overlay text-white tarjeta-evento-overlay styleposition: absolute; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(33,37,41,0.85);> div classcard-body> h5 classcard-title text-white tarjeta-evento-titulo animacion-cargar-texto>4to Desafío Selva Alegre/h5> p classtext-light tarjeta-evento-fecha animacion-cargar-texto>small>Fecha: 30 Nov 2025 06:00/small>/p> p classtext-warning animacion-cargar-mensaje>small>Este evento ya ha finalizado./small>/p> /div> div classcard-footer bg-dark text-center> button classbtn btn-secondary w-100 tarjeta-evento-boton-deshabilitado animacion-cargar-boton animacion-cargar-texto-boton disabled>Evento Concluido/button> /div> /div> /div> /div> div classcol-md-3 mb-4 animacion-scroll tarjeta-evento-contenedor> a hrefevent.php?id15> div classcard h-100 styleposition: relative; overflow: hidden; min-height: 280px;> img srcassets/images/eventos/principales/evento_1754341420_57abc0f7.jpg classcard-img tarjeta-evento-imagen alt3er Desafío Socabaya styleposition: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;>/a> div classevento-pasado-badge animacion-cargar-badge-evento styleposition: absolute; top: 10px; right: 10px; z-index: 2; background-color: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 3px;>EVENTO CONCLUIDO/div> div classcard-content-overlay text-white tarjeta-evento-overlay styleposition: absolute; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(33,37,41,0.85);> div classcard-body> h5 classcard-title text-white tarjeta-evento-titulo animacion-cargar-texto>3er Desafío Socabaya /h5> p classtext-light tarjeta-evento-fecha animacion-cargar-texto>small>Fecha: 21 Sep 2025 06:30/small>/p> p classtext-warning animacion-cargar-mensaje>small>Este evento ya ha finalizado./small>/p> /div> div classcard-footer bg-dark text-center> button classbtn btn-secondary w-100 tarjeta-evento-boton-deshabilitado animacion-cargar-boton animacion-cargar-texto-boton disabled>Evento Concluido/button> /div> /div> /div> /div> div classcol-md-3 mb-4 animacion-scroll tarjeta-evento-contenedor> a hrefevent.php?id7> div classcard h-100 styleposition: relative; overflow: hidden; min-height: 280px;> img srcassets/images/eventos/principales/evento_1748972156_989165bf.jpg classcard-img tarjeta-evento-imagen alt2do DESAFIO QUEQUEÑA styleposition: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;>/a> div classevento-pasado-badge animacion-cargar-badge-evento styleposition: absolute; top: 10px; right: 10px; z-index: 2; background-color: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 3px;>EVENTO CONCLUIDO/div> div classcard-content-overlay text-white tarjeta-evento-overlay styleposition: absolute; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(33,37,41,0.85);> div classcard-body> h5 classcard-title text-white tarjeta-evento-titulo animacion-cargar-texto>2do DESAFIO QUEQUEÑA/h5> p classtext-light tarjeta-evento-fecha animacion-cargar-texto>small>Fecha: 20 Jul 2025 06:30/small>/p> p classtext-warning animacion-cargar-mensaje>small>Este evento ya ha finalizado./small>/p> /div> div classcard-footer bg-dark text-center> button classbtn btn-secondary w-100 tarjeta-evento-boton-deshabilitado animacion-cargar-boton animacion-cargar-texto-boton disabled>Evento Concluido/button> /div> /div> /div> /div> div classcol-md-3 mb-4 animacion-scroll tarjeta-evento-contenedor> a hrefevent.php?id9> div classcard h-100 styleposition: relative; overflow: hidden; min-height: 280px;> img srcassets/images/eventos/principales/evento_1751745685_4feb29bc.jpg classcard-img tarjeta-evento-imagen alt🪼 GRAN CARRERA PEDESTRE de SOLIDARIDAD styleposition: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;>/a> div classevento-pasado-badge animacion-cargar-badge-evento styleposition: absolute; top: 10px; right: 10px; z-index: 2; background-color: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 3px;>EVENTO CONCLUIDO/div> div classcard-content-overlay text-white tarjeta-evento-overlay styleposition: absolute; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(33,37,41,0.85);> div classcard-body> h5 classcard-title text-white tarjeta-evento-titulo animacion-cargar-texto>🪼 GRAN CARRERA PEDESTRE de SOLIDARIDAD/h5> p classtext-light tarjeta-evento-fecha animacion-cargar-texto>small>Fecha: 06 Jul 2025 06:30/small>/p> p classtext-warning animacion-cargar-mensaje>small>Este evento ya ha finalizado./small>/p> /div> div classcard-footer bg-dark text-center> button classbtn btn-secondary w-100 tarjeta-evento-boton-deshabilitado animacion-cargar-boton animacion-cargar-texto-boton disabled>Evento Concluido/button> /div> /div> /div> /div> div classcol-md-3 mb-4 animacion-scroll tarjeta-evento-contenedor> a hrefevent.php?id8> div classcard h-100 styleposition: relative; overflow: hidden; min-height: 280px;> img srcassets/images/eventos/principales/evento_1749583260_dacfadbb.jpg classcard-img tarjeta-evento-imagen altGRANDE PAPÁ styleposition: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;>/a> div classevento-pasado-badge animacion-cargar-badge-evento styleposition: absolute; top: 10px; right: 10px; z-index: 2; background-color: rgba(0,0,0,0.7); color: white; padding: 5px; border-radius: 3px;>EVENTO CONCLUIDO/div> div classcard-content-overlay text-white tarjeta-evento-overlay styleposition: absolute; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(33,37,41,0.85);> div classcard-body> h5 classcard-title text-white tarjeta-evento-titulo animacion-cargar-texto>GRANDE PAPÁ/h5> p classtext-light tarjeta-evento-fecha animacion-cargar-texto>small>Fecha: 15 Jun 2025 06:15/small>/p> p classtext-warning animacion-cargar-mensaje>small>Este evento ya ha finalizado./small>/p> /div> div classcard-footer bg-dark text-center> button classbtn btn-secondary w-100 tarjeta-evento-boton-deshabilitado animacion-cargar-boton animacion-cargar-texto-boton disabled>Evento Concluido/button> /div> /div> /div> /div> /div> div classtext-center mt-4> a hrefeventos.php classbtn btn-primary animacion-cargar-boton-ver-todos animacion-cargar-boton>Ver Todos los Eventos/a> /div> /div> /section>/main>style> .carousel-concluido-badge { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(220, 53, 69, 0.8); color: white; padding: 8px 15px; border-radius: 5px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; font-size: 1.1em; text-shadow: 1px 1px 2px rgba(0,0,0,0.5); z-index: 10; } .carousel-indicators button { width: 10px !important; height: 10px !important; border-radius: 50% !important; background-color: rgba(255, 255, 255, 0.5) !important; border: none !important; margin: 0 5px !important; transition: background-color 0.3s ease, transform 0.3s ease; } .carousel-indicators button.active { background-color: rgba(255, 255, 255, 1) !important; transform: scale(1.2) !important; } .carousel-media-container, .countdown-media-background { position: absolute; inset: 0; overflow: hidden; z-index: 0; /* Added flexbox properties to ensure centering if object-fit has issues */ display: flex; align-items: center; justify-content: center; } .carousel-item { position: relative; width: 100%; padding-bottom: 56.25%; height: 0; overflow: hidden; background-color: #333; } .carousel-link { position: absolute; inset: 0; z-index: 1; } .carousel-event-title-overlay { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.6); color: white; padding: 8px 15px; border-radius: 8px; font-size: 1.2em; font-weight: bold; text-align: center; z-index: 11; max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .carousel-event-title-overlay h3 { margin: 0; color: inherit; } @media (max-width: 767.98px) { .carousel-event-title-overlay { font-size: 1em; padding: 5px 10px; top: 10px; max-width: 90%; } } .carousel-media-item, .countdown-media-item { width: 100%; height: 100%; object-fit: cover; display: block; /* Added min-width/height for robustness, though object-fit should handle it */ min-width: 100%; min-height: 100%; } .countdown-overlay { position: relative; z-index: 1; background-color: rgba(0,0,0,0.5); height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; } .countdown-section { position: relative; min-height: 400px; display: flex; align-items: center; justify-content: center; overflow: hidden; } .animacion-cargar { opacity: 0; transform: translateY(40px); transition: opacity 1.5s ease-out, transform 1.5s ease-out; } .animacion-cargar.visible { opacity: 1; transform: translateY(0); } .animacion-cargar-imagen { transform: scale(1.1); transition: transform 2s ease-in-out; } .animacion-cargar-imagen.visible { transform: scale(1); } .animacion-cargar-titulo { animation: fadeInSlideDown 1.8s ease-in-out forwards; opacity: 0; transform: translateY(-30px); } @keyframes fadeInSlideDown { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } } .animacion-cargar-badge, .animacion-cargar-badge-evento, .animacion-cargar-badge-evento-agotado { opacity: 0; transform: scale(0.6); animation: zoomInFadeIn 1.2s ease-out forwards; animation-delay: 1s; } @keyframes zoomInFadeIn { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: scale(1); } } .animacion-cargar-texto, .animacion-cargar-texto-boton, .animacion-cargar-contador, .animacion-cargar-numero, .animacion-cargar-boton, .animacion-cargar-mensaje, .animacion-cargar-titulo-pdf, .animacion-cargar-pdf-viewer, .animacion-cargar-botones-pdf, .animacion-cargar-texto-boton-pdf, .animacion-cargar-boton-descargar-pdf, .animacion-cargar-boton-fullscreen-pdf, .animacion-cargar-texto-boton-fullscreen-pdf, .animacion-cargar-titulo-seccion, .animacion-cargar-boton-ver-todos { opacity: 0; animation: fadeInFromBottom 1.5s ease-out forwards; } .animacion-cargar-texto { animation-delay: 0.5s; } .animacion-cargar-texto-boton { animation-delay: 0.6s; } .animacion-cargar-contador { animation-delay: 0.7s; } .animacion-cargar-numero { animation-delay: 0.8s; } .animacion-cargar-boton { animation-delay: 0.9s; } .animacion-cargar-mensaje { animation-delay: 1s; } .animacion-cargar-titulo-pdf { animation-delay: 1.1s; } .animacion-cargar-pdf-viewer { animation-delay: 1.2s; } .animacion-cargar-botones-pdf { animation-delay: 1.3s; } .animacion-cargar-texto-boton-pdf { animation-delay: 1.4s; } .animacion-cargar-boton-descargar-pdf { animation-delay: 1.5s; } .animacion-cargar-boton-fullscreen-pdf { animation-delay: 1.6s; } .animacion-cargar-texto-boton-fullscreen-pdf { animation-delay: 1.7s; } .animacion-cargar-titulo-seccion { animation-delay: 1.8s; } .animacion-cargar-boton-ver-todos { animation-delay: 1.9s; } @keyframes fadeInFromBottom { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } } .animacion-cargar-contador { opacity: 0; animation: fadeInFromBottom 1.5s ease-out forwards; animation-delay: 1.5s; } .animacion-cargar-numero { opacity: 0; animation: fadeInScaleUp 1s ease-out forwards; animation-delay: calc(1.8s + var(--order) * 0.2s); transform: scale(0.8); display: inline-block; } .animacion-cargar-label-numero::after { opacity: 0; animation: fadeInFromBottom 1s ease-out forwards; animation-delay: calc(2s + var(--order) * 0.2s); transform: translateY(20px); display: block; } @keyframes fadeInScaleUp { from { opacity: 0; transform: scale(0.8) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .animacion-scroll { opacity: 0; transform: translateX(-150px); transition: opacity 2.8s ease-out, transform 2.8s ease-out; will-change: opacity, transform; } .animacion-scroll.visible { opacity: 1; transform: translateX(0); } .tarjeta-evento-contenedor { transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; } .tarjeta-evento-contenedor:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); } .tarjeta-evento-imagen { filter: brightness(0.8); transition: filter 0.3s ease-in-out; } .tarjeta-evento-contenedor:hover .tarjeta-evento-imagen { filter: brightness(1); } .tarjeta-evento-overlay { transition: background-color 0.3s ease-in-out; } .tarjeta-evento-contenedor:hover .tarjeta-evento-overlay { background-color: rgba(33, 37, 41, 0.95) !important; } .tarjeta-evento-titulo, .tarjeta-evento-fecha { transition: color 0.3s ease-in-out; } .tarjeta-evento-contenedor:hover .tarjeta-evento-titulo { color: #00bcd4 !important; } .tarjeta-evento-contenedor:hover .tarjeta-evento-fecha { color: #e0e0e0 !important; } .tarjeta-evento-boton, .tarjeta-evento-boton-deshabilitado { transition: background-color 0.3s ease-in-out, transform 0.3s ease-in-out; } .tarjeta-evento-boton:hover { transform: scale(1.02); } @media (max-width: 767.98px) { .carousel-item { min-height: 250px; } .carousel-media-container { height: 100%; } .carousel-media-item { object-fit: cover; position: absolute; top: 0; left: 0; width: 100%; height: 100%; } }/style>div classmodal fade idverificacionModal tabindex-1 aria-labelledbyverificacionModalLabel aria-hiddentrue data-bs-backdropfalse> div classmodal-dialog> div classmodal-content> div classmodal-header> h5 classmodal-title idverificacionModalLabel>Verificación de Inscripción/h5> button typebutton classbtn-close data-bs-dismissmodal aria-labelClose>/button> /div> div classmodal-body> form idverificacionForm classneeds-validation novalidate> div classmb-3> label fordniVerificacion classform-label>DNI o Carnet de Extranjería/label> input typetext classform-control iddniVerificacion required pattern0-9{8,20}> div classinvalid-feedback>Por favor ingrese un DNI válido (8-20 dígitos)/div> /div> input typehidden ideventoId nameevento_id value> /form> /div> div classmodal-footer> button typebutton classbtn btn-secondary data-bs-dismissmodal>Cancelar/button> button typebutton classbtn btn-primary idverificarDni>Continuar/button> /div> /div> /div>/div>script>document.addEventListener(DOMContentLoaded, function() { // 1. Inicialización del modal const verificacionModalEl document.getElementById(verificacionModal); if (!verificacionModalEl) { console.error(No se encontró el elemento verificacionModal); return; } const verificacionModal new bootstrap.Modal(verificacionModalEl); const dniInput document.getElementById(dniVerificacion); const verificarBtn document.getElementById(verificarDni); const verificacionForm document.getElementById(verificacionForm); // 2. Mostrar modal desde otros botones document.querySelectorAll(data-actioninscribirse).forEach(btn > { btn.addEventListener(click, function(e) { e.preventDefault(); const eventoId this.dataset.eventoId || ; document.getElementById(eventoId).value eventoId; console.log(Abriendo modal...); // Debug verificacionModal.show(); // Enfocar el campo DNI después de que el modal se muestre setTimeout(() > dniInput.focus(), 500); }); }); // 3. Manejar el formulario verificacionForm.addEventListener(submit, function(e) { e.preventDefault(); if (verificacionForm.checkValidity()) { verificarDni(); } verificacionForm.classList.add(was-validated); }); // 4. Manejar Enter en el campo DNI dniInput.addEventListener(keydown, function(e) { if (e.key Enter) { e.preventDefault(); if (verificacionForm.checkValidity()) { verificarDni(); } else { verificacionForm.classList.add(was-validated); } } }); function calcularEdad() { console.log(DEBUG Valor de fecha_nacimiento (input):, this.value); // Ver valor crudo del input const fechaNacimiento new Date(this.value); console.log(DEBUG Fecha parseada:, fechaNacimiento); // Ver cómo se interpreta la fecha const hoy new Date(); console.log(DEBUG Fecha actual:, hoy); // Ver fecha de referencia para el cálculo let edad hoy.getFullYear() - fechaNacimiento.getFullYear(); const mes hoy.getMonth() - fechaNacimiento.getMonth(); console.log(DEBUG Cálculo inicial - Años:, edad, | Mes diferencia:, mes); if (mes 0 || (mes 0 && hoy.getDate() fechaNacimiento.getDate())) { edad--; console.log(DEBUG Ajuste aplicado - Nueva edad:, edad); // Ver si se resta un año } console.log(DEBUG Edad calculada final:, edad); // Resultado final document.getElementById(edad).value edad; } // 5. Función para verificar DNI function verificarDni() { const formData { dni: dniInput.value.trim(), evento_id: document.getElementById(eventoId).value }; console.log(Verificando DNI:, formData); // Debug fetch(api/verificar_dni.php, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(formData) }) .then(response > { if (!response.ok) throw new Error(Error en la red); return response.json(); }) .then(data > { console.log(Respuesta recibida:, data); // Debug if (data.ya_inscrito) { alert(data.message || Ya estás inscrito en este evento); return; } verificacionModal.hide(); // Abrir modal de inscripción después de un breve retraso setTimeout(() > { const inscripcionModalEl document.getElementById(inscripcionModal); if (inscripcionModalEl) { const inscripcionModal new bootstrap.Modal(inscripcionModalEl); if (data.existe && data.usuario) { Object.entries(data.usuario).forEach((key, value) > { const field document.getElementById(key); if (field) { field.value value; // Si es fecha_nacimiento, calcular edad if (key fecha_nacimiento) { calcularEdad.call(field); // Ejecutar manualmente } } }); } else { // Nuevo usuario - limpiar formulario const inscripcionForm document.getElementById(inscripcionForm); if (inscripcionForm) { inscripcionForm.reset(); const dniField document.getElementById(dni); if (dniField) dniField.value formData.dni; } } inscripcionModal.show(); } else { console.error(No se encontró el modal de inscripción); } }, 300); }) .catch(error > { console.error(Error:, error); alert(Error al verificar el DNI. Por favor intente nuevamente.); }); } // 6. Asignar evento al botón Continuar if (verificarBtn) { verificarBtn.addEventListener(click, function() { if (verificacionForm.checkValidity()) { verificarDni(); } else { verificacionForm.classList.add(was-validated); } }); }});/script>div classmodal fade modal-styled animate__animated animate__fadeInDown idinscripcionModal tabindex-1 aria-labelledbyinscripcionModalLabel aria-hiddentrue data-bs-backdropfalse> div classmodal-dialog modal-lg> div classmodal-content> div classmodal-header> h5 classmodal-title idinscripcionModalLabel>Formulario de Inscripción/h5> button typebutton classbtn-close data-bs-dismissmodal aria-labelClose>/button> /div> div classmodal-body> form idinscripcionForm enctypemultipart/form-data> input typehidden idusuario_id nameusuario_id> input typehidden idmodal_evento_id nameevento_id value> div idcamposDinamicos> /div> /form> /div> div classmodal-footer> div classcampos-obligatorios-info mt-2 small> i classbi bi-exclamation-circle-fill>/i> Todos los campos marcados con * son obligatorios /div> button typebutton classbtn btn-secondary data-bs-dismissmodal>Cancelar/button> button typebutton classbtn btn-primary idenviarInscripcion> Enviar Inscripción /button> /div> /div> /div>/div>div classmodal fade idmodalDorsales tabindex-1 aria-hiddentrue> div classmodal-dialog> div classmodal-content> div classmodal-header> h5 classmodal-title>Seleccionar Dorsal/h5> button typebutton classbtn-close data-bs-dismissmodal aria-labelClose>/button> /div> div classmodal-body> p idinfoDorsales>Cargando dorsales disponibles.../p> div idcontenedorDorsales classdorsales-grid>/div> /div> div classmodal-footer> button typebutton classbtn btn-secondary data-bs-dismissmodal>Cerrar/button> /div> /div> /div>/div>style>/* ✅ CSS ESPECÍFICO PARA EL CAMPO DE EQUIPOS */.equipo-select, .equipo-input { padding-right: 70px !important; /* Espacio para el botón */}.equipo-toggle { border: none !important; background: transparent !important; font-size: 14px; padding: 2px 6px; border-radius: 3px;}.equipo-toggle:hover { background-color: rgba(0,123,255,0.1) !important;}/* Dropdown para sugerencias cuando está en modo input */.equipo-suggestions { position: absolute; top: 100%; left: 0; right: 0; background: #3b3e42; border: 1px solid #4a4a4a; border-top: none; border-radius: 0 0 8px 8px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none;}.equipo-suggestion-item { padding: 8px 12px; cursor: pointer; color: #f0f2f5; border-bottom: 1px solid #4a4a4a;}.equipo-suggestion-item:hover,.equipo-suggestion-item.highlighted { background-color: #2196F3; color: white;}.equipo-suggestion-item:last-child { border-bottom: none;}.campos-obligatorios-info { color: #fff !important; background-color: #1e88e500; padding: 0.6rem 1rem; border-radius: 8px; display: inline-block; font-weight: 500;}.campos-obligatorios-info i { color: #fff;}/* ESTILOS GENERALES PARA EL MODAL EN MODO OSCURO */.modal-styled .modal-content { border-radius: 12px; box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4); animation: fadeIn 0.6s ease; background-color: #2c2f33; color: #f0f2f5; }.modal-styled .modal-header { background: linear-gradient(135deg, #1A2C40, #34495E); color: white; border-top-left-radius: 12px; border-top-right-radius: 12px; padding: 1rem 1.5rem; border-bottom: 1px solid #4a4a4a; }.modal-styled .modal-title { font-weight: bold; font-size: 1.3rem; color: #e0e6ed; }.modal-styled .btn-close { filter: invert(1); }.modal-styled .form-label { font-weight: 600; color: #d1d5da; }.modal-styled .form-control,.modal-styled .form-select { border-radius: 8px; transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease; background-color: #3b3e42; color: #f0f2f5; }.modal-styled .form-control:focus,.modal-styled .form-select:focus { border-color: #2196F3; background-color: #4a4a4a; box-shadow: 0 0 0 0.25rem rgba(33, 150, 243, 0.25); }.modal-styled .form-control.is-valid,.modal-styled .form-select.is-valid { border-color: #28a745 !important; background-color: #3b3e42; color: #f0f2f5; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25) !important; position: relative;}.modal-styled .form-control.is-invalid,.modal-styled .form-select.is-invalid { border-color: #dc3545 !important; background-color: #3b3e42; color: #f0f2f5; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important; position: relative;}/* ✅ Iconos de validación para campos válidos e inválidos */.modal-styled .form-control.is-valid::after,.modal-styled .form-select.is-valid::after { content: ✓; position: absolute; right: 15px; top: 50%; transform: translateY(-50%); color: #28a745; font-weight: bold; font-size: 18px; pointer-events: none; z-index: 10;}.modal-styled .form-control.is-invalid::after,.modal-styled .form-select.is-invalid::after { content: ✕; position: absolute; right: 15px; top: 50%; transform: translateY(-50%); color: #dc3545; font-weight: bold; font-size: 18px; pointer-events: none; z-index: 10;}/* ✅ Contenedor para los iconos de validación */.modal-styled .mb-3 { position: relative;}.modal-styled .mb-3.field-valid::after { content: ✓; position: absolute; right: 15px; top: 40px; color: #28a745; font-weight: bold; font-size: 18px; pointer-events: none; z-index: 10;}.modal-styled .mb-3.field-invalid::after { content: ✕; position: absolute; right: 15px; top: 40px; color: #dc3545; font-weight: bold; font-size: 18px; pointer-events: none; z-index: 10;}/* ✅ Mensajes de error mejorados para tema oscuro */.modal-styled .invalid-feedback { color: #ff6b6b !important; font-size: 0.875rem; margin-top: 0.25rem; display: block;}.modal-styled .valid-feedback { color: #51cf66 !important; font-size: 0.875rem; margin-top: 0.25rem; display: block;}/* OTROS ESTILOS DEL MODAL */.modal-styled .btn-primary { background-color: #2196F3; border: none; transition: background-color 0.3s ease;}.modal-styled .btn-primary:hover { background-color: #1976D2; }.modal-styled .btn-secondary { background-color: #6c757d; border: none; transition: background-color 0.3s ease;}.modal-styled .btn-secondary:hover { background-color: #5a6268; }.modal-styled .modal-footer { background-color: #25282b; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; border-top: 1px solid #4a4a4a; color: #b0b0b0; }.modal-styled .dorsales-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 10px; margin-top: 15px;}/* Estilo para el span del dorsal seleccionado */.modal-styled .input-group-text { background-color: #3b3e42; color: #f0f2f5; border: 1px solid #4a4a4a; border-left: none; }/* Ajuste de la posición del modal para evitar el navbar y asegurar visibilidad del footer */.modal.fade:not(.show) { top: 50px; transform: translate(0, 0); }.modal.show { transform: translate(0, 0); z-index: 1055; /* Aumenta el z-index del modal mismo, por encima del navbar */ display: block !important; /* Asegura que el modal esté visible */}.modal-dialog { max-width: calc(100vw - 20px) !important; margin: 0 10px !important; max-height: calc(100vh - 70px); }/* Disposición de 2 columnas para los campos dinámicos */#camposDinamicos { display: grid; grid-template-columns: 1fr 1fr; gap: 15px 30px; }/* Asegurarse de que ciertos campos ocupen todo el ancho */#camposDinamicos .mb-3.dorsales-grid,#camposDinamicos .mb-3:has(select#distancia_id), #camposDinamicos .mb-3:has(textarea), #camposDinamicos .mb-3:has(.form-check), #camposDinamicos .mb-3:has(.form-group) { grid-column: 1 / -1; }/* ✅ ESTILOS MEJORADOS PARA AUTOCOMPLETADO DE EQUIPOS */.equipo-autocomplete-container { position: relative;}.dropdown-menu-equipos { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; background-color: #3b3e42 !important; /* Tema oscuro */ border: 1px solid #4a4a4a !important; border-radius: 0.375rem; width: 100%; max-height: 200px; overflow-y: auto; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.3); margin-top: 2px;}.dropdown-menu-equipos .dropdown-item { display: block; width: 100%; padding: 8px 16px; clear: both; font-weight: 400; color: #f0f2f5 !important; /* Texto claro para tema oscuro */ text-align: inherit; white-space: nowrap; background-color: transparent; border: 0; cursor: pointer; text-decoration: none; transition: background-color 0.15s ease-in-out;}.dropdown-menu-equipos .dropdown-item:hover,.dropdown-menu-equipos .dropdown-item:focus,.dropdown-menu-equipos .dropdown-item.active { background-color: #2196F3 !important; color: #fff !important;}.dropdown-menu-equipos .dropdown-item.highlighted { background-color: #2196F3 !important; color: #fff !important;}/* Otros estilos existentes... */.select-editable-container { position: relative;}.registration-modal .modal-body { padding: 2rem !important;}.registration-modal .modal-content { background-color: rgb(255, 255, 255) !important; border-radius: 0.5rem !important;}.dorsales-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 10px;}.dorsales-grid button { width: 100%; height: 60px; font-size: 1.1rem; font-weight: bold;}.hidden-by-dependency { display: none !important;}/* ✅ ESTILOS ADICIONALES PARA SWEETALERT */.swal-wide { width: 600px !important;}.swal-wide .swal2-html-container { text-align: left !important;}/* ✅ AGREGA ESTE CSS A TU ARCHIVO - CORRECCIONES PARA EL CAMPO DE EQUIPOS *//* Color del texto y cursor blanco para el input de equipos */.equipo-input { color: #f0f2f5 !important; /* Texto blanco */ caret-color: #f0f2f5 !important; /* Cursor blanco */}.equipo-input:focus { color: #f0f2f5 !important; /* Asegurar texto blanco al hacer focus */ caret-color: #f0f2f5 !important; /* Asegurar cursor blanco al hacer focus */}/* Asegurar que el placeholder también sea visible */.equipo-input::placeholder { color: #a0a0a0 !important; /* Placeholder gris claro */}/* Asegurar que la selección de texto sea visible */.equipo-input::selection { background-color: #2196F3 !important; /* Fondo azul para texto seleccionado */ color: #ffffff !important; /* Texto blanco cuando está seleccionado */}/* ✅ REEMPLAZA EL CSS ANTERIOR CON ESTE - APLICA A TODOS LOS INPUTS DEL FORMULARIO *//* Color del texto y cursor blanco para TODOS los inputs del formulario */.modal-styled .form-control,.modal-styled .form-select { color: #f0f2f5 !important; /* Texto blanco */ caret-color: #f0f2f5 !important; /* Cursor blanco */}.modal-styled .form-control:focus,.modal-styled .form-select:focus { color: #f0f2f5 !important; /* Asegurar texto blanco al hacer focus */ caret-color: #f0f2f5 !important; /* Asegurar cursor blanco al hacer focus */}/* Asegurar que todos los placeholders también sean visibles */.modal-styled .form-control::placeholder { color: #a0a0a0 !important; /* Placeholder gris claro */}/* Asegurar que la selección de texto sea visible en todos los inputs */.modal-styled .form-control::selection,.modal-styled .form-select::selection { background-color: #2196F3 !important; /* Fondo azul para texto seleccionado */ color: #ffffff !important; /* Texto blanco cuando está seleccionado */}/* ✅ MAYÚSCULAS AUTOMÁTICAS PARA EL CAMPO EQUIPO */.equipo-input,.equipo-select { text-transform: uppercase !important; /* Mostrar en mayúsculas */}/* Opcional: También para otros campos que quieras en mayúsculas */#nombre_completo { text-transform: capitalize !important; } /* ✅ AGREGA ESTE CSS PARA RESPONSIVE MÓVIL *//* Una columna en móviles */@media (max-width: 768px) { #camposDinamicos { display: grid; grid-template-columns: 1fr !important; /* Una sola columna */ gap: 15px !important; } /* Todos los campos ocupan el ancho completo en móviles */ #camposDinamicos .mb-3 { grid-column: 1 / -1 !important; } /* Ajustar modal para móviles */ .modal-dialog { margin: 5px !important; max-width: calc(100vw - 10px) !important; } .modal-body { padding: 1rem !important; } /* Ajustar botón toggle de equipos para móviles */ .equipo-toggle { right: 15px !important; }}/style>script srchttps://code.jquery.com/jquery-3.6.0.min.js>/script>link hrefhttps://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css relstylesheet>script srchttps://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js>/script>script srchttps://cdn.jsdelivr.net/npm/sweetalert2@11>/script>script> // Objeto para mapear field_id a field_name (usado como ID en el HTML)const fieldIdToNameMap {}; // This is correctly defined globally$(document).ready(function() { // --- CONSTRUIR EL MAPEO fieldIdToNameMap --- // Iterar sobre todos los contenedores de campos dinámicos $(#camposDinamicos .mb-3).each(function() { const $thisContainer $(this); const fieldId $thisContainer.data(field-id); // Obtener el field_id del nuevo data-attribute if (fieldId) { // Intenta obtener el ID o Name del input/select/textarea dentro de este contenedor const $inputElement $thisContainer.find(inputname, selectname, textareaname, inputid, selectid, textareaid).first(); if ($inputElement.length) { const fieldNameUsedInHtml $inputElement.attr(name) || $inputElement.attr(id); if (fieldNameUsedInHtml) { fieldIdToNameMapfieldId fieldNameUsedInHtml; console.log(`MAPEO CONSTRUCCION field_id: ${fieldId} -> HTML_ID/NAME: ${fieldNameUsedInHtml} (Element ID: ${$inputElement.attr(id) || N/A}, Name: ${$inputElement.attr(name) || N/A})`); } else { console.warn(`MAPEO CONSTRUCCION Elemento interactivo encontrado en ${$thisContainer.attr(id)} pero sin ID ni NAME.`); } } else { console.warn(`MAPEO CONSTRUCCION Contenedor ${$thisContainer.attr(id)} (field_id: ${fieldId}) no contiene un input/select/textarea principal.`); } } }); console.log(MAPEO FINAL fieldIdToNameMap completo:, fieldIdToNameMap); // Función para registrar logs detallados function log(message, data null) { console.log(`${new Date().toISOString()} ${message}`); if (data ! null) { console.log(Datos:, data); } } // ✅ FUNCIÓN PARA VERIFICAR SI UN CAMPO EXISTE EN EL FORMULARIO function campoExiste(fieldName) { return $(`#${fieldName}`).length > 0; } // ✅ FUNCIÓN PARA OBTENER VALOR DE CAMPO SI EXISTE function obtenerValorCampo(fieldName, defaultValue null) { if (campoExiste(fieldName)) { const valor $(`#${fieldName}`).val(); return (valor ! null && typeof valor string && valor.trim() ! ) ? valor : defaultValue; } return defaultValue; } // ✅ FUNCIÓN MEJORADA PARA VALIDACIÓN VISUAL DE CAMPOS function aplicarEstilosValidacion(input, esValido, mensajeError ) { const $input $(input); const $container $input.closest(.mb-3); // Limpiar estados previos $input.removeClass(is-valid is-invalid); $container.removeClass(field-valid field-invalid); $container.find(.invalid-feedback, .valid-feedback).remove(); if (esValido) { $input.addClass(is-valid); $container.addClass(field-valid); // Opcional: agregar mensaje de éxito $input.after(div class\valid-feedback\>Campo válido/div>); } else { $input.addClass(is-invalid); $container.addClass(field-invalid); if (mensajeError) { $input.after(`div class\invalid-feedback\>${mensajeError}/div>`); } } } // ✅ FUNCIÓN COMPLETA CORREGIDA PARA EL AUTOCOMPLETADO// REEMPLAZA TODA LA FUNCIÓN setupEquipoAutocomplete() CON ESTA VERSIÓNfunction setupEquipoAutocomplete() { const $select $(#equipo.equipo-select); const $input $(.equipo-input); const $toggle $(.equipo-toggle); const $container $select.closest(.position-relative); if (!$select.length) { console.log(Campo de equipos no encontrado); return; } let isInputMode false; let equipos ; // Array para almacenar todos los equipos // Cargar equipos desde el select function cargarEquiposDelSelect() { equipos ; $select.find(option).each(function() { const value $(this).val(); if (value && value ! ) { equipos.push(value); } }); console.log(Equipos cargados:, equipos); } // Crear dropdown de sugerencias function crearDropdownSugerencias() { if (!$container.find(.equipo-suggestions).length) { $container.append(div class\equipo-suggestions\>/div>); } } // ✅ FUNCIÓN MEJORADA PARA MOSTRAR SUGERENCIAS function mostrarSugerencias(query) { const $suggestions $container.find(.equipo-suggestions); const queryUpper query.toUpperCase(); // Filtrar equipos que contengan el query const equiposFiltrados equipos.filter(equipo > equipo.toUpperCase().includes(queryUpper) ); // Ordenar para que aparezcan primero los que empiezan con el query equiposFiltrados.sort((a, b) > { const aStartsWith a.toUpperCase().startsWith(queryUpper); const bStartsWith b.toUpperCase().startsWith(queryUpper); if (aStartsWith && !bStartsWith) return -1; if (!aStartsWith && bStartsWith) return 1; return a.localeCompare(b); }); $suggestions.empty(); if (equiposFiltrados.length > 0 && query.length > 0) { equiposFiltrados.forEach(equipo > { const $item $(div class\equipo-suggestion-item\>/div>) .text(equipo) .data(value, equipo); $suggestions.append($item); }); $suggestions.show(); } else { $suggestions.hide(); } } // ✅ FUNCIÓN CORREGIDA PARA AUTOCOMPLETAR TEXTO CON SELECCIÓN function autocompletarTexto(inputElement, sugerencia) { const inputValue inputElement.value; const inputValueUpper inputValue.toUpperCase(); const sugerenciaUpper sugerencia.toUpperCase(); // Solo autocompletar si la sugerencia empieza con lo que se ha escrito if (sugerenciaUpper.startsWith(inputValueUpper) && inputValue.length > 0) { const startPos inputValue.length; // Establecer el valor completo inputElement.value sugerenciaUpper; // Seleccionar la parte autocompletada if (inputElement.setSelectionRange) { inputElement.setSelectionRange(startPos, sugerenciaUpper.length); } else if (inputElement.createTextRange) { // Compatibilidad con IE const range inputElement.createTextRange(); range.collapse(true); range.moveStart(character, startPos); range.moveEnd(character, sugerenciaUpper.length); range.select(); } console.log(`Autocompletado: ${inputValue} → ${sugerenciaUpper} (seleccionado desde ${startPos})`); } } // ✅ CAMBIAR ENTRE MODO SELECT E INPUT (CON MAYÚSCULAS) function toggleMode() { if (isInputMode) { // Cambiar a modo select let inputValue $input.val(); if (inputValue) { inputValue inputValue.toUpperCase(); // ✅ Convertir a mayúsculas // Si hay valor en input, agregarlo como opción si no existe if (!$select.find(`optionvalue\${inputValue}\`).length) { $select.append(`option value\${inputValue}\ selected>${inputValue}/option>`); } else { $select.val(inputValue); } } $input.hide().val(); $select.show(); $container.find(.equipo-suggestions).hide(); $toggle.text(✏️); isInputMode false; } else { // Cambiar a modo input const selectValue $select.val(); $input.val(selectValue).show().focus(); // ✅ AUTOSELECCIONAR TODO EL TEXTO CUANDO SE CAMBIE A MODO INPUT setTimeout(() > { if ($input0 && $input0.setSelectionRange) { $input0.setSelectionRange(0, $input.val().length); } else if ($input0 && $input0.select) { $input0.select(); } }, 10); $select.hide(); $toggle.text(📋); isInputMode true; } } // ✅ FUNCIÓN PARA CONVERTIR A MAYÚSCULAS EN TIEMPO REAL function configurarMayusculasEquipo() { // Esta función ya no es necesaria aquí porque manejamos mayúsculas en el evento input console.log(Mayúsculas configuradas en evento input); } // Eventos del botón toggle $toggle.on(click, function(e) { e.preventDefault(); e.stopPropagation(); toggleMode(); }); // ✅ EVENTO INPUT CORREGIDO PARA MANEJAR AUTOCOMPLETADO CORRECTAMENTE // ✅ CORRECCIÓN PARA PERMITIR ESPACIOS EN NOMBRES DE EQUIPOS// REEMPLAZA EL EVENTO INPUT EN TU setupEquipoAutocomplete() CON ESTE:$input.on(input, function(e) { const query this.value; // ✅ NO usar trim() aquí para preservar espacios const cursorPosition this.selectionStart; const cursorEnd this.selectionEnd; // Si hay texto seleccionado, significa que es parte del autocompletado // y el usuario está escribiendo encima if (cursorPosition ! cursorEnd) { // El usuario está escribiendo sobre texto seleccionado // Mantener solo la parte no seleccionada const newValue this.value.substring(0, cursorPosition); this.value newValue.toUpperCase(); this.setSelectionRange(newValue.length, newValue.length); // Continuar con el nuevo valor (SOLO trim para mostrar sugerencias, no para el valor) mostrarSugerencias(newValue.trim()); // Buscar nueva sugerencia SOLO si hay contenido después de trim if (newValue.trim().length > 0) { const equiposQueEmpiezanCon equipos.filter(equipo > equipo.toUpperCase().startsWith(newValue.toUpperCase()) ); if (equiposQueEmpiezanCon.length > 0) { setTimeout(() > { autocompletarTexto(this, equiposQueEmpiezanCon0); }, 1); } } } else { // Usuario escribiendo normalmente const upperQuery query.toUpperCase(); this.value upperQuery; this.setSelectionRange(upperQuery.length, upperQuery.length); // ✅ Solo mostrar sugerencias si hay contenido real (después de trim) const trimmedQuery upperQuery.trim(); if (trimmedQuery.length > 0) { mostrarSugerencias(trimmedQuery); // Autocompletado automático SOLO si no termina en espacio if (!upperQuery.endsWith( )) { const equiposQueEmpiezanCon equipos.filter(equipo > equipo.toUpperCase().startsWith(trimmedQuery) ); if (equiposQueEmpiezanCon.length > 0) { setTimeout(() > { autocompletarTexto(this, equiposQueEmpiezanCon0); }, 1); } } } else { // Si no hay contenido real, ocultar sugerencias const $suggestions $container.find(.equipo-suggestions); $suggestions.hide(); } }}); // Eventos del input para navegación con teclado $input.on(keydown, function(e) { const $suggestions $container.find(.equipo-suggestions); const $items $suggestions.find(.equipo-suggestion-item); let highlighted $items.filter(.highlighted).index(); switch(e.keyCode) { case 40: // Flecha abajo e.preventDefault(); if ($items.length > 0) { $items.removeClass(highlighted); highlighted (highlighted + 1) % $items.length; $items.eq(highlighted).addClass(highlighted); } break; case 38: // Flecha arriba e.preventDefault(); if ($items.length > 0) { $items.removeClass(highlighted); highlighted highlighted 0 ? $items.length - 1 : highlighted - 1; $items.eq(highlighted).addClass(highlighted); } break; case 13: // Enter e.preventDefault(); if (highlighted > 0) { const selectedValue $items.eq(highlighted).data(value); $input.val(selectedValue.toUpperCase()); // ✅ En mayúsculas $suggestions.hide(); toggleMode(); // Volver a modo select } break; case 27: // Escape $suggestions.hide(); toggleMode(); // Volver a modo select break; case 9: // Tab if (highlighted > 0) { const selectedValue $items.eq(highlighted).data(value); $input.val(selectedValue.toUpperCase()); // ✅ En mayúsculas } $suggestions.hide(); setTimeout(() > toggleMode(), 100); // Volver a modo select después de tab break; } }); // Eventos de sugerencias $container.on(click, .equipo-suggestion-item, function() { const selectedValue $(this).data(value); $input.val(selectedValue.toUpperCase()); // ✅ En mayúsculas $container.find(.equipo-suggestions).hide(); toggleMode(); // Volver a modo select }); $container.on(mouseenter, .equipo-suggestion-item, function() { $(this).siblings().removeClass(highlighted); $(this).addClass(highlighted); }); // Ocultar sugerencias al hacer clic fuera $(document).on(click, function(e) { if (!$container.is(e.target) && $container.has(e.target).length 0) { $container.find(.equipo-suggestions).hide(); } }); // Obtener valor final para el formulario window.getEquipoValue function() { const $select $(#equipo.equipo-select); const $input $(.equipo-input); // Si está en modo input y tiene valor, usar ese if ($input.is(:visible) && $input.val().trim()) { return $input.val().toUpperCase().trim(); } // Si no, usar el valor del select const selectValue $select.val(); return selectValue ? selectValue.toUpperCase() : selectValue; }; // Inicializar cargarEquiposDelSelect(); crearDropdownSugerencias(); configurarMayusculasEquipo(); // ✅ Llamar a la función de autocompletado corregida console.log(✅ Campo combo editable de equipos configurado correctamente);}// Asegúrate de que jQuery esté cargado antes de estas funciones.function updateFieldVisibility() { console.log(---------------- Ejecutando updateFieldVisibility ----------------); $(#camposDinamicos .mb-3).each(function () { const fieldContainer $(this); const fieldContainerId fieldContainer.attr(id) || fieldContainer.data(field-id) || (sin ID); const mainInteractiveElement fieldContainer.find(input, select, textarea).first(); const labelElement fieldContainer.find(label).first(); const fieldName mainInteractiveElement.attr(name) || mainInteractiveElement.attr(id) || `Campo-${fieldContainerId}`; // Añadir log del valor por defecto del campo let defaultValue mainInteractiveElement.is(:checkbox) ? mainInteractiveElement.is(:checked) : mainInteractiveElement.val(); if (defaultValue undefined || defaultValue null) { defaultValue (vacío); } console.log(` Valor por defecto de ${fieldName}: ${defaultValue}`); const asteriskSpan labelElement.find(.required-asterisk-dynamic); // Capturar el estado required original del elemento interactivo. // Esto asume que el atributo required ya está establecido por PHP al cargar la página. const originalRequired mainInteractiveElement.prop(required); console.log(`\n--- Evaluando Campo Dependiente: ${fieldName} (Contenedor ID: ${fieldContainerId}) ---`); console.log(` Estado required original del campo: ${originalRequired}`); const overrideDependenciesRaw fieldContainer.attr(data-dependencies); const baseDependenciesRaw fieldContainer.attr(data-base-dependencies); let dependenciesToParse ; let logMessage ; // Inicializar el estado de visibilidad basado en la clase d-none actual del contenedor. // Esto respeta la visibilidad inicial establecida por PHP. let finalShouldBeVisible !fieldContainer.hasClass(d-none); let finalShouldBeDisabled false; // true disabled (no se envía valor) let finalShouldBeReadonly false; // true readonly (se envía valor) let finalShouldBeRequired originalRequired; // Empieza con el estado original // Resetear solo el estado de habilitación/lectura/requerido para la evaluación mainInteractiveElement.prop(disabled, false); mainInteractiveElement.prop(readonly, false); mainInteractiveElement.prop(required, originalRequired); fieldContainer.removeClass(required-by-dependency); if (originalRequired && asteriskSpan.length) { asteriskSpan.show(); } else if (asteriskSpan.length) { asteriskSpan.hide(); // Ocultar si no es originalmente requerido } if (overrideDependenciesRaw && overrideDependenciesRaw.trim() ! && overrideDependenciesRaw.trim() ! ) { dependenciesToParse overrideDependenciesRaw; logMessage ` Campo ${fieldName} (ID: ${fieldContainerId}): **Usando dependencias de SOBREESCRITURA (data-dependencies).**`; } else if (baseDependenciesRaw && baseDependenciesRaw.trim() ! && baseDependenciesRaw.trim() ! ) { dependenciesToParse baseDependenciesRaw; logMessage ` Campo ${fieldName} (ID: ${fieldContainerId}): **Usando dependencias BASE (data-base-dependencies).**`; } else { console.log(` Campo ${fieldName} (ID: ${fieldContainerId}): Sin dependencias definidas.`); // Si no hay dependencias, el campo se muestra con su estado original. // Asegurarse de que el asterisco se muestre si es originalmente requerido y no existe. if (originalRequired && asteriskSpan.length 0 && labelElement.length) { labelElement.append(span classtext-danger required-asterisk-dynamic>*/span>); } else if (!originalRequired && asteriskSpan.length) { asteriskSpan.hide(); } else if (originalRequired && asteriskSpan.length) { asteriskSpan.show(); } console.log(` Campo ${fieldName}: Visible, habilitado, requerido: ${originalRequired}.`); return; // No hay dependencias que procesar, salir de este campo. } try { console.log(logMessage); const dependencies JSON.parse(dependenciesToParse); console.log(`Campo ${fieldName} (ID: ${fieldContainerId}): Dependencias parseadas:`, JSON.stringify(dependencies, null, 2)); if (dependencies.length 0) { console.log(`Campo ${fieldName} (ID: ${fieldContainerId}): Dependencias vacías parseadas. Restaurando a estado original.`); mainInteractiveElement.prop(required, originalRequired); if (originalRequired && asteriskSpan.length 0 && labelElement.length) { labelElement.append(span classtext-danger required-asterisk-dynamic>*/span>); } else if (!originalRequired && asteriskSpan.length) { asteriskSpan.hide(); } else if (originalRequired && asteriskSpan.length) { asteriskSpan.show(); } return; } // ✅ FILTRAR DEPENDENCIAS POR TIPO DE ACCIÓN const showDependencies dependencies.filter(dep > dep.action_type show); const hideDependencies dependencies.filter(dep > dep.action_type hide); const enableDependencies dependencies.filter(dep > dep.action_type enable); const disableDependencies dependencies.filter(dep > dep.action_type disable); const readonlyDependencies dependencies.filter(dep > dep.action_type readonly); const requireDependencies dependencies.filter(dep > dep.action_type require); const unrequireDependencies dependencies.filter(dep > dep.action_type unrequire); console.groupCollapsed(`Evaluando reglas para ${fieldName} (ID: ${fieldContainerId})`); // --- EVALUACIÓN DE VISIBILIDAD --- // Corrección: Asegurar que el campo sea visible por defecto si no hay reglas de ocultamiento o si ninguna se cumple if (showDependencies.length > 0) { const anyShowConditionMet showDependencies.some(dep > evaluateCondition(dep, fieldName)); finalShouldBeVisible anyShowConditionMet; console.log(`EVAL Reglas show: Alguna condición cumplida: ${anyShowConditionMet}`); } else if (hideDependencies.length > 0) { const anyHideConditionMet hideDependencies.some(dep > evaluateCondition(dep, fieldName)); finalShouldBeVisible !anyHideConditionMet; // Mostrar si NINGUNA condición hide se cumple console.log(`EVAL Reglas hide: Alguna condición cumplida: ${anyHideConditionMet}`); } else { // Si no hay reglas show ni hide, respetar el estado inicial de visibilidad finalShouldBeVisible !fieldContainer.hasClass(d-none); console.log(`EVAL Sin reglas show ni hide: Usando visibilidad inicial: ${finalShouldBeVisible ? VISIBLE : OCULTO}`); } console.log(`EVAL Visibilidad final: ${finalShouldBeVisible ? VISIBLE : OCULTO}`); // --- EVALUACIÓN DE HABILITACIÓN/DESHABILITACIÓN/SOLO LECTURA --- // Primero, determinamos si debe estar deshabilitado (highest priority for disable) if (disableDependencies.length > 0) { const anyDisableConditionMet disableDependencies.some(dep > evaluateCondition(dep, fieldName)); if (anyDisableConditionMet) { finalShouldBeDisabled true; console.log(`EVAL Habilitación: Marcado para DESHABILITAR por regla disable.`); } } // Segundo, si no está deshabilitado, evaluamos si debe estar habilitado o si hay reglas de enable if (!finalShouldBeDisabled) { if (enableDependencies.length > 0) { const anyEnableConditionMet enableDependencies.some(dep > evaluateCondition(dep, fieldName)); if (!anyEnableConditionMet) { // Si hay reglas enable pero ninguna se cumple, entonces debe deshabilitarse finalShouldBeDisabled true; console.log(`EVAL Habilitación: Marcado para DESHABILITAR porque ninguna regla enable se cumple.`); } else { console.log(`EVAL Habilitación: Habilitado por regla enable.`); } } // Si no hay reglas de enable/disable explícitas, finalShouldBeDisabled permanece false (habilitado por defecto) // Tercero, si sigue sin estar deshabilitado, evaluamos si debe ser solo lectura if (!finalShouldBeDisabled && readonlyDependencies.length > 0) { const anyReadonlyConditionMet readonlyDependencies.some(dep > evaluateCondition(dep, fieldName)); if (anyReadonlyConditionMet) { finalShouldBeReadonly true; console.log(`EVAL Habilitación: Marcado como SOLO LECTURA por regla readonly.`); } } } console.log(`EVAL Estado final de interacción: Deshabilitado: ${finalShouldBeDisabled}, Solo Lectura: ${finalShouldBeReadonly}`); // --- Aplicar Visibilidad, Habilitación y Solo Lectura --- if (finalShouldBeVisible) { fieldContainer.show().removeClass(d-none hidden-by-dependency); if (finalShouldBeDisabled) { // El campo debe estar deshabilitado (y por lo tanto su valor no se envía) mainInteractiveElement.prop(disabled, true); mainInteractiveElement.prop(readonly, false); // Asegurarse de que no sea readonly mainInteractiveElement.prop(required, false); // Deshabilitado implica no requerido mainInteractiveElement.val(); // Limpiar el valor si el campo está deshabilitado fieldContainer.removeClass(required-by-dependency); if (asteriskSpan.length) { asteriskSpan.hide(); } console.log(`APLICAR Campo ${fieldName}: Visible, DESHABILITADO. Valor NO se enviará.`); } else if (finalShouldBeReadonly) { // El campo debe ser solo lectura (y su valor sí se envía) mainInteractiveElement.prop(disabled, false); // No deshabilitado mainInteractiveElement.prop(readonly, true); // Pero sí solo lectura // Un campo readonly PUEDE ser requerido. Su estado required se decide más abajo. // Por ahora, solo nos aseguramos de que no esté disabled. console.log(`APLICAR Campo ${fieldName}: Visible, SOLO LECTURA. Valor SÍ se enviará.`); } else { // El campo está visible y completamente habilitado para interacción mainInteractiveElement.prop(disabled, false); mainInteractiveElement.prop(readonly, false); console.log(`APLICAR Campo ${fieldName}: Visible y HABILITADO para interacción.`); } } else { // El campo no debe ser visible (OCULTO) fieldContainer.hide().addClass(d-none hidden-by-dependency); mainInteractiveElement.prop(disabled, false); // IMPORTANTE: NO disabled si quieres que el valor se envíe mainInteractiveElement.prop(readonly, false); // No readonly mainInteractiveElement.prop(required, false); // Si se oculta, no es requerido para el navegador // No limpiamos el valor aquí si quieres que los valores de campos ocultos se envíen // mainInteractiveElement.val(); // COMENTADO para que el valor de campos ocultos se envíe fieldContainer.removeClass(required-by-dependency); mainInteractiveElement.removeClass(is-invalid); fieldContainer.find(.invalid-feedback).remove(); if (asteriskSpan.length) { asteriskSpan.hide(); } console.log(`APLICAR Campo ${fieldName}: OCULTO. Valor SÍ se enviará (si no está disabled por otra regla).`); console.groupEnd(); // Si el campo está oculto, aun así queremos que se envíe su valor si no fue deshabilitado explícitamente // Por lo tanto, no retornamos aquí inmediatamente si no está disabled. } // --- EVALUACIÓN Y APLICACIÓN DE REQUERIMIENTO (Solo si no está DESHABILITADO) --- // Un campo oculto o readonly puede ser considerado requerido por el backend // El navegador ya no lo validará si está oculto o readonly. // La validación real de requerido para el envío ocurrirá en el backend. if (!finalShouldBeDisabled) { // Solo si el campo no está DISABLED (su valor se enviará) console.log(`EVAL Requerimiento: ${requireDependencies.length} reglas require, ${unrequireDependencies.length} reglas unrequire.`); const isMadeRequiredByDependency requireDependencies.some(dep > evaluateCondition(dep, fieldName)); const isUnrequiredByDependency unrequireDependencies.some(dep > evaluateCondition(dep, fieldName)); // Lógica de prioridad para `required`: // 1. `unrequire` tiene la máxima prioridad si se cumple. if (isUnrequiredByDependency) { finalShouldBeRequired false; } // 2. Si no es `unrequired`, `require` tiene prioridad. else if (isMadeRequiredByDependency) { finalShouldBeRequired true; } // 3. Si ninguna regla de requerimiento/no-requerimiento se cumple, se mantiene el estado original. else { finalShouldBeRequired originalRequired; } // Aplicar el estado `required` final para la validación HTML5 // Esto solo afecta al navegador; el backend es la fuente de verdad. mainInteractiveElement.prop(required, finalShouldBeRequired); // Manejo visual del asterisco if (finalShouldBeRequired) { fieldContainer.addClass(required-by-dependency); if (asteriskSpan.length) { asteriskSpan.show(); } else if (labelElement.length) { labelElement.append(span classtext-danger required-asterisk-dynamic>*/span>); } console.log(`APLICAR Campo ${fieldName}: Marcado como requerido (${finalShouldBeRequired}) para HTML5.`); } else { fieldContainer.removeClass(required-by-dependency); if (asteriskSpan.length) { asteriskSpan.hide(); } console.log(`APLICAR Campo ${fieldName}: Marcado como NO requerido (${finalShouldBeRequired}) para HTML5.`); } } else { // Si el campo está DISABLED, no puede ser requerido por HTML5. // El asterisco ya debería estar oculto desde la sección de visibilidad/habilitación. mainInteractiveElement.prop(required, false); fieldContainer.removeClass(required-by-dependency); console.log(`APLICAR Campo ${fieldName}: No requerido (campo deshabilitado).`); } console.groupEnd(); } catch (e) { console.error(`Error al parsear dependencias para ${fieldContainerId}:`, e); // En caso de error, restaurar a los valores originales. // Aquí, si el campo estaba inicialmente oculto por PHP, debería permanecer oculto. // Por lo tanto, no llamamos a .show() incondicionalmente. // La clase d-none ya debería estar presente si PHP la añadió. // Si no estaba inicialmente oculto, se asume visible. // No se necesita `fieldContainer.show()` aquí si queremos respetar el estado inicial. mainInteractiveElement.prop(disabled, false); mainInteractiveElement.prop(readonly, false); mainInteractiveElement.prop(required, originalRequired); fieldContainer.removeClass(required-by-dependency); if (originalRequired && asteriskSpan.length 0 && labelElement.length) { labelElement.append(span classtext-danger required-asterisk-dynamic>*/span>); } else if (!originalRequired && asteriskSpan.length) { asteriskSpan.hide(); } else if (originalRequired && asteriskSpan.length) { asteriskSpan.show(); } console.log(`APLICAR Campo ${fieldName}: Restaurado a estado original debido a error.`); } }); console.log(---------------- updateFieldVisibility finalizado ----------------\n);}function evaluateCondition(condition, dependentFieldId N/A) { if (condition.conditions) { console.log(` DEBUG: Evaluando condición anidada con operador: ${condition.operator}`); if (condition.operator AND) { return condition.conditions.every(subCondition > evaluateCondition(subCondition, dependentFieldId)); } else if (condition.operator OR) { return condition.conditions.some(subCondition > evaluateCondition(subCondition, dependentFieldId)); } } const parentFieldIdentifier condition.parent_field_name || condition.parent_field_id; const operator condition.operator || ; let dependencyValue condition.trigger_value; let $targetField null; let targetHtmlIdOrName null; if (condition.parent_field_name) { targetHtmlIdOrName condition.parent_field_name; } else if (condition.parent_field_id) { // Usa el mapa pre-construido para obtener el ID/Nombre HTML del field_id targetHtmlIdOrName fieldIdToNameMapcondition.parent_field_id; if (!targetHtmlIdOrName) { console.warn(` ADVERTENCIA DEP field_id ${condition.parent_field_id} no encontrado en fieldIdToNameMap para el campo dependiente ${dependentFieldId}. Esto puede causar comportamiento inesperado.`); return false; } } if (targetHtmlIdOrName) { // Primero, intenta encontrar el elemento directamente por su ID o Nombre $targetField $(`#${targetHtmlIdOrName}`); if (!$targetField.length) { $targetField $(`name${targetHtmlIdOrName}`); } // Si aún no se encuentra, podría ser que targetHtmlIdOrName sea el ID del CONTENEDOR // En este caso, busca el elemento interactivo principal dentro de ese contenedor. if (!$targetField.length) { const $containerById $(`#${targetHtmlIdOrName}Container`); // Asumiendo formato de ID de contenedor if ($containerById.length) { $targetField $containerById.find(input, select, textarea).first(); } else { // Si el propio contenedor es el parent_field_id, intenta encontrar el elemento interactivo dentro de él const $containerByDataFieldId $(`data-field-id${condition.parent_field_id}`); if ($containerByDataFieldId.length) { $targetField $containerByDataFieldId.find(input, select, textarea).first(); } } } } if (!$targetField || !$targetField.length) { console.warn(` ADVERTENCIA DEP Campo de dependencia objetivo ${parentFieldIdentifier} (ID/Nombre HTML resuelto: ${targetHtmlIdOrName || N/A}) no encontrado en el DOM para el campo dependiente ${dependentFieldId}. Esto puede causar comportamiento inesperado.`); return false; } let currentValue; // --- Lógica para selects, checkboxes, radios y otros inputs --- if ($targetField.is(select)) { const selectedOption $targetField.find(option:selected); currentValue selectedOption.val(); const currentText selectedOption.text(); if (contains, contains_ci.includes(operator) && typeof dependencyValue string) { const valueMatches compareValues(currentValue, dependencyValue, operator); const textMatches compareValues(currentText, dependencyValue, operator); console.log(` Evaluando (Select): Valor: ${currentValue} (${valueMatches}), Texto: ${currentText} (${textMatches}) para operador ${operator} con ${dependencyValue}`); return valueMatches || textMatches; } } else if ($targetField.is(:checkbox)) { currentValue $targetField.is(:checked); dependencyValue (dependencyValue true || dependencyValue true); } else if ($targetField.is(:radio)) { currentValue $(`inputname${$targetField.attr(name)}:checked`).val(); } else { currentValue $targetField.val(); } if (typeof currentValue ! string && currentValue ! null && currentValue ! undefined) { currentValue String(currentValue); } if (typeof dependencyValue ! string && dependencyValue ! null && dependencyValue ! undefined) { dependencyValue String(dependencyValue); } console.log(` Evaluando: Target ${parentFieldIdentifier} (${operator}) ${dependencyValue} (Valor actual: ${currentValue}) para campo dependiente ${dependentFieldId}`); const result compareValues(currentValue, dependencyValue, operator); console.log(` -> Resultado de la evaluación: ${result}`); return result;}// Nueva función para encapsular la lógica de comparaciónfunction compareValues(value1, value2, operator) { let result; switch (operator) { case : result String(value1).trim() String(value2).trim(); break; case !: result String(value1).trim() ! String(value2).trim(); break; case >: result Number(value1) > Number(value2); break; case : result Number(value1) Number(value2); break; case >: result Number(value1) > Number(value2); break; case : result Number(value1) Number(value2); break; case contains: result value1 && String(value1).includes(String(value2)); break; case contains_ci: // Nuevo operador case-insensitive result value1 && String(value1).toLowerCase().includes(String(value2).toLowerCase()); break; case not_contains: result value1 && !String(value1).includes(String(value2)); break; case starts_with: result value1 && String(value1).startsWith(String(value2)); break; case ends_with: result value1 && String(value1).endsWith(String(value2)); break; case is_empty: result (value1 null || value1 undefined || String(value1).trim() ); break; case is_not_empty: result (value1 ! null && value1 ! undefined && String(value1).trim() ! ); break; default: console.warn(` Operador desconocido: ${operator}.`); result false; break; } return result;}// Función para adjuntar eventos a los campos controladores (sin cambios sustanciales, solo un log de inicio)function attachDependencyListeners() { console.log(LISTENERS Adjuntando listeners para dependencias.); $(#camposDinamicos .mb-3).each(function() { const fieldContainer $(this); const mainInteractiveElement fieldContainer.find(input, select, textarea).first(); const fieldName mainInteractiveElement.attr(name) || mainInteractiveElement.attr(id); if (fieldName) { // Asegúrate de que los listeners se adjunten al elemento interactivo, no al contenedor if (mainInteractiveElement.is(select) || mainInteractiveElement.is(:checkbox) || mainInteractiveElement.is(:radio)) { mainInteractiveElement.off(change.dependency).on(change.dependency, function() { console.log(`EVENTO Cambio detectado en ${fieldName}. Recalculando visibilidad.`); updateFieldVisibility(); }); } else if (mainInteractiveElement.is(inputtypetext) || mainInteractiveElement.is(inputtypenumber) || mainInteractiveElement.is(textarea)) { mainInteractiveElement.off(input.dependency).on(input.dependency, function() { console.log(`EVENTO Input/Keyup detectado en ${fieldName}. Recalculando visibilidad.`); updateFieldVisibility(); }); } } }); console.log(LISTENERS Listeners de dependencia adjuntados.);}// ✅ NUEVA FUNCIÓN: Inicializar valores por defecto para campos dinámicos (especialmente selects como con_polo)function inicializarValoresPorDefecto(defaultValuesFromBackend {}) { console.log(INICIALIZACION Iniciando valores por defecto para campos dinámicos...); // Recorrer todos los contenedores de campos usando el mapeo existente for (const fieldId in fieldIdToNameMap) { const fieldName fieldIdToNameMapfieldId; if (campoExiste(fieldName)) { const $input $(`#${fieldName}`); const valorActual obtenerValorCampo(fieldName); // Obtener valor por defecto del backend (si se proporciona) o de data-attribute const valorPorDefecto defaultValuesFromBackendfieldName || $input.data(default-value) || null; if (!valorActual && valorPorDefecto) { if ($input.is(select)) { // Para selects: priorizar opción con selected del HTML const $defaultOption $input.find(optionselected); if ($defaultOption.length) { $input.val($defaultOption.val()); console.log(`INICIALIZACION Select ${fieldName}: Establecido desde selected HTML: ${$defaultOption.val()}`); } else { // Si no, establecer directamente $input.val(valorPorDefecto); console.log(`INICIALIZACION Select ${fieldName}: Establecido directo: ${valorPorDefecto}`); } } else { // Para otros campos $input.val(valorPorDefecto); console.log(`INICIALIZACION Campo ${fieldName}: Establecido: ${valorPorDefecto}`); } // Disparar evento change para actualizar dependencias (ej. talla_polo) $input.trigger(change); } else if (valorActual) { console.log(`INICIALIZACION Campo ${fieldName}: Ya tiene valor inicial: ${valorActual}`); } else { console.log(`INICIALIZACION Campo ${fieldName}: Sin valor por defecto.`); } } } console.log(INICIALIZACION Valores por defecto completados.);} // --- Función para guardar equipo si no existe ---async function manejarEquipo(equipo) { const cleanedEquipo equipo.toUpperCase().trim(); if (!cleanedEquipo) return; // No procesar equipos vacíos log(`Manejando equipo: ${cleanedEquipo}`); try { const checkResponse await fetch(`../api/verificar_equipo.php?nombre${encodeURIComponent(cleanedEquipo)}`); if (!checkResponse.ok) { const errorText await checkResponse.text(); throw new Error(`HTTP error! status: ${checkResponse.status}, message: ${errorText}`); } const checkData await checkResponse.json(); if (!checkData.exists) { log(`Equipo ${cleanedEquipo} no existe, intentando guardar.`); const saveResponse await fetch(../api/guardar_equipo.php, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded, }, body: `nuevo_equipo${encodeURIComponent(cleanedEquipo)}`, }); if (!saveResponse.ok) { const errorText await saveResponse.text(); throw new Error(`HTTP error! status: ${saveResponse.status}, message: ${errorText}`); } const saveData await saveResponse.json(); if (saveData.success) { log(`✅ Equipo ${cleanedEquipo} guardado exitosamente.`); // Opcional: Añadir el nuevo equipo al select si es necesario const $select $(#equipo.equipo-select); if ($select.length && !$select.find(`optionvalue\${cleanedEquipo}\`).length) { $select.append(`option value\${cleanedEquipo}\>${cleanedEquipo}/option>`); log(Equipo agregado al select local); } } else { console.error(Error al guardar el equipo:, saveData.message); throw new Error(saveData.message || Error al guardar equipo en BD); } } else { log(El equipo ya existe en BD.); } } catch (error) { console.error(❌ Error al manejar el equipo:, error); // ✅ MOSTRAR ERROR AL USUARIO Swal.fire({ icon: warning, title: Advertencia, text: `No se pudo verificar/guardar el equipo ${cleanedEquipo}. La inscripción continuará, pero verifica que el equipo sea correcto.`, confirmButtonText: Entendido }); }} // --- Función para calcular la edad a partir de una fecha de nacimiento --- function calculateAge(birthdateString) { if (!birthdateString) return null; const birthdate new Date(birthdateString + T00:00:00); // Añadir T00:00:00 para evitar problemas de zona horaria if (isNaN(birthdate.getTime())) { console.error(Fecha de nacimiento inválida:, birthdateString); return null; } const today new Date(); let age today.getFullYear() - birthdate.getFullYear(); const m today.getMonth() - birthdate.getMonth(); if (m 0 || (m 0 && today.getDate() birthdate.getDate())) { age--; } log(`Edad calculada: ${age} años para fecha ${birthdateString}`); return age; } // --- Función para cargar distancias desde el backend ---async function cargarDistancias(eventoId) { // Referencias a los elementos del DOM const distanciaSelect $(#distancia_id); const costoSeleccionadoSpan $(#costoSeleccionado); // Verificación inicial del campo de selección de distancia if (!campoExiste(distancia_id)) { log(No hay campo distancia_id en este formulario, saltando carga de distancias.); return; } // Estado inicial del selector y mensaje de carga distanciaSelect.html(option value\\>Cargando distancias.../option>); distanciaSelect.prop(disabled, true); $(#distanciaError).addClass(d-none).text(); // Limpiar errores previos costoSeleccionadoSpan.empty(); // Limpiar el span del costo al iniciar la carga o recarga // Verificar si se proporcionó un ID de evento if (!eventoId) { log(⚠️ No se puede cargar distancias: evento_id no disponible.); distanciaSelect.html(option value\\>No hay evento ID/option>); distanciaSelect.prop(disabled, true); return; } // Obtener edad y género de otros campos del formulario const edad obtenerValorCampo(edad); const genero obtenerValorCampo(genero); try { log(`Cargando distancias para evento ID: ${eventoId} (Edad: ${edad}, Género: ${genero})`); // Preparar parámetros de la URL para la solicitud const params new URLSearchParams(); params.append(evento_id, eventoId); if (edad ! null && edad ! ) params.append(edad, edad); if (genero) params.append(genero, genero); const url `../api/distancias_por_evento.php?${params.toString()}`; log(Fetching distancias desde:, url); // Realizar la solicitud a la API const response await fetch(url); if (!response.ok) { const errorText await response.text(); throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } const data await response.json(); log(Respuesta de distancias_por_evento.php:, data); // Vaciar el selector antes de añadir nuevas opciones distanciaSelect.empty(); // Mapa para almacenar los costos de las distancias por su ID const distanciasMap new Map(); // Procesar las distancias obtenidas de la API if (data.success && data.distancias && data.distancias.length > 0) { distanciaSelect.append(option value\\>Seleccionar distancia/option>); data.distancias.forEach(distancia > { let costoDisplayEnOpcion; // Formatear el texto del costo para la opción del select if (parseFloat(distancia.costo) 0) { costoDisplayEnOpcion Gratis; } else { costoDisplayEnOpcion `S/. ${distancia.costo}`; } // Construir el texto completo para la opción del select // NO usar spans aquí si quieres compatibilidad total de color en todos los navegadores const optionText `${distancia.nombre} (${distancia.valor_km} km) - Costo: ${costoDisplayEnOpcion}`; distanciaSelect.append(`option value${distancia.id}>${optionText}/option>`); // Guardar el costo asociado al ID de la distancia para buscarlo más tarde distanciasMap.set(String(distancia.id), distancia.costo); }); distanciaSelect.prop(disabled, false); log(✅ Distancias cargadas y select actualizado.); } else { // Manejo de caso donde no hay distancias disponibles distanciaSelect.append(option value\\>No hay distancias disponibles para tu categoría/option>); distanciaSelect.prop(disabled, true); $(#distanciaError).text(data.message || No se encontraron distancias disponibles.).removeClass(d-none); log(No se encontraron distancias o la respuesta no fue exitosa., data.message); } // Añadir un evento change al selector de distancia // Este evento se dispara cuando el usuario selecciona una opción. distanciaSelect.off(change).on(change, function() { const selectedDistanciaId $(this).val(); if (selectedDistanciaId) { const costo distanciasMap.get(selectedDistanciaId); let costoDisplayEnSpan ; // Formatear el texto del costo para el span (aquí sí podemos aplicar color) if (parseFloat(costo) 0) { costoDisplayEnSpan span stylecolor: green;>Gratis/span>; } else { costoDisplayEnSpan `span stylecolor: green;>S/. ${costo}/span>`; } // Actualizar el contenido del span con el costo formateado y en color verde costoSeleccionadoSpan.html(`Costo de la distancia: ${costoDisplayEnSpan}`); } else { // Limpiar el span si no hay una selección válida (ej. Seleccionar distancia) costoSeleccionadoSpan.empty(); } }); } catch (error) { // Manejo de errores durante la carga de distancias console.error(❌ Error al cargar distancias:, error); distanciaSelect.html(option value\\>Error al cargar distancias/option>); distanciaSelect.prop(disabled, true); $(#distanciaError).text(`Error al cargar distancias: ${error.message}`).removeClass(d-none); costoSeleccionadoSpan.empty(); // Limpiar el span del costo también en caso de error }} // --- Función optimizada para cargar y mostrar dorsales ---async function cargarDorsales() { if (!campoExiste(dorsal)) { log(No hay campo dorsal en este formulario, saltando carga de dorsales.); return; } const eventoId $(#modal_evento_id).val(); const distanciaId $(#distancia_id).val(); const infoDorsales $(#infoDorsales); const contenedorDorsales $(#contenedorDorsales); infoDorsales.text(Cargando dorsales disponibles...); contenedorDorsales.empty(); if (!eventoId) { infoDorsales.text(Selecciona un evento válido para ver los dorsales.); log(⚠️ No se pueden cargar dorsales: evento_id no disponible.); return; } try { log(`Cargando dorsales para Evento ID: ${eventoId}, Distancia ID: ${distanciaId}`); // Usar directamente la API mejorada que maneja rangos y stock const url `../api/dorsales_disponibles.php?evento_id${eventoId}&distancia_id${distanciaId}`; const response await fetch(url); if (!response.ok) { const errorText await response.text(); throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } const data await response.json(); log(Respuesta de la API:, data); // Manejar diferentes escenarios if (data.todos_ocupados) { infoDorsales.text(Todos los dorsales están ocupados o bloqueados para esta distancia.); mostrarAlertaDorsalesNoDisponibles(); return; } if (data.sin_cupos) { infoDorsales.text(No hay cupos disponibles para esta distancia.); mostrarAlertaDorsalesNoDisponibles(); return; } if (data.success && data.dorsales && data.dorsales.length > 0) { // Construir mensaje informativo let mensaje `Dorsales disponibles: ${data.total_disponibles}`; if (data.usando_rango_general) { mensaje + ` (Rango general del evento: ${data.rango_min}-${data.rango_max})`; } else { mensaje + ` (Rango específico: ${data.rango_min}-${data.rango_max})`; } if (data.usando_stock_evento) { mensaje + ` - Cupos restantes: ${data.cupos_restantes}`; } infoDorsales.text(mensaje); // Ordenar y mostrar dorsales data.dorsales.sort((a, b) > a - b).forEach(dorsalNum > { const dorsalItem $(`button typebutton classdorsal-item btn btn-outline-primary btn-sm m-1>`) .text(dorsalNum) .data(dorsal, dorsalNum) .click(function() { $(#dorsal).val(dorsalNum).trigger(change); $(#modalDorsales).modal(hide); }); contenedorDorsales.append(dorsalItem); }); log(✅ Dorsales cargados correctamente.); } else { infoDorsales.text(data.message || No hay dorsales disponibles.); log(No se encontraron dorsales disponibles., data); } } catch (error) { console.error(❌ Error al cargar dorsales:, error); infoDorsales.text(Error al cargar dorsales.); Swal.fire({ icon: error, title: Error, text: No se pudieron cargar los dorsales. Por favor intenta nuevamente. }); }}function mostrarAlertaDorsalesNoDisponibles() { Swal.fire({ icon: warning, title: Sin dorsales disponibles, text: No hay dorsales disponibles para esta distancia. Por favor selecciona otra opción., timer: 3000 });} // --- Configurar todos los eventos de la página --- function configurarEventos() { // --- Actualizar edad al cambiar fecha de nacimiento --- if (campoExiste(fecha_nacimiento) && campoExiste(edad)) { $(#fecha_nacimiento).on(change, function() { log(Fecha de nacimiento cambiada:, this.value); const edad calculateAge(this.value); if (edad ! null) { $(#edad).val(edad); $(#edad).trigger(change); // Disparar change para que otros listeners reaccionen } else { $(#edad).val(); } }); } // --- Recargar distancias cuando cambie edad o género --- const fieldsToWatchForDistances ; if (campoExiste(edad)) fieldsToWatchForDistances.push(#edad); if (campoExiste(genero)) fieldsToWatchForDistances.push(#genero); // Asegúrate de que el cambio en la distancia_id también dispare updateFieldVisibility si es necesario if (campoExiste(distancia_id)) fieldsToWatchForDistances.push(#distancia_id); if (fieldsToWatchForDistances.length > 0) { $(fieldsToWatchForDistances.join(, )).on(change, function() { log(`Cambio en ${this.id} (${this.value}). Recargando distancias y/o re-evaluando visibilidad.`); const eventoId $(#modal_evento_id).val(); if (eventoId && (this.id edad || this.id genero)) { cargarDistancias(eventoId); } updateFieldVisibility(); // Reevaluar visibilidad si el cambio afecta dependencias }); } // --- Manejar el clic en el botón para seleccionar dorsal --- $(#btnSeleccionarDorsal).on(click, function() { if (campoExiste(dorsal) && campoExiste(distancia_id) && campoExiste(modal_evento_id)) { const distanciaSeleccionada $(#distancia_id).val(); if (!distanciaSeleccionada) { Swal.fire({ icon: warning, title: Selecciona una distancia, text: Por favor, selecciona una distancia antes de elegir un dorsal. }); return; } log(Mostrando modal de dorsales.); cargarDorsales(); // Carga los dorsales antes de mostrar el modal $(#modalDorsales).modal(show); } else { log(Intento de abrir modal de dorsales pero faltan campos requeridos (dorsal, distancia_id, o modal_evento_id).); } }); // --- Seleccionar dorsal en el modal de dorsales --- $(#contenedorDorsales).on(click, .dorsal-item, function() { const selectedDorsal $(this).data(dorsal); $(#dorsal).val(selectedDorsal); $(#dorsalSeleccionado).html(` div stylefont-weight:600; color:#0c3c78; font-size:16px;> Dorsal seleccionado: /div> div stylefont-weight:700; color:#0a58ca; font-size:24px;> ${selectedDorsal} /div> `); // Solo oculta el modal de dorsales, no todos $(#modalDorsales).modal(hide); log(`✅ Dorsal ${selectedDorsal} seleccionado y modal de dorsales cerrado.`); }); // Listener para el evento de cierre del modal de dorsales $(#modalDorsales).on(hidden.bs.modal, function () { log(Modal de dorsales cerrado.); }); // --- Listener para el evento de mostrar el modal de inscripción --- $(#inscripcionModal).on(shown.bs.modal, function() { log(Modal de inscripción mostrado. Inicializando elementos y cargando distancias.); // Asegura que las animaciones de los campos dinámicos se reinicien al abrir el modal $(#camposDinamicos > div).each(function(index) { $(this).removeClass(animate__fadeInUp); void this.offsetWidth; // Trigger reflow $(this).css(animation-delay, `0.5s`); $(this).addClass(animate__fadeInUp); }); // Cargar distancias iniciales (si el campo existe) const eventoId $(#modal_evento_id).val(); if (eventoId && campoExiste(distancia_id)) { cargarDistancias(eventoId); } // Calcular edad inicialmente si fecha_nacimiento tiene valor if (campoExiste(fecha_nacimiento) && $(#fecha_nacimiento).val()) { const edad calculateAge($(#fecha_nacimiento).val()); if (edad ! null) { $(#edad).val(edad); } } updateFieldVisibility(); // Asegurarse de que la visibilidad inicial sea correcta inicializarValoresPorDefecto(); }); // --- Listener para el evento de ocultar el modal de inscripción --- $(#inscripcionModal).on(hidden.bs.modal, function() { log(Modal de inscripción ocultado. Reseteando formulario y campos.); $(#inscripcionForm)0.reset(); // Restablece el formulario // Quita las clases de validación de Bootstrap $(#inscripcionForm).find(.is-invalid, .is-valid).removeClass(is-invalid is-valid); $(#inscripcionForm).find(.invalid-feedback, .valid-feedback).remove(); // Quitar mensajes de error $(.mb-3).removeClass(field-valid field-invalid); // Limpiar clases de contenedor // Re-evalúa la visibilidad para limpiar valores de campos ocultos updateFieldVisibility(); // Restablece el estado del botón de envío const enviarBtn $(#enviarInscripcion); enviarBtn.html(Enviar Inscripción); enviarBtn.prop(disabled, false); }); // ✅ VALIDACIÓN EN TIEMPO REAL MEJORADA $(#inscripcionForm).on(input change, input:not(type\hidden\), select, textarea, function() { const $input $(this); const isRequired this.hasAttribute(required) || $input.data(required-if-visible); const $container $input.closest(.mb-3); // Solo validar si el campo es requerido y está visible if (isRequired && $container.is(:visible) && !$container.hasClass(hidden-by-dependency)) { let esValido false; let mensajeError ; if (this.type checkbox) { esValido this.checked; mensajeError esValido ? : Este campo es obligatorio; } else if (this.type radio) { const name this.name; esValido $(`inputname\${name}\:checked`).length > 0; mensajeError esValido ? : Debe seleccionar una opción; } else { const valor $input.val().trim(); esValido valor.length > 0; mensajeError esValido ? : Este campo es obligatorio; } aplicarEstilosValidacion(this, esValido, mensajeError); } }); // ✅ FUNCIÓN PARA OBTENER EL NOMBRE DEL EVENTO async function obtenerNombreEvento(eventoId) { if (!eventoId) return Evento no especificado; try { const response await fetch(`../api/obtener_evento.php?id${eventoId}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data await response.json(); if (data.success && data.evento && data.evento.titulo) { return data.evento.titulo; } else { return Evento no encontrado; } } catch (error) { console.error(Error al obtener nombre del evento:, error); return Error al cargar evento; } } // --- Manejar el envío del formulario de inscripción CON CONFIRMACIÓN ---$(#enviarInscripcion).on(click, async function(event) { event.preventDefault(); log(Iniciando proceso de confirmación de inscripción.); // ✅ PASO 1: OBTENER INFORMACIÓN PARA LA CONFIRMACIÓN const eventoId $(#modal_evento_id).val(); const distanciaSelect $(#distancia_id); const dorsalInput $(#dorsal); // ✅ OBTENER NOMBRE DEL EVENTO const nombreEvento await obtenerNombreEvento(eventoId); // Obtener texto de la distancia seleccionada let distanciaTexto No seleccionada; if (campoExiste(distancia_id) && distanciaSelect.val()) { const selectedOption distanciaSelect.find(option:selected); distanciaTexto selectedOption.text(); } // Obtener valor del dorsal si existe let dorsalValor ; if (campoExiste(dorsal) && dorsalInput.val()) { dorsalValor dorsalInput.val(); } // ✅ PASO 2: CONSTRUIR MENSAJE DE CONFIRMACIÓN CON NOMBRE DEL EVENTO let htmlContent ` div classtext-start> p classmb-3>Estás a punto de inscribirte con los siguientes datos:/p> div classalert alert-primary stylebackground-color: #e3f2fd; border-color: #2196F3; color: #1976d2;> p>strong>🏆 EVENTO:/strong> span stylefont-weight: bold; font-size: 1.1em; color: #1565c0;>${nombreEvento}/span>/p> p>strong>🏁 Distancia:/strong> ${distanciaTexto}/p> `; if (dorsalValor) { htmlContent + `p>strong>🏃♂️ Dorsal seleccionado:/strong> ${dorsalValor}/p>`; } htmlContent + ` /div> p classtext-muted>¿Confirmas que deseas proceder con la inscripción?/p> /div> `; // ✅ PASO 3: MOSTRAR SWEETALERT DE CONFIRMACIÓN const confirmResult await Swal.fire({ title: Confirmar Inscripción, html: htmlContent, icon: question, showCancelButton: true, confirmButtonColor: #2196F3, cancelButtonColor: #6c757d, confirmButtonText: Sí, inscribirme, cancelButtonText: Cancelar, customClass: { popup: swal-wide }, allowOutsideClick: false, allowEscapeKey: true }); // ✅ PASO 4: SI NO CONFIRMA, SALIR if (!confirmResult.isConfirmed) { log(Usuario canceló la inscripción.); return; } // ✅ PASO 5: SI CONFIRMA, PROCEDER CON LA VALIDACIÓN Y ENVÍO log(Usuario confirmó la inscripción. Procediendo con validación y envío.); // ✅ MOSTRAR SWEETALERT DE CARGA INMEDIATAMENTE DESPUÉS DE CONFIRMAR Swal.fire({ title: Procesando inscripción..., html: Por favor espera mientras procesamos tu inscripción., icon: info, allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () > { Swal.showLoading(); } }); const btn $(this); const originalBtnText btn.html(); btn.html(span class\spinner-border spinner-border-sm\ role\status\ aria-hidden\true\>/span> Procesando...); btn.prop(disabled, true); // --- ✅ VALIDACIÓN MEJORADA DE CAMPOS VISIBLES Y HABILITADOS --- let isValid true; let errorMessagesHtml ; // Para acumular mensajes de error en HTML // Limpiar validaciones previas de todos los campos del formulario $(#inscripcionForm).find(.is-invalid, .is-valid).removeClass(is-invalid is-valid); $(#inscripcionForm).find(.invalid-feedback, .valid-feedback).remove(); $(#inscripcionForm).find(.mb-3).removeClass(field-valid field-invalid); $(#inscripcionForm).find(.mb-3).each(function() { // Iteramos sobre TODOS los contenedores de campo const $container $(this); const $input $container.find(input:not(type\hidden\), select, textarea).first(); if ($input.length 0) return; // Saltar si no hay input interactivo en este contenedor const fieldName $input.attr(name) || $input.attr(id) || Campo; // Elimina el asterisco del label para el mensaje de error const fieldLabel $container.find(label).first().text().replace(*, ).trim() || fieldName; // --- Determinar el estado REAL del campo para la validación frontend --- const isHiddenByDependency $container.hasClass(hidden-by-dependency); const isDisabled $input.prop(disabled); // const isReadonly $input.prop(readonly); // Readonly no afecta la validación de required HTML5 // Un campo SÓLO debe ser validado como requerido por esta función (frontend) // si NO está oculto por dependencia Y NO está deshabilitado, Y si tiene el atributo `required` // (el cual `updateFieldVisibility` ya maneja dinámicamente). const isRequiredForFrontendValidation $input.prop(required) && !isHiddenByDependency && !isDisabled; // Siempre limpiar estilos de validación antes de aplicar nuevos (si aplica) aplicarEstilosValidacion($input0, true, ); // Limpia cualquier error previo para este campo if (isRequiredForFrontendValidation) { let fieldValid false; let errorMessage ; if ($input.is(:checkbox)) { fieldValid $input.is(:checked); errorMessage fieldValid ? : `${fieldLabel} es obligatorio.`; } else if ($input.is(:radio)) { const name $input.attr(name); fieldValid $(`inputname\${name}\:checked`).length > 0; errorMessage fieldValid ? : `Debe seleccionar una opción en ${fieldLabel}.`; } else { const value $input.val(); fieldValid value ! null && value ! undefined && value.toString().trim() ! ; errorMessage fieldValid ? : `${fieldLabel} es obligatorio.`; } aplicarEstilosValidacion($input0, fieldValid, errorMessage); if (!fieldValid) { isValid false; errorMessagesHtml + `li>${errorMessage}/li>`; } } }); if (!isValid) { // ✅ CERRAR EL SWEETALERT DE CARGA ANTES DE MOSTRAR ERROR Swal.close(); btn.html(originalBtnText); btn.prop(disabled, false); Swal.fire({ icon: error, title: Campos Incompletos, html: `p>Por favor, completa los siguientes campos obligatorios:/p>ul classtext-start>${errorMessagesHtml}/ul>`, confirmButtonText: Entendido, customClass: { popup: swal-wide } }); return; // Detener el envío del formulario si hay errores de validación frontend } // --- ✅ PREPARAR DATOS DEL FORMULARIO --- try { const formData new FormData(); // Agregar todos los campos interactivos que NO están deshabilitados // Campos readonly y ocultos (sin disabled) SÍ se incluyen. $(#inscripcionForm).find(input:not(type\hidden\), select, textarea).each(function() { const $input $(this); const name this.name; // Solo incluir el campo si tiene un atributo name y NO está deshabilitado if (name && !$input.prop(disabled)) { if (this.type checkbox) { // Para checkboxes, siempre envía 0 o 1 formData.append(name, this.checked ? 1 : 0); } else if (this.type radio) { if (this.checked) { // Solo si el radio button está seleccionado formData.append(name, this.value); } } else if (this.type file) { if (this.files.length > 0) { formData.append(name, this.files0); } } else { formData.append(name, this.value); } } }); // Agregar campos hidden (normalmente siempre se envían si tienen un name) $(#inscripcionForm).find(inputtype\hidden\).each(function() { if (this.name) { formData.append(this.name, this.value); } }); // ✅ MANEJAR CAMPO EQUIPO ESPECIAL (si existe y tiene valor) // Asegúrate de que manejarEquipo sea una función asíncrona si realiza operaciones asíncronas if (campoExiste(equipo)) { // window.getEquipoValue es una buena práctica si equipo tiene lógica compleja // Si es un campo simple, puedes usar directo $(#equipo).val(); const equipoValue window.getEquipoValue ? window.getEquipoValue() : $(#equipo).val(); if (equipoValue) { formData.set(equipo, equipoValue); // Usa set para asegurar que solo haya una entrada // Si manejarEquipo hace algo que modifica el formData o es esencial antes del envío // y es asíncrono, debería ser await. Si solo es una función auxiliar, no necesita await. // await manejarEquipo(equipoValue); } } log(Datos del formulario preparados:, Object.fromEntries(formData.entries())); // --- ✅ ENVIAR FORMULARIO VIA AJAX (USANDO FETCH) --- const response await fetch(../api/procesar_inscripcion.php, { method: POST, body: formData }); if (!response.ok) { const errorText await response.text(); // Intenta leer el texto del error // Asume que el backend podría enviar un JSON con message incluso en un error 400 let errorMessageFromBackend Error desconocido del servidor.; try { const errorJson JSON.parse(errorText); errorMessageFromBackend errorJson.message || errorMessageFromBackend; // Si el backend envía errores de validación específicos, los manejamos aquí if (errorJson.errors) { let backendValidationErrorsHtml ul classtext-start>; for (const fieldName in errorJson.errors) { // Opcional: aplicar estilos de validación del backend si tienes los campos correspondientes en el DOM // const $inputWithError $(`name${fieldName}`); // if ($inputWithError.length) { // aplicarEstilosValidacion($inputWithError0, false, errorJson.errorsfieldName); // } backendValidationErrorsHtml + `li>${errorJson.errorsfieldName}/li>`; } backendValidationErrorsHtml + /ul>; errorMessageFromBackend `p>${errorMessageFromBackend}/p>${backendValidationErrorsHtml}`; } } catch (e) { // No es un JSON, usar el texto plano del error errorMessageFromBackend `Error del servidor: ${errorText}`; } throw new Error(`HTTP error! status: ${response.status}. ${errorMessageFromBackend}`); } const data await response.json(); log(Respuesta del servidor:, data); if (data.success) { // ✅ MOSTRAR SWEETALERT DE ÉXITO const nombre obtenerValorCampo(nombre_completo) || obtenerValorCampo(nombre); const genero obtenerValorCampo(genero); const dorsal obtenerValorCampo(dorsal); let nombreDistancia No especificada; if (data.user_data?.distancia?.nombre_completo) { nombreDistancia data.user_data.distancia.nombre_completo; } else if (campoExiste(distancia_id)) { const selectedOption $(#distancia_id option:selected); if (selectedOption.length && selectedOption.val()) { nombreDistancia selectedOption.text(); } } let emailInfo data.email_info || null; const saludoGenero genero Femenino ? bienvenida : bienvenido; const saludo nombre ? `, ${saludoGenero} strong>${nombre}/strong>` : ; let secciones ; if (nombreDistancia && nombreDistancia ! No seleccionada) { // Evitar No seleccionada si no hay valor real secciones.push(`p>strong>🏁 Distancia:/strong> ${nombreDistancia}/p>`); } if (dorsal) { secciones.push(`p>strong>🏃♂️ Dorsal:/strong> ${dorsal}/p>`); } if (emailInfo?.enviado) { secciones.push(`p classtext-success>i classfas fa-check-circle>/i> Correo de confirmación enviado a: ${emailInfo.destinatario}/p>`); } else if (emailInfo && !emailInfo.enviado) { secciones.push(`p classtext-warning>i classfas fa-exclamation-triangle>/i> ${emailInfo.mensaje}/p>`); } const seccionesHTML secciones.length > 0 ? secciones.join() : p class\text-info\>Inscripción procesada correctamente./p>; const contenidoCompleto ` div classtext-start> p classmb-3>¡Inscripción exitosa${saludo}! 🎉/p> ${seccionesHTML} hr> p classtext-muted small> i classfas fa-camera>/i> Puedes tomar una captura de pantalla de esta información. /p> /div> `; log(Contenido final del SweetAlert:, contenidoCompleto); Swal.fire({ icon: success, title: ¡Inscripción Realizada!, html: contenidoCompleto, showConfirmButton: true, allowOutsideClick: false, allowEscapeKey: false, confirmButtonText: Aceptar y Cerrar, customClass: { popup: swal-wide } }).then((result) > { if (result.isConfirmed) { $(#inscripcionModal).modal(hide); location.reload(); } }); } else { log(Error en la inscripción (backend), data); // Mostrar errores específicos del backend si existen let backendErrorsHtml ; if (data.errors) { backendErrorsHtml ul classtext-start>; for (const fieldName in data.errors) { backendErrorsHtml + `li>${data.errorsfieldName}/li>`; // Opcional: aplicar estilos de validación del backend a los campos frontend // const $inputWithError $(`name${fieldName}`); // if ($inputWithError.length) { // aplicarEstilosValidacion($inputWithError0, false, data.errorsfieldName); // } } backendErrorsHtml + /ul>; } Swal.fire({ icon: error, title: Error en la inscripción, html: `p>${data.message || Ocurrió un error inesperado al procesar tu inscripción. Inténtalo de nuevo.}/p>${backendErrorsHtml}`, confirmButtonText: Entendido, customClass: { popup: swal-wide } }); } } catch (error) { console.error(❌ Error al enviar el formulario (fetch o parseo):, error); Swal.fire({ icon: error, title: Error de comunicación o validación, html: `Hubo un problema al comunicarse con el servidor o al procesar la respuesta: br>em>${error.message || Error desconocido.}/em>br>Por favor, inténtalo de nuevo.`, confirmButtonText: Entendido, customClass: { popup: swal-wide } }); } finally { btn.html(originalBtnText); btn.prop(disabled, false); log(Proceso de envío finalizado); }}); } // --- Inicialización al cargar el DOM --- configurarEventos(); setupEquipoAutocomplete(); // ✅ Llamar a la función de autocompletado corregida const eventoIdOnInit $(#modal_evento_id).val(); if (eventoIdOnInit && campoExiste(distancia_id)) { log(Cargando distancias iniciales al cargar la página...); cargarDistancias(eventoIdOnInit); } // Listener para campos dinámicos - cada vez que un campo cambia, re-evaluar la visibilidad // Se adjunta a #camposDinamicos para delegar los eventos $(#camposDinamicos).on(change keyup, input, select, textarea, function() { // Añadido keyup para input de texto log(`Cambio/Keyup detectado en campo dinámico: ${$(this).attr(id) || $(this).attr(name)}. Recalculando visibilidad.`); updateFieldVisibility(); }); $(#camposDinamicos .mb-3).each(function() { const mainInteractiveElement $(this).find(input, select, textarea).first(); if (mainInteractiveElement.length) { // Guardamos si el campo tenía required originalmente en el HTML mainInteractiveElement.data(original-required, mainInteractiveElement.prop(required)); console.log(` Campo ${mainInteractiveElement.attr(name) || mainInteractiveElement.attr(id)}: Originalmente requerido ${mainInteractiveElement.data(original-required)}`); } }); // Llamada inicial para establecer la visibilidad correcta al cargar la página updateFieldVisibility(); attachDependencyListeners(); console.log(Formulario de inscripción listo y dependencias inicializadas.); //const fieldMap inicializarCamposDinamicos(); // Ahora puedes usar fieldMap en otras partes de tu JS para gestionar dependencias, etc. // ... tu código para inicializar el formulario, listeners, etc. ... //console.log(INICIALIZACION Formulario de inscripción listo y dependencias inicializadas.);}); // Cierre de $(document).ready(function() { ... });/script>!-- SweetAlert2 CSS -->link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css>!-- SweetAlert2 JS -->script srchttps://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js>/script>script>// Verificar estado de inscripciones cada 30 segundoslet intervaloCheck setInterval(() > { const path window.location.pathname; const filename path.substring(path.lastIndexOf(/) + 1); if (filename evento.php) { const eventoId document.getElementById(modal_evento_id).value; fetch(`api/check_inscripciones.php?evento_id${eventoId}`) .then(response > response.json()) .then(data > { if (!data.activo) { document.getElementById(enviarInscripcion).disabled true; document.querySelectorAll(#inscripcionForm input, #inscripcionForm select) .forEach(el > el.disabled true); Swal.fire({ icon: info, title: Inscripciones Cerradas, text: Las inscripciones para este evento han sido cerradas por el organizador, confirmButtonText: Entendido }); clearInterval(intervaloCheck); } }); } else { clearInterval(intervaloCheck); }}, 30000);/script>style> /* Estilos generales del body, si no los tienes ya */ /* Si tu archivo principal ya tiene estilos para body, puedes omitir o ajustar estos */ body { font-family: Inter, sans-serif; /* Animación de fondo de gradiente background: linear-gradient(45deg, #f0f4f8, #e6eaf0, #dce0e6);*/ background-size: 400% 400%; animation: gradientAnimation 15s ease infinite; /*display: flex;*/ justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0px; box-sizing: border-box; } @keyframes gradientAnimation { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .main-content-wrapper { /* Nuevo div para envolver todo */ width: 100%; display: flex; justify-content: center; align-items: center; /*min-height: 100vh; Asegura que ocupe al menos toda la altura */ } .gallery-container { max-width: 1200px; width: 100%; margin: 0 auto; padding: 2rem; background-color: #ffffff; border-radius: 1.5rem; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } h2 { text-align: center; font-family: Segoe UI, sans-serif; margin-bottom: 1.5rem; font-size: 2.25rem; /* text-3xl de Tailwind */ font-weight: 800; /* font-extrabold de Tailwind */ color: #1a202c; /* gray-900 de Tailwind */ } .image-grid { display: grid; /* Columnas flexibles: al menos 150px, expandiéndose para llenar el espacio */ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Filas de altura fija para facilitar el span de filas */ grid-auto-rows: 150px; grid-auto-flow: dense; /* Es clave para rellenar los huecos y evitar espacios en blanco */ gap: 10px; align-items: start; /* Alinea los elementos al inicio de su celda */ } /* Animación para la carga de imágenes */ @keyframes fadeInSlideUp { from { opacity: 0; transform: translateY(70px); /* Más notorio */ } to { opacity: 1; transform: translateY(0); } } /* Contenedor individual para el efecto dock y animación de carga */ .galeria-hover { display: block; /* Ocupa el espacio de la celda de la cuadrícula */ width: 100%; /* Ocupa el 100% del ancho de su celda de cuadrícula */ height: 100%; /* Ocupa el 100% de la altura de su celda de cuadrícula */ position: relative; /* Necesario para que z-index funcione correctamente en la imagen */ overflow: hidden; /* Oculta cualquier desbordamiento si la imagen se sale un poco */ /* Animación de carga para el contenedor */ opacity: 0; /* Estado inicial para la animación */ transform: translateY(70px); /* Estado inicial para la animación */ animation: fadeInSlideUp 1s ease-out forwards; /* Aplicar animación al contenedor */ animation-delay: var(--animation-delay, 0s); /* Usa una variable CSS para el retraso */ } .image-grid img { width: 100%; /* La imagen ocupa todo el ancho de su contenedor .galeria-hover */ height: 100%; /* La imagen ocupa toda la altura de su contenedor .galeria-hover */ object-fit: cover; /* Recorta la imagen para que cubra el área sin distorsión */ /* Estilo inicial de la imagen */ border-radius: 8px; /* Borde redondeado por defecto, puede ser sobrescrito por rounded-full */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); /* Sombra suave inicial */ /* Transiciones para el efecto dock y otros */ transition: transform 0.4s cubic-bezier(0.25, 1.5, 0.5, 1), /* Transición elástica para transform */ box-shadow 0.3s ease, border-radius 0.3s ease, border 0.3s ease; transform-origin: center; /* Origen de la transformación en el centro */ z-index: 1; /* Z-index por defecto para la imagen */ position: relative; /* Necesario para que z-index funcione */ border: 3px solid transparent; /* Borde inicial transparente para la animación */ cursor: pointer; /* Indica que la imagen es clickeable */ } .image-grid img:hover { transform: scale(1.4); /* Crece 40% */ z-index: 10; /* Se superpone a otras imágenes */ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3), 0 0 25px rgba(30, 144, 255, 0.8); /* Sombra y resplandor más intensos */ border: 3px solid #1e90ff; /* Borde azul al pasar el cursor */ } /* Clases para diferentes relaciones de aspecto y spans */ .image-grid img.aspect-square { aspect-ratio: 1 / 1; } .image-grid img.aspect-wide { aspect-ratio: 16 / 9; } .image-grid img.aspect-tall { aspect-ratio: 9 / 16; } .image-grid img.rounded-full { border-radius: 50%; /* Sobrescribe el border-radius por defecto para círculos */ } /* Las clases span ahora se aplican al contenedor .galeria-hover */ .image-grid .span-2-col { grid-column: span 2; } .image-grid .span-3-col { grid-column: span 3; } .image-grid .span-2-row { grid-row: span 2; } .button-container { text-align: center; margin-top: 2rem; } .view-more-button { padding: 0.75rem 1.5rem; background-color: #1e90ff; /* Azul vibrante */ color: white; text-decoration: none; font-weight: bold; border-radius: 5px; transition: background-color 0.3s ease, transform 0.2s ease; display: inline-block; /* Para que el padding y margin funcionen correctamente */ } .view-more-button:hover { background-color: #007bff; /* Azul más oscuro al pasar el ratón */ transform: translateY(-2px); } /* Estilos del Modal */ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.95); /* Fondo semitransparente más oscuro */ display: flex; justify-content: center; align-items: center; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0.4s ease; /* Transición más larga */ } .modal-overlay.visible { opacity: 1; visibility: visible; } .modal-content { position: relative; max-width: 90%; max-height: 90%; background-color: transparent; /* Sin fondo visible */ border-radius: 0; /* Sin bordes */ display: flex; justify-content: center; align-items: center; flex-direction: column; /* Para apilar la imagen y los botones si es necesario */ /* Animación de entrada del modal */ transform: scale(0.5); /* Escala inicial más pequeña para un efecto más dramático */ opacity: 0; /* Opacidad inicial para la animación */ transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55), opacity 0.4s ease-out; /* Curva de rebote para la escala */ } .modal-overlay.visible .modal-content { transform: scale(1); /* Escala final */ opacity: 1; /* Opacidad final */ } #modal-image { max-width: 100%; max-height: 100%; object-fit: contain; /* Asegura que la imagen se ajuste sin recortarse */ border-radius: 10px; /* Bordes ligeramente redondeados para la imagen */ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); /* Sombra sutil para la imagen */ /* Transición para el efecto crossfade al cambiar de imagen */ transition: opacity 0.3s ease-in-out; /* Duración ligeramente mayor */ } .close-button { position: absolute; top: 15px; right: 25px; color: white; font-size: 40px; font-weight: bold; cursor: pointer; transition: color 0.2s ease; background: none; /* Sin fondo */ border: none; /* Sin borde */ padding: 0; line-height: 1; } .close-button:hover { color: #ddd; } .nav-button { position: absolute; top: 50%; transform: translateY(-50%); background-color: rgba(0, 0, 0, 0.6); /* Fondo semitransparente más oscuro para los botones */ color: white; border: none; padding: 15px 20px; font-size: 30px; cursor: pointer; border-radius: 5px; transition: background-color 0.2s ease; user-select: none; /* Evita selección de texto */ display: flex; /* Para centrar el SVG */ justify-content: center; align-items: center; } .nav-button:hover { background-color: rgba(0, 0, 0, 0.8); } .nav-button svg { fill: white; /* Color del icono SVG */ width: 24px; /* Tamaño del icono */ height: 24px; } #prev-button { left: 20px; } #next-button { right: 20px; } /* Ajustes responsivos */ @media (max-width: 768px) { .gallery-container { padding: 1rem; } h2 { font-size: 1.75rem; /* text-2xl en pantallas pequeñas */ } .image-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); /* Ajuste para pantallas más pequeñas */ grid-auto-rows: 100px; /* Reduce la altura de la fila base para móviles */ } .nav-button { padding: 10px 15px; font-size: 24px; } #prev-button { left: 10px; } #next-button { right: 10px; } .close-button { font-size: 30px; top: 10px; right: 15px; } }/style>div classmain-content-wrapper> !-- Div que envuelve toda la galería --> div classgallery-container> h2>Galería destacada/h2> div classimage-grid> div classgaleria-hover style--animation-delay: 0s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f09bfb82_8198.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f09bfb82_8198.jpeg classaspect-square altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.1s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f09a7464_6000.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f09a7464_6000.jpeg classaspect-square rounded-full altImagen de la galería> /div> div classgaleria-hover span-2-col style--animation-delay: 0.2s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f09956d9_5759.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f09956d9_5759.jpeg classaspect-wide altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.3s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f0981c4b_8782.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f0981c4b_8782.jpeg classaspect-tall altImagen de la galería> /div> div classgaleria-hover span-2-col span-2-row style--animation-delay: 0.4s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f09567e3_1223.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f09567e3_1223.jpeg classaspect-square altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.5s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f0948ca1_3925.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f0948ca1_3925.jpeg classaspect-wide altImagen de la galería> /div> div classgaleria-hover span-2-row style--animation-delay: 0.6s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f093e151_3586.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f093e151_3586.jpeg classaspect-tall altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.7s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f092c0e5_9536.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f092c0e5_9536.jpeg classaspect-square altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.8s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f092023a_5574.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f092023a_5574.jpeg classaspect-wide altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 0.9s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-57_68d33f09090b4_2258.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-57_68d33f09090b4_2258.jpeg classaspect-square rounded-full altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 1s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-56_68d33f08e83c5_5104.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-56_68d33f08e83c5_5104.jpeg classaspect-tall altImagen de la galería> /div> div classgaleria-hover span-3-col style--animation-delay: 1.1s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-56_68d33f08d385f_2745.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-56_68d33f08d385f_2745.jpeg classaspect-wide altImagen de la galería> /div> div classgaleria-hover style--animation-delay: 1.2s;> img src/galeria/admin/uploads/thumbnails/thumb_2025-09-23_19-44-56_68d33f08bb39b_7596.jpeg data-full-src/galeria/admin/uploads/fotos/2025-09-23_19-44-56_68d33f08bb39b_7596.jpeg classaspect-square altImagen de la galería> /div> /div> div classbutton-container> a href/galeria/ classview-more-button> Ver más → /a> /div> /div>/div>!-- Estructura del Modal (debe estar fuera del main-content-wrapper si este se incluye en otro div) -->div idimage-modal classmodal-overlay hidden> div classmodal-content> button classclose-button>×/button> img idmodal-image src altImagen en modal> !-- Botones de navegación con iconos SVG --> button idprev-button classnav-button> svg viewBox0 0 24 24> path dM15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6z/> /svg> /button> button idnext-button classnav-button> svg viewBox0 0 24 24> path dM8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z/> /svg> /button> /div>/div>script> document.addEventListener(DOMContentLoaded, () > { const galleryImages document.querySelectorAll(.image-grid img); const modalOverlay document.getElementById(image-modal); const modalImage document.getElementById(modal-image); const closeButton document.querySelector(.close-button); const prevButton document.getElementById(prev-button); const nextButton document.getElementById(next-button); let currentImageIndex 0; // Ahora obtenemos la ruta completa de la imagen del atributo data-full-src const allImageSources Array.from(galleryImages).map(img > ({ src: img.dataset.fullSrc, // Corrección: de data-full-src a data-fullSrc (camelCase) alt: img.alt })); // Función para abrir el modal function openModal(index) { currentImageIndex index; updateModalImage(); modalOverlay.classList.add(visible); } // Función para cerrar el modal function closeModal() { modalOverlay.classList.remove(visible); } // Función para actualizar la imagen en el modal con crossfade function updateModalImage() { modalImage.style.opacity 0; // Inicia el desvanecimiento de la imagen actual setTimeout(() > { // Pequeño retraso para permitir que el desvanecimiento comience antes de cambiar la fuente modalImage.src allImageSourcescurrentImageIndex.src; modalImage.alt allImageSourcescurrentImageIndex.alt; modalImage.style.opacity 1; // Desvanece la nueva imagen }, 100); // Este retraso debe coincidir o ser ligeramente menor que la duración de la transición CSS } // Event listeners para abrir el modal al hacer clic en una imagen de la galería galleryImages.forEach((img, index) > { img.addEventListener(click, () > openModal(index)); }); // Event listener para cerrar el modal closeButton.addEventListener(click, closeModal); modalOverlay.addEventListener(click, (e) > { // Cierra el modal si se hace clic fuera de la imagen (en el overlay) if (e.target modalOverlay) { closeModal(); } }); // Event listener para el botón Siguiente nextButton.addEventListener(click, (e) > { e.stopPropagation(); // Evita que el clic se propague al overlay y cierre el modal currentImageIndex (currentImageIndex + 1) % allImageSources.length; updateModalImage(); }); // Event listener para el botón Anterior prevButton.addEventListener(click, (e) > { e.stopPropagation(); // Evita que el clic se propague al overlay y cierre el modal currentImageIndex (currentImageIndex - 1 + allImageSources.length) % allImageSources.length; updateModalImage(); }); // Navegación con teclado (opcional) document.addEventListener(keydown, (e) > { if (modalOverlay.classList.contains(visible)) { if (e.key ArrowRight) { nextButton.click(); } else if (e.key ArrowLeft) { prevButton.click(); } else if (e.key Escape) { closeModal(); } } }); });/script>meta charsetUTF-8>!-- Tailwind CSS CDN (DEBE IR ANTES DE LA CONFIGURACIÓN DE TAILWIND) -->script srchttps://cdn.tailwindcss.com>/script>!-- Animate.css para animaciones (DEBE IR ANTES DE LOS ESTILOS PERSONALIZADOS) -->link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css>!-- Font Awesome para iconos -->link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css>script> tailwind.config { theme: { extend: { colors: { tw-primary-accent: #FF5733, tw-secondary-dark: #2C3E50, tw-light-bg: #ECF0F1, tw-text-light: #FDFEFE, tw-text-dark: #34495E, }, keyframes: { tw-fadeInUp: { from: { opacity: 0, transform: translateY(50px) }, to: { opacity: 1, transform: translateY(0) }, }, tw-fadeInDown: { from: { opacity: 0, transform: translateY(-50px) }, to: { opacity: 1, transform: translateY(0) }, }, tw-fadeInLeft: { from: { opacity: 0, transform: translateX(-50px) }, to: { opacity: 1, transform: translateX(0) }, }, tw-fadeInRight: { from: { opacity: 0, transform: translateX(50px) }, to: { opacity: 1, transform: translateX(0) }, }, tw-zoomIn: { from: { opacity: 0, transform: scale(0.9) }, to: { opacity: 1, transform: scale(1) }, } }, animation: { tw-fadeInUp: tw-fadeInUp 1.8s ease-out forwards, tw-fadeInDown: tw-fadeInDown 1.8s ease-out forwards, tw-fadeInLeft: tw-fadeInLeft 1.7s ease-out forwards, tw-fadeInRight: tw-fadeInRight 1.7s ease-out forwards, tw-zoomIn: tw-zoomIn 0.5s ease-out forwards, } } }, prefix: tw-, /* ¡IMPORTANTE! Prefijo para todas las clases de Tailwind */ }/script>style> /* Definición de variables CSS para los colores */ :root { --tw-primary-accent: #FF5733; /* Rojo-Naranja vibrante */ --tw-secondary-dark: #1f1f1f; /* Azul oscuro profundo */ --tw-light-bg: #ECF0F1; /* Gris claro casi blanco */ --tw-text-light: #FDFEFE; /* Blanco para texto sobre fondos oscuros */ --tw-text-dark: #34495E; /* Gris oscuro para texto sobre fondos claros */ --tw-spacing-16: 4rem; /* Equivalente a Tailwind mb-16 */ } /* Animaciones personalizadas (replicando animate.css y Tailwind custom keyframes) */ @keyframes tw-fadeInUp { from { opacity: 0; transform: translateY(50px); } to { opacity: 1; transform: translateY(0); } } @keyframes tw-fadeInDown { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } } @keyframes tw-fadeInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } } @keyframes tw-fadeInRight { from { opacity: 0; transform: translateX(50px); } to { opacity: 1; transform: translateX(0); } } @keyframes tw-zoomIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } /* Clases para aplicar animaciones con JavaScript */ .tw-animate-on-scroll { opacity: 0; /* Por defecto oculto hasta que se active la animación */ /* Transición para suavizar la aparición si no se usa animation-fill-mode: forwards */ transition: opacity 1.5s ease-out, transform 1.5s ease-out; } .tw-animate-on-scroll.tw-animated { opacity: 1; transform: translateY(0) translateX(0) scale(1); /* Resetear transformaciones */ } /* Las animaciones específicas se aplicarán vía JavaScript añadiendo la clase directamente */ .tw-animate-on-scroll.tw-fadeInUp { animation: tw-fadeInUp 1.8s ease-out forwards; } .tw-animate-on-scroll.tw-fadeInDown { animation: tw-fadeInDown 1.8s ease-out forwards; } .tw-animate-on-scroll.tw-fadeInLeft { animation: tw-fadeInLeft 1.7s ease-out forwards; } .tw-animate-on-scroll.tw-fadeInRight { animation: tw-fadeInRight 1.7s ease-out forwards; } .tw-animate-on-scroll.tw-zoomIn { animation: tw-zoomIn 0.5s ease-out forwards; } /* Estilos para la sección de resultados (Tailwind equivalents and custom) */ .tw-results-card { background-color: rgba(255, 255, 255, 0.1); /* bg-white/10 */ border-radius: 0.75rem; /* rounded-xl */ padding: 2rem; /* p-8 */ backdrop-filter: blur(8px); /* backdrop-blur-sm */ transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Added transition for hover */ } .tw-results-card:hover { transform: scale(1.02); /* Subtle scale up on hover */ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Increased shadow */ } .tw-results-item { background-color: rgba(255, 255, 255, 0.05); /* bg-white/5 */ border-radius: 0.5rem; /* rounded-lg */ padding: 1rem; /* p-4 */ border-left: 0.25rem solid var(--tw-primary-accent); /* border-l-4 border-primary-accent */ transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out; /* Added transition for transform hover */ } .tw-results-item:hover { background-color: rgba(255, 255, 255, 0.1); /* bg-white/10 on hover */ transform: scale(1.01); /* Slight scale for individual items */ } .tw-results-stats-card { background-color: rgba(255, 255, 255, 0.1); /* bg-white/10 */ border-radius: 0.75rem; /* rounded-xl */ padding: 1.5rem; /* p-6 */ backdrop-filter: blur(8px); /* backdrop-blur-sm */ transition: box-shadow 0.3s ease-in-out, transform 0.3s ease-in-out; /* Added transition for hover */ } .tw-results-stats-card:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Increased shadow */ transform: scale(1.02); /* Subtle scale up on hover */ } /* Clases de texto personalizadas para coincidir con Tailwind */ .tw-text-primary-accent { color: var(--tw-primary-accent) !important; } .tw-text-secondary-dark { color: var(--tw-secondary-dark) !important; } .tw-text-light-bg { color: var(--tw-light-bg) !important; } .tw-text-text-light { color: var(--tw-text-light) !important; } .tw-text-text-dark { color: var(--tw-text-dark) !important; } .tw-bg-primary-accent { background-color: var(--tw-primary-accent) !important; } .tw-bg-secondary-dark { background-color: var(--tw-secondary-dark) !important; } .tw-bg-light-bg { background-color: var(--tw-light-bg) !important; } /* General text colors for this section */ .tw-text-gray-300 { color: #D1D5DB; } /* A light gray for text on dark backgrounds */ /* Ajustes específicos para igualar la estructura del index estatico */ .tw-section-title-margin-bottom { margin-bottom: var(--tw-spacing-16); /* Equivalente a mb-16 de Tailwind */ } /* Font sizes based on Tailwinds default scale for consistency */ .tw-text-5xl-custom { font-size: 3rem; } /* text-5xl */ .tw-text-3xl-custom { font-size: 1.875rem; } /* text-3xl */ .tw-text-2xl-custom { font-size: 1.5rem; } /* text-2xl */ .tw-text-xl-custom { font-size: 1.25rem; } /* text-xl */ .tw-text-base-custom { font-size: 1rem; } /* text-base */ .tw-text-sm-custom { font-size: 0.875rem; } /* text-sm */ .tw-h-96-custom { height: 24rem; /* h-96 en Tailwind */ } .tw-image-hover-effect { transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; } .tw-image-hover-effect:hover { transform: scale(1.05); /* Scale up by 5% on hover */ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); /* Re-apply original shadow or enhance */ }/style>section idresultados classtw-py-20 tw-bg-secondary-dark tw-text-text-light> div classtw-container tw-mx-auto tw-px-6> h2 classtw-text-5xl-custom tw-font-extrabold tw-text-center tw-section-title-margin-bottom tw-text-primary-accent tw-animate-on-scroll data-animation-classtw-fadeInDown> Resultados de Carreras /h2> div classtw-grid tw-grid-cols-1 lg:tw-grid-cols-2 tw-gap-12> div classtw-results-card tw-animate-on-scroll tw-hover:tw-scale-102 data-animation-classtw-fadeInLeft data-delay0s> h3 classtw-text-3xl-custom tw-font-bold tw-mb-6 tw-text-primary-accent>Últimos Resultados/h3> div classtw-space-y-4> p classtw-text-gray-300 tw-text-center tw-text-lg> ¡Mantente atento! Los resultados se publicarán pronto. br> Mientras tanto, ¡anímate a inscribirte en nuestros próximos eventos! /p> /div> div classtw-mt-8> a href/galeria/admin/resultados_generales.php classtw-inline-block tw-bg-primary-accent hover:tw-bg-orange-600 tw-text-text-light tw-font-bold tw-py-3 tw-px-8 tw-rounded-full tw-transition tw-duration-300 tw-ease-in-out tw-transform hover:tw-translate-y-1 tw-shadow-md> Ver Todos los Resultados /a> /div> /div> div classlg:tw-col-span-1> div classtw-animate-on-scroll data-animation-classtw-fadeInRight data-delay0.2s> !-- La imagen ahora es dinámica, tomada de la base de datos o usando una de respaldo --> img src/galeria/admin/imageresultados.png altResultados de Carrera classtw-w-full tw-h-96-custom tw-object-cover tw-rounded-xl tw-shadow-2xl tw-mb-6 tw-image-hover-effect> div classtw-results-stats-card tw-hover:tw-scale-102> h3 classtw-text-2xl-custom tw-font-bold tw-mb-4 tw-text-primary-accent>Estadísticas Destacadas/h3> div classtw-grid tw-grid-cols-2 tw-gap-4> div classtw-text-center> p classtw-text-3xl-custom tw-font-bold tw-text-primary-accent>N/A/p> p classtw-text-gray-300 tw-text-sm-custom>Participantes Totales/p> /div> div classtw-text-center> p classtw-text-3xl-custom tw-font-bold tw-text-primary-accent>N/A/p> p classtw-text-gray-300 tw-text-sm-custom>Ritmo (Posición 1)/p> /div> div classtw-text-center> p classtw-text-3xl-custom tw-font-bold tw-text-primary-accent>N/A/p> p classtw-text-gray-300 tw-text-sm-custom>Género Mayoritario/p> /div> div classtw-text-center> p classtw-text-3xl-custom tw-font-bold tw-text-primary-accent>N/A/p> p classtw-text-gray-300 tw-text-sm-custom>Categoría Mayoritaria/p> /div> /div> /div> /div> /div> /div> /div>/section>script> // Animaciones al hacer scroll function animateOnScroll() { const elements document.querySelectorAll(.tw-animate-on-scroll); elements.forEach(element > { // Solo animar si el elemento aún no ha sido animado if (!element.classList.contains(tw-animated)) { const elementTop element.getBoundingClientRect().top; const elementVisible 150; // Ajusta este valor según sea necesario if (elementTop window.innerHeight - elementVisible) { const animationClass element.getAttribute(data-animation-class); const delay element.getAttribute(data-delay) || 0s; element.style.animationDelay delay; element.classList.add(tw-animated, animationClass); element.style.opacity 1; // Asegura que la opacidad final sea 1 } } }); } // Ejecutar animaciones al cargar y al hacer scroll // Asegúrate de que estas llamadas no se dupliquen si el archivo principal ya las tiene window.addEventListener(scroll, animateOnScroll); window.addEventListener(load, animateOnScroll);/script> !-- Tailwind CSS CDN (DEBE IR ANTES DE LA CONFIGURACIÓN DE TAILWIND) --> script srchttps://cdn.tailwindcss.com>/script> !-- Animate.css para animaciones (DEBE IR ANTES DE LOS ESTILOS PERSONALIZADOS) --> link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css/> !-- Script de configuración de Tailwind CSS para esta sección --> script> tailwind.config { theme: { extend: { colors: { primary-accent: #FF5733, // Rojo-Naranja vibrante para acentos modernos secondary-accent: #FF8C42, // Naranja cálido tertiary-accent: #FFC947, // Amarillo dorado primary-dark: #2C3E50, // Azul oscuro profundo para fondos secondary-dark: #34495E, // Gris azulado oscuro light-bg: #ECF0F1, // Gris claro casi blanco warm-bg: #FDF2E9, // Fondo cálido text-light: #FDFEFE, // Blanco para texto sobre fondos oscuros text-dark: #2C3E50, // Azul oscuro para texto sobre fondos claros text-muted: #7F8C8D, // Gris para texto secundario success: #27AE60, // Verde para elementos de éxito info: #3498DB, // Azul para información }, keyframes: { fade-in-up: { 0%: { opacity: 0, transform: translateY(20px) }, 100%: { opacity: 1, transform: translateY(0) }, }, fadeInDown: { 0%: { opacity: 0, transform: translateY(-20px) }, 100%: { opacity: 1, transform: translateY(0) }, }, zoomIn: { 0%: { opacity: 0, transform: scale(0.9) }, 100%: { opacity: 1, transform: scale(1) }, } }, animation: { fade-in-up: fade-in-up 0.8s ease-out forwards, fade-in-down: fadeInDown 0.8s ease-out forwards, zoom-in: zoomIn 0.5s ease-out forwards, } } } } /script> style> /* Clases para aplicar animaciones con JavaScript */ .animate-on-scroll { opacity: 0; /* La transformación inicial se manejará por Animate.css, solo necesitamos ocultarlo */ } /* Gradientes personalizados */ .gradient-text { background: linear-gradient(135deg, #FF5733, #FF8C42, #FFC947); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } /style> !-- Sección: Beneficios --> section idbeneficios classpy-20 bg-warm-bg> div classcontainer mx-auto px-6> h2 classtext-5xl font-extrabold text-center mb-16 gradient-text animate-on-scroll data-animation-classanimate__fadeInDown> Beneficios de Participar /h2> div classgrid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8> div classtext-center animate-on-scroll data-animation-classanimate__fadeInUp data-delay0s> div classw-20 h-20 mx-auto mb-6 bg-primary-accent rounded-full flex items-center justify-center> svg classw-10 h-10 text-white fillcurrentColor viewBox0 0 20 20> path dM9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z>/path> /svg> /div> h3 classtext-xl font-bold mb-3 text-secondary-dark>Salud y Bienestar/h3> p classtext-gray-700>Mejora tu condición física y mental participando en nuestros eventos./p> /div> div classtext-center animate-on-scroll data-animation-classanimate__fadeInUp data-delay0.2s> div classw-20 h-20 mx-auto mb-6 bg-secondary-accent rounded-full flex items-center justify-center> svg classw-10 h-10 text-white fillcurrentColor viewBox0 0 20 20> path dM13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3z>/path> /svg> /div> h3 classtext-xl font-bold mb-3 text-secondary-dark>Comunidad/h3> p classtext-gray-700>Conecta con otros corredores y forma parte de una comunidad activa./p> /div> div classtext-center animate-on-scroll data-animation-classanimate__fadeInUp data-delay0.4s> div classw-20 h-20 mx-auto mb-6 bg-tertiary-accent rounded-full flex items-center justify-center> svg classw-10 h-10 text-white fillcurrentColor viewBox0 0 20 20> path fill-ruleevenodd dM6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z clip-ruleevenodd>/path> /svg> /div> h3 classtext-xl font-bold mb-3 text-secondary-dark>Logros/h3> p classtext-gray-700>Obtén medallas, certificados y reconocimientos por tu participación./p> /div> div classtext-center animate-on-scroll data-animation-classanimate__fadeInUp data-delay0.6s> div classw-20 h-20 mx-auto mb-6 bg-success rounded-full flex items-center justify-center> svg classw-10 h-10 text-white fillcurrentColor viewBox0 0 20 20> path dM3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z>/path> /svg> /div> h3 classtext-xl font-bold mb-3 text-secondary-dark>Causa Social/h3> p classtext-gray-700>Muchos de nuestros eventos apoyan causas benéficas importantes./p> /div> /div> /div> /section> !-- Script de JavaScript para animaciones (debe ir al final del body principal) --> script> // Función para aplicar animaciones al hacer scroll function animateOnScroll() { const elements document.querySelectorAll(.animate-on-scroll); elements.forEach(element > { // Solo animar si el elemento aún no ha sido animado if (!element.classList.contains(animated)) { const elementTop element.getBoundingClientRect().top; const elementVisible 150; // Ajusta este valor según cuándo quieres que la animación se active if (elementTop window.innerHeight - elementVisible) { const animationClass element.getAttribute(data-animation-class) || animate__fadeInUp; // Valor por defecto const delay element.getAttribute(data-delay) || 0s; element.style.animationDelay delay; element.classList.add(animated, animate__animated, animationClass); element.style.opacity 1; // Hace el elemento visible una vez que la animación comienza } } }); } // Ejecutar animaciones al cargar la página y al hacer scroll window.addEventListener(scroll, animateOnScroll); window.addEventListener(load, animateOnScroll); // Ejecutar la función una vez al cargar para elementos que ya están en el viewport animateOnScroll(); /script>!DOCTYPE html>html langes>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>Comentarios/title> link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css> style> /* Estilos generales y reseteo básico */ :root { --primary-color: #007bff; --secondary-color: #6c757d; --success-color: #28a745; --error-color: #dc3545; --warning-color: #ffc107; --info-color: #17a2b8; --bg-light: #f8f9fa; --text-dark: #343a40; --border-color: #dee2e6; --card-bg: #ffffff; --shadow-light: 0 4px 12px rgba(0,0,0,0.08); --border-radius: 8px; } body { font-family: Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; line-height: 1.6; color: var(--text-dark); margin: 0; justify-content: center; /*background-color: var(--bg-light); Añadido para mejor visualización */ } .comments-container { background: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow-light); padding: 30px; max-width: 1200px; width: 100%; margin: 30px auto; /* Margen superior e inferior para centrar verticalmente */ } h2, h3 { color: var(--text-dark); margin-bottom: 25px; text-align: center; } /* --- Mensajes de Alerta --- */ .alert { padding: 15px 20px; margin-bottom: 25px; border: 1px solid transparent; border-radius: var(--border-radius); font-size: 0.95em; display: flex; align-items: center; } .alert-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; } .alert-error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; } .alert-warning { background-color: #fff3cd; color: #856404; border-color: #ffeeba; } .alert-info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb; } .alert i { margin-right: 10px; font-size: 1.2em; } /* Iconos para alertas */ /* --- Formularios --- */ form { margin-bottom: 30px; padding: 25px; border: 1px solid #e0e0e0; border-radius: var(--border-radius); background-color: #fcfcfc; } form label { display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-dark); } form inputtypetext, form textarea, form inputtypefile { width: calc(100% - 20px); padding: 12px; margin-bottom: 15px; border: 1px solid var(--border-color); border-radius: 6px; box-sizing: border-box; font-size: 1em; transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; } form inputtypetext:focus, form textarea:focus { border-color: var(--primary-color); box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); outline: none; } form textarea { resize: vertical; min-height: 80px; } form p.help-text { font-size: 0.85em; color: var(--secondary-color); margin-top: -10px; margin-bottom: 15px; } form button { background-color: var(--primary-color); color: white; padding: 12px 25px; border: none; border-radius: 6px; cursor: pointer; font-size: 1.05em; font-weight: 600; transition: background-color 0.2s ease-in-out, transform 0.1s ease; } form button:hover { background-color: #0056b3; transform: translateY(-1px); } /* --- Listado de Comentarios --- */ .comment-section { margin-top: 40px; } .comment-card { background: var(--card-bg); border: 1px solid #e0e0e0; border-radius: var(--border-radius); padding: 20px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); display: flex; flex-direction: column; } .comment-author-info { display: flex; align-items: center; margin-bottom: 10px; } .profile-img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; margin-right: 12px; border: 2px solid #eee; } .profile-img-small { /* Para respuestas */ width: 30px; height: 30px; border-radius: 50%; object-fit: cover; margin-right: 8px; border: 1px solid #eee; } .author-name { font-weight: 700; margin: 0; color: var(--text-dark); font-size: 1.1em; } .comment-date { font-size: 0.8em; color: var(--secondary-color); margin-left: 10px; } .comment-text { margin-top: 0; margin-bottom: 15px; line-height: 1.7; } .comment-text strong { color: #212529; } /* Color para negritas */ .comment-text em { font-style: italic; } .comment-text u { text-decoration: underline; } .comment-media { max-width: 250px; /* Tamaño máximo para imágenes de comentarios */ height: auto; border-radius: var(--border-radius); margin-top: 10px; border: 1px solid #e9ecef; box-shadow: 0 1px 4px rgba(0,0,0,0.08); display: block; /* Para que ocupe su propia línea */ } .comment-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; /* Permite que los botones se envuelvan en pantallas pequeñas */ align-items: center; } .btn-toggle-replies, .btn-reply, .btn-delete { background-color: var(--secondary-color); color: white; padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; transition: background-color 0.2s ease; display: flex; align-items: center; gap: 5px; } .btn-toggle-replies:hover, .btn-reply:hover { background-color: #5a6268; } .btn-delete { background-color: var(--error-color); } .btn-delete:hover { background-color: #c82333; } .comment-actions .delete-form { display: flex; align-items: center; gap: 8px; background: none; border: none; padding: 0; margin: 0; } .comment-actions .dni-confirm-input { width: 120px; padding: 6px 10px; margin-bottom: 0; font-size: 0.85em; border-radius: 4px; } /* Respuestas anidadas */ .comment-replies { margin-left: 20px; padding-left: 15px; border-left: 3px solid #e9ecef; margin-top: 20px; } .comment-reply { padding: 15px; background-color: #fbfbfb; border-bottom: 1px solid #f0f0f0; margin-bottom: 10px; border-radius: var(--border-radius); } .comment-reply:last-child { border-bottom: none; margin-bottom: 0; } .reply-form { background-color: #f5f5f5; padding: 15px; border-radius: var(--border-radius); margin-top: 15px; } .reply-form inputtypetext, .reply-form textarea, .reply-form inputtypefile { width: calc(100% - 16px); /* Ajuste para el padding */ padding: 8px; margin-bottom: 10px; font-size: 0.9em; } .reply-form button { padding: 8px 15px; font-size: 0.9em; background-color: var(--success-color); } .reply-form button:hover { background-color: #218838; } /* Paginación */ .pagination { display: flex; justify-content: center; align-items: center; margin-top: 40px; font-size: 1.1em; gap: 15px; } .pagination a { color: var(--primary-color); text-decoration: none; padding: 8px 15px; border: 1px solid var(--primary-color); border-radius: 5px; transition: all 0.3s ease; } .pagination a:hover { background-color: var(--primary-color); color: white; box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2); } .pagination span { color: var(--secondary-color); } /* Estilos para el botón de nuevo comentario */ .btn-new-comment { background-color: var(--success-color); color: white; padding: 12px 25px; border: none; border-radius: 6px; cursor: pointer; font-size: 1.05em; font-weight: 600; transition: background-color 0.2s ease-in-out, transform 0.1s ease; display: block; /* Para que ocupe su propia línea */ margin: 20px auto; /* Centrar el botón */ } .btn-new-comment:hover { background-color: #218838; transform: translateY(-1px); } /* Responsive adjustments */ @media (max-width: 600px) { .comments-container { padding: 15px; } form { padding: 15px; } .comment-card { padding: 15px; } .profile-img { width: 40px; height: 40px; } .profile-img-small { width: 25px; height: 25px; } .comment-replies { margin-left: 10px; padding-left: 10px; } .pagination { flex-direction: column; gap: 10px; } .comment-actions .dni-confirm-input { width: 100%; margin-bottom: 8px; /* Espacio para que el botón no se pegue */ } .comment-actions .delete-form { flex-direction: column; align-items: flex-start; } } /style>/head>body>main classcomments-container> section classcomment-section> h3>i classfas fa-comments>/i> Comentarios de la comunidad:/h3> article classcomment-card> div classcomment-author-info> img src../includes/imagenes_comentarios/perfil_68e575bb81d59_selva-alegre-3851484707-L.jpg altPerfil classprofile-img> p classauthor-name>arodi sotil/p> time datetime2025-10-07 15:16:42 classcomment-date>07 Oct 2025 - 15:16/time> /div> p classcomment-text>A los amigos de AFABPERU, les extiendo un saludo de gran admiración; es realmente bonito ver el apoyo y el compromiso que demuestran con los deportistas de Arequipa, ya que ustedes encarnan a la perfección el lema de "mente sana en cuerpo sano"; sabemos que en el camino del crecimiento y la mejora, siempre habrán pequeños errores o desafíos que afrontar, pero la clave está en seguir adelante con la frente en alto, trabajando siempre para mejorar y alcanzar nuevas metas en sus carreras y en su misión de apoyo; ¡Felicidades por su labor y a seguir impulsando el deporte arequipeño!/p> div classcomment-actions> button onclicktoggleReplies(12) classbtn-toggle-replies>i classfas fa-reply-all>/i> Ver/Ocultar Respuestas/button> /div> div idreplies-12 classcomment-replies styledisplay: none;> form methodpost classreply-form enctypemultipart/form-data> input typehidden nameparent_id value12> input typetext namedni placeholderTu DNI required> textarea namecomentario rows2 placeholderResponder a este comentario... required>/textarea> label formedia_comentario_reply_12 styledisplay: block; margin-top: 5px;>Imagen o GIF (opcional):/label> input typefile idmedia_comentario_reply_12 namemedia_comentario acceptimage/*, .gif> button typesubmit nameenviar_comentario classbtn-reply>i classfas fa-reply>/i> Responder/button> /form> /div> /article> /section> !-- Botón para mostrar el formulario de nuevo comentario --> button idtoggleCommentFormBtn classbtn-new-comment>i classfas fa-plus-circle>/i> Deja un nuevo comentario/button> div idcommentFormContainer styledisplay: none;> section> h3>i classfas fa-comment-dots>/i> Deja tu comentario:/h3> form idcommentForm methodpost enctypemultipart/form-data> label fordni>Tu DNI:/label> input typetext iddni namedni placeholderIngresa tu DNI required> label forfoto_perfil>Foto de perfil (opcional):/label> input typefile idfoto_perfil namefoto_perfil acceptimage/*> label forcomentario_principal>Comentario:/label> textarea idcomentario_principal namecomentario rows4 placeholderEscribe tu comentario aquí... required>/textarea> p classhelp-text> i classfas fa-bold>/i> **Negrita** | i classfas fa-italic>/i> *Cursiva* | i classfas fa-underline>/i> __Subrayado__ | 😄 Emojis (Windows: `Win + .` o `Win + ;` | Mac: `Ctrl + Cmd + Espacio`) /p> label formedia_comentario>Imagen o GIF (opcional):/label> input typefile idmedia_comentario namemedia_comentario acceptimage/*, .gif> button typesubmit nameenviar_comentario>i classfas fa-paper-plane>/i> Publicar Comentario/button> /form> /section> /div> /main>script> /** * Alterna la visibilidad de las respuestas de un comentario. * @param {number} id El ID del comentario padre. */ function toggleReplies(id) { const repliesDiv document.getElementById(`replies-${id}`); if (repliesDiv) { repliesDiv.style.display repliesDiv.style.display none ? block : none; } } /** * Alterna la visibilidad del formulario principal de comentarios. */ function toggleCommentForm() { const commentFormContainer document.getElementById(commentFormContainer); if (commentFormContainer) { commentFormContainer.style.display commentFormContainer.style.display none ? block : none; } } // Asignar el evento click al botón document.addEventListener(DOMContentLoaded, () > { const toggleBtn document.getElementById(toggleCommentFormBtn); if (toggleBtn) { toggleBtn.addEventListener(click, toggleCommentForm); } // Ocultar el formulario automáticamente si se acaba de publicar un comentario if (window.location.hash #comment_posted) { const commentFormContainer document.getElementById(commentFormContainer); if (commentFormContainer) { commentFormContainer.style.display none; } // Limpiar el hash para futuras interacciones history.replaceState(, document.title, window.location.pathname + window.location.search); } });/script>/body>/html>script> // --- Configuraciones de ScrollReveal.js para la sección de Sponsors ---// Animación para el título de la sección de sponsorsScrollReveal().reveal(data-sr-idsponsors-title, { origin: top, // Viene desde arriba distance: 50px, // Se mueve 50px duration: 900, // Dura 900ms delay: 200, // Empieza después de 200ms de ser visible easing: ease-out, // Suavizado opacity: 0, // Empieza invisible cleanup: false // Mantiene los estilos finales (no vuelve a su estado inicial)});// Animación para cada item de sponsorScrollReveal().reveal(.sponsor-item, { origin: bottom, // Viene desde abajo distance: 40px, // Se mueve 40px duration: 800, // Dura 800ms delay: 150, // Retraso inicial para el primer item visible interval: 80, // Retraso escalonado de 80ms entre cada item easing: ease-out, // Suavizado opacity: 0, // Empieza invisible cleanup: true, // Se limpian los estilos de animación después de la primera vez reset: true // La animación se repetirá cada vez que el elemento entre/salga del viewport});// Animación para los logos dentro de los items (si no están ya animados por el item)// Puedes añadir una animación específica si quieres que el logo aparezca con un efecto diferente// a la tarjeta principal. Por ejemplo, un zoom.ScrollReveal().reveal(.sponsor-item .sponsor-logo, { scale: 0.8, // Empieza en 80% de su tamaño opacity: 0, // Empieza invisible duration: 700, // Dura 700ms delay: 300, // Aparece un poco después que la tarjeta easing: ease-out, cleanup: true, reset: true});// Animación para los iconos de redes sociales (dentro de .social-links)ScrollReveal().reveal(.social-links .social-icon-link, { scale: 0.5, // Empieza pequeño opacity: 0, // Empieza invisible duration: 600, // Dura 600ms delay: 400, // Aparece después del logo interval: 50, // Retraso escalonado entre iconos easing: ease-out, cleanup: true, reset: true});/script> /div> !-- Footer dinámico --> footer classfooter-dynamic> style> /* CONFIGURACIÓN DINÁMICA DEL FOOTER */ .footer-dynamic { background: linear-gradient(135deg, #ff6200, #573e00); color: #ffffff; position: relative; overflow: hidden; } .footer-dynamic::before { content: ; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, #666666, transparent); } .footer-dynamic * { color: #ffffff; } .footer-dynamic .footer-title { color: #ffffff; font-weight: 700; font-size: 1.5rem; margin-bottom: 1rem; } .footer-dynamic .footer-description { color: #ffffff; opacity: 0.9; line-height: 1.6; } .footer-dynamic .footer-section-title { color: #ffffff; font-weight: 600; font-size: 1.1rem; margin-bottom: 1.5rem; position: relative; padding-bottom: 0.5rem; } .footer-dynamic .footer-section-title::after { content: ; position: absolute; bottom: 0; left: 0; width: 40px; height: 2px; background-color: #ffffff; border-radius: 1px; } .footer-dynamic .footer-link { color: #ffffff; text-decoration: none; transition: all 0.3s ease; display: flex; align-items: center; padding: 0.5rem 0; } .footer-dynamic .footer-link:hover { /*color: #ffffff;*/ color: #fbef90; transform: translateX(5px); } .footer-dynamic .footer-link i { color: #ffffff; margin-right: 0.75rem; font-size: 1.1rem; width: 20px; text-align: center; } .footer-dynamic .social-links { display: flex; gap: 1rem; flex-wrap: wrap; } .footer-dynamic .social-link { display: inline-flex; align-items: center; justify-content: center; width: 45px; height: 45px; background-color: rgba(255, 255, 255, 0.1); color: #ffffff; border-radius: 12px; text-decoration: none; transition: all 0.3s ease; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); } .footer-dynamic .social-link:hover { background-color: #ffffff; color: #0d7126; transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); } .footer-dynamic .newsletter-form { background-color: rgba(255, 255, 255, 0.05); padding: 1rem; border-radius: 15px; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); } .footer-dynamic .newsletter-input { background-color: #ffffff; color: #333333; border: 2px solid #dddddd; border-radius: 8px; padding: 0.75rem 1rem; transition: all 0.3s ease; font-size: 1rem; } .footer-dynamic .newsletter-input:focus { outline: none; border-color: #435700; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); } .footer-dynamic .newsletter-button { background-color: #435700; color: #ffffff; border: none; border-radius: 8px; padding: 0.75rem 2rem; font-weight: 600; transition: all 0.3s ease; cursor: pointer; } .footer-dynamic .newsletter-button:hover { background-color: #435700dd; transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .footer-dynamic .footer-divider { border-color: #666666; opacity: 0.3; margin: 1rem 0; } .footer-dynamic .copyright-text { color: #ffffff; opacity: 0.8; font-size: 0.9rem; } .footer-dynamic .payment-logos { display: flex; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap; } .footer-dynamic .payment-logos img { max-height: 40px; opacity: 0.8; transition: opacity 0.3s ease; filter: brightness(0) invert(1); } .footer-dynamic .payment-logos img:hover { opacity: 1; } .footer-dynamic .contact-info-item { display: flex; align-items: flex-start; margin-bottom: 0rem; padding: 0.5rem 0; } .footer-dynamic .contact-info-item i { color: #ffffff; margin-right: 1rem; font-size: 1.2rem; width: 24px; text-align: center; margin-top: 0.2rem; } .footer-dynamic .contact-info-item .contact-text { line-height: 1.5; } /* Responsive */ @media (max-width: 768px) { .footer-dynamic .social-links { justify-content: center; } .footer-dynamic .newsletter-form { padding: 1.5rem; } .footer-dynamic .payment-logos { justify-content: center; } .footer-dynamic .footer-section { text-align: center; margin-bottom: 1rem; } } /* Animaciones */ .footer-dynamic .fade-in { animation: fadeInUp 0.6s ease-out; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } /style> !-- Contenido del footer --> div classcontainer py-5> div classrow g-4> !-- Columna 1: Información de la empresa --> div classcol-lg-4 col-md-6 footer-section fade-in> h3 classfooter-title> img src../assets/images/uploads/1754341202_FB_IMG_1754341069279-modified.png altAFABPERU styleheight: 40px; margin-right: 0.75rem;> AFABPERU /h3> p classfooter-description> Promoviendo el fondismo en Arequipa y todo el Perú /p> !-- Redes sociales --> /div> !-- Columna 2: Información de contacto --> div classcol-lg-4 col-md-6 footer-section fade-in idContacto> h5 classfooter-section-title>Información de Contacto/h5> div classcontact-info-item> i classbi bi-geo-alt>/i> div classcontact-text> Av Ejército 907 2do Piso /div> /div> div classcontact-info-item> i classbi bi-telephone>/i> div classcontact-text> a hreftel:+51 959 138 128 classfooter-link> +51 959 138 128 /a> /div> /div> div classcontact-info-item> i classbi bi-envelope>/i> div classcontact-text> a hrefmailto:info@afabperu.org.pe classfooter-link> info@afabperu.org.pe /a> /div> /div> !-- Enlaces rápidos --> div classmt-4> h6 classfooter-section-title>Enlaces Rápidos/h6> a href/ classfooter-link> i classbi bi-house>/i> Inicio /a> a href/eventos.php classfooter-link> i classbi bi-calendar-event>/i> Eventos /a> a href/Nosotros/ classfooter-link> i classbi bi-people>/i> Nosotros /a> a href#Contacto classfooter-link> i classbi bi-telephone>/i> Contacto /a> /div> /div> !-- Columna 3: Newsletter --> div classcol-lg-4 col-md-12 footer-section fade-in> div classnewsletter-form> h5 classfooter-section-title>Boletín/h5> p classmb-3>Suscríbete a nuestro boletín/p> form idnewsletterForm classnewsletter-form-inner> div classmb-3> input typeemail classform-control newsletter-input idnewsletterEmail nameemail placeholderTu email aquí... required> /div> button typesubmit classbtn newsletter-button w-100> i classbi bi-envelope me-2>/i> Suscribirse /button> /form> /div> !-- Métodos de pago --> div classmt-4> h6 classfooter-section-title>Métodos de Pago/h6> div classpayment-logos> img src../assets/images/uploads/1749446948_yape.png altMétodos de pago aceptados loadinglazy> /div> /div> /div> /div> !-- Divider --> hr classfooter-divider> !-- Copyright y enlaces legales --> div classrow align-items-center> div classcol-md-6 text-center text-md-start> p classcopyright-text mb-0> © 2025 AFABPERU /p> /div> div classcol-md-6 text-center text-md-end> div classd-flex justify-content-center justify-content-md-end gap-3 flex-wrap> a hrefTERMINOS Y CONDICIONES classfooter-link small> Términos y Condiciones /a> a hrefpolitica de privacidad classfooter-link small> Política de Privacidad /a> /div> /div> /div> /div> /footer> !-- Scripts --> !-- jQuery --> script srchttps://code.jquery.com/jquery-3.6.0.min.js>/script> !-- SweetAlert2 --> script srchttps://cdn.jsdelivr.net/npm/sweetalert2@11>/script> script srchttps://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js>/script>!--script srchttps://unpkg.com/scrollreveal>/script>--> script srchttps://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js>/script> script src../assets/js/countdown-jquery.js>/script> !-- Newsletter form handler --> script> document.addEventListener(DOMContentLoaded, function() { const newsletterForm document.getElementById(newsletterForm); if(newsletterForm) { newsletterForm.addEventListener(submit, function(e) { e.preventDefault(); const formData new FormData(this); fetch(/subscribe.php, { method: POST, body: formData }) .then(response > response.json()) .then(data > { if(data.success) { Swal.fire({ icon: success, title: ¡Gracias!, text: data.message, timer: 3000 }); this.reset(); } else { Swal.fire({ icon: error, title: Error, text: data.message }); } }) .catch(error > { Swal.fire({ icon: error, title: Error, text: Ocurrió un error inesperado }); }); });} // Animaciones de entrada const observerOptions { threshold: 0.1, rootMargin: 0px 0px -50px 0px }; const observer new IntersectionObserver((entries) > { entries.forEach(entry > { if (entry.isIntersecting) { entry.target.style.opacity 1; entry.target.style.transform translateY(0); } }); }, observerOptions); document.querySelectorAll(.footer-section).forEach(section > { section.style.opacity 0; section.style.transform translateY(30px); section.style.transition all 0.6s ease-out; observer.observe(section); }); console.log(🦶 Footer dinámico cargado correctamente); }); /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]