1<!DOCTYPE html><!-- This site was created in Webflow. https://webflow.com --><!-- Last Published: Sat Oct 03 2026 13:28:24 GMT+0000 (Coordinated Universal Time) --><html data-wf-domain="redentrenadores-268bb8.webflow.io" data-wf-page="6a836f59023080aa5e2a44fe" data-wf-site="68c5a729c71fa3f37858fbbe" lang="es"><head><meta charset="utf-8"/><link href="https://cdn.prod.website-files.com" rel="preconnect" crossorigin="anonymous"/><title>Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores</title><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allà y escrÃbele gratis." name="description"/><meta content="Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores" property="og:title"/><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allà y escrÃbele gratis." property="og:description"/><meta content="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&fit=crop&w=1200&h=630&q=60" property="og:image"/><meta content="Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores" name="twitter:title"/><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allà y escrÃbele gratis." name="twitter:description"/><meta content="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&fit=crop&w=1200&h=630&q=60" name="twitter:image"/><meta property="og:type" content="website"/><meta content="summary_large_image" name="twitter:card"/><meta content="width=device-width, initial-scale=1" name="viewport"/><meta content="Ibl2YfMxHx18YwgwBo_Cf7Y0noaXDXPp8qxAux6bR7I" name="google-site-verification"/><meta content="Webflow" name="generator"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/css/redentrenadores-268bb8.webflow.shared.5fdd5e548.min.css" rel="stylesheet" type="text/css" integrity="sha384-X91eVImZbdbFw8t2Y6QKDl7HUD/gaoQ44rfY64Qq+p7zJMJt635EzDj9JzMjxLwR" crossorigin="anonymous"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/css/redentrenadores-268bb8.webflow.6a836f59023080aa5e2a44fe.72bed271c.opt.min.css" rel="stylesheet" type="text/css" integrity="sha384-cr7SccRUHWG1uANAmMLnXF3Y90TamgViBCc6pmJQEOVqTKXFL4Z3sz5x1YdPU2Kk" crossorigin="anonymous"/><link href="https://fonts.googleapis.com" rel="preconnect"/><link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="anonymous"/>
1<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js" type="text/javascript"></script>
1<script type="text/javascript">WebFont.load({ google: { families: ["Open Sans:300,300italic,400,400italic,600,600italic,700,700italic,800,800italic","Barlow:300,400,500,600,700","DM Sans:300,400,500,600,700","Plus Jakarta Sans:300,400,500,600,700"] }});</script>
1<script type="text/javascript">!function(o,c){var n=c.documentElement,t=" w-mod-";n.className+=t+"js",("ontouchstart"in o||o.DocumentTouch&&c instanceof DocumentTouch)&&(n.className+=t+"touch")}(window,document);</script>
1<link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68cf1cb49e59be031d2f13dc_favicon-32x32.png" rel="shortcut icon" type="image/x-icon"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68cf1d169359063e6094caad_logo-256x256.png" rel="apple-touch-icon"/>
1<script type="application/ld+json"> 2{ 3 "@context": "https://schema.org", 4 "@type": "CollectionPage", 5 "@id": "https://www.redentrenadores.com/donde-entrenar#webpage", 6 "name": "Dónde entrenar cerca de ti", 7 "description": "Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allà y escrÃbele gratis.", 8 "url": "https://www.redentrenadores.com/donde-entrenar", 9 "inLanguage": "es", 10 "publisher": { 11 "@id": "https://www.redentrenadores.com/#organization" 12 }, 13 "breadcrumb": { 14 "@type": "BreadcrumbList", 15 "itemListElement": [ 16 { 17 "@type": "ListItem", 18 "position": 1, 19 "name": "Inicio", 20 "item": "https://www.redentrenadores.com" 21 }, 22 { 23 "@type": "ListItem", 24 "position": 2, 25 "name": "Dónde entrenar", 26 "item": "https://www.redentrenadores.com/donde-entrenar" 27 } 28 ] 29 } 30} 31</script>
31<script>(function(w,i,g){w[g]=w[g]||[];if(typeof w[g].push=='function')w[g].push.apply(w[g],Array.isArray(i)?i:[i]);})(window,['G-GSEP18CGXX'],'google_tags_first_party');</script>
31<script async="" src="/avljl2rk9q5pNjhjNWE3MjljNzFmYTNmMzc4NThmYmJl/peOuxr4X3pGz_RiXea8PdbkeTQo"></script>
31<script type="text/javascript">window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('set', 'developer_id.dZGVlNj', true);gtag('set', 'developer_id.dYWYxNW', true);gtag('js', new Date());gtag('config', 'G-GSEP18CGXX');</script>
31<script type="text/javascript">!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.agent='plwebflow';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', '1689976748571004');fbq('track', 'PageView');</script>
31<!-- Google Tag Manager -->
32<script>
vendor: 327 bytes, lines 32-36
32(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 33new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 34j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 35'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 36})(window,document,'script','dataLayer','
36GTM-KBD83QKS
vendor: 3 bytes, line 36
36');
36</script>
36 37<!-- End Google Tag Manager --> 38 39<!-- Start cookieyes banner -->
40<script id="cookieyes" type="text/javascript" src="https://cdn-cookieyes.com/client_data/3b90490e3918e885f5cdab3d/script.js"></script>
40 41<!-- End cookieyes banner --> 42 43 44<!-- Hotjar / Contentsquare: async porque es analitica pura, nada en el repo 45 depende de que este cargado (grep sin resultados fuera de backups de este 46 mismo bloque). Eran 623 KB bloqueando el primer pintado: el mayor peso de 47 los 4760ms de "solicitudes que bloquean el renderizado" de PageSpeed. -->
48<script src="https://t.contentsquare.net/uxa/e67e16087d6b8.js" async></script>
48 49 50 51<!-- Memberstack: solo fuera de /lp/*. Fuente y porque: 52 widgets/custom-code-memberstack-por-ruta.html -->
53<script> 54(function () { 55 // Las landings de paid no tienen nada que dependa de Memberstack. Cualquier 56 // otra ruta lo carga igual que siempre, bloqueando, porque los widgets del 57 // panel dan por hecho que $memberstackDom ya existe cuando ellos corren. 58 if (/^\/lp(\/|$)/.test(location.pathname)) return; 59 60 document.write( 61 '
61<script data-memberstack-app="app_cmgl62z3b00kw0tt21gk97j2x"' + 62 ' src="https://static.memberstack.com/scripts/v1/memberstack.js"' + 63 ' type="text/javascript"><\/script>' 64 ); 65})(); 66</script>
66 67 68<!-- Para que los dropdown sean blancos --> 69<style> 70.w-dropdown-list { 71 background-color: white !important; 72} 73 74.w-dropdown.w--open .w-dropdown-list { 75 background-color: white !important; 76} 77 78.search-dropdown { 79 background-color: white !important; 80} 81</style> 82 83 84<!-- Limita el texto descriptivo de cada entrenador -->
85<script> 86document.addEventListener("DOMContentLoaded", function() { 87 const elements = document.querySelectorAll(".limit-text"); 88 89 elements.forEach(el => { 90 const text = el.textContent.trim(); 91 92 if (text.length > 80) { 93 el.textContent = text.substring(0, 80) + "..."; 94 } 95 }); 96}); 97</script>
97 98 99 100<!-- Canonical autorreferenciado (auditoria SEO 2026-08) -->
101<script> 102(function () { 103 if (document.querySelector('link[rel="canonical"]')) return; 104 var p = location.pathname; 105 while (p.length > 1 && p.charAt(p.length - 1) === '/') p = p.slice(0, -1); 106 var l = document.createElement('link'); 107 l.rel = 'canonical'; 108 l.href = 'https://www.redentrenadores.com' + (p || '/'); 109 document.head.appendChild(l); 110})(); 111</script>
111 112 113 114<!-- Impide que se descargue el video de fondo de la seccion de FAQ. 115 Es decorativo, esta al final de la pagina y su webm pesa 4,6 MB en las 61 116 paginas que lo llevan (home, listado y las 59 de ciudad). Ocultarlo por CSS 117 no basta: Webflow crea el <video> con autoplay y el navegador se lo baja 118 igual aunque el contenedor este en display:none (comprobado: paused=false, 119 buffered=15s). Sin data-video-urls el script de Webflow no crea el <video>, 120 asi que no hay nada que pedir. 121 Para revertirlo, basta con borrar este bloque y quitar el display:none de 122 la clase bg-video_faqs. 123 ACTUALIZACION 2026-08-11: en /entrenadores-personales/* el elemento se 124 borro directamente (ver commit), asi que este bloque ya no hace nada ahi. 125 Sigue haciendo falta en / y /entrenador-personal hasta que se borre el 126 elemento tambien en esas dos plantillas (vive dentro de un componente que 127 la API no puede recorrer). -->
128<script> 129document.addEventListener('DOMContentLoaded', function () { 130 document.querySelectorAll('[data-video-urls]').forEach(function (el) { 131 el.removeAttribute('data-video-urls'); 132 }); 133}); 134</script>
134 135 136 137<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 138 AMPLITUDE - analitica + session replay (2026-08-14) 139 El proyecto no tiene bundler (no existe package.json en el repo), asi que 140 en vez de `npm install @amplitude/unified` se carga EL MISMO paquete como 141 modulo ESM desde jsDelivr por import() dinamico. La llamada de init es la 142 de la documentacion: initAll, no init. 143 144 CONSENTIMIENTO: no arranca hasta que cookieyes tiene la categoria 145 "analytics" en yes. Esto vale tanto para la analitica como para Session 146 Replay, que graba el 100% de las sesiones (sampleRate 1) y sin 147 consentimiento no puede grabar nada. Verificado sobre el propio banner.js 148 que sirve el sitio: la cookie es "cookieyes-consent" con pares 149 clave:valor separados por coma, las categorias son necessary / functional 150 / analytics / performance / advertisement, y al aceptar se despacha 151 "cookieyes_consent_update" sobre document. Bajo RGPD analytics arranca en 152 "no", asi que por defecto aqui no se carga nada. 153 154 El stub de window.amplitude existe desde el primer momento y encola los 155 track() previos a la carga del modulo; al inicializar se vacia la cola. Si 156 el usuario nunca da su consentimiento, la cola se descarta sin enviarse. 157 Los embeds de widgets/ llaman a amplitude.track(...) contando con esto. 158 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
159<script> 160(function () { 161 window.amplitude = window.amplitude || { _q: [], track: function () { this._q.push(['track', arguments]); } }; 162 163 function hayConsentimiento() { 164 var m = document.cookie.match(/(?:^|;\s*)cookieyes-consent=([^;]*)/); 165 if (!m) return false; 166 return decodeURIComponent(m[1]).split(',').indexOf('analytics:yes') !== -1; 167 } 168 169 var arrancado = false; 170 function arrancar() { 171 if (arrancado || !hayConsentimiento()) return; 172 arrancado = true; 173 import('https://cdn.jsdelivr.net/npm/@amplitude/unified@1/+esm').then(function (amplitude) { 174 var pendientes = (window.amplitude && window.amplitude._q) || []; 175 // Amplitude ingestion key â public by design; move to an env var when you set up environments. 176 amplitude.initAll('21ce582269eabdf7c4208d1960a7731e', {"analytics":{"autocapture":true},"sessionReplay":{"sampleRate":1}}); 177 window.amplitude = amplitude; 178 pendientes.forEach(function (c) { amplitude[c[0]].apply(null, c[1]); }); 179 }).catch(function () { arrancado = false; }); 180 } 181 182 arrancar(); 183 document.addEventListener('cookieyes_consent_update', arrancar); 184})(); 185</script>
185 186 187<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 188 SIMULAR PANEL DE ENTRENADOR (2026-08-21) 189 Deja que el equipo recorra /panel-entrenador viendo los datos de un 190 entrenador concreto, sin entrar con su cuenta y sin poder tocar nada. Lo 191 enciende la pestana "Simular entrenador" de /admin, que abre el panel con 192 ?simular=<token>. Solo lectura: el shim corta cualquier metodo que no sea 193 GET, y las Edge Functions lo rechazan tambien en el servidor. 194 195 Va aqui y no como registered script porque esos tienen un tope de 2.000 196 caracteres y este no cabe (son 10 KB). Se pega MINIFICADO: la fuente 197 legible, con el porque de cada parte, es 198 widgets/custom-code-simular-entrenador.js del repo, y lo de abajo sale de 199 widgets/custom-code-simular-entrenador.min.js. Para cambiarlo: editar la 200 fuente, regenerar con terser y volver a pegar. No editar esto a mano. 201 202 Se corta solo en cuanto la ruta no empieza por /panel-entrenador, asi que 203 en el resto del sitio no hace nada mas que existir. 204 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
205<script> 206/*! Simular panel de entrenador â RedEntrenadores 207 * Version minificada. La fuente legible, con el porque de cada parte, esta en 208 * widgets/custom-code-simular-entrenador.js del repo. Para cambiarlo: editar 209 * ese fichero, regenerar con `npx terser` y volver a pegar aqui. No editar 210 * este bloque a mano. */ 211!function(){var e='https://yrzanatzshdpvypazgeb.supabase.co/functions/v1/',t='rt-simulacion',r=['comprobar-titulo'],n=/^rt-perfil-/;if(0===location.pathname.indexOf('/panel-entrenador')){var o=new URL(location.href),a=o.searchParams.get('simular');if(a){!function(e){try{sessionStorage.setItem(t,JSON.stringify(e))}catch(e){}}({token:a,nombre:o.searchParams.get('simular_nombre')||'este entrenador'}),o.searchParams.delete('simular'),o.searchParams.delete('simular_nombre');var i=o.pathname+(o.searchParams.toString()?'?'+o.searchParams:'')+o.hash;history.replaceState(null,'',i)}var s=function(){try{return JSON.parse(sessionStorage.getItem(t)||'null')}catch(e){return null}}();if(s&&s.token)if(g(s.token)<Date.now())h();else{window.rtSimulacion={entrenador:s.nombre,expira:new Date(g(s.token))};var l='rt-sim:'+s.token.split('.')[1]+':',c=Storage.prototype.getItem,u=Storage.prototype.setItem,d=Storage.prototype.removeItem;Storage.prototype.getItem=function(e){return c.call(this,v(this)?b(e):e)},Storage.prototype.setItem=function(e,t){return u.call(this,v(this)?b(e):e,t)},Storage.prototype.removeItem=function(e){return d.call(this,v(this)?b(e):e)};var m=window.fetch;if(window.fetch=function(t,n){var o='string'==typeof t?t:t&&t.url||'';if(0!==o.indexOf(e))return m.apply(this,arguments);var a=(n&&n.method||t&&t.method||'GET').toUpperCase(),i=o.slice(54).split(/[/?#]/)[0];if('GET'!==a&&'OPTIONS'!==a&&r.indexOf(i)<0)return function(){var e=document.getElementById('rt-sim-aviso');if(!e)return;e.textContent='No se ha guardado nada: el panel simulado es solo de lectura.',e.hidden=!1,clearTimeout(f),f=setTimeout(function(){e.hidden=!0},5e3)}(),Promise.resolve(new Response('simulacion_solo_lectura',{status:403}));if('string'==typeof t){var l=new Headers(n&&n.headers||{});l.set('Authorization','Bearer '+s.token);var c={};for(var u in n||{})c[u]=n[u];return c.headers=l,m.call(this,t,c)}var d=new Request(t,n);return d.headers.set('Authorization','Bearer '+s.token),m.call(this,d)},window.$memberstackDom)y(window.$memberstackDom);else try{var p;Object.defineProperty(window,'$memberstackDom',{configurable:!0,get:function(){return p},set:function(e){p=y(e)}})}catch(e){}var f=null;'loading'===document.readyState?document.addEventListener('DOMContentLoaded',x):x()}}function h(){try{sessionStorage.removeItem(t)}catch(e){}}function g(e){var t=String(e||'').split('.');return 4===t.length?Number(t[2]):0}function b(e){return n.test(String(e))?l+e:e}function v(e){try{return e===window.localStorage}catch(e){return!1}}function y(e){if(!e||'function'!=typeof e.getMemberCookie||e.__rtSimParcheado)return e;var t=e.getMemberCookie.bind(e);return e.getMemberCookie=function(){return Promise.resolve().then(t).then(function(e){return e||s.token}).catch(function(){return s.token})},e.__rtSimParcheado=!0,e}function x(){if(!document.getElementById('rt-sim-banner')){var e=document.createElement('style');e.textContent=['#rt-sim-banner{position:fixed;left:0;right:0;bottom:0;z-index:10000;','background:#FEF3C7;border-top:1px solid #92400E;color:#92400E;','font-family:\'Plus Jakarta Sans\',-apple-system,system-ui,sans-serif;font-size:13px;','display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:10px 16px;box-sizing:border-box}','#rt-sim-banner .rt-sim-texto{flex:1;
211min-width:200px;margin:0}','#rt-sim-banner strong{color:#1B1E22}','#rt-sim-banner .rt-sim-salir{padding:9px 18px;border:1px solid #089F3B;border-radius:8px;','background:#fff;color:#089F3B;text-transform:uppercase;font-weight:700;letter-spacing:0.03em;','font-size:11px;font-family:inherit;cursor:pointer;flex-shrink:0}','#rt-sim-banner .rt-sim-salir:hover{background:#EAF3DE}','#rt-sim-aviso{flex-basis:100%;background:#FCEBEB;color:#791F1F;border-radius:8px;','padding:10px 12px;font-size:13px;margin:0}','#rt-sim-aviso[hidden]{display:none}','body{padding-bottom:64px}'].join(''),document.head.appendChild(e);var t,r=document.createElement('div');r.id='rt-sim-banner',r.innerHTML='<p class="rt-sim-texto">Estás viendo el panel de <strong>'+(t=s.nombre,String(null==t?'':t).replace(/[&<>"']/g,function(e){return{'&':'&','<':'<','>':'>','"':'"','\'':'''}[e]})+'</strong>. Solo lectura: nada de lo que hagas aquà se guarda.</p><button type="button" class="rt-sim-salir">Salir de la simulación</button><p id="rt-sim-aviso" hidden></p>'),document.body.appendChild(r),r.querySelector('.rt-sim-salir').addEventListener('click',function(){h(),function(){try{for(var e=[],t=0;t<localStorage.length;t++){var r=localStorage.key(t);r&&0===r.indexOf('rt-sim:')&&e.push(r)}e.forEach(function(e){d.call(localStorage,e)})}catch(e){}}(),location.href='/admin'})}}}(); 212 213</script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a7b1ad624d451ff7ab95b57%2Fdeep_link_panel_tras_login-0.0.1.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a8188e2745a991cf146025e%2Fredirect_urls_antiguas_panel-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a90ae0a69c0c92f2517a3b6%2Fawintracking2-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aa83d1b2d69974135a99d99%2Fguardian_sesion_panel-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aaed9e15b8290737a4ca482%2Fguardia_onboarding_entrenador-1.0.0.js" type="text/javascript"></script>
213<script type="text/javascript">window.__WEBFLOW_CURRENCY_SETTINGS = {"currencyCode":"USD","symbol":"$","decimal":".","fractionDigits":2,"group":",","template":"{{wf {\"path\":\"symbol\",\"type\":\"PlainText\"} }} {{wf {\"path\":\"amount\",\"type\":\"CommercePrice\"} }} {{wf {\"path\":\"currencyCode\",\"type\":\"PlainText\"} }}","hideDecimalForWholeNumbers":false};</script>
213</head><body><div data-animation="default" class="rl_navbar5_component padding-top-medium w-nav" data-easing2="ease" data-easing="ease" data-collapse="medium" role="banner" data-duration="400" data-wf--header-v6--variant="base" ms-code-scroll-offset="40"><div class="rl_navbar5_container"><a href="/" class="rl_navbar5_logo-link w-nav-brand"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif" loading="lazy" width="63" sizes="63px" alt="Logotipo de RedEntrenadores" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-500.avif 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-800.avif 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif 2246w" class="rl_navbar5_logo"/></a><nav role="navigation" data-ms-content="!members" class="rl_navbar5_menu w-nav-menu"><div class="nav-pills-grupo"><a href="/pregunta-al-experto" class="nav-pill-item">Pregunta al experto</a><div class="nav-pill-sep"></div><a href="/donde-entrenar" aria-current="page" class="nav-pill-item w--current">Dónde entrenar</a></div><div class="rl_navbar5_menu-left"><div data-hover="true" data-delay="300" class="rl_navbar5_menu-dropdown w-dropdown"><div class="rl_navbar5_dropdown-toggle w-dropdown-toggle"><div class="rl-dropdown-icon w-embed"><svg width=" 100%" height=" 100%" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 214<path fill-rule="evenodd" clip-rule="evenodd" d="M2.55806 6.29544C2.46043 6.19781 2.46043 6.03952 2.55806 5.94189L3.44195 5.058C3.53958 4.96037 3.69787 4.96037 3.7955 5.058L8.00001 9.26251L12.2045 5.058C12.3021 4.96037 12.4604 4.96037 12.5581 5.058L13.4419 5.94189C13.5396 6.03952 13.5396 6.19781 13.4419 6.29544L8.17678 11.5606C8.07915 11.6582 7.92086 11.6582 7.82323 11.5606L2.55806 6.29544Z" fill="currentColor"/> 215</svg></div><div class="rl_navbar5_link-text">Recursos</div></div><nav class="rl_navbar5_dropdown-list-copy w-dropdown-list"><div class="rl_navbar5_container"><div class="rl_navbar5_dropdown-content"><div class="rl_navbar5_dropdown-content-left"><div class="rl_navbar5_dropdown-link-list"><h4 class="rl_navbar5_dropdown-column-text">Herramientas gratis</h4><a href="/calculadoras/precio-entrenador-personal" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a637ee83ad287f97c093036_euro%20coin.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Cuánto cuesta un entrenador</div><p class="rl_navbar5_text-small">La horquilla real por sesión y al mes, con 126 tarifas publicadas.</p></div></a><a href="/calculadoras/donde-te-conviene-entrenar" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a6333b155392ae72dc3544a_icono%20lugar.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">¿Dónde te conviene entrenar?</div><p class="rl_navbar5_text-small">Gimnasio, a domicilio, al aire libre u online: seis preguntas y la pega de cada uno.</p></div></a><a href="/calculadoras/plan-primer-mes" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176197f33ec0043320f2a_dumbbell-01.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Tu primer mes de entrenamiento</div><p class="rl_navbar5_text-small">Cuántas sesiones y con qué foco cada semana, según los dÃas que tengas y dónde entrenes.</p></div></a><a href="/calculadoras/calculadora-calorias" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176422d38a3a4f0d2241b_weight-scale.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de calorÃas</div><p class="rl_navbar5_text-small">Calcula las calorÃas diarias que necesitas según tu edad, peso y nivel de actividad fÃsica.</p></div></a><a href="/calculadoras/calculadora-proteina" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a17619966cea24bd9bfe87_ta
215pe-measure.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de proteÃna</div><p class="rl_navbar5_text-small">Cuánta proteÃna necesitas al dÃa según tu peso y tu objetivo, con las calorÃas al lado.</p></div></a><a href="/calculadoras/calculadora-imc" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6ac016796b7d40191f7cf036_indice-imc.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de IMC</div><p class="rl_navbar5_text-small">Tu Ãndice de masa corporal y el rango de peso que corresponde a tu altura.</p></div></a><a href="/calculadoras/test-nivel-fitness" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176695fe1a56f4bbbe03b_workout-run.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Test de nivel de fitness</div><p class="rl_navbar5_text-small">Evalúa tu condición fÃsica actual y descubre qué tipo de entrenador necesitas.</p></div></a><a href="/calculadoras/pruebas-fisicas-oposiciones" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69c411fa43d84eac3751591f_medal.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Pruebas fÃsicas de oposición</div><p class="rl_navbar5_text-small">Compara tus marcas con las que te piden y descubre cuál es tu punto débil.</p></div></a></div><div class="rl_navbar5_dropdown-link-list"><h4 class="rl_navbar5_dropdown-column-text">Entrenadores por ciudad</h4><a href="https://www.redentrenadores.com/entrenadores-personales/madrid" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Madrid</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/barcelona" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Barcelona</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/zaragoza" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Zaragoza</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/valencia" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Valencia</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/sevilla" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Sevilla</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/malaga" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Málaga</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/murcia" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a59d-03a4a53d" class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Murcia</div></div></a><a href="/donde-entrenar" aria-current="page" class="rl_navbar5_dropdown-link w--current"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Dónde entrenar</div></div></a><a id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5a0-03a4a53d" href="/entrenador-personal" class="btn-green indide-menu w-inline-block"><div>Ver más ciudades</div></a></div></div><div class="rl_navbar5_dropdown-content-right"><div class="rl_navbar5_dropdown-content-wrapper"><h4 class="rl_navbar5_dropdown-column-text white">ArtÃculos destacados</h4><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5a7-03a4a53d" class="rl_navbar5_blog-list-wrapper"><div class="rl_navbar5_blog-list"><a href="https://www.redentrenadores.com/blog/precio-entrenador-personal-online" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-800.webp 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-1080.webp 1080w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-1600.webp 1600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-2000.webp 2000w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-2600.webp 2600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-3200.webp 3200w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840.webp 4152w" alt="Persona realizando un entrenamiento personal" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">¿Cuánto cuesta realmente un entrenador personal online?</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a><a href="https://www.redentrenadores.com/blog/tipos-entrenamiento-personal" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training.webp 600w" alt="Entrenador personal entrenando a un cliente a domicilio" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">Tipos de entrenamiento personal: descubre cuál se adapta mejor a ti</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a><a href="https://www.redentrenadores.com/blog/entrenador-personal-nutricionista-fisioterapeuta" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5c2-03a4a53d" class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-800.webp 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-1080.webp 1080w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-1600.webp 1600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-2000.webp 2000w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-2600.webp 2600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-3200.webp 3200w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341.webp 7680w" alt="Persona realizando un entrenamiento personal" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">Entrenador personal, nutricionista y fisioterapeuta: el equipo perfecto para transformar tu cuerpo</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a></div></div><div class="rl-button-group"><a href="/blog" class="rl-button-link w-inline-block"><div class="rl-button-link-text">Ver todos los artÃculos</div><div class="rl-button-link-icon w-embed">
215<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 216<path d="M6 3L11 8L6 13" stroke="CurrentColor" stroke-width="1.5"/> 217</svg></div></a></div></div><div class="rl_navbar5_dropdown-background-layer"></div></div></div></div></nav></div></div><div class="rl_navbar5_menu-right"><div data-hover="true" data-delay="0" data-ms-content="!members" class="drop-menu_entrenador w-dropdown"><div class="rl_navbar5_dropdown-toggle menu w-dropdown-toggle"><div class="rl-dropdown-icon w-icon-dropdown-toggle"></div><div>¿Eres entrenador?</div></div><nav class="dropdown-list-2 w-dropdown-list"><a href="/panel-entrenador" class="w-dropdown-link">Zona para entrenadores</a><a href="/registrate" class="w-dropdown-link">Activar perfil gratuito</a></nav></div><a href="https://www.redentrenadores.com/encuentra-entrenador" class="btn-green menu w-button">Buscar entrenador</a></div></nav><div class="w-layout-hflex"><a data-ms-content="members" href="/panel-entrenador" class="btn-green white_version w-button">Mi panel</a><div class="rl_navbar5_menu-button w-nav-button"><div data-ms-content="!members" class="rl_menu-icon"><div class="rl_menu-icon_line-top"></div><div class="rl_menu-icon_line-middle"><div class="rl_menu-icon_line-middle-inner"></div></div><div class="rl_menu-icon_line-bottom"></div></div></div></div></div><div class="w-embed w-script">
217<script> 218/* Webflow solo permite 991/767/479 como punto de colapso y su JS decide por su 219 cuenta (ignora el CSS). Con data-collapse=all lo gestiona en todos los anchos 220 y el <style> de abajo devuelve la fila a partir de 1180px. Este embed es hijo 221 del navbar y se ejecuta al parsearse, antes de que cargue webflow.js al final 222 del body, asi que el atributo ya esta puesto cuando Webflow inicializa. 223 El listener de resize hace falta porque, al creer que siempre esta colapsado, 224 Webflow ya no cierra el menu al cruzar un breakpoint: sin esto, quien abre el 225 menu a 1100px y luego amplia la ventana se queda con el menu enganchado 226 (Webflow mueve el <nav> dentro de .w-nav-overlay al abrir). Se cierra con su 227 propio boton para que Webflow restaure el DOM y limpie sus estilos inline. */ 228(function(){ 229 var s=document.currentScript,n=s&&s.parentElement&&s.parentElement.closest(".w-nav"); 230 if(!n)return; 231 n.setAttribute("data-collapse","all"); 232 var t; 233 window.addEventListener("resize",function(){ 234 clearTimeout(t); 235 t=setTimeout(function(){ 236 if(window.innerWidth<1180)return; 237 var b=n.querySelector(".w-nav-button.w--open"); 238 if(b)b.click(); 239 },100); 240 }); 241})(); 242</script>
242 243<style> 244/* El menu colapsa a hamburguesa hasta 1179px: por debajo de ~1165 no caben los 245 dos enlaces destacados y el flexbox aplasta el logo de 250 a 99px (medido). 246 Este bloque replica, para 992-1179px, las reglas que Webflow ya aplica bajo 247 991px al desplegar el menu. Revertir: borrar este embed entero y poner 248 "Collapse menu at" = medium en el Navbar. 249 Fuente en el repo: widgets/custom-code-header-colapso.css */ 250@media screen and (min-width:992px) and (max-width:1179px){.nav-pills-grupo{flex-direction:column;width:100%;background-color:transparent;border-radius:0;padding:0;row-gap:0}.nav-pill-item{width:100%;justify-content:center;font-size:1.125rem;padding:.75rem 0}.nav-pill-sep{display:none}.rl_menu-icon_line-middle{background-color:#000;justify-content:center;align-items:center;width:24px;height:2px;margin-top:6px;margin-bottom:6px;padding-bottom:0;padding-right:0;display:flex}.rl_navbar5_dropdown-toggle{flex-flow:row;justify-content:center;align-items:center;min-width:250px;padding:.75rem 0;font-size:1.125rem;display:flex}.rl_navbar5_dropdown-toggle.menu{width:100%;min-width:100%}.rl_navbar5_dropdown-toggle.menu.profile{justify-content:flex-end;align-items:center}.rl_navbar5_blog-item{width:100%}.rl_menu-icon_line-top{background-color:#000;width:24px;height:2px;padding-bottom:0;padding-right:0}.rl_menu-icon{flex-direction:column;justify-content:center;align-items:center;width:48px;height:48px;margin-right:-.5rem;padding-bottom:0;padding-right:0;display:flex}.rl_navbar5_dropdown-background-layer{border-radius:4px;min-width:100%}.rl_navbar5_dropdown-content-left{align-content:start;align-items:start;padding-left:0;padding-right:0}.rl_navbar5_large-item-content{width:100%}.rl_navbar5_dropdown-content{flex-direction:column}.rl_navbar5_menu-button{flex-flow:row}.rl_navbar5_menu-button.w--open{background-color:#0000}.rl_navbar5_blog-image{object-fit:cover}.rl_navbar5_blog-list{width:100%}.rl_menu-icon_line-middle-inner{width:4px;height:0;padding-bottom:0;padding-right:0}.rl-dropdown-icon{grid-column-gap:0px;grid-row-gap:0px;justify-content:center;align-items:center;inset:auto 0% auto auto}.rl_menu-icon_line-bottom{background-color:#000;width:24px;height:2px;padding-bottom:0;padding-right:0}.rl_navbar5_menu-right{flex-direction:column;grid-template-rows:auto auto;grid-template-columns:1fr;align-items:stretch;margin-top:1rem}.rl_navbar5_menu{-webkit-text-fill-color:inherit;background-color:#fff;background-clip:border-box;border-bottom:1px solid #000;align-items:flex-start;height:100vh;margin-left:0;padding:1rem 5% 15rem;position:absolute;overflow:auto}.rl_navbar5_dropdown-content-right{align-items:flex-start;max-width:none}.rl_navbar5_menu-left{flex-flow:column;justify-content:center;align-items:center}.rl_navbar5_menu-dropdown{flex-flow:column;width:100%;display:flex;position:relative}.rl_navbar5_dropdown-content-wrapper{width:100%}.drop-menu_entrenador{width:100%}.drop-menu_entrenador.profile{margin-left:0}.rl_navbar5_dropdown-list-copy{position:relative;overflow:hidden}.rl_navbar5_dropdown-list-copy.w--open{border-style:none;align-items:flex-start;min-width:auto;margin-bottom:1.5rem;padding:0 0 0;position:relative;top:0;left:0;right:0}} 251@media screen and (min-width:1180px){.w-nav[data-collapse=all] .w-nav-menu{display:flex}.w-nav[data-collapse=all] .w-nav-button{display:none}} 252</style></div></div><div class="mapa-hero"><h1 class="mapa-hero_titulo">Dónde entrenar cerca de ti</h1><p class="mapa-hero_entradilla">Gimnasios, parques, pistas y playas donde entrenan de verdad los entrenadores personales. Mira cómo es cada sitio, descubre quién entrena allà y escrÃbele directamente.</p><img src="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&fit=crop&w=1200&q=60" alt="Aparatos de ejercicio al aire libre en un parque" width="1200" height="300" fetchpriority="high" decoding="async" class="mapa-hero_img"/></div><div><div class="w-embed w-script"><!-- RedEntrenadores · Mapa de lugares. COMPONENTE, no página. 253 254 Esto es solo el mapa: filtros, lienzo, pines, burbuja y panel. Ni H1, ni
255 entradilla, ni FAQ: todo eso vive fuera, como elementos nativos de la 256 página que lo use. Asà el mismo mapa se puede meter en /donde-entrenar, en 257 las páginas de tipo, en las de ciudad o donde haga falta sin arrastrar un 258 segundo H1 ni duplicar contenido. 259 260 Sale de partir widget-donde-entrenar-mapa.html con 261 scripts/partir-embed-mapa.js. El id pasó de rt-donde-entrenar (nombre de 262 página) a rt-mapa-lugares (nombre de componente). 263 264 El filtro inicial no se decide aquÃ: lo pone la página, y de aplicarlo se 265 encarga widget-donde-entrenar-filtro-glue.html, que lee un data-tipo o un 266 data-familia del contenedor âo el hash de la URLâ y pulsa los propios 267 botones del mapa. 268 269 Lee directo de Supabase con la clave publicable: `lugares`, 270 `entrenador_lugares` y el RPC `lugares_en_bbox` tienen lectura pública. --> 271<style> 272#rt-mapa-lugares{font-family:"Plus Jakarta Sans",sans-serif;color:#1B1E22;background:#fff;} 273#rt-mapa-lugares *{box-sizing:border-box;} 274#rt-mapa-lugares .rt-ancho{max-width:1200px;margin:0 auto;padding:0 24px;} 275#rt-mapa-lugares .rt-filtros-caja{background:#F9FAFB;border:1px solid #d9dde2;border-radius:14px;padding:14px 16px;margin:0 auto 16px;max-width:1200px;} 276#rt-mapa-lugares .rt-segmentado{display:inline-flex;background:#fff;border:1px solid #d9dde2;border-radius:999px;padding:4px;gap:4px;max-width:100%;overflow-x:auto;} 277#rt-mapa-lugares .rt-seg-btn{border:none;background:none;border-radius:999px;padding:9px 20px;font-family:inherit;font-size:13.5px;font-weight:600;color:#4B5563;cursor:pointer;white-space:nowrap;} 278#rt-mapa-lugares .rt-seg-btn:hover{color:#089F3B;} 279#rt-mapa-lugares .rt-seg-btn.sel{background:#089F3B;color:#fff;} 280#rt-mapa-lugares .rt-fila{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;} 281#rt-mapa-lugares .rt-fila-tipos{margin-top:12px;padding-top:12px;border-top:1px dashed #d9dde2;} 282#rt-mapa-lugares .rt-etiqueta{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;flex-shrink:0;}
283#rt-mapa-lugares .rt-tipos{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;} 284#rt-mapa-lugares .rt-chip{border:1px solid #d9dde2;background:#fff;border-radius:999px;padding:10px 20px;font-size:14px;font-weight:600;font-family:inherit;color:#4B5563;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.06);} 285#rt-mapa-lugares .rt-chip:hover{border-color:#089F3B;color:#089F3B;} 286#rt-mapa-lugares .rt-chip.sel{border-color:#089F3B;background:#089F3B;color:#fff;} 287#rt-mapa-lugares .rt-chip-peq{padding:7px 15px;font-size:12.5px;font-weight:500;box-shadow:none;} 288#rt-mapa-lugares .rt-chip-peq.sel{background:#EAF3DE;color:#089F3B;border-color:#089F3B;} 289#rt-mapa-lugares .rt-lienzo{position:relative;border:1px solid #d9dde2;border-radius:14px;overflow:hidden;max-width:1200px;margin:0 auto;} 290#rt-mapa-lugares .rt-mapa{height:600px;} 291#rt-mapa-lugares .rt-pin{width:34px;height:34px;display:flex;align-items:center;justify-content:center;border:2.5px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.35);color:#fff;} 292#rt-mapa-lugares .rt-pin.centro{border-radius:9px;} 293#rt-mapa-lugares .rt-pin.publico{border-radius:50% 50% 50% 4px;transform:rotate(-45deg);} 294#rt-mapa-lugares .rt-pin.publico svg{transform:rotate(45deg);} 295#rt-mapa-lugares .rt-pin svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;} 296#rt-mapa-lugares .leaflet-popup-content-wrapper{border-radius:12px;box-shadow:0 6px 24px rgba(0,0,0,.22);} 297#rt-mapa-lugares .leaflet-popup-content{margin:0;width:290px!important;font-family:"Plus Jakarta Sans",sans-serif;} 298#rt-mapa-lugares .rt-bur-cab{padding:14px 16px 12px;} 299#rt-mapa-lugares .rt-bur-cab h3{font-family:"Barlow",sans-serif;font-size:17px;font-weight:700;margin:0 0 3px;line-height:1.25;} 300#rt-mapa-lugares .rt-bur-meta{font-size:12.5px;color:#6b7280;margin:0;} 301#rt-mapa-lugares .rt-bur-ents{border-top:1px solid #eef0f2;padding:12px 16px 14px;} 302#rt-mapa-lugares .rt-bur-tit{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;margin:0 0 10px;} 303#rt-mapa-lugares .rt-caras{display:flex;align-items:center;gap:-8px;margin-bottom:10px;} 304#rt-mapa-lugares .rt-cara{width:46px;height:46px;border-radius:50%;border:2px solid #fff;margin-right:-8px;background:#F3F3F3;object-fit:cover;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#1B1E22;flex-shrink:0;} 305/* La nota va siempre en verde de marca, tambien dentro de un parrafo gris: la 306 estrella es marca, no estado (design system §48). */ 307#rt-mapa-lugares .rt-estrellas{color:#089F3B;font-weight:700;white-space:nowrap;} 308#rt-mapa-lugares .rt-bur-nombres{font-size:12.5px;color:#4B5563;line-height:1.45;margin:0 0 12px;} 309#rt-mapa-lugares .rt-bur-btn{width:100%;padding:10px;border:none;border-radius:8px;background:#089F3B;color:#fff;font-family:inherit;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;cursor:pointer;} 310#rt-mapa-lugares .rt-bur-btn:hover{background:#067A2D;} 311#rt-mapa-lugares .rt-bur-btn-sec{background:#fff;color:#089F3B;border:1px solid #089F3B;} 312#rt-mapa-lugares .rt-bur-btn-sec:hover{background:#EAF3DE;color:#067A2D;} 313#rt-mapa-lugares .rt-panel{position:absolute;top:0;right:0;bottom:0;width:380px;max-width:100%;background:#fff;border-left:1px solid #d9dde2;overflow-y:auto;transform:translateX(101%);transition:transform .2s ease;z-index:1000;} 314#rt-mapa-lugares .rt-panel.abierto{transform:translateX(0);} 315#rt-mapa-lugares .rt-panel-cab{padding:20px 18px 16px;border-bottom:1px solid #d9dde2;position:relative;} 316#rt-mapa-lugares .rt-cerrar{position:absolute;top:12px;right:12px;background:none;border:none;font-size:22px;line-height:1;color:#6b7280;cursor:pointer;padding:4px;} 317#rt-mapa-lugares .rt-panel h2{font-family:"Barlow",sans-serif;font-size:20px;font-weight:700;margin:0 26px 4px 0;} 318#rt-mapa-lugares .rt-meta{font-size:12.5px;color:#6b7280;margin:0;} 319#rt-mapa-lugares .rt-ent{display:flex;gap:12px;padding:16px 18px;border-bottom:1px solid #d9dde2;} 320#rt-mapa-lugares .rt-avatar{width:64px;height:64px;border-radius:50%;background:#F3F3F3;flex-shrink:0;object-fit:cover;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px;} 321#rt-mapa-lugares .rt-ent-info{min-width:0;flex:1;} 322#rt-mapa-lugares .rt-ent-nombre{font-size:14.5px;font-weight:600;margin:0;} 323#rt-mapa-lugares .rt-ent-meta{font-size:12.5px;color:#089F3B;font-weight:600;margin:2px 0 0;} 324#rt-mapa-lugares .rt-ent-desc{font-size:12.5px;color:#4B5563;line-height:1.5;margin:6px 0 0;} 325#rt-mapa-lugares .btn{padding:9px 18px;border:none;border-radius:8px;background:#089F3B;color:#fff;text-transform:uppercase;font-weight:700;letter-spacing:.03em;font-size:11px;font-family:inherit;cursor:pointer;} 326#rt-mapa-lugares .btn:hover:not(:disabled){background:#067A2D;} 327#rt-mapa-lugares .btn:disabled{opacity:.5;cursor:not-allowed;} 328#rt-mapa-lugares .btn-peq{padding:7px 14px;margin-top:8px;} 329#rt-mapa-lugares .rt-vacio{padding:26px 18px;font-size:13px;color:#6b7280;text-align:center;} 330#rt-mapa-lugares .input,#rt-mapa-lugares textarea{border:1px solid #d9dde2;border-radius:8px;padding:9px 11px;font-size:13.5px;font-family:inherit;width:100%;margin-bottom:10px;} 331#rt-mapa-lugares .input:focus,#rt-mapa-lugares textarea:focus{outline:none;border-color:#089F3B;box-shadow:0 0 0 3px rgba(8,159,59,.15);} 332#rt-mapa-lugares .rt-form{padding:16px 18px;background:#F9FAFB;} 333#rt-mapa-lugares .rt-form h3{font-size:14px;margin:0 0 4px;} 334#rt-mapa-lugares .rt-ayuda{font-size:12px;color:#6b7280;margin:0 0 12px;} 335#rt-mapa-lugares .rt-error{font-size:12.5px;color:#791F1F;margin:8px 0 0;} 336#rt-mapa-lugares .rt-ok{font-size:13px;color:#067A2D;line-height:1.5;} 337@media(max-width:700px){#rt-mapa-lugares .rt-mapa{height:440px;} 338#rt-mapa-lugares .rt-panel{width:100%;top:auto;height:72%;border-left:none;border-top:1px solid #d9dde2;transform:translateY(101%);} 339#rt-mapa-lugares .rt-panel.abierto{transform:translateY(0);} 340#rt-mapa-lugares .leaflet-popup-content{width:230px!important;} 341#rt-mapa-lugares .rt-ancho{padding-left:16px;padding-right:16px;} 342#rt-mapa-lugares .rt-fila{flex-direction:column;gap:8px;} 343#rt-mapa-lugares .rt-segmentado{width:100%;justify-content:flex-start;} 344#rt-mapa-lugares .rt-seg-btn{flex:1;padding:9px 12px;font-size:12.5px;}} 345 346</style> 347 348<div id="rt-mapa-lugares"> 349 350 351 <div class="rt-ancho"> 352 <div class="rt-filtros-caja"> 353 <div class="rt-fila" id="rt-de-fam">
354 <span class="rt-etiqueta">Qué buscas</span> 355 <div class="rt-segmentado"> 356 <button class="rt-seg-btn sel" data-familia="">Todos</button> 357 <button class="rt-seg-btn" data-familia="centro">Centros</button> 358 <button class="rt-seg-btn" data-familia="publico">Al aire libre</button> 359 </div> 360 </div> 361 <div id="rt-de-tipos"></div> 362 </div> 363 <div class="rt-lienzo"> 364 <div class="rt-mapa" id="rt-de-mapa"></div> 365 <div class="rt-panel" id="rt-de-panel"></div> 366 </div> 367 </div> 368 369 370</div> 371
372<script> 373(function () { 374 var SUPABASE_URL = 'https://yrzanatzshdpvypazgeb.supabase.co'; 375 var ANON_KEY = 'sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD'; 376 var raiz = document.getElementById('rt-mapa-lugares'); 377 if (!raiz) return; 378 379 // Cada tipo tiene su icono; la familia decide la forma del pin (cuadrado 380 // redondeado para lo cerrado, gota para lo abierto). Jugar solo con el color 381 // no bastaba: hay que ver de un vistazo si es un gimnasio o un parque. 382 var ICONOS = { 383 'gimnasio': '<path d="M4 9v6M8 7v10M16 7v10M20 9v6M8 12h8"/>', 384 'estudio-box': '<path d="M5 8v8M19 8v8M9 6v12M15 6v12"/>', 385 'polideportivo-instalacion': '<path d="M4 20V9l8-5 8 5v11"/><path d="M9 20v-6h6v6"/>', 386 'parque': '<path d="M12 3l5 7h-3l3 5H7l3-5H7z"/><path d="M12 15v6"/>', 387 'calistenia': '<path d="M5 5v14M19 5v14M5 9h14M5 14h14"/>', 388 'pista-publica': '<circle cx="12" cy="12" r="8"/><path d="M4 12h16M12 4a8 8 0 0 1 0 16a8 8 0 0 1 0-16"/>', 389 'ruta-sendero': '<path d="M6 20c0-6 4-4 4-8s-4-2-4-8"/><path d="M18 4c0 6-4 4-4 8s4 2 4 8"/>', 390 'playa': '<path d="M3 17c2 0 2 2 4 2s2-2 4-2 2 2 4 2 2-2 4-2"/><path d="M12 14V6M12 6c-3 0-5 2-5 4h10c0-2-2-4-5-4z"/>' 391 }; 392 393 var FAMILIAS = { 394 centro: { 395 etiqueta: 'Centros', 396 color: '#089F3B', 397 tipos: { 398 'gimnasio': 'Gimnasios', 399 'estudio-box': 'Estudios y boxes', 400 'polideportivo-instalacion': 'Polideportivos' 401 } 402 }, 403 publico: { 404 etiqueta: 'Al aire libre', 405 color: '#1B4D89', 406 tipos: { 407 'parque': 'Parques', 408 'calistenia': 'Calistenia y barras', 409 'pista-publica': 'Pistas públicas', 410 'ruta-sendero': 'Rutas y senderos', 411 'playa': 'Playas' 412 } 413 } 414 }; 415 416 var SINGULAR = { 417 'gimnasio': 'Gimnasio', 'estudio-box': 'Estudio o box', 418 'polideportivo-instalacion': 'Polideportivo o instalación', 419 'parque': 'Parque', 'calistenia': 'Calistenia y barras', 420 'pista-publica': 'Pista o cancha pública', 'ruta-sendero': 'Ruta o sendero', 421 'playa': 'Playa' 422 }; 423 424 var CENTRO_ESPANA = [40.4168, -3.7038]; 425 426 function escapar(t) { 427 return String(t == null ? '' : t).replace(/[&<>"']/g, function (c) { 428 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 429 }); 430 } 431 function plural(n, s, p) { return n + ' ' + (n === 1 ? s : p); } 432 function iniciales(n) { 433 return (n || '?').trim().split(/\s+/).slice(0, 2).map(function (p) { return p[0]; }).join('').toUpperCase(); 434 } 435 // Devuelve HTML ya escapado, no texto: la nota tiene que ir en verde de marca 436 // aunque el parrafo que la rodea sea gris. La estrella es un elemento de 437 // marca, no un estado, y va fija en #089F3B (design system §48). 438 function metaEntrenador(e) { 439 var trozos = []; 440 var esp = e.especialidades && e.especialidades.length ? e.especialidades[0] : null; 441 if (esp) trozos.push(escapar(esp)); 442 if (e.rating_promedio && e.total_reviews) { 443 trozos.push('<span class="rt-estrellas">â ' 444 + escapar(Number(e.rating_promedio).toFixed(1).replace('.', ',')) 445 + '</span> (' + escapar(e.total_reviews) + ')'); 446 } 447 return trozos.join(' · '); 448 } 449 450 function nombreFamilia(tipo) { 451 for (var f in FAMILIAS) if (FAMILIAS[f].tipos[tipo]) return f; 452 return 'publico'; 453 } 454 455 function api(ruta, opciones) { 456 opciones = opciones || {}; 457 var cab = { apikey: ANON_KEY, Authorization: 'Bearer ' + ANON_KEY }; 458 if (opciones.cuerpo) cab['Content-Type'] = 'application/json'; 459 return fetch(SUPABASE_URL + ruta, { 460 method: opciones.metodo || 'GET', headers: cab, 461 body: opciones.cuerpo ? JSON.stringify(opciones.cuerpo) : undefined 462 }).then(function (r) { 463 return r.json().catch(function () { return null; }).then(function (d) { 464 if (!r.ok) throw new Error((d && (d.error || d.message)) || 'Error'); 465 return d; 466 }); 467 }); 468 } 469 470 var leafletListo = null; 471 function cargarLeaflet() { 472 if (leafletListo) return leafletListo; 473 leafletListo = new Promise(function (ok, mal) { 474 if (window.L) return ok(window.L); 475 var css = document.createElement('link'); 476 css.rel = 'stylesheet'; 477 css.href = 'https://unpkg.com/[email protected]/dist/leaflet.css'; 478 document.head.appendChild(css); 479 var js = document.createElement('script'); 480 js.src = 'https://unpkg.com/[email protected]/dist/leaflet.js'; 481 js.onload = function () { ok(window.L); }; 482 js.onerror = function () { mal(new Error('Leaflet')); }; 483 document.head.appendChild(js); 484 }); 485 return leafletListo; 486 } 487 488 var mapa = null, capaPines = null, filtro = { familia: null, tipo: null }, temporizador = null; 489 490 // Los filtros ya vienen en el HTML del embed; aqui solo se les da vida. Si 491 // anades una familia a FAMILIAS, anade tambien su boton alli a mano. 492 function conectarFiltros() { 493 raiz.querySelector('#rt-de-fam').addEventListener('click', function (e) { 494 var chip = e.target.closest('[data-familia]'); 495 if (!chip) return; 496 filtro.familia = chip.dataset.familia || null; 497 filtro.tipo = null; 498 raiz.querySelectorAll('[data-familia]').forEach(function (c) { c.classList.remove('sel'); }); 499 chip.classList.add('sel'); 500 pintarTipos(); 501 // Cambiar de filtro cierra la burbuja: su pin ya no tiene por que seguir 502 // en el mapa, y cargarPines() solo respeta al que la sostiene si esta abierta. 503 if (mapa) mapa.closePopup(); 504 cargarPines(); 505 }); 506 } 507 508 function pintarTipos() { 509 var caja = raiz.querySelector('#rt-de-tipos'); 510 if (!filtro.familia) { caja.innerHTML = ''; return; } 511 var tipos = FAMILIAS[filtro.familia].tipos; 512 caja.innerHTML = '<div class="rt-fila rt-fila-tipos">' 513 + '<span class="rt-etiqueta">De qué tipo</span>' 514 + '<div class="rt-tipos">' 515 + Object.keys(tipos).map(function (t) { 516 return '<button class="rt-chip rt-chip-peq" data-tipo="' + t + '">' + tipos[t] + '</button>'; 517 }).join('') 518 + '</div></div>'; 519 caja.querySelectorAll('[data-tipo]').forEach(function (chip) { 520 chip.addEventListener('click', function () { 521 var yaEstaba = chip.classList.contains('sel'); 522 caja.querySelectorAll('[data-tipo]').forEach(function (c) { c.classList.remove('sel'); }); 523 filtro.tipo = yaEstaba ? null : chip.dataset.tipo; 524 if (filtro.tipo) chip.classList.add('sel'); 525 if (mapa) mapa.closePopup(); 526 cargarPines(); 527 }); 528 }); 529 } 530 531 function iconoPin(L, lugar) { 532 var fam = lugar.familia; 533 var color = (FAMILIAS[fam] || FAMILIAS.publico).color; 534 var svg = '<svg viewBox="0 0 24 24">' + (ICONOS[lugar.tipo] || ICONOS.parque) + '</svg>'; 535 return L.divIcon({ 536 className: '', 537 html: '<div class="rt-pin ' + fam + '" style="background:' + color + '">' + svg + '</div>', 538 iconSize: [34, 34], 539 iconAnchor: [17, fam === 'publico' ? 34 : 17], 540 popupAnchor: [0, fam === 'publico' ? -34 : -18] 541 }); 542 } 543 544 // El RPC devuelve solo lo que pinta un pin; el detalle se pide al abrirlo. 545 // 546 // El contador evita que dos cargas solapadas âun paneo y el ResizeObserver 547 // disparando a la vezâ se pisen: si las dos limpian antes de que la primera 548 // añada, los pines salen duplicados. 549 // 550 // Los pines se refrescan por diferencia, no borrando la capa entera. Vaciarla 551 // mataba la burbuja abierta: al abrirla Leaflet mueve el mapa para que quepa 552 // (autoPan), ese movimiento dispara 'moveend', y 250 ms despues la recarga se 553 // llevaba por delante el marcador que la sostenia. En movil pasaba siempre
554 // âel mapa mide 440 px de alto y la burbuja no cabe sin desplazarâ, asi que 555 // la burbuja se abria y se cerraba sola. 556 var cargaActual = 0; 557 var pines = {}; 558 function cargarPines() { 559 if (!mapa) return; 560 var miCarga = ++cargaActual; 561 var b = mapa.getBounds(); 562 api('/rest/v1/rpc/lugares_en_bbox', { 563 metodo: 'POST', 564 cuerpo: { 565 min_lat: b.getSouth(), max_lat: b.getNorth(), 566 min_lng: b.getWest(), max_lng: b.getEast(), 567 p_familia: filtro.familia, p_tipo: filtro.tipo, p_limite: 500 568 } 569 }).then(function (lugares) { 570 if (miCarga !== cargaActual) return; 571 var vivos = {}; 572 (lugares || []).forEach(function (l) { 573 vivos[l.id] = true; 574 if (pines[l.id]) return; 575 // La burbuja se ata al crear el pin: de abrirla y cerrarla se encarga 576 // Leaflet, y aqui solo se rellena cuando ya esta abierta. 577 var marcador = L.marker([l.lat, l.lng], { icon: iconoPin(L, l), title: l.nombre }) 578 .bindPopup('<div class="rt-bur-cab"><h3>' + escapar(l.nombre) + '</h3>' 579 + '<p class="rt-bur-meta">Cargandoâ¦</p></div>', { maxWidth: 300, autoPanPadding: [30, 30] }) 580 .on('popupopen', function () { rellenarBurbuja(l, marcador); }); 581 marcador.addTo(capaPines); 582 pines[l.id] = marcador; 583 }); 584 Object.keys(pines).forEach(function (id) { 585 if (vivos[id]) return; 586 // El pin de la burbuja abierta sobrevive aunque el paneo lo saque del 587 // encuadre: quitarlo la cerraria. Al cambiar de filtro se cierra antes. 588 if (pines[id].isPopupOpen()) return; 589 capaPines.removeLayer(pines[id]); 590 delete pines[id]; 591 }); 592 }).catch(function () { /* un paneo fallido no rompe el mapa */ }); 593 } 594 595 // Detalle de un lugar: quién entrena ahà y los datos que no vienen en el RPC. 596 // Se memoriza porque la burbuja y el panel piden lo mismo. 597 var cache = {}; 598 function detalleDe(lugar) { 599 if (cache[lugar.id]) return Promise.resolve(cache[lugar.id]); 600 var ruta = '/rest/v1/entrenador_lugares?lugar_id=eq.' + encodeURIComponent(lugar.id) 601 + '&select=descripcion,created_at,entrenadores_publico!inner(slug,nombre,foto_url,titulo_perfil,especialidades,rating_promedio,total_reviews)' 602 + '&order=created_at.asc'; 603 return Promise.all([ 604 api(ruta).catch(function () { return []; }), 605 // El slug de la ficha es el del CENTRO, no el del lugar. Coinciden por 606 // casualidad en Mitolab y no tienen por qué: el lugar puede llamarse 607 // distinto o llevar sufijo si el slug estaba cogido. 608 api('/rest/v1/lugares?id=eq.' + encodeURIComponent(lugar.id) + '&select=direccion,centro_id,centros(slug,nombre)').catch(function () { return []; }) 609 ]).then(function (res) { 610 var vinculos = (res[0] || []).filter(function (v) { return v.entrenadores_publico; }); 611 // Orden por aportación (§8.1): primero quien ha descrito el sitio. 612 vinculos.sort(function (a, b) { return (a.descripcion ? 0 : 1) - (b.descripcion ? 0 : 1); }); 613 var d = { vinculos: vinculos, extra: (res[1] || [])[0] || {} }; 614 cache[lugar.id] = d; 615 return d; 616 }); 617 } 618 619 // La burbuja: lo mÃnimo para decidir si te interesa, encima del mapa. Esto 620 // SOLO la rellena; de abrirla y cerrarla se encarga Leaflet con el manejador 621 // de clic que instala bindPopup. 622 // 623 // Sin autoClose ni closeOnClick: los dos estaban en false para que la burbuja 624 // dejara de cerrarse sola, pero eso no era un clic âera la recarga de pines 625 // borrando su marcadorâ y a cambio dejaban la burbuja pegada: no se cerraba 626 // ni pulsando fuera. Con los valores por defecto, tocas fuera y se cierra. 627 // 628 // Y no la abrimos nosotros: antes el clic del pin llamaba a openPopup() y de 629 // paso volvia a atar la burbuja con bindPopup. El manejador de Leaflet corria 630 // despues sobre ese mismo clic, veia la burbuja ya abierta y la cerraba âo 631 // sea que, una vez cerrada, el pin no la volvia a abrir nunca. 632 function rellenarBurbuja(lugar, marcador) { 633 history.replaceState(null, '', '#lugar=' + lugar.slug); 634 635 detalleDe(lugar).then(function (d) { 636 var meta = [SINGULAR[lugar.tipo] || lugar.tipo, d.extra.direccion].filter(
636Boolean).join(' · '); 637 var html = '<div class="rt-bur-cab"><h3>' + escapar(lugar.nombre) + '</h3>' 638 + '<p class="rt-bur-meta">' + escapar(meta) + '</p></div>'; 639 640 // Un lugar puede ser un centro con ficha propia en /centros/[slug] y no 641 // tener todavÃa ningún entrenador vinculado â es el caso de Mitolab, el 642 // primero real. Sin este enlace la burbuja se quedaba sin salida: decÃa 643 // "todavÃa no hay entrenadores" y ahà morÃa, teniendo una ficha completa 644 // detrás. 645 var enlaceCentro = d.extra.centros 646 ? '<a class="rt-bur-btn" style="display:block;text-align:center;text-decoration:none;box-sizing:border-box;" target="_blank" rel="noopener" href="/centros/' 647 + escapar(d.extra.centros.slug) + '">Ver ficha del centro</a>' 648 : ''; 649 650 if (!d.vinculos.length) { 651 html += '<div class="rt-bur-ents">' 652 + (enlaceCentro 653 ? enlaceCentro 654 : '<p class="rt-bur-meta">TodavÃa no hay entrenadores en este sitio.</p>') 655 + '</div>'; 656 } else { 657 var tres = d.vinculos.slice(0, 3); 658 var nombres = tres.map(function (v) { return v.entrenadores_publico.nombre.trim(); }); 659 var resto = d.vinculos.length - tres.length; 660 html += '<div class="rt-bur-ents">' 661 + '<p class="rt-bur-tit">Entrenadores que puedes encontrar aquÃ</p>' 662 + '<div class="rt-caras">' 663 + tres.map(function (v) { 664 var e = v.entrenadores_publico; 665 return e.foto_url 666 ? '<img class="rt-cara" src="' + escapar(e.foto_url) + '" alt="' + escapar(e.nombre) + '" loading="lazy" />' 667 : '<div class="rt-cara">' + escapar(iniciales(e.nombre)) + '</div>'; 668 }).join('') 669 + '</div>' 670 + '<p class="rt-bur-nombres">' + escapar(nombres.join(', ')) + (resto > 0 ? ' y ' + plural(resto, 'más', 'más') : '') + '</p>' 671 + (d.vinculos.length === 1 && metaEntrenador(d.vinculos[0].entrenadores_publico) 672 ? '<p class="rt-bur-meta" style="margin:-6px 0 12px;">' + metaEntrenador(d.vinculos[0].entrenadores_publico) + '</p>' 673 : '') 674 + (d.vinculos.length === 1 675 ? '<a class="rt-bur-btn" style="display:block;text-align:center;text-decoration:none;" target="_blank" rel="noopener" href="/entrenador-personal/' 676 + escapar(d.vinculos[0].entrenadores_publico.slug) + '">Ver perfil del entrenador</a>' 677 : '<button class="rt-bur-btn" data-ver>Ver los ' + d.vinculos.length + ' entrenadores</button>') 678 + (enlaceCentro ? '<div style="margin-top:8px;">' + enlaceCentro.replace('rt-bur-btn', 'rt-bur-btn rt-bur-btn-sec') + '</div>' : '') 679 + '</div>'; 680 } 681 682 marcador.setPopupContent(html); 683 var pop = marcador.getPopup().getElement(); 684 var btn = pop && pop.querySelector('[data-ver]'); 685 if (btn) btn.addEventListener('click', function () { abrirPanel(lugar, d); }); 686 }); 687 } 688 689 // El panel: el detalle completo y el contacto. 690 function abrirPanel(lugar, d) { 691 var panel = raiz.querySelector('#rt-de-panel'); 692 panel.classList.add('abierto'); 693 var meta = [SINGULAR[lugar.tipo] || lugar.tipo, d.extra.direccion].filter(Boolean).join(' · '); 694 695 var html = '<div class="rt-panel-cab">' 696 + '<button class="rt-cerrar" aria-label="Cerrar">×</button>' 697 + '<h2>' + escapar(lugar.nombre) + '</h2>' 698 + '<p class="rt-meta">' + escapar(meta) + '</p>' 699 + (d.extra.centros ? '<p style="margin:10px 0 0;"><a target="_blank" rel="noopener" href="/centros/' + escapar(d.extra.centros.slug) + '" style="font-size:12.5px;color:#089F3B;font-weight:600;">Ver ficha completa del centro</a></p>' : '') 700 + '</div>'; 701 702 if (!d.vinculos.length) { 703 html += '<div class="rt-vacio">TodavÃa no hay entrenadores en este sitio.<br><br>' 704 + '<a class="btn" href="/encuentra-entrenador" style="display:inline-block;text-decoration:none;">Buscar entrenador</a></div>'; 705 } else { 706 d.vinculos.forEach(function (v) { 707 var e = v.entrenadores_publico; 708 html += '<div class="rt-ent">' 709 + (e.foto_url 710 ? '<img class="rt-avatar" src="' + escapar(e.foto_url) + '" alt="" loading="lazy" />' 711 : '<div class="rt-avatar">' + escapar(iniciales(e.nombre)) + '</div>') 712 + '<div class="rt-ent-info">' 713 + '<p class="rt-ent-nombre"><a target="_blank" rel="noopener" href="/entrenador-personal/' + escapar(e.slug) + '" style="color:inherit;text-decoration:none;">' + escapar(e.nombre) + '</a></p>' 714 + (metaEntrenador(e) ? '<p class="rt-ent-meta">' + metaEntrenador(e) + '</p>' : '') 715 + (e.titulo_perfil ? '<p class="rt-meta">' + escapar(e.titulo_perfil) + '</p>' : '') 716 + (v.descripcion ? '<p class="rt-ent-desc">' + escapar(v.descripcion) + '</p>' : '') 717 + '<a class="btn btn-peq" style="display:inline-block;text-decoration:none;" target="_blank" rel="noopener" href="/entrenador-personal/' + escapar(e.slug) + '">Ver perfil del entrenador</a>' 718 + '</div></div>'; 719 }); 720 } 721 722 panel.innerHTML = html; 723 panel.querySelector('.rt-cerrar').addEventListener('click', cerrarPanel); 724 } 725 726 function cerrarPanel() { 727 raiz.querySelector('#rt-de-panel').classList.remove('abierto'); 728 } 729 730 // Deep link: /donde-entrenar#lugar=parque-del-retiro abre ese sitio (§6.3). 731 function abrirLugarPorSlug(slug) { 732 api('/rest/v1/lugares?slug=eq.' + encodeURIComponent(slug) + '&select=id,nombre,slug,familia,tipo,lat,lng&limit=1') 733 .then(function (filas) { 734 var l = (filas || [])[0]; 735 if (!l) return;
736 mapa.setView([l.lat, l.lng], 15); 737 detalleDe(l).then(function (d) { abrirPanel(l, d); }); 738 }).catch(function () {}); 739 } 740 741 function abrirDesdeHash() { 742 var m = (location.hash || '').match(/lugar=([a-z0-9-]+)/i); 743 if (m) abrirLugarPorSlug(m[1]); 744 } 745 746 function irAlMapa() { 747 var lienzo = raiz.querySelector('.rt-lienzo'); 748 if (lienzo) lienzo.scrollIntoView({ behavior: 'smooth', block: 'center' }); 749 } 750 751 conectarFiltros(); 752 753 cargarLeaflet().then(function (L) { 754 mapa = L.map('rt-de-mapa').setView(CENTRO_ESPANA, 6); 755 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 756 attribution: '© OpenStreetMap', maxZoom: 19 757 }).addTo(mapa); 758 capaPines = L.layerGroup().addTo(mapa); 759 760 // Sin espera, arrastrar el mapa lanzarÃa decenas de consultas. 761 mapa.on('moveend', function () { 762 clearTimeout(temporizador); 763 temporizador = setTimeout(cargarPines, 250); 764 }); 765 766 // Si el contenedor todavÃa no tiene ancho cuando Leaflet arranca âun embed 767 // dentro de una pestaña, un acordeón o una sección que se dimensiona 768 // tardeâ, el mapa nace de 0px: `getBounds()` devuelve una caja degenerada, 769 // la consulta no trae nada y no se pinta ni un pin. Se vigila el tamaño y, 770 // en cuanto cambia de verdad, se recalcula y se vuelven a pedir. 771 var anchoPrevio = 0; 772 function ajustar() { 773 var ancho = raiz.querySelector('#rt-de-mapa').clientWidth; 774 if (!ancho || ancho === anchoPrevio) return; 775 anchoPrevio = ancho; 776 mapa.invalidateSize(); 777 cargarPines(); 778 } 779 780 if (window.ResizeObserver) { 781 new ResizeObserver(ajustar).observe(raiz.querySelector('#rt-de-mapa')); 782 } 783 window.addEventListener('resize', ajustar); 784 setTimeout(ajustar, 300); 785 786 cargarPines(); 787 abrirDesdeHash(); 788 789 // La lista de abajo enlaza a #lugar=..., asi que un clic ahi tiene que 790 // abrir el sitio en el mapa y subir hasta el. abrirDesdeHash solo corria al 791 // cargar; sin esto los enlaces de la lista no harian nada visible. 792 window.addEventListener('hashchange', function () { 793 irAlMapa(); 794 abrirDesdeHash(); 795 }); 796 797 // Y ademas hay que escuchar el clic, porque hay un caso en el que el 798 // navegador NO dispara hashchange: cuando el hash ya apunta a ese mismo 799 // sitio. Pasa constantemente âabrir un pin deja el hash puestoâ y entonces 800 // su enlace de la lista no hacia absolutamente nada. Si el hash es otro no 801 // tocamos nada: la navegacion normal dispara hashchange y guarda su entrada 802 // en el historial, para que el boton Atras siga sirviendo. 803 document.addEventListener('click', function (e) { 804 var a = e.target && e.target.closest && e.target.closest('a[href*="#lugar="]'); 805 if (!a) return; 806 var m = (a.getAttribute('href') || '').match(/#lugar=([a-z0-9-]+)/i); 807 if (!m || location.hash !== '#lugar=' + m[1]) return; 808 irAlMapa(); 809 abrirLugarPorSlug(m[1]); 810 }); 811 }).catch(function () { 812 raiz.querySelector('#rt-de-mapa').innerHTML = 813 '<p style="padding:24px;color:#791F1F;">No se ha podido cargar el mapa. Recarga la página.</p>'; 814 }); 815})(); 816</script>
816 817</div><div class="w-embed w-script"><!-- RedEntrenadores · Glue del mapa: arrancar con un filtro puesto. 818 819 Va DEBAJO del embed del mapa, dentro del mismo componente. Deja el mapa 820 con una familia y un tipo seleccionados, como si los hubiera pulsado el 821 usuario. El filtro sale, por este orden: 822 823 1. Un data-tipo o un data-familia en el contenedor del mapa o en 824 cualquier ancestro suyo. Es como lo piden las páginas de tipo: 825 <div data-tipo="calistenia"> alrededor de la instancia. 826 2. El hash de la URL: #tipo=calistenia o #familia=publico. Es como 827 llegan los enlaces desde fuera. 828 829 Por qué un glue y no una opción dentro del mapa: el embed del mapa son 830 30 KB que solo se suben pegándolos (la API no escribe embeds), asà que 831 cada cambio ahà cuesta una pegada a mano. Esto son cuarenta lÃneas que se 832 escriben por API y no lo tocan. Mismo patrón que 833 widget-listado-ciudad-glue.html con el listado compartido. 834 835 No inventa estado: pulsa los propios botones del mapa, asà que la interfaz 836 queda coherente âlas pÃldoras salen marcadasâ y si el mapa cambiara sus 837 filtros, esto deja de encontrar los botones y no hace nada. Degrada a 838 "mapa sin filtrar", nunca a mapa roto. 839 840 Tipos válidos, los de la spec §4.1: gimnasio, estudio-box, 841 polideportivo-instalacion, parque, calistenia, pista-publica, 842 ruta-sendero, playa. --> 843
844<script> 845(function () { 846 var FAMILIA_DE = { 847 'gimnasio': 'centro', 'estudio-box': 'centro', 'polideportivo-instalacion': 'centro', 848 'parque': 'publico', 'calistenia': 'publico', 'pista-publica': 'publico', 849 'ruta-sendero': 'publico', 'playa': 'publico' 850 }; 851 852 // rt-donde-entrenar es el id que tenÃa el mapa cuando vivÃa pegado al hub. 853 // Se sigue mirando por si queda alguna página sin migrar al componente. 854 function raizMapa() { 855 return document.getElementById('rt-mapa-lugares') || 856 document.getElementById('rt-donde-entrenar'); 857 } 858 859 function normaliza(tipo, familia) { 860 if (tipo && FAMILIA_DE[tipo]) return { familia: FAMILIA_DE[tipo], tipo: tipo }; 861 if (familia === 'centro' || familia === 'publico') return { familia: familia, tipo: null }; 862 return null; 863 } 864 865 function delContenedor(raiz) { 866 var el = raiz && raiz.closest('[data-tipo],[data-familia]'); 867 if (!el) return null; 868 return normaliza(el.getAttribute('data-tipo'), el.getAttribute('data-familia')); 869 } 870 871 function delHash() { 872 var h = location.hash || ''; 873 var t = h.match(/tipo=([a-z-]+)/i); 874 var f = h.match(/familia=(centro|publico)/i); 875 return normaliza(t && t[1], f && f[1].toLowerCase()); 876 } 877 878 // El mapa monta sus escuchas al parsearse (conectarFiltros corre en lÃnea), 879 // pero este embed puede ejecutarse antes de que el navegador haya terminado 880 // con el suyo. Se reintenta un par de segundos y se abandona en silencio. 881 function aplicar(pedido, intento) { 882 var raiz = raizMapa(); 883 var fam = raiz && raiz.querySelector('#rt-de-fam [data-familia="' + pedido.familia + '"]'); 884 if (fam) { 885 if (!fam.classList.contains('sel')) fam.click(); 886 if (!pedido.tipo) return; 887 // pintarTipos() corre dentro del clic anterior, asà que la pÃldora del 888 // tipo ya existe en este punto. 889 var chip = raiz.querySelector('#rt-de-tipos [data-tipo="' + pedido.tipo + '"]'); 890 if (chip) { 891 if (!chip.classList.contains('sel')) chip.click(); 892 return; 893 } 894 } 895 if (intento < 20) setTimeout(function () { aplicar(pedido, intento + 1); }, 100); 896 } 897 898 function arrancar() { 899 var pedido = delHash() || delContenedor(raizMapa()); 900 if (pedido) aplicar(pedido, 0); 901 } 902 903 window.addEventListener('hashchange', arrancar); 904 if (document.readyState === 'loading') { 905 document.addEventListener('DOMContentLoaded', arrancar); 906 } else { 907 arrancar(); 908 } 909})(); 910</script>
910 911</div></div><div class="w-embed"><!-- RedEntrenadores · Embed de la lista de lugares de /donde-entrenar (spec §6.1). 912 Va DEBAJO del embed del mapa, en la misma página. Son dos embeds a 913 propósito: el mapa se edita a mano y esto lo regenera un script. 914 915 Por qué existe: el mapa lo pinta Leaflet leyendo Supabase, y un pin no es 916 un enlace. Sin esta lista la página no enlaza a ninguna otra del sitio 917 âlos enlaces a fichas y centros solo nacen al pulsar un pin, o sea nunca 918 para un rastreadorâ y encima no tiene texto que Google pueda leer. 919 920 Lo que hay entre LISTA:INICIO y LISTA:FIN lo reescribe entero 921 `node scripts/generar-lista-lugares.js`. Todo lo de fuera de las marcas 922 âeste comentario, el H2, la entradilla y el CSSâ se edita a mano y el 923 script no lo toca. 924 925 El CSS cuelga de .rt-lista, no de #rt-donde-entrenar: este embed está 926 fuera del contenedor del mapa. --> 927 928<section class="rt-lista"> 929 <h2>Todos los sitios donde entrenar</h2> 930 <p class="rt-lista-pie">Estos son los lugares que los entrenadores de RedEntrenadores han añadido al mapa. Pulsa cualquiera para verlo y descubrir quién entrena allÃ.</p> 931<!-- LISTA:INICIO --> 932 <div class="rt-lista-ciudad"> 933 <h3><a href="/entrenadores-personales/madrid">Dónde entrenar en Madrid</a></h3> 934 <p class="rt-lista-intro">37 sitios donde entrenan entrenadores personales en Madrid y alrededores.</p> 935 <p class="rt-lista-fam">Al aire libre</p> 936 <ul> 937 <li><a href="/donde-entrenar#lugar=madrid-rio">Madrid RÃo</a> <span class="rt-lista-tipo">· Parque · Arganzuela</span> â entrenan aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a> y <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li> 938 <li><a href="/donde-entrenar#lugar=parque-2">Parque</a> <span class="rt-lista-tipo">· Parque</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 939 <li><a href="/donde-entrenar#lugar=parque-calero">Parque Calero</a> <span class="rt-lista-tipo">· Parque · Ciudad Lineal · Calle de José del Hierro</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 940 <li><a href="/donde-entrenar#lugar=parque-de-las-siete-tetas">Parque Cerro del TÃo PÃo (Siete Tetas)</a> <span class="rt-lista-tipo">· Parque · Vallecas · Calle de Ramón Pérez de Ayala, Puente de Vallecas</span> â entrenan aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a> y <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 941 <li><a href="/donde-entrenar#lugar=parque-calistenia-arturo-soria">Parque de calistenia de Arturo Soria</a> <span class="rt-lista-tipo">· Calistenia · Ciudad Lineal · Arturo Soria-Gran VÃa de Hortaleza, Calle de Arturo Soria</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 942 <li><a href="/donde-entrenar#lugar=parque-de-la-gavia">Parque de La Gavia</a> <span class="rt-lista-tipo">· Parque · Vallecas · Ensanche de Vallecas</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 943 <li><a href="/donde-entrenar#lugar=parque-del-oeste">Parque del Oeste</a> <span class="rt-lista-tipo">· Parque · Moncloa-Aravaca</span> â entrena aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li> 944 <li><a href="/donde-entrenar#lugar=parque-del-retiro">Parque del Retiro</a> <span class="rt-lista-tipo">· Parque · Retiro</span> â entrenan aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a> y <a href="/entrenador-personal/ines-narvaez-madrid">Inés Narváez</a></li> 945 <li><a href="/donde-entrenar#lugar=parque-juan-carlos-i">Parque Juan Carlos I</a> <span class="rt-lista-tipo">· Parque · Juan de Borbón-Ribera del Sena, Glorieta Juan de Borbón y Battenberg</span> â entrena aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li> 946 <li><a href="/donde-entrenar#lugar=pinar-del-rey">Pinar del Rey</a> <span class="rt-lista-tipo">· Parque · Hortaleza · Avenida de la Gran VÃa de Hortaleza</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 947 </ul> 948 <p class="rt-lista-fam">
948Gimnasios y centros</p> 949 <ul> 950 <li><a href="/donde-entrenar#lugar=basic-fit-calle-de-orense">Basic-fit calle de Orense</a> <span class="rt-lista-tipo">· Gimnasio</span> â entrena aquà <a href="/entrenador-personal/hugo-arias-madrid">Hugo Arias</a></li> 951 <li><a href="/donde-entrenar#lugar=basic-fit-calle-sor-angela">Basic-fit calle Sor Ãngela</a> <span class="rt-lista-tipo">· Gimnasio · Centro · Basic-fit Sor Ãngela</span> â entrenan aquà <a href="/entrenador-personal/hugo-arias-madrid">Hugo Arias</a> y <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 952 <li><a href="/donde-entrenar#lugar=basic-fit-vallecas">Basic-Fit Vallecas</a> <span class="rt-lista-tipo">· Gimnasio · Vallecas · Avenida Albufera 6</span> â entrena aquà <a href="/entrenador-personal/aitor-cuesta-madrid">Aitor Cuesta</a></li> 953 <li><a href="/donde-entrenar#lugar=ciclismo-y-rendimiento">Ciclismo y Rendimiento</a> <span class="rt-lista-tipo">· Estudio o box · Hortaleza · Calle de AgustÃn de Iturbide, 24</span> · <a href="/centros/ciclismo-y-rendimiento">ficha del centro</a></li> 954 <li><a href="/donde-entrenar#lugar=fitup-serrano">Fitup Serrano</a> <span class="rt-lista-tipo">· Gimnasio · Salamanca · Calle Serrano, 50</span> â entrenan aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a>, <a href="/entrenador-personal/aitor-cuesta-madrid">Aitor Cuesta</a> y <a href="/entrenador-personal/laura-saez-culturismo-online">Laura Sáez</a> · <a href="/centros/fitup-serrano">ficha del centro</a></li> 955 <li><a href="/donde-entrenar#lugar=gimnasio-crom">Gimnasio Crom</a> <span class="rt-lista-tipo">· Gimnasio · Ciudad Lineal · Calle de Gregorio Donas, 10</span> â entrena aquà <a href="/entrenador-personal/rodrigo-bermejo">Rodrigo Bermejo</a></li> 956 <li><a href="/donde-entrenar#lugar=idm-basica-carril-del-conde">Instalación Deportiva Municipal Básica Carril del Conde</a> <span class="rt-lista-tipo">· Polideportivo · Hortaleza · Calle del Carril del Conde, Canillas</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 957 <li><a href="/donde-entrenar#lugar=idm-canillas">Instalación Deportiva Municipal Canillas</a> <span class="rt-lista-tipo">· Polideportivo · Hortaleza · Calle de AgustÃn de Iturbide, 1</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 958 <li><a href="/donde-entrenar#lugar=polideportivo">Polideportivo</a> <span class="rt-lista-tipo">· Polideportivo · Centro</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 959 <li><a href="/donde-entrenar#lugar=polideportivo-municipal-casa-de-campo">Polideportivo Municipal Casa de Campo</a> <span class="rt-lista-tipo">· Polideportivo · Moncloa-Aravaca · Ronda del Lago</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 960 <li><a href="/donde-entrenar#lugar=polideportivo-san-juan-bautista">Polideportivo San Juan Bautista</a> <span class="rt-lista-tipo">· Polideportivo · Ciudad Lineal · Calle de RamÃrez de Arellano, San Juan Bautista</span> â entrena aquà <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li> 961 <li><a href="/donde-entrenar#lugar=polideportivo-vicalvaro">Polideportivo Vicálvaro</a> <span class="rt-lista-tipo">· Polideportivo · Paseo del Polideportivo, Casco Histórico de Vicálvaro</span> â entrena aquà <a href="/entrenador-personal/alberto-ullner-madrid">Alberto Ullner</a></li> 962 <li><a href="/donde-entrenar#lugar=vibra-bienestar">Vibra Bienestar</a> <span class="rt-lista-tipo">· Gimnasio · Calle Jaime Hermida 9</span> â entrena aquà <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li> 963 <li><a href="/donde-entrenar#lugar=wellzone-las-tablas">Wellzone Las Tablas</a> <span class="rt-lista-tipo">· Gimnasio · Calle de MarÃa Tubau, 5</span> · <a href="/centros/wellzone-las-tablas">ficha del centro</a></li> 964 <li><a href="/donde-entrenar#lugar=wezone-almagro">Wezone Almagro</a> <span class="rt-lista-tipo">· Estudio o box · Chamberà · Calle de Zurbano, 56</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 965 <li><a href="/donde-entrenar#lugar=wezone-arturo-soria">Wezone Arturo Soria</a> <span class="rt-lista-tipo">· Estudio o box · Ciudad Lineal · Calle de Arturo Soria, 195</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 966 <li><a href="/donde-entrenar#lugar=wezone-barrio-salamanca">Wezone Barrio Salamanca</a> <span class="rt-lista-tipo">· Estudio o box · Salamanca · Calle de Diego de León, 31</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 967 <li><a href="/donde-entrenar#lugar=wezone-bernabeu">Wezone Bernabéu</a> <span class="rt-lista-tipo">· Estudio o box · ChamartÃn · Calle de VÃctor Andrés Belaúnde, 25</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 968 <li><a href="/donde-entrenar#lugar=wezone-chamberi">Wezone ChamberÃ</a> <span class="rt-lista-tipo">· Estudio o box · Chamberà · Calle de Vallehermoso, 27</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 969 <li><a href="/donde-entrenar#lugar=wezone-conde-duque">Wezone Conde Duque</a> <span class="rt-lista-tipo">· Estudio o box · Centro · Calle de Alberto Aguilera, 15</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 970 <li><a href="/donde-entrenar#lugar=wezone-embajadores">Wezone Embajadores</a> <span class="rt-lista-tipo">· Estudio o box · Arganzuela · Calle de MartÃn Soler, 8</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 971 <li><a href="/donde-entrenar#lugar=wezone-pacifico">Wezone PacÃfico</a> <span class="rt-lista-tipo">· Estudio o box · Retiro · Calle de AndalucÃa, 6</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 972 <li><a href="/donde-entrenar#lugar=wezone-plaza-castilla">Wezone Plaza Castilla</a> <span class="rt-lista-tipo">· Estudio o box · Tetuán · Calle de Bravo Murillo, 309</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 973 <li><a href="/donde-entrenar#lugar=wezone-ventas">Wezone Ventas</a> <span class="rt-lista-tipo">· Estudio o box · Salamanca · Calle de Azcona, 44</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 974 <li><a href="/donde-entrenar#lugar=wifit-guindalera">Wifit Guindalera</a> <span class="rt-lista-tipo">· Gimnasio · Salamanca · Calle de Cartagena, 16</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 975 <li><a href="/donde-entrenar#lugar=wifit-moratalaz">Wifit Moratalaz</a> <span class="rt-lista-tipo">· Gimnasio · Avenida de Moratalaz, 181</span> â entrena aquà <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li> 976 </ul> 977 <p class="rt-lista-fam">Alrededores · Las Rozas de Madrid</p> 978 <ul> 979 <li><a href="/donde-entrenar#lugar=cep-las-rozas">CEP Las Rozas</a> <span class="rt-lista-tipo">· Estudio o box · Calle Módena, 25</span> · <a href="/centros/cep-las-rozas">ficha del centro</a></li> 980 </ul> 981 <p class="rt-lista-barrios"><strong>Barrios de Madrid con sitios para entrenar:</strong> Arganzuela, Centro, ChamartÃn, ChamberÃ, Ciudad Lineal, Hortaleza, Moncloa-Aravaca, Retiro, Salamanca, Tetuán, V
981allecas.</p> 982 </div> 983 <div class="rt-lista-ciudad"> 984 <h3><a href="/entrenadores-personales/malaga">Dónde entrenar en Málaga</a></h3> 985 <p class="rt-lista-intro">8 sitios donde entrenan entrenadores personales en Málaga.</p> 986 <p class="rt-lista-fam">Al aire libre</p> 987 <ul> 988 <li><a href="/donde-entrenar#lugar=joaquin-ramirez-entrenador-personal">JoaquÃn RamÃrez Entrenador Personal</a> <span class="rt-lista-tipo">· Calistenia · Málaga Este</span> â entrena aquà <a href="/entrenador-personal/joaquin-ramirez-malaga">JoaquÃn RamÃrez</a></li> 989 <li><a href="/donde-entrenar#lugar=outdoor-workout-parque-de-santa-ana-teatinos-universidad">Outdoor Workout - Parque de Santa Ana - Teatinos-Universidad</a> <span class="rt-lista-tipo">· Calistenia · Teatinos · Avenida Diego Fernández de Mendoza</span> â entrena aquà <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li> 990 <li><a href="/donde-entrenar#lugar=outdoor-workout-parque-virgen-de-las-canas-los-almendros">Outdoor Workout - Parque Virgen de las Cañas - Los Almendros</a> <span class="rt-lista-tipo">· Parque · Puerto de la Torre · Avenida Andersen</span> â entrena aquà <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li> 991 <li><a href="/donde-entrenar#lugar=parque-calistenia-one-piece">Parque Calistenia "One Piece"</a> <span class="rt-lista-tipo">· Calistenia · Puerto de la Torre · Lope de Vega - Hacienda Cabello, Avenida Lope de Vega</span> â entrena aquà <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li> 992 <li><a href="/donde-entrenar#lugar=parque-deportivo-work-out-teatinos">Parque Deportivo Work Out Teatinos</a> <span class="rt-lista-tipo">· Calistenia · Teatinos · Calle Antonio RaÃz 27</span> â entrena aquà <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li> 993 <li><a href="/donde-entrenar#lugar=parque-forestal-de-la-laguna-de-la-barrera-maria-eugenia-can">Parque Forestal de la Laguna de la Barrera "MarÃa Eugenia Candau Rámila"</a> <span class="rt-lista-tipo">· Parque · Teatinos · Laguna de la Barrera</span> â entrena aquà <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li> 994 </ul> 995 <p class="rt-lista-fam">Gimnasios y centros</p> 996 <ul> 997 <li><a href="/donde-entrenar#lugar=on-air">On Air</a> <span class="rt-lista-tipo">· Gimnasio · Centro · Calle Armengual de la Mota 39</span> â entrena aquà <a href="/entrenador-personal/antonio-porras-malaga">Antonio Porras</a></li> 998 <li><a href="/donde-entrenar#lugar=vals-sport-ave-maria">Vals Sport Ave MarÃa</a> <span class="rt-lista-tipo">· Gimnasio · Carretera de Cádiz · Avenida Sor Teresa Prat, 51</span> â entrena aquà <a href="/entrenador-personal/sergio-bermudez-malaga">Sergio Bermúdez</a></li> 999 </ul> 1000 <p class="rt-lista-barrios"><strong>Barrios de Málaga con sitios para entrenar:</strong> Carretera de Cádiz, Centro, Málaga Este, Puerto de la Torre, Teatinos.</p> 1001 </div> 1002 <div class="rt-lista-ciudad"> 1003 <h3><a href="/entrenadores-personales/barcelona">Dónde entrenar en Barcelona</a></h3> 1004 <p class="rt-lista-intro">7 sitios donde entrenan entrenadores personales en Barcelona y alrededores.</p> 1005 <p class="rt-lista-fam">Al aire libre</p> 1006 <ul> 1007 <li><a href="/donde-entrenar#lugar=montjuic">Montjuïc</a> <span class="rt-lista-tipo">· Parque · Sants-Montjuïc</span> â entrena aquà <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li> 1008 <li><a href="/donde-entrenar#lugar=parc-de-la-ciutadella">Parc de la Ciutadella</a> <span class="rt-lista-tipo">· Parque · Ciutat Vella</span> â entrena aquà <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li> 1009 <li><a href="/donde-entrenar#lugar=parc-del-diagonal-mar">Parc del Diagonal Mar</a> <span class="rt-lista-tipo">· Parque · Sant MartÃ</span> â entrena aquà <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li> 1010 <li><a href="/donde-entrenar#lugar=parque-calistenia-puerto">Parque Calistenia Puerto</a> <span class="rt-lista-tipo">· Calistenia · Sant Martà · Moll de Mestral</span> â entrena aquà <a href="/entrenador-personal/nabil-darwish-barcelona">Nabil Darwish</a></li> 1011 </ul> 1012 <p class="rt-lista-fam">
1012Gimnasios y centros</p> 1013 <ul> 1014 <li><a href="/donde-entrenar#lugar=kenko-training">Kenko Training</a> <span class="rt-lista-tipo">· Estudio o box · Sant Martà · Carrer d'Edison 12</span></li> 1015 <li><a href="/donde-entrenar#lugar=welfare">Welfare</a> <span class="rt-lista-tipo">· Gimnasio · Sarrià -Sant Gervasi · Calle Santalo 15</span> â entrena aquà <a href="/entrenador-personal/raul-casillas-barcelona">Raúl Casillas</a></li> 1016 </ul> 1017 <p class="rt-lista-fam">Alrededores · Premià de Mar</p> 1018 <ul> 1019 <li><a href="/donde-entrenar#lugar=parque-de-calistenia-premia-de-mar">Parque de calistenia de Premià de Mar</a> <span class="rt-lista-tipo">· Calistenia · Carrer de Núria</span> â entrenan aquà <a href="/entrenador-personal/gonzalo-cruz-barcelona">Gonzalo Cruz</a> y <a href="/entrenador-personal/aron-barcelona">Aron</a></li> 1020 </ul> 1021 <p class="rt-lista-barrios"><strong>Barrios de Barcelona con sitios para entrenar:</strong> Ciutat Vella, Sant MartÃ, Sants-Montjuïc, Sarrià -Sant Gervasi.</p> 1022 </div> 1023 <div class="rt-lista-ciudad"> 1024 <h3><a href="/entrenadores-personales/alicante">Dónde entrenar en Alicante</a></h3> 1025 <p class="rt-lista-intro">2 sitios donde entrenan entrenadores personales en Alicante.</p> 1026 <p class="rt-lista-fam">Gimnasios y centros</p> 1027 <ul> 1028 <li><a href="/donde-entrenar#lugar=fitness-park-luceros">Fitness Park Luceros</a> <span class="rt-lista-tipo">· Gimnasio · Avinguda de l'Estació, 5</span> â entrenan aquà <a href="/entrenador-personal/pablo-perez">Pablo Pérez</a> y <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li> 1029 <li><a href="/donde-entrenar#lugar=fitness-park-san-vicente-del-raspeig">Fitness Park San Vicente del Raspeig</a> <span class="rt-lista-tipo">· Gimnasio · Carrer Vicent Savall Pascual, 21, 03690 San Vicente del Raspeig</span> â entrenan aquà <a href="/entrenador-personal/pablo-perez">Pablo Pérez</a> y <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li> 1030 </ul> 1031 </div> 1032 <div class="rt-lista-ciudad"> 1033 <h3><a href="/entrenadores-personales/estepona">Dónde entrenar en Estepona</a></h3> 1034 <p class="rt-lista-intro">2 sitios donde entrenan entrenadores personales en Estepona.</p> 1035 <p class="rt-lista-fam">Gimnasios y centros</p> 1036 <ul> 1037 <li><a href="/donde-entrenar#lugar=centro-fitness-la-academia">Centro Fitness La Academia</a> <span class="rt-lista-tipo">· Gimnasio · Calle La Viña</span> â entrena aquà <a href="/entrenador-personal/ivan-rivero-estepona">Ivan Rivero</a></li> 1038 <li><a href="/donde-entrenar#lugar=fitbike-estepona">Fitbike Estepona</a> <span class="rt-lista-tipo">· Gimnasio</span> â entrena aquà <a href="/entrenador-personal/ivan-rivero-estepona">Ivan Rivero</a></li> 1039 </ul> 1040 </div> 1041 <div class="rt-lista-ciudad"> 1042 <h3><a href="/entrenadores-personales/a-coruna">Dónde entrenar en A Coruña</a></h3> 1043 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en A Coruña.</p> 1044 <p class="rt-lista-fam">Gimnasios y centros</p> 1045 <ul> 1046 <li><a href="/donde-entrenar#lugar=pilar-fernandez-entrenamiento-personal">Pilar Fernández â Entrenamiento Personal</a> <span class="rt-lista-tipo">· Estudio o box · Agra do Orzán, 38, 1D</span> â entrena aquà <a href="/entrenador-personal/pilar-fernandez-a-coruna">Pilar Fernández</a></li> 1047 </ul> 1048 </div> 1049 <div class="rt-lista-ciudad"> 1050 <h3>Dónde entrenar en Albelda de Iregua</h3> 1051 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Albelda de Iregua.</p> 1052 <p class="rt-lista-fam">Gimnasios y centros</p> 1053 <ul> 1054 <li><a href="/donde-entrenar#lugar=recovery-training-lesion">Recovery Training Lesion</a> <span class="rt-lista-tipo">· Estudio o box · 26120 La Rioja</span> â entrena aquà <a href="/entrenador-personal/recovery-training-lesion-online">Recovery Training Lesion</a> · <a href="/centros/recovery-training-lesion">ficha del centro</a></li> 1055 </ul> 1056 </div> 1057 <div class="rt-lista-ciudad"> 1058 <h3><a href="/entrenadores-personales/alhaurin-de-la-torre">Dónde entrenar en AlhaurÃn de la Torre</a></h3> 1059 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en AlhaurÃn de la Torre.</p> 1060 <p class="rt-lista-fam">
1060Gimnasios y centros</p> 1061 <ul> 1062 <li><a href="/donde-entrenar#lugar=elite-wellness-alhaurin">Elite Wellness AlhaurÃn</a> <span class="rt-lista-tipo">· Gimnasio · Avenida 9, PolÃgono Industrial Fase II, Nave 73</span> â entrena aquà <a href="/entrenador-personal/antonio-porras-malaga">Antonio Porras</a></li> 1063 </ul> 1064 </div> 1065 <div class="rt-lista-ciudad"> 1066 <h3><a href="/entrenadores-personales/almeria">Dónde entrenar en AlmerÃa</a></h3> 1067 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en AlmerÃa.</p> 1068 <p class="rt-lista-fam">Gimnasios y centros</p> 1069 <ul> 1070 <li><a href="/donde-entrenar#lugar=basic-fit-almeria">Basic-Fit AlmerÃa Federico GarcÃa Lorca</a> <span class="rt-lista-tipo">· Gimnasio · Avenida Federico GarcÃa Lorca, 240</span> â entrena aquà <a href="/entrenador-personal/brayan-esteban-atuesta-almeria">Brayan Esteban Atuesta</a></li> 1071 </ul> 1072 </div> 1073 <div class="rt-lista-ciudad"> 1074 <h3>Dónde entrenar en Basauri</h3> 1075 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Basauri.</p> 1076 <p class="rt-lista-fam">Gimnasios y centros</p> 1077 <ul> 1078 <li><a href="/donde-entrenar#lugar=polideportivo-de-basauri">Polideportivo de Basauri</a> <span class="rt-lista-tipo">· Polideportivo · Artunduaga kalea</span> â entrena aquà <a href="/entrenador-personal/aingeru-hernandez-bilbao">Aingeru Hernández</a></li> 1079 </ul> 1080 </div> 1081 <div class="rt-lista-ciudad"> 1082 <h3><a href="/entrenadores-personales/cantillana">Dónde entrenar en Cantillana</a></h3> 1083 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Cantillana.</p> 1084 <p class="rt-lista-fam">Gimnasios y centros</p> 1085 <ul> 1086 <li><a href="/donde-entrenar#lugar=tocasport-cantillana">Tocasport Cantillana</a> <span class="rt-lista-tipo">· Gimnasio · Carretera de Lora, 33</span> â entrena aquà <a href="/entrenador-personal/jose-angel-perez-sevilla">José Ãngel Pérez</a></li> 1087 </ul> 1088 </div> 1089 <div class="rt-lista-ciudad"> 1090 <h3><a href="/entrenadores-personales/cordoba">Dónde entrenar en Córdoba</a></h3> 1091 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Córdoba.</p> 1092 <p class="rt-lista-fam">Gimnasios y centros</p> 1093 <ul> 1094 <li><a href="/donde-entrenar#lugar=gimnasio-el-euro">Gimnasio El Euro</a> <span class="rt-lista-tipo">· Gimnasio · Calle Músico Ziryab</span> â entrena aquà <a href="/entrenador-personal/oscar-herman-cordoba">Oscar Herman</a></li> 1095 </ul> 1096 </div> 1097 <div class="rt-lista-ciudad"> 1098 <h3><a href="/entrenadores-personales/elche">Dónde entrenar en Elche</a></h3> 1099 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Elche.</p> 1100 <p class="rt-lista-fam">Gimnasios y centros</p> 1101 <ul> 1102 <li><a href="/donde-entrenar#lugar=fitness-park-elche-aljub">Fitness Park Elche Aljub</a> <span class="rt-lista-tipo">· Gimnasio · Carrer Jacarilla, 7 (C.C. L'Aljub)</span> â entrena aquà <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li> 1103 </ul> 1104 </div> 1105 <div class="rt-lista-ciudad"> 1106 <h3><a href="/entrenadores-personales/llinars-del-valles">Dónde entrenar en Llinars del Vallès</a></h3> 1107 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Llinars del Vallès.</p> 1108 <p class="rt-lista-fam">Gimnasios y centros</p> 1109 <ul> 1110 <li><a href="/donde-entrenar#lugar=bodybuilding-center-llinars">Bodybuilding Center Llinars</a> <span class="rt-lista-tipo">· Gimnasio · Carrer de la Tecnologia, PolÃgon Industrial el Mogent</span> â entrena aquà <a href="/entrenador-personal/albert-fradera-riells">Albert Fradera</a></li> 1111 </ul> 1112 </div> 1113 <div class="rt-lista-ciudad"> 1114 <h3><a href="/entrenadores-personales/murcia">Dónde entrenar en Murcia</a></h3> 1115 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Murcia.</p> 1116 <p class="rt-lista-fam">
1116Gimnasios y centros</p> 1117 <ul> 1118 <li><a href="/donde-entrenar#lugar=amador-ruiz-murcia">Amador Ruiz â Entrenamiento Personal</a> <span class="rt-lista-tipo">· Gimnasio · Calle Cisne, 3</span> â entrena aquà <a href="/entrenador-personal/amador-ruiz-murcia">Amador Ruiz</a> · <a href="/centros/amador-ruiz-murcia">ficha del centro</a></li> 1119 </ul> 1120 </div> 1121 <div class="rt-lista-ciudad"> 1122 <h3><a href="/entrenadores-personales/oviedo">Dónde entrenar en Oviedo</a></h3> 1123 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Oviedo.</p> 1124 <p class="rt-lista-fam">Al aire libre</p> 1125 <ul> 1126 <li><a href="/donde-entrenar#lugar=parque-del-oeste-oviedo">Parque del Oeste</a> <span class="rt-lista-tipo">· Parque · Olivares</span> â entrena aquà <a href="/entrenador-personal/ivan-patallo-oviedo">Ivan Patallo</a></li> 1127 </ul> 1128 </div> 1129 <div class="rt-lista-ciudad"> 1130 <h3>Dónde entrenar en Santovenia de Pisuerga</h3> 1131 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Santovenia de Pisuerga.</p> 1132 <p class="rt-lista-fam">Al aire libre</p> 1133 <ul> 1134 <li><a href="/donde-entrenar#lugar=parque-remuel">Parque Remuel</a> <span class="rt-lista-tipo">· Calistenia · Calle Real</span> â entrena aquà <a href="/entrenador-personal/adrian-garcia-valladolid">Adrian Garcia</a></li> 1135 </ul> 1136 </div> 1137 <div class="rt-lista-ciudad"> 1138 <h3><a href="/entrenadores-personales/torrelodones">Dónde entrenar en Torrelodones</a></h3> 1139 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Torrelodones.</p> 1140 <p class="rt-lista-fam">Gimnasios y centros</p> 1141 <ul> 1142 <li><a href="/donde-entrenar#lugar=mitolab">Mitolab â Fitness Celular Integrativo</a> <span class="rt-lista-tipo">· Estudio o box · Calle San Ramón 2</span> · <a href="/centros/mitolab">ficha del centro</a></li> 1143 </ul> 1144 </div> 1145 <div class="rt-lista-ciudad"> 1146 <h3><a href="/entrenadores-personales/valencia">Dónde entrenar en Valencia</a></h3> 1147 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Valencia.</p> 1148 <p class="rt-lista-fam">Al aire libre</p> 1149 <ul> 1150 <li><a href="/donde-entrenar#lugar=cauce-del-rio-turia-sin-ubicacion-concreta">Cauce del rÃo turia, sin ubicación concreta.</a> <span class="rt-lista-tipo">· Parque · Cauce del rÃo turia</span> â entrena aquà <a href="/entrenador-personal/xavi-orti-entrenamiento-personalizado">Xavi OrtÃ</a></li> 1151 </ul> 1152 </div> 1153 <div class="rt-lista-ciudad"> 1154 <h3>Dónde entrenar en Villa de Otura</h3> 1155 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Villa de Otura.</p> 1156 <p class="rt-lista-fam">Gimnasios y centros</p> 1157 <ul> 1158 <li><a href="/donde-entrenar#lugar=alberto-diaz-entrenamiento-personal">Alberto DÃaz â Entrenamiento Personal</a> <span class="rt-lista-tipo">· Estudio o box · Calle los Olivos, 10</span> â entrena aquà <a href="/entrenador-personal/alberto-diaz-granada">Alberto DÃaz</a></li> 1159 </ul> 1160 </div> 1161 <div class="rt-lista-ciudad"> 1162 <h3><a href="/entrenadores-personales/villaverde-del-rio">Dónde entrenar en Villaverde del RÃo</a></h3> 1163 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Villaverde del RÃo.</p> 1164 <p class="rt-lista-fam">Gimnasios y centros</p> 1165 <ul> 1166 <li><a href="/donde-entrenar#lugar=tocasport-villaverde">Tocasport Villaverde</a> <span class="rt-lista-tipo">· Gimnasio · Avenida del Convento, 54</span> â entrena aquà <a href="/entrenador-personal/jose-angel-perez-sevilla">José Ãngel Pérez</a></li> 1167 </ul> 1168 </div> 1169 <div class="rt-lista-ciudad"> 1170 <h3><a href="/entrenadores-personales/zaragoza">Dónde entrenar en Zaragoza</a></h3> 1171 <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Zaragoza.</p> 1172 <p class="rt-lista-fam">Gimnasios y centros</p> 1173 <ul> 1174 <li><a href="/donde-entrenar#lugar=torrero-gym">Torrero Gym</a> <span class="rt-lista-tipo">· Gimnasio · Calle Monterregado, 31</span> â entrena aquà <a href="/entrenador-personal/torrero-gym-zaragoza">Juan de Dios Sanzo</a> · <a href="/centros/torrero-gym">ficha del centro</a></li> 1175 </ul> 1176 </div> 1177<!-- LISTA:FIN --> 1178</section> 1179 1180<style> 1181.rt-lista{font-family:"Plus Jakarta Sans",sans-serif;color:#1B1E22;max-width:1200px;margin:0 auto;padding:44px 24px 0;} 1182.rt-lista *{box-sizing:border-box;} 1183.rt-lista h2{font-family:"Barlow",sans-serif;font-size:clamp(20px,3vw,26px);font-weight:800;text-transform:uppercase;text-align:center;margin:0 0 8px;} 1184.rt-lista-pie{font-size:14px;line-height:1.6;color:#6b7280;text-align:center;max-width:680px;margin:0 auto 26px;} 1185.rt-lista-ciudad{margin:0 0 30px;} 1186.rt-lista-ciudad h3{font-family:"Barlow",sans-serif;font-size:22px;font-weight:700;margin:0 0 4px;} 1187.rt-lista-intro{font-size:15px;line-height:1.6;color:#4B5563;margin:0;} 1188.rt-lista-fam{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;margin:16px 0 6px;} 1189.rt-lista ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0 28px;} 1190.rt-lista li{font-size:15px;line-height:1.55;color:#4B5563;border-bottom:1px solid #d9dde2;padding:9px 0;} 1191.rt-lista a{color:#089F3B;font-weight:600;text-decoration:none;} 1192.rt-lista a:hover{color:#067A2D;text-decoration:underline;} 1193.rt-lista-tipo{color:#6b7280;} 1194/* Los barrios se repiten como texto suelto porque es como se busca 1195 ("entrenar en ChamberÃ") y ahà no hay nada que enlazar todavÃa. */ 1196.rt-lista-barrios{font-size:13.5px;line-height:1.6;color:#6b7280;margin:14px 0 0;} 1197@media(max-width:700px){ 1198 .rt-lista{padding-left:16px;padding-right:16px;} 1199} 1200</style> 1201</div><section class="mapa-faq"><h2 class="mapa-faq_titulo">Preguntas frecuentes</h2><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Qué es este mapa?</h3><p class="mapa-faq_respuesta">Un mapa de los sitios donde entrenan de verdad los entrenadores personales de RedEntrenadores: gimnasios, estudios, parques, pistas públicas, rutas y playas. Cada punto lo ha añadido un profesional que entrena allÃ.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Cómo contacto con un entrenador del mapa?</h3><p class="mapa-faq_respuesta">Al abrir un sitio verás quién entrena en él. Desde ahà entras en su perfil, donde puedes ver su experiencia, sus precios y las opiniones de sus clientes, y contactar con él sin compromiso.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Cuesta algo contactar?</h3><p class="mapa-faq_respuesta">No. Contactar con un entrenador es gratis y sin compromiso. Ãl decide si puede ayudarte y te responde directamente.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Puedo entrenar en un parque con un entrenador personal?</h3><p class="mapa-faq_respuesta">SÃ, y es más habitual de lo que parece. Muchos entrenadores dan sus sesiones al aire libre âparques con barras, pistas de atletismo, playasâ y llevan ellos el material. En el mapa están marcados como sitios al aire libre.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">Soy entrenador, ¿cómo añado los sitios donde entreno?</h3><p class="mapa-faq_respuesta">Desde tu panel, en "Dónde entreno". Buscas la dirección, colocas el punto y en unos segundos aparece en el mapa con tu nombre. Si el sitio ya está, puedes unirte a él en vez de crearlo otra vez.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿En qué ciudades funciona?</h3><p class="mapa-faq_respuesta">Estamos empezando por Madrid, Barcelona y Málaga, que es donde hay más entrenadores. El mapa crece según los profesionales van añadiendo sus sitios, asà que irán aparecien
1201do más.</p></div></section><section class="general-seccion"><div class="wrap_general"><div class="rl_navbar5_item-title">Por tipo de sitio</div><a href="/donde-entrenar/parques-de-calistenia" class="enlace-mapa-ciudad">Parques de calistenia y barras cerca de ti</a><a href="/donde-entrenar/gimnasios-al-aire-libre" class="enlace-mapa-ciudad">Gimnasios al aire libre: qué máquinas hay y dónde están</a></div></section><footer class="footer"><div class="footer-middle footer-v1"><div class="footer-grid"><a id="w-node-_5855e537-ce68-c8ac-151a-51c4bee40e4e-1e51f0f5" href="#" class="w-inline-block"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif" alt="Logotipo de RedEntrenadores" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-500.avif 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-800.avif 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif 2246w" class="footer-logo"/></a><div class="w-layout-vflex"><div>Encuentra entrenador personal para alcanzar tus objetivos en cualquier ciudad de España</div><div class="space_36"></div><div><strong>SÃguenos</strong></div><div class="social-grid"><a href="https://twitter.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î ©</div></a><a href="https://www.instagram.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î </div></a><a href="https://facebook.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î </div></a></div><div><strong>EscrÃbenos si necesitas ayuda.</strong></div><a href="mailto:[email protected]" class="footer-link">[email protected]</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Páginas</h4><a href="/sobre-nosotros" class="footer-link">Nosotros</a><a href="/blog" class="footer-link">Blog</a><a href="/donde-entrenar" aria-current="page" class="footer-link w--current">Dónde entrenar</a><a href="/calculadoras/precio-entrenador-personal" class="footer-link">Cuánto cuesta un entrenador</a><a href="/calculadoras/donde-te-conviene-entrenar" class="footer-link">¿Dónde te conviene entrenar?</a><a href="/calculadoras/plan-primer-mes" class="footer-link">Tu primer mes de entrenamiento</a><a href="/calculadoras/calculadora-calorias" class="footer-link">Calculadora de calorÃas</a><a href="/calculadoras/calculadora-proteina" class="footer-link">Calculadora de proteÃna</a><a href="/calculadoras/calculadora-imc" class="footer-link">Calculadora de IMC</a><a href="/calculadoras/test-nivel-fitness" class="footer-link">Test de nivel de fitness</a><a href="/calculadoras/pruebas-fisicas-oposiciones" class="footer-link">Pruebas fÃsicas de oposición</a><a href="/herramientas" class="footer-link">Todas las herramientas</a><a href="https://www.redentrenadores.com/entrenamiento/online" class="footer-link">Entrenador personal online</a><a href="https://www.redentrenadores.com/entrenamiento/a-domicilio" class="footer-link">Entrenador personal a domicilio</a><a href="https://www.redentrenadores.com/entrenamiento/en-un-gimnasio" class="footer-link">Entrenador personal en gimnasio</a><a href="https://www.redentrenadores.com/entrenamiento/en-exteriores" class="footer-link">Entrenador personal al aire libre</a><a href="https://www.redentrenadores.com/entrenamiento/presencial" class="footer-link">Entrenador personal presencial</a><a href="/usuarios/mi-cuenta" class="footer-link">Ãrea de usuario</a><a href="/legal/politica-de-privacidad" class="footer-link">PolÃtica de privacidad</a><a href="/legal/terminos-y-condiciones" class="footer-link">Términos y condiciones</a><a href="/legal/aviso-legal" class="footer-link">Aviso legal</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Entrenadores por ciudad</h4><a href="https://www.redentrenadores.com/entrenadores-personales/barcelona" class="footer-link">Entrenadores personales Barcelona</a><a href="https://www.redentrenadores.com/entrenadores-personales/madrid" class="footer-link">Entrenadores personales Madrid</a><a href="https://www.redentrenadores.com/entrenadores-personales/valencia" class="footer-link">Entrenadores personales Valencia</a><a href="https://www.redentrenadores.com/entrenadores-personales/malaga" class="footer-link">Entrenadores personales Málaga</a><a href="https://www.redentrenadores.com/entrenadores-personales/zaragoza" class="footer-link">Entrenadores personales Zaragoza</a><a href="https://www.redentrenadores.com/entrenadores-personales/sevilla" class="footer-link">Entrenadores personales Sevilla</a><a href="https://www.redentrenadores.com/entrenadores-personales/granada" class="footer-link">Entrenadores personales Granada</a><a href="https://www.redentrenadores.com/entrenadores-personales/alicante" class="footer-link">Entrenadores personales Alicante</a><a href="https://www.redentrenadores.com/entrenadores-personales/getafe" class="footer-link">Entrenadores personales Getafe</a><a href="https://www.redentrenadores.com/entrenadores-personales/alcala-de-henares" class="footer-link">Entrenadores personales Alcalá de Henares</a><a href="https://www.redentrenadores.com/entrenadores-personales/pozuelo-de-alarcon" class="footer-link">Entrenadores personales Pozuelo de Alarcón</a><a href="https://www.redentrenadores.com/entrenadores-personales/terrassa" class="footer-link">Entrenadores personales Terrassa</a><a href="https://www.redentrenadores.com/entrenadores-personales/badalona" class="footer-link">Entrenadores personales Badalona</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Entrenadores por objetivo</h4>
1201<a href="https://www.redentrenadores.com/objetivo/movilidad-flexibilidad" class="footer-link">Movilidad y flexibilidad</a><a href="https://www.redentrenadores.com/objetivo/prevencion-lesiones" class="footer-link">Prevención de lesiones</a><a href="https://www.redentrenadores.com/objetivo/preparacion-oposiciones" class="footer-link">Preparación de oposiciones</a><a href="https://www.redentrenadores.com/objetivo/mejora-forma-fisica" class="footer-link">Mejora de forma fÃsica</a><a href="https://www.redentrenadores.com/objetivo/aumento-masa-muscular" class="footer-link">Aumento de masa muscular</a><a href="https://www.redentrenadores.com/objetivo/entrenamiento-funcional" class="footer-link">Entrenamiento funcional</a><a href="https://www.redentrenadores.com/objetivo/recuperacion-lesiones" class="footer-link">Recuperación de lesiones</a><a href="https://www.redentrenadores.com/objetivo/preparacion-deportistas" class="footer-link">Preparación para deportistas</a><a href="https://www.redentrenadores.com/objetivo/correccion-postural" class="footer-link">Corrección postural</a><a href="https://www.redentrenadores.com/objetivo/perdida-peso" class="footer-link">Pérdida de peso</a><a href="https://www.redentrenadores.com/objetivo/recuperacion-posparto" class="footer-link">Recuperación posparto</a></div></div></div><div class="footer-bottom-copy"><div id="w-node-_2744af48-cdd0-c61e-6264-4fdb1e51f15a-1e51f0f5" class="text-block-14">© 2026 RedEntrenadores. Todos los derechos reservados.</div></div><div class="footer-bottom"><a href="/legal/politica-de-privacidad" class="footer-link xs">PolÃtica de Privacidad</a><a href="/legal/aviso-legal" class="footer-link xs">Aviso Legal</a><a href="/legal/terminos-y-condiciones" class="footer-link xs">Términos y condiciones</a></div><div class="w-embed"><style> 1202 /* Namespace propio `re-rec-*` en TODO. El sitio ya tiene `.re-modal`, 1203 `.re-cta` y `.re-field` en las landings y en el listado, y este bloque 1204 carga en las mismas páginas: reutilizar esos nombres es garantizarse un 1205 choque de estilos el dÃa que alguien toque cualquiera de los dos. */ 1206 #re-rec-overlay { 1207 position: fixed; 1208 inset: 0; 1209 /* En móvil `inset: 0` mide el viewport GRANDE â el de la barra del 1210 navegador escondida â, asà que el borde de abajo del overlay cae por 1211 debajo de lo que se ve. Como en móvil el modal se ancla justo ahà 1212 (bottom sheet), el último campo y el botón de enviar quedaban tapados 1213 por la barra del navegador, sin forma de llegar a ellos: el modal ya 1214 está en su tope de scroll interno y lo que sobra queda fuera de 1215 pantalla. `dvh` mide el viewport visible de verdad; el `vh` de arriba 1216 es el respaldo para navegadores sin `dvh`, que se quedan como estaban. 1217 El teclado no lo arregla ninguna unidad CSS â de eso se encarga 1218 `ajustarAlViewport()` en el bloque 3. */ 1219 height: 100vh; 1220 height: 100dvh; 1221 z-index: 10000; 1222 display: none; 1223 align-items: center; 1224 justify-content: center; 1225 padding: 20px; 1226 background: rgba(27, 30, 34, 0.55); 1227 font-family: 'Plus Jakarta Sans', sans-serif; 1228 box-sizing: border-box; 1229 } 1230 #re-rec-overlay.re-rec-abierto { display: flex; } 1231 1232 #re-rec-overlay .re-rec-modal { 1233 position: relative; 1234 width: 100%; 1235 max-width: 520px; 1236 max-height: 90vh; 1237 overflow-y: auto; 1238 /* Que el scroll del modal no arrastre la página de detrás al llegar al 1239 final: en iOS el encadenado hace que el sheet parezca que "se va". */ 1240 overscroll-behavior: contain; 1241 background: #fff; 1242 border-radius: 12px; 1243 box-shadow: 0 4px 14px rgba(0, 0, 0, 0.09); 1244 padding: 28px; 1245 box-sizing: border-box; 1246 /* El paso de confirmación es bastante más ancho que las preguntas (tres 1247 fichas en fila); se ensancha con una clase en vez de dejar el modal 1248 ancho todo el rato, que harÃa que las 4 preguntas naden en el hueco. */ 1249 } 1250 #re-rec-overlay .re-rec-modal.re-rec-ancho { max-width: 640px; } 1251 1252 #re-rec-overlay .re-rec-cerrar { 1253 position: absolute; 1254 top: 10px; 1255 right: 10px; 1256 width: 44px; 1257 height: 44px; 1258 border: 0; 1259 background: none; 1260 color: #6b7280; 1261 font-size: 20px; 1262 line-height: 1; 1263 cursor: pointer; 1264 } 1265 #re-rec-overlay .re-rec-cerrar:hover { color: #1B1E22; } 1266 1267 #re-rec-overlay .re-rec-dots { display: flex; gap: 6px; margin-bottom: 18px; } 1268 #re-rec-overlay .re-rec-dots span { 1269 width: 26px; 1270 height: 4px; 1271 border-radius: 999px; 1272 background: #d9dde2; 1273 } 1274 #re-rec-overlay .re-rec-dots span.re-rec-hecho { background: #089F3B; } 1275 1276 #re-rec-overlay .re-rec-eyebrow { 1277 font-size: 12px; 1278 font-weight: 700; 1279 letter-spacing: 0.48px; 1280 text-transform: uppercase; 1281 color: #089F3B; 1282 margin: 0 0 6px; 1283 } 1284 #re-rec-overlay h3 { 1285 font-family: 'Barlow', sans-serif; 1286 font-size: 24px; 1287 line-height: 1.25; 1288 font-weight: 700; 1289 color: #1B1E22; 1290 margin: 0 0 6px; 1291 padding-right: 34px; 1292 } 1293 #re-rec-overlay .re-rec-sub { 1294 font-size: 16px; 1295 line-height: 1.6; 1296 color: #4B5563; 1297 margin: 0 0 18px; 1298 } 1299 1300 #re-rec-overlay .re-rec-pregunta { 1301 font-size: 14px; 1302 font-weight: 600; 1303 color: #1B1E22; 1304 margin: 0 0 8px; 1305 } 1306 1307 /* Opciones: botón grande de fila completa. Objetivo táctil holgado (48px) 1308 por el punto 13 del Apéndice A del design system â el código nuevo no 1309 deberÃa nacer ya con deuda de accesibilidad. */ 1310 #re-rec-overlay .re-rec-opciones { display: grid; gap: 8px; } 1311 #re-rec-overlay .re-rec-op { 1312 display: flex; 1313 align-items: center; 1314 gap: 12px; 1315 width: 100%; 1316 min-height: 48px; 1317 padding: 10px 14px; 1318 text-align: left; 1319 background: #fff; 1320 border: 1px solid #d9dde2; 1321 border-radius: 8px; 1322 font-family: inherit; 1323 font-size: 16px; 1324 font-weight: 500; 1325 color: #1B1E22; 1326 cursor: pointer; 1327 box-sizing: border-box; 1328 } 1329 #re-rec-overlay .re-rec-op:hover { border-color: #089F3B; background: #EAF3DE; } 1330 #re-rec-overlay .re-rec-op.re-rec-op-on { border-color: #089F3B; background: #EAF3DE; color: #067A2D; font-weight: 600; } 1331 #re-rec-overlay .re-rec-op-txt { flex: 1; min-width: 0; } 1332 #re-rec-overlay .re-rec-op-nota { display: block; font-size: 13px; font-weight: 400; color: #6b7280; margin-top: 2px; } 1333 1334 /* El icono va en su propia pastilla verde clara y no suelto: suelto se 1335 pierde contra el blanco de la fila, y es lo que hace que la lista se lea 1336 de un vistazo en vez de leerse renglón a renglón. Al pasar por encima el 1337 fondo de la fila se vuelve `#EAF3DE`, el mismo de la pastilla, asà que 1338 ahà la pastilla invierte a verde macizo para no desaparecer. */ 1339 #re-rec-overlay .re-rec-op-ico { 1340 display: flex; 1341 align-items: center; 1342 justify-content: center; 1343 width: 38px; 1344 height: 38px; 1345 flex-shrink: 0; 1346 border-radius: 10px; 1347 background: #EAF3DE; 1348 color: #089F3B; 1349 } 1350 #re-rec-overlay .re-rec-op-ico svg { width: 21px; height: 21px; display: block; } 1351 #re-rec-overlay .re-rec-op:hover .re-rec-op-ico, 1352 #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-ico { background: #089F3B; color: #fff; } 1353 1354 #re-rec-overlay .re-rec-op-flecha { 1355 width: 17px; 1356 height: 17px; 1357 flex-shrink: 0; 1358 color: #C9CED4; 1359 } 1360 #re-rec-overlay .re-rec-op:hover .re-rec-op-flecha, 1361 #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-flecha { color: #089F3B; } 1362 1363 /* "Ver más opciones" no es una opción más: va sin caja para que no compita 1364 con las dos que sà son elegibles. */ 1365 #re-rec-overlay .re-rec-vermas { 1366 justify-self: start; 1367 min-height: 44px; 1368 padding: 4px 0; 1369 background: none; 1370 border: 0; 1371 cursor: pointer; 1372 font-family: inherit; 1373 font-size: 14px; 1374 font-weight: 600; 1375 color: #089F3B; 1376 text-decoration: underline; 1377 } 1378 #re-rec-overlay .re-rec-vermas:hover { color: #067A2D; } 1379 1380 #re-rec-overlay .re-rec-campo { margin-bottom: 14px; } 1381 /* La ciudad aparece debajo de las opciones de modalidad, no en su propia 1382 pantalla: necesita aire por arriba para no leerse como una quinta opción. */ 1383 #re-rec-overlay .re-rec-campo-ciudad { margin-top: 18px; margin-bottom: 0; } 1384 #re-rec-overlay .re-rec-campo label { 1385 display: block; 1386 font-size: 14px; 1387 font-weight: 600; 1388 color: #1B1E22; 1389 margin-bottom: 6px; 1390 } 1391 #re-rec-overlay .re-rec-campo input { 1392 width: 100%; 1393 min-height: 48px; 1394 padding: 12px 14px; 1395 border: 1px solid #d9dde2; 1396 border-radius: 8px; 1397 font-family: inherit; 1398 font-size: 16px; 1399 color: #1B1E22; 1400 background: #fff; 1401 box-sizing: border-box; 1402 } 1403 /* 16px en los inputs y no 14: por debajo de 16 Safari en iPhone hace zoom 1404 al enfocar el campo y descoloca el modal entero. */ 1405 #re-rec-overlay .re-rec-campo input:focus { 1406 outline: none; 1407 border-color: #089F3B; 1408 box-shadow: 0 0 0 3px rgba(8, 159, 59, 0.15); 1409 } 1410 #re-rec-overlay .re-rec-opcional { font-weight: 400; color: #6b7280; } 1411 1412 #re-rec-overlay .re-rec-btn { 1413 display: inline-block; 1414 border: 0; 1415 border-radius: 8px; 1416 background: #089F3B; 1417 color: #fff; 1418 font-family: inherit; 1419 font-size: 16px; 1420 font-weight: 600; 1421 padding: 13px 22px; 1422 min-height: 48px; 1423 cursor: pointer; 1424 } 1425 #re-rec-overlay .re-rec-btn:hover { background: #067A2D; } 1426 #re-rec-overlay .re-rec-btn:disabled { opacity: 0.5;
1426 cursor: not-allowed; } 1427 1428 #re-rec-overlay .re-rec-nav { display: flex; align-items: center; gap: 14px; margin-top: 12px; } 1429 #re-rec-overlay .re-rec-atras { 1430 background: none; 1431 border: 0; 1432 padding: 12px 0; 1433 cursor: pointer; 1434 font-family: inherit; 1435 font-size: 14px; 1436 font-weight: 600; 1437 color: #4B5563; 1438 text-decoration: underline; 1439 } 1440 #re-rec-overlay .re-rec-atras:hover { color: #067A2D; } 1441 1442 #re-rec-overlay .re-rec-error { 1443 display: none; 1444 margin: 12px 0 0; 1445 font-size: 14px; 1446 color: #791F1F; 1447 background: #FCEBEB; 1448 border-radius: 8px; 1449 padding: 10px 12px; 1450 } 1451 #re-rec-overlay .re-rec-legal { 1452 margin: 12px 0 0; 1453 font-size: 12px; 1454 line-height: 1.45; 1455 color: #6b7280; 1456 } 1457 #re-rec-overlay .re-rec-legal a { color: #6b7280; text-decoration: underline; } 1458 1459 /* El pie: lo que antes era el subtÃtulo de arriba. Va separado por un filete 1460 y en cuerpo pequeño â es contexto para quien duda, no la instrucción de la 1461 pantalla, que ahora es el tÃtulo más las propias opciones. */ 1462 #re-rec-overlay .re-rec-pie { 1463 display: flex; 1464 align-items: flex-start; 1465 gap: 9px; 1466 margin: 18px 0 0; 1467 padding-top: 14px; 1468 border-top: 1px solid #E5E6E8; 1469 font-size: 13px; 1470 line-height: 1.5; 1471 color: #6b7280; 1472 } 1473 #re-rec-overlay .re-rec-pie-ico { color: #089F3B; flex-shrink: 0; line-height: 0; } 1474 #re-rec-overlay .re-rec-pie-ico svg { width: 17px; height: 17px; display: block; } 1475 1476 /* ---------------------------------------------------------- confirmación */ 1477 1478 #re-rec-overlay .re-rec-fichas { 1479 display: grid; 1480 grid-template-columns: repeat(3, minmax(0, 1fr)); 1481 gap: 12px; 1482 margin: 18px 0 0; 1483 } 1484 /* Sin `:hover` ni cursor de mano: la ficha no es clicable (ver fichaHTML), y 1485 un borde que reacciona al pasar por encima promete un clic que no existe. */ 1486 #re-rec-overlay .re-rec-ficha { 1487 display: block; 1488 text-align: center; 1489 border: 1px solid #d9dde2; 1490 border-radius: 12px; 1491 padding: 14px 10px; 1492 color: inherit; 1493 } 1494 /* El envoltorio existe solo para anclar el badge: la foto es un <img> y no 1495 puede tener hijos. Se lleva el margen que antes tenÃa la foto. */ 1496 #re-rec-overlay .re-rec-foto-wrap { 1497 position: relative; 1498 display: block; 1499 width: 96px; 1500 margin: 0 auto 10px; 1501 } 1502 /* La foto crece dentro de la misma tarjeta: es lo único de la ficha que se 1503 reconoce de un vistazo, y a 64px competÃa con el texto en vez de mandar.
1504 Cuadrada y no circular: a igual ancho, el cÃrculo recorta los laterales y 1505 se come parte de la cara. Las esquinas van a 8px, el mismo radio que los 1506 botones â no a 12px, que es el de la tarjeta que la contiene. */ 1507 #re-rec-overlay .re-rec-foto { 1508 width: 100%; 1509 height: 96px; 1510 border-radius: 8px; 1511 object-fit: cover; 1512 display: block; 1513 background: #F3F3F3; 1514 } 1515 1516 /* Badge "Solo online" encima de la foto, arriba a la izquierda, como en el 1517 resto de tarjetas del sitio. Solo el icono: a 96px ây a 64px en móvilâ el 1518 literal no cabe, y ya sale escrito como primer dato de la ficha, justo 1519 debajo. El texto lo llevan title y aria-label. Blanco con borde y sombra, 1520 no verde: informa de la modalidad, no premia nada (§50). */ 1521 #re-rec-overlay .re-rec-online { 1522 position: absolute; 1523 top: 6px; 1524 left: 6px; 1525 z-index: 2; 1526 display: inline-flex; 1527 align-items: center; 1528 justify-content: center; 1529 width: 22px; 1530 height: 22px; 1531 box-sizing: border-box; 1532 background: #fff; 1533 border: 1px solid #d9dde2; 1534 border-radius: 999px; 1535 box-shadow: 0 2px 6px rgba(0,0,0,.18); 1536 color: #4B5563; 1537 } 1538 #re-rec-overlay .re-rec-online svg { width: 13px; height: 13px; display: block; } 1539 #re-rec-overlay .re-rec-ficha-nombre { 1540 font-family: 'Barlow', sans-serif; 1541 font-size: 15px; 1542 font-weight: 700; 1543 color: #1B1E22; 1544 display: block; 1545 line-height: 1.25; 1546 } 1547 /* PÃldora pública: radio 999px, nunca los 12px de la pÃldora de panel. */ 1548 #re-rec-overlay .re-rec-pill { 1549 display: inline-block; 1550 margin-top: 6px; 1551 padding: 4px 10px; 1552 border-radius: 999px; 1553 background: #F3F4F6; 1554 color: #4B5563; 1555 font-size: 11px; 1556 font-weight: 600; 1557 line-height: 1.3; 1558 } 1559 #re-rec-overlay .re-rec-pill-match { background: #EAF3DE; color: #089F3B; } 1560 #re-rec-overlay .re-rec-ficha-meta { 1561 font-size: 12px; 1562 line-height: 1.4; 1563 color: #4B5563; 1564 display: block; 1565 margin-top: 5px; 1566 } 1567 #re-rec-overlay .re-rec-estrella { vertical-align: -1px; } 1568 1569 #re-rec-overlay .re-rec-p2 { margin-top: 22px; padding-top: 18px; border-top: 1px solid #d9dde2; } 1570 #re-rec-overlay .re-rec-p2 h4 { 1571 font-family: 'Barlow', sans-serif; 1572 font-size: 18px; 1573 font-weight: 700; 1574 color: #1B1E22; 1575 margin: 0 0 4px; 1576 } 1577 /* minmax(0, 1fr) y no 1fr: el mÃnimo "auto" de un <select> es el ancho de su 1578 opción más larga, asà que con 1fr las columnas no encogen y desbordan. */ 1579 #re-rec-overlay .re-rec-p2-grid { 1580 display: grid; 1581 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 1582 gap: 0 14px; 1583 } 1584 #re-rec-overlay .re-rec-p2-grid > div { min-width: 0; } 1585 #re-rec-overlay .re-rec-p2-full { grid-column: 1 / -1; } 1586 #re-rec-overlay .re-rec-campo select, 1587 #re-rec-overlay .re-rec-campo textarea { 1588 width: 100%; 1589 min-height: 48px; 1590 padding: 12px 14px; 1591 border: 1px solid #d9dde2; 1592 border-radius: 8px; 1593 font-family: inherit; 1594 font-size: 16px; 1595 color: #1B1E22; 1596 background: #fff; 1597 box-sizing: border-box; 1598 } 1599 #re-rec-overlay .re-rec-campo textarea { min-height: 70px; resize: vertical; } 1600 #re-rec-overlay .re-rec-acciones { display: flex; align-items: center; gap: 18px; margin-top: 8px; } 1601 #re-rec-overlay .re-rec-saltar { 1602 background: none; 1603 border: 0; 1604 padding: 12px 0; 1605 cursor: pointer; 1606 font-family: inherit; 1607 font-size: 14px; 1608 font-weight: 600; 1609 color: #4B5563; 1610 text-decoration: underline; 1611 } 1612 1613 @media (max-width: 560px) { 1614 #re-rec-overlay { 1615 padding: 12px; 1616 /* La barra de gestos del iPhone se come los últimos ~34px: sin esto el 1617 pie del modal queda por debajo de ella. */ 1618 padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); 1619 align-items: flex-end; 1620 } 1621 /* `100%` es el alto del overlay ya descontado su padding, o sea lo que de 1622 verdad se ve. Antes era `92vh`, que se mide contra el viewport grande y 1623 dejaba el pie del modal fuera de pantalla. */ 1624 #re-rec-overlay .re-rec-modal { padding: 22px 18px; max-height: 100%; } 1625 1626 /* Con el teclado abierto el modal se queda en ~350px de alto y el 1627 contenido no cabe de ninguna manera: al escribir la ciudad, el botón 1628 quedaba por debajo del borde del modal. Se veÃa cortado y no habÃa nada 1629 que indicara que el modal tiene scroll propio, asà que el paso parecÃa 1630 roto. Pegado abajo, el botón está siempre a la vista y lo que scrolla es 1631 lo de arriba. Solo en móvil: en escritorio no hay teclado que robe media 1632 pantalla y el modal casi nunca desborda. 1633 1634 El fondo blanco es obligatorio âel contenido pasa por detrásâ y el 1635 padding de abajo replica el del modal, porque `bottom: 0` ancla contra 1636 el borde del scrollport y no contra su padding; el margen negativo lo 1637 descuenta para que en reposo, sin pegar, nada se mueva de sitio. */ 1638 #re-rec-overlay .re-rec-nav { 1639 position: sticky; 1640 bottom: 0; 1641 background: #fff; 1642 padding-bottom: 22px; 1643 margin-bottom: -22px; 1644 /* El padding de arriba se descuenta del margen para que en reposo la 1645 separación siga siendo la misma. La sombra es lo que hace que, cuando 1646 hay contenido pasando por detrás, la barra se lea como barra y no como 1647 un campo cortado a medias; a esta opacidad no se nota cuando la barra 1648 está en su sitio y no tapa nada. */ 1649 padding-top: 10px; 1650 margin-top: 2px; 1651 box-shadow: 0 -6px 10px -8px rgba(27, 30, 34, 0.16); 1652 } 1653 #re-rec-overlay h3 { font-size: 21px; } 1654 #re-rec-overlay .re-rec-fichas { grid-template-columns: minmax(0, 1fr); } 1655 #re-rec-overlay .re-rec-ficha { display: flex; align-items: center; gap: 14px; text-align: left; } 1656 #re-rec-overlay .re-rec-foto-wrap { width: 64px; margin: 0; flex-shrink: 0; } 1657 #re-rec-overlay .re-rec-foto { height: 64px; } 1658 #re-rec-overlay .re-rec-online { top: 4px; left: 4px;
1658 width: 20px; height: 20px; } 1659 #re-rec-overlay .re-rec-ficha-datos { min-width: 0; } 1660 #re-rec-overlay .re-rec-p2-grid { grid-template-columns: minmax(0, 1fr); } 1661 } 1662</style></div><div class="w-embed w-script">
1662<script> 1663(function () { 1664 "use strict"; 1665 1666 window.RE_RECOMENDADOR_VERSION = "1.0.0"; 1667 1668 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 1669 var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyemFuYXR6c2hkcHZ5cGF6Z2ViIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ0NjczNzcsImV4cCI6MjEwMDA0MzM3N30.WNRs7Bc8lA1JUw-KbOHYDBlEjTIPjwxx3cvlBmdpgHU"; 1670 var PRIVACIDAD_URL = "https://www.redentrenadores.com/legal/politica-de-privacidad"; 1671 1672 // Rutas donde el pop-up no debe salir nunca. El panel y el área de usuario 1673 // son zonas privadas de alguien que ya está dentro; /lp/ son las landings de 1674 // paid, que ya llevan este mismo flujo dentro de su propio widget y verÃan 1675 // dos modales peleándose; /crear-cuenta y /crea-tu-perfil son el alta y el 1676 // onboarding del entrenador, a quien ofrecerle buscar entrenador no le sirve 1677 // de nada y encima le tapa el formulario a medio rellenar; en 1678 // /pregunta-al-experto es donde contesta preguntas. /donde-entrenar queda 1679 // fuera por decisión de Esteve (20/08/2026). 1680 var RUTAS_EXCLUIDAS = [ 1681 "/panel-entrenador", "/usuarios/", "/mi-cuenta", "/admin", "/lp/", 1682 "/crear-cuenta", "/crea-tu-perfil", "/pregunta-al-experto", "/donde-entrenar" 1683 ]; 1684 1685 var SEGUNDOS_ESPERA = 25; 1686 var SCROLL_DISPARO = 0.5; 1687 1688 var CLAVE_APAGADO = "re_recomendador_off"; // localStorage: no volver a salir 1689 var CLAVE_SESION = "re_recomendador_visto"; // sessionStorage: ya salió hoy 1690 1691 // Los `value` son las etiquetas exactas de `entrenadores.tipo_atencion`, no 1692 // slugs, y tienen que seguir siéndolo: es lo que ya hay guardado en 1693 // `solicitudes_contacto.modalidad` y lo que el panel del entrenador pinta 1694 // sin traducir. El label es lo único redactable para el usuario. 1695 // 1696 // El orden importa: se enseñan solo las dos primeras y el resto se destapa 1697 // con "Ver más", igual que hace el wizard de /encuentra-entrenador. Online y
1698 // gimnasio son las dos que cubren a casi todo el mundo, y una pantalla con 1699 // cuatro opciones grandes pesa más de lo que aporta como primera impresión. 1700 var MODALIDADES = [ 1701 { value: "Online", label: "Online, por videollamada", nota: "Desde donde quieras" }, 1702 { value: "En un gimnasio", label: "En un gimnasio", nota: "En su centro o en el tuyo" }, 1703 { value: "A domicilio", label: "En mi casa", nota: "El entrenador se desplaza hasta ti" }, 1704 { value: "En exteriores", label: "Al aire libre", nota: "Parques y espacios abiertos" } 1705 ]; 1706 // Cuántas opciones se enseñan de entrada en cada paso; el resto va tras 1707 // "Ver más opciones". Las listas están ordenadas para que las visibles sean 1708 // las que cubren a más gente: en objetivos, las tres primeras son las que 1709 // más entrenadores trabajan (53, 47 y 45 de 95). 1710 var VISIBLES = { donde: 2, objetivo: 3 }; 1711 1712 // Son las especialidades tal cual están en `entrenadores.especialidades`, 1713 // porque la RPC filtra con `especialidades @> ARRAY[p_objetivo]`: cualquier 1714 // slug inventado aquà no casarÃa con nadie y devolverÃa lista vacÃa. Se 1715 // ofrecen las 6 con cobertura real (11 y 5 entrenadores las menos vistas de 1716 // esta lista); las de cola larga caen en "Aún no lo sé", que no filtra. 1717 var OBJETIVOS = [ 1718 { value: "Pérdida de peso", label: "Perder peso" }, 1719 { value: "Ganar masa muscular", label: "Ganar masa muscular" }, 1720 { value: "Mejora de forma fÃsica", label: "Estar en forma" }, 1721 { value: "Entrenamiento funcional", label: "Entrenamiento funcional" }, 1722 { value: "Recuperación de lesiones", label: "Recuperarme de una lesión" }, 1723 { value: "Preparación para deportistas", label: "Preparar una prueba o competición" }, 1724 { value: "", label: "Aún no lo tengo claro", nota: "Te ayudarán a definirlo" } 1725 ]; 1726 1727 // `cuando_empieza` no tiene hoy ni una fila rellena en toda la tabla (el 1728 // campo existe en el formulario de la ficha pero no llega), asà que estos 1729 // valores no chocan con nada previo. Son texto legible a propósito: el 1730 // entrenador los lee tal cual en su panel, sin traducción. 1731 var CUANDOS = [ 1732 { value: "Lo antes posible", label: "Lo antes posible" }, 1733 { value: "En 2-4 semanas", label: "En 2-4 semanas" }, 1734 { value: "En 1-2 meses", label: "En 1 o 2 meses" }, 1735 { value: "Solo me estoy informando", label: "Solo me estoy informando" } 1736 ]; 1737 1738 var NIVELES = [ 1739 { value: "sedentario", label: "Sedentario, casi no me muevo" }, 1740 { value: "poco-activo", label: "Poco activo, 1-2 dÃas" }, 1741 { value: "activo", label: "Activo, 3-4 dÃas" }, 1742 { value: "muy-activo", label: "Muy activo, 5 dÃas o más" } 1743 ]; 1744 1745 var estado = { 1746 indice: 0, // posición dentro de pasos() 1747 fase: "preguntas", // "preguntas" | "confirmacion" | "cierre" 1748 modalidad: "", 1749 abiertas: {}, // pasos cuyo "Ver más" ya se ha pulsado 1750 ciudad: "", 1751 ciudadDePagina: false, // en las 60 páginas de ciudad tampoco se pregunta 1752 objetivo: "", 1753 objetivoDePagina: false, // en /objetivo/* no se pregunta, ya lo sabemos 1754 objetivoRespondido: false, 1755 cuando: "", 1756 nombre: "", 1757 email: "", 1758 telefono: "", 1759 enviando: false, 1760 error: "", 1761 top3: [], 1762 top3Clave: null, // respuestas con las que se busco ese reparto 1763 claimToken: null, 1764 1765 // Solo para la medición: qué pantallas se han contado ya en esta apertura, 1766 // cuándo se abrió y si esta apertura cuenta. Las previsualizaciones y el 1767 // ?recomendador=1 no cuentan: si no, cada vez que se revisa el copy se 1768 // ensucia el embudo con una sesión que no es de nadie. 1769 vistas: {}, 1770 abiertoEn: 0, 1771 midiendo: true 1772 }; 1773 1774 // La secuencia de pantallas no es fija: quien entrena online no tiene que 1775 // decir su ciudad, y en una página de ciudad o de objetivo esa respuesta ya 1776 // la da la propia página. Se calcula en cada render en vez de encadenar 1777 // condiciones dentro de cada paso â un contador fijo de "paso N de 4" con 1778 // ramas de distinta longitud acaba pintando 5 pantallas bajo 4 puntos. 1779 //
1780 // Nunca se pregunta algo que ya sabemos: quien entra por 1781 // /entrenadores-personales/valencia ya ha dicho dónde busca, y volver a 1782 // preguntárselo se lee como que no le hemos escuchado. 1783 function pasos() { 1784 var lista = ["donde"]; 1785 if (!estado.objetivoDePagina) lista.push("objetivo"); 1786 lista.push("cuando", "datos"); 1787 return lista; 1788 } 1789 1790 function pasoActual() { 1791 return pasos()[estado.indice]; 1792 } 1793 1794 var ciudades = []; 1795 1796 // ------------------------------------------------------------------ utils 1797 1798 function esc(str) { 1799 var d = document.createElement("div"); 1800 d.textContent = str == null ? "" : String(str); 1801 return d.innerHTML; 1802 } 1803 1804 function leer(clave, almacen) { 1805 try { return almacen.getItem(clave); } catch (e) { return null; } 1806 } 1807 1808 function escribir(clave, valor, almacen) { 1809 try { almacen.setItem(clave, valor); } catch (e) {} 1810 } 1811 1812 // La ciudad de las 60 páginas de ciudad sale de #re-ciudad-nombre, el 1813 // elemento oculto bindeado al campo Nombre de la colección Ciudades que ya 1814 // usa el glue del listado. Se usa el nombre y no el slug porque la RPC casa 1815 // por `ciudad_nombre ILIKE` y hay ciudades cuyo slug no coincide con su 1816 // nombre ("la-sagra-toledo" contra "La sagra Toledo"). 1817 function ciudadDeLaPagina() { 1818 var el = document.getElementById("re-ciudad-nombre"); 1819 if (el && el.textContent.trim()) return el.textContent.trim(); 1820 var param = new URLSearchParams(location.search).get("ciudad"); 1821 return param ? param.trim() : ""; 1822 } 1823 1824 // Las marcas que deja NFD al separar las tildes (U+0300-U+036F). Va con 1825 // fromCharCode y no con un escape unicode porque este bloque se sube al 1826 // embed dentro del JSON de una llamada MCP y esa secuencia no sobrevive: 1827 // queda el carácter combinante suelto, invisible en el editor. El rango es 1828 // exactamente el mismo. La propiedad Diacritic no vale como atajo: deja 1829 // fuera 19 puntos de este bloque. 1830 var MARCAS_COMBINANTES = new RegExp( 1831 "[" + String.fromCharCode(0x300) + "-" + String.fromCharCode(0x36f) + "]", "g"); 1832 1833 // En /objetivo/* el objetivo ya lo dice la propia página: preguntarlo otra 1834 // vez es hacerle repetir al usuario lo que acaba de elegir al entrar. 1835 function aSlug(texto) { 1836 return texto.toLowerCase()
1837 .normalize("NFD").replace(MARCAS_COMBINANTES, "") 1838 .replace(/[^a-z0-9]+/g, "-") 1839 .replace(/^-|-$/g, ""); 1840 } 1841 1842 function objetivoDeLaPagina() { 1843 var m = location.pathname.match(/^\/objetivo\/([^/]+)/); 1844 if (!m) return null; 1845 var encontrado = OBJETIVOS.filter(function (o) { return o.value; }) 1846 .find(function (o) { return aSlug(o.value) === m[1]; }); 1847 return encontrado ? encontrado.value : null; 1848 } 1849 1850 // ------------------------------------------------------------------ datos 1851 1852 function rest(ruta, opciones) { 1853 var o = opciones || {}; 1854 return fetch(SUPABASE_URL + ruta, { 1855 method: o.method || "GET", 1856 headers: { 1857 apikey: SUPABASE_ANON_KEY, 1858 Authorization: "Bearer " + SUPABASE_ANON_KEY, 1859 "Content-Type": "application/json" 1860 }, 1861 body: o.body ? JSON.stringify(o.body) : undefined 1862 }).then(function (res) { 1863 if (!res.ok) throw new Error(ruta + " -> " + res.status); 1864 return res.json(); 1865 }); 1866 } 1867 1868 function cargarCiudades() { 1869 if (ciudades.length) return Promise.resolve(ciudades); 1870 return rest("/rest/v1/entrenadores_publico?select=ciudad_nombre&tipo_entrenador=eq.personal&ciudad_nombre=not.is.null") 1871 .then(function (filas) { 1872 ciudades = Array.from(new Set(filas.map(function (f) { return f.ciudad_nombre; }))) 1873 .sort(function (a, b) { return a.localeCompare(b, "es"); }); 1874 var dl = document.getElementById("re-rec-ciudades"); 1875 if (dl) dl.innerHTML = ciudades.map(function (c) { return '<option value="' + esc(c) + '">'; }).join(""); 1876 return ciudades; 1877 }) 1878 .catch(function (err) { 1879 // Sin la lista el campo sigue siendo un input de texto libre y la RPC 1880 // casa por ILIKE, asà que se puede enviar igual: solo se pierde el 1881 // autocompletado. No es motivo para bloquear el formulario. 1882 console.warn("[re-recomendador] no se pudieron cargar las ciudades", err); 1883 return []; 1884 }); 1885 } 1886 1887 // A quién se le manda. Se pide a la MISMA RPC que pinta el listado público, 1888 // asà que el orden es el mismo que ve el usuario si navega él solo â no un 1889 // ranking paralelo que dijera otra cosa. 1890 // 1891 // NO se fuerza a que salgan tres. Si con lo que ha pedido solo encaja uno, se 1892 // le manda a ese uno y ya: rellenar el hueco con gente que no encaja es un 1893 // mal reparto para el usuario y un contacto quemado para el entrenador. 1894 // Valladolid es el caso real: un único entrenador en toda la ciudad, y la 1895 // versión vieja acababa soltando la ciudad para llegar a tres, asà que 1896 // devolvÃa tres de cualquier sitio menos el de Valladolid. 1897 // 1898 // Los filtros solo se sueltan cuando no encaja NADIE, de menos a más 1899 // importante: primero el objetivo (casi cualquier entrenador sirve para un 1900 // objetivo que no tenga marcado), luego la modalidad, y la ciudad la última 1901 // porque quien no cubre la zona no puede atender a este cliente y punto. 1902 function buscarTop3() { 1903 var intentos = [ 1904 { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad }, 1905 { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad }, 1906 { objetivo: "", modalidad: "", ciudad: estado.ciudad }, 1907 { objetivo: "", modalidad: estado.modalidad, ciudad: "" } 1908 ]; 1909 1910 function intentar(i) { 1911 if (i >= intentos.length) return Promise.resolve([]); 1912 var f = intentos[i]; 1913 return rest("/rest/v1/rpc/buscar_entrenadores_listado", { 1914 method: "POST", 1915 body: { 1916 p_objetivo: f.objetivo || null, 1917 p_modalidad: f.modalidad || null, 1918 p_ciudad: f.ciudad || null, 1919 p_perfil: null, 1920 p_precio_min: null, 1921 p_precio_max: null, 1922 p_orden: "relevancia", 1923 p_limite: 3, 1924 p_offset: 0 1925 } 1926 }).then(function (filas) { 1927 // Basta con uno: el siguiente intento no serÃa "mejor", serÃa menos 1928 // fiel a lo que ha pedido. 1929 if (filas && filas.length) return filas; 1930 return intentar(i + 1); 1931 }); 1932 } 1933 1934 return intentar(0); 1935 } 1936 1937 // ------------------------------------------------------------------ vistas 1938 1939 function dotsHTML() { 1940 var total = pasos().length; 1941 var html = ""; 1942 for (var i = 0; i < total; i++) { 1943 html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>'; 1944 } 1945 return '<div class="re-rec-dots">' + html + "</div>"; 1946 } 1947 1948 function cabeceraHTML(eyebrow, titulo, sub) { 1949 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 1950 dotsHTML() + 1951 (eyebrow ? '<p class="re-rec-eyebrow">' + esc(eyebrow) + "</p>" : "") + 1952 "<h3>" + esc(titulo) + "</h3>" + 1953 (sub ? '<p class="re-rec-sub">' + esc(sub) + "</p>" : ""); 1954 } 1955 1956 // Las opciones se identifican por su valor y no por su posición: con la 1957 // lista de modalidades plegada, un Ãndice apuntarÃa a la posición dentro de 1958 // lo que se está pintando y no dentro de la lista real. 1959 // Pinta las opciones de un paso ya recortadas a las visibles, y decide sola 1960 // si hace falta el "Ver más": asà ningún paso puede quedarse con el enlace 1961 // puesto pero sin nada que destapar. 1962 function opcionesDelPaso(paso, lista, seleccionado) { 1963 var tope = VISIBLES[paso]; 1964 var plegado = tope && !estado.abiertas[paso] && lista.length > tope; 1965 return opcionesHTML(plegado ? lista.slice(0, tope) : lista, seleccionado, plegado); 1966 } 1967 1968 function opcionesHTML(lista, seleccionado, verMas) { 1969 return '<div class="re-rec-opciones">' + lista.map(function (o) { 1970 var on = o.value === seleccionado; 1971 return '<button type="button" class="re-rec-op' + (on ? " re-rec-op-on" : "") + '"' + 1972 ' data-accion="opcion" data-valor="' + esc(o.value) + '">' + 1973 "<span>" + esc(o.label) + 1974 (o.nota ? '<span class="re-rec-op-nota">' + esc(o.nota) + "</span>" : "") + 1975 "</span></button>"; 1976 }).join("") + 1977 (verMas ? '<button type="button" class="re-rec-vermas" data-accion="ver-mas">Ver más opciones</button>' : "") + 1978 "</div>"; 1979 } 1980 1981 function navHTML(conAtras, textoBoton) { 1982 return '<div class="re-rec-nav">' + 1983 (conAtras ? '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' : "") + 1984 (textoBoton ? '<button type="button" class="re-rec-btn" data-accion="siguiente">' + esc(textoBoton) + "</button>" : "") + 1985 "</div>"; 1986 } 1987 1988 function errorHTML() { 1989 return '<div class="re-rec-error"' + (estado.error ? ' style="display:block"' : "") + ">" + esc(estado.error) + "</div>"; 1990 } 1991 1992 // ¿Hace falta preguntar la ciudad? Solo si ya ha dicho que quiere entrenar 1993 // presencialmente y la página no nos la ha dado hecha. Entrenar por
1994 // videollamada no depende de dónde viva, y filtrar por su ciudad ahà solo 1995 // servirÃa para dejar fuera a entrenadores que le valdrÃan igual. 1996 function pideCiudad() { 1997 return !!estado.modalidad && estado.modalidad !== "Online" && !estado.ciudadDePagina; 1998 } 1999 2000 // Modalidad y ciudad comparten pantalla: son la misma pregunta ("¿dónde?") 2001 // partida en dos, y separarlas metÃa un paso entero para un solo campo. El 2002 // campo de ciudad no está desde el principio â aparece al elegir una 2003 // modalidad presencial, porque hasta entonces no se sabe si hace falta. 2004 function pasoDondeHTML() { 2005 // El número va escrito y se cuenta solo. Dos motivos: el recorrido no 2006 // siempre tiene los mismos pasos (el objetivo se salta en /objetivo/*), y 2007 // la última pantalla no es una pregunta sino el formulario de contacto â 2008 // por eso se descuenta uno y no se anuncian cuatro preguntas. 2009 var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" }; 2010 return cabeceraHTML( 2011 "Te ayudamos a decidir", 2012 "¿TodavÃa no lo tienes claro? Te recomendamos un entrenador.", 2013 (PALABRAS[pasos().length - 1] || "Unas") + " preguntas rápidas" + 2014 " y te ponemos en contacto con los entrenadores" + 2015 (estado.ciudad ? " de " + estado.ciudad : "") + 2016 " que mejor encajan contigo. Gratis y sin compromiso." 2017 ) + 2018 // Al dejar de ser el tÃtulo, la primera pregunta necesita su propia 2019 // etiqueta: sin ella las cuatro opciones quedan colgando del subtÃtulo 2020 // y no se entiende qué se está eligiendo. 2021 '<p class="re-rec-pregunta">¿Dónde te gustarÃa entrenar?</p>' + 2022 opcionesDelPaso("donde", MODALIDADES, estado.modalidad) + 2023 (pideCiudad() 2024 ? '<div class="re-rec-campo re-rec-campo-ciudad">' + 2025 '<label for="re-rec-ciudad">¿En qué ciudad?</label>' + 2026 '<input type="text" id="re-rec-ciudad" list="re-rec-ciudades" autocomplete="address-level2" placeholder="Empieza a escribirâ¦" value="' + esc(estado.ciudad) + '">' + 2027 '<datalist id="re-rec-ciudades"></datalist>' + 2028 "</div>" 2029 : "") + 2030 errorHTML() + 2031 (pideCiudad() ? navHTML(false, "Siguiente") : ""); 2032 } 2033 2034 function pasoObjetivoHTML() { 2035 return cabeceraHTML("", "¿Cuál es tu objetivo?", "Con esto sabemos qué entrenadores están especializados en lo que buscas.") + 2036 // El centinela es `null` y no "": "" es una respuesta valida ("aun no lo 2037 // tengo claro"), y sin distinguirla esa opcion saldria ya marcada antes 2038 // de que el usuario hubiera contestado nada. 2039 opcionesDelPaso("objetivo", OBJETIVOS, estado.objetivoRespondido ? estado.objetivo : null) + 2040 errorHTML() + 2041 navHTML(true, ""); 2042 } 2043 2044 function pasoCuandoHTML() { 2045 return cabeceraHTML("", "¿Cuándo te gustarÃa empezar?", "Asà te proponen un plan realista desde el primer mensaje.") + 2046 opcionesHTML(CUANDOS, estado.cuando) + 2047 errorHTML() + 2048 navHTML(true, ""); 2049 } 2050 2051 // "Ana, Marcos y Juanma" â coma entre todos menos el último, que va con "y". 2052 function enumerar(nombres) { 2053 if (nombres.length === 1) return nombres[0]; 2054 return nombres.slice(0, -1).join(", ") + " y " + nombres[nombres.length - 1]; 2055 } 2056 2057 // El subtÃtulo de la última pantalla nombra a los tres entrenadores en 2058 // cuanto se sabe quiénes son. Mientras la búsqueda va en camino (o si 2059 // falla) se cae a la versión sin nombres: pedir el email es el momento de 2060 // menos paciencia del recorrido y no puede quedarse esperando a nada. 2061 function subtituloDatosHTML() { 2062 if (!estado.top3.length) { 2063 return "Enviaremos tu solicitud a los entrenadores que mejor encajan contigo. Te escribirán ellos directamente."; 2064 } 2065 var nombres = estado.top3.map(function (e) { return "<strong>" + esc(e.nombre) + "</strong>"; }); 2066 return enumerar(nombres) + 2067 (estado.top3.length === 1 ? " es el que mejor encaja" : " son los que mejor encajan") + 2068 " contigo. Te " + (estado.top3.length === 1 ? "escribirá" : "escribirán") + " directamente."; 2069 } 2070 2071 // El aviso legal tiene que decir con cuánta gente se comparte el dato, y 2072 // ya no siempre son tres: si en la ciudad solo encaja uno, es uno. Si la 2073 // búsqueda aún no ha vuelto no se inventa un número. 2074 function conQuienHTML() { 2075 var cuantos = estado.top3.length; 2076 if (cuantos === 1) return "este entrenador"; 2077 if (cuantos) return "estos " + cuantos + " entrenadores"; 2078 return "los entrenadores que mejor encajen contigo"; 2079 } 2080 2081 function pasoDatosHTML() { 2082 return cabeceraHTML("", "Ãltimo paso: ¿cómo te contactamos?", null) + 2083 '<p class="re-rec-sub">' + subtituloDatosHTML() + "</p>" + 2084 '<div class="re-rec-campo">' + 2085 '<label for="re-rec-nombre">Nombre</label>' + 2086 '<input type="text" id="re-rec-nombre" autocomplete="given-name" value="' + esc(estado.nombre) + '">' + 2087 "</div>" + 2088 // El email vuelve al paso 1 y es obligatorio: sin él no hay chat, porque 2089 // no hay a quién avisar de un mensaje nuevo ni con qué entrar al área 2090 // (la cuenta es passwordless por email). Ver specs/spec-chat-plataforma.md. 2091 '<div class="re-rec-campo">' + 2092 '<label for="re-rec-email">Email</label>' + 2093 '<input type="email" id="re-rec-email" autocomplete="email" inputmode="email" placeholder="[email protected]" value="' + esc(estado.email) + '">' + 2094 "</div>" + 2095 // El teléfono se queda, pero pasa a opcional. No se quita: es el dato con 2096 // el que el entrenador llama, y en los 90 dÃas previos las solicitudes 2097 // que solo lo traÃan se gestionaron casi todas, frente a dos tercios de 2098 // las que solo traÃan email. 2099 '<div class="re-rec-campo">' + 2100 '<label for="re-rec-telefono">Teléfono <span class="re-rec-opcional">(opcional)</span></label>' + 2101 '<input type="tel" id="re-rec-telefono" autocomplete="tel" inputmode="tel" placeholder="600 11 22 33" value="' + esc(estado.telefono) + '">' + 2102 "</div>" + 2103 errorHTML() + 2104 '<div class="re-rec-nav">' + 2105 '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' + 2106 '<button type="button" class="re-rec-btn" data-accion="enviar"' + (estado.enviando ? " disabled" : "") + ">" +
2107 (estado.enviando ? "Enviandoâ¦" : "Enviar mi solicitud") + 2108 "</button>" + 2109 "</div>" + 2110 '<p class="re-rec-legal">Al enviar aceptas nuestra <a href="' + PRIVACIDAD_URL + '" target="_blank" rel="noopener">polÃtica de privacidad</a>. Compartiremos tu nombre y tus datos de contacto con ' + conQuienHTML() + ' para que puedan responderte.</p>'; 2111 } 2112 2113 // Estrella siempre en el verde de marca, con independencia de la nota: es la 2114 // convención del listado público (§48 del design system), no un color de 2115 // estado que deba variar con el resultado. 2116 var ESTRELLA_SVG = '<svg class="re-rec-estrella" width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0l2.47 5.01 5.53.8-4 3.9.94 5.5L8 12.6l-4.94 2.6.94-5.5-4-3.9 5.53-.8L8 0z" fill="#089F3B"/></svg>'; 2117 2118 // La pÃldora solo lleva el check cuando ese entrenador trabaja de verdad el 2119 // objetivo que ha pedido el usuario. Cuando el reparto ha tenido que soltar 2120 // el filtro de objetivo para completar los tres (o cuando ha contestado "aún 2121 // no lo tengo claro"), se enseña su especialidad principal sin check: decir 2122 // "coincide con lo que buscas" de alguien que no lo trabaja es mentir en la 2123 // pantalla donde más confianza hace falta. 2124 function especialidadHTML(e) { 2125 var suyas = e.especialidades || []; 2126 if (!suyas.length) return ""; 2127 2128 var coincide = estado.objetivo && suyas.indexOf(estado.objetivo) !== -1; 2129 var texto = coincide ? etiquetaObjetivo(estado.objetivo) : suyas[0]; 2130 return '<span class="re-rec-pill' + (coincide ? " re-rec-pill-match" : "") + '">' + 2131 (coincide ? "â " : "") + esc(texto) + 2132 "</span>"; 2133 } 2134 2135 // "Pérdida de peso" es como está guardado; "Perder peso" es como se lo hemos 2136 // preguntado. Se le devuelve su propia palabra. 2137 function etiquetaObjetivo(valor) { 2138 var op = OBJETIVOS.find(function (o) { return o.value === valor; }); 2139 return op ? op.label : valor; 2140 } 2141 2142 function fichaHTML(e) { 2143 // Sin `loading="lazy"`: son tres fotos que se pintan ya visibles y en el 2144 // momento en que se muestra el modal. Diferirlas solo consigue que la 2145 // confirmación aparezca un instante con tres huecos grises. 2146 var foto = e.foto_url 2147 ? '<img class="re-rec-foto" src="' + esc(e.foto_url) + '" alt="' + esc(e.nombre) + '">' 2148 : '<div class="re-rec-foto"></div>'; 2149 2150 // Nota y experiencia, y nada más: el precio se probó como relleno y se 2151 // descartó (decisión de Esteve del 17/08/2026). Esta pantalla va de si el 2152 // entrenador encaja, y meter una cifra en euros justo cuando el usuario 2153 // acaba de dar sus datos convierte una confirmación en un escaparate. 2154 // 2155 // La consecuencia asumida es que muchas fichas se quedan solo con nombre y 2156 // especialidad: de los 93 entrenadores personales, 22 tienen años de 2157 // experiencia y 15 tienen opiniones. Eso no se arregla aquà sino haciendo 2158 // que rellenen esos campos. 2159 // 2160 // Nunca se pinta un cero: un "0 opiniones" o un "0 años" en la pantalla 2161 // que tiene que dar confianza juega en contra del entrenador. 2162 var datos = []; 2163 if (e.total_reviews > 0) { 2164 datos.push(ESTRELLA_SVG + " " + e.rating_promedio + " (" + e.total_reviews + 2165 (e.total_reviews === 1 ? " opinión)" : " opiniones)")); 2166 } 2167 if (e.anos_experiencia > 0) { 2168 // "de experiencia" completo y no solo "2 años": suelto, se lee como la 2169 // edad del entrenador. 2170 datos.push(e.anos_experiencia + (e.anos_experiencia === 1 ? " año" : " años") + " de experiencia"); 2171 } 2172 2173 // La ficha NO es un enlace, a propósito. Debajo de estas tres tarjetas 2174 // está el paso de completar el perfil, que es lo único que queda por 2175 // hacer y lo que da valor a la solicitud recién enviada; un enlace al 2176 // perfil aquà arriba se lleva al usuario a otra pestaña justo antes de 2177 // llegar a él. Las fotos y los datos ya cumplen su función, que es 2178 // demostrar que la solicitud ha ido a gente real. 2179 return '<div class="re-rec-ficha">' + 2180 foto + 2181 '<span class="re-rec-ficha-datos">' + 2182 '<span class="re-rec-ficha-nombre">' + esc(e.nombre) + "</span>" + 2183 especialidadHTML(e) + 2184 // Cada dato en su lÃnea y no separados por "·": en una tarjeta de 2185 // 180px, "5 (12 opiniones) · 8 años de experiencia" se parte por donde 2186 // le toca y deja huérfano medio dato en la lÃnea siguiente. 2187 datos.map(function (d) { return '<span class="re-rec-ficha-meta">' + d + "</span>"; }).join("") + 2188 "</span>" + 2189 "</div>"; 2190 } 2191 2192 function opcionesSelectHTML(lista, placeholder) { 2193 return '<option value="">' + esc(placeholder) + "</option>" + lista.map(function (o) { 2194 return '<option value="' + esc(o.value) + '">
2194' + esc(o.label) + "</option>"; 2195 }).join(""); 2196 } 2197 2198 // Paso 2 del journey de usuario. Va SIEMPRE después de la solicitud, nunca 2199 // antes: es opcional y no puede convertirse en un peaje para escribir a un 2200 // entrenador. `claim_token` es lo que devuelve solicitar-contacto-entrenador 2201 // y lo único que prueba que quien rellena esto es quien acaba de enviar. 2202 function confirmacionHTML() { 2203 var cuantos = estado.top3.length; 2204 var p2 = estado.claimToken 2205 ? '<div class="re-rec-p2">' + 2206 "<h4>Cuéntanos algo más</h4>" + 2207 '<p class="re-rec-sub" style="font-size:14px;margin-bottom:14px">Opcional y son 20 segundos, pero con esto te preparan una propuesta a tu medida.</p>' + 2208 // El nivel de actividad va a fila completa porque su etiqueta no 2209 // cabe a media columna; edad y peso comparten fila, que son dos 2210 // campos de tres caracteres. 2211 '<div class="re-rec-p2-grid">' + 2212 // El email ya no se pide aquÃ: lo trae el paso 1, obligatorio. 2213 // Pedirlo dos veces es peor que pedirlo una. 2214 '<div class="re-rec-campo re-rec-p2-full"><label for="re-rec-actividad">¿Cuánto ejercicio haces ahora?</label><select id="re-rec-actividad">' + opcionesSelectHTML(NIVELES, "Selecciona tu nivel") + "</select></div>" + 2215 '<div class="re-rec-campo"><label for="re-rec-edad">Edad</label><input type="number" id="re-rec-edad" min="10" max="120" inputmode="numeric" placeholder="Años"></div>' + 2216 '<div class="re-rec-campo"><label for="re-rec-peso">Peso</label><input type="number" id="re-rec-peso" min="20" max="400" step="0.1" inputmode="decimal" placeholder="Kg"></div>' + 2217 // Altura y disponibilidad los pide la tarjeta de "Solicitud inicial" 2218 // del panel del entrenador, que enseña ocho datos del cliente. 2219 // Están aquà y no en el paso 1 por lo mismo que el resto: el paso 1 2220 // pide lo mÃnimo para poder responder, esto es lo que ayuda a 2221 // preparar la propuesta y se pregunta cuando ya no cuesta conversión. 2222 '<div class="re-rec-campo"><label for="re-rec-altura">Altura</label><input type="number" id="re-rec-altura" min="100" max="250" inputmode="numeric" placeholder="Cm"></div>' + 2223 '<div class="re-rec-campo"><label for="re-rec-disponibilidad">¿Cuándo puedes entrenar?</label><input type="text" id="re-rec-disponibilidad" placeholder="Tardes entre semana"></div>' + 2224 '<div class="re-rec-campo re-rec-p2-full"><label for="re-rec-notas">¿Algo más que debamos saber?</label><textarea id="re-rec-notas" placeholder="Lesiones, horarios que te van bien, experiencia previaâ¦"></textarea></div>' + 2225 "</div>" + 2226 '<div class="re-rec-error" id="re-rec-p2-error"></div>' + 2227 '<div class="re-rec-acciones">' + 2228 '<button type="button" class="re-rec-btn" data-accion="guardar-perfil">Enviar mis datos</button>' + 2229 '<button type="button" class="re-rec-saltar" data-accion="cerrar">Ahora no</button>' + 2230 "</div>" + 2231 "</div>" 2232 : ""; 2233 2234 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 2235 '<p class="re-rec-eyebrow">Solicitud enviada</p>' + 2236 "<h3>" + (cuantos === 1 ? "Este es tu entrenador" : "Estos son tus " + cuantos + " entrenadores") + "</h3>" + 2237 '<p class="re-rec-sub">Ya tienen tu solicitud y te escribirán directamente.</p>' + 2238 '<div class="re-rec-fichas">' + estado.top3.map(fichaHTML).join("") + "</div>" + 2239 p2; 2240 } 2241 2242 // `ofreceCuenta` sigue siendo un parámetro y no un `true` fijo aunque el 2243 // email vuelva a ser obligatorio en el paso 1: este cierre también lo pinta 2244 // el camino en el que el paso 2 falla, y ahà no se da nada por hecho. 2245 function cierreHTML(guardado, ofreceCuenta) { 2246 var corto = estado.nombre ? estado.nombre.trim().split(" ")[0] : ""; 2247 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 2248 "<h3>" + esc(guardado ? "¡Listo!" : (corto ? "¡Gracias, " + corto + "!" : "¡Gracias!")) + "</h3>" + 2249 '<p class="re-rec-sub">' + 2250 (guardado 2251 ? "Tus datos ya van con la solicitud. Se pondrán en contacto contigo muy pronto." 2252 : "Tu solicitud está enviada. Se pondrán en contacto contigo muy pronto.") + 2253 "</p>" + 2254 (estado.claimToken && ofreceCuenta 2255 ? '<div class="re-rec-p2">' + 2256 "<h4>¿Quieres seguir tus solicitudes?</h4>" + 2257 '<p class="re-rec-sub" style="font-size:14px;margin-bottom:14px">Crea tu cuenta y consulta cuándo las ve cada entrenador. Sin contraseña y sin salir de aquÃ.</p>' + 2258 '<div class="re-rec-error" id="re-rec-cuenta-error"></div>' + 2259 '<button type="button" class="re-rec-btn" data-accion="crear-cuenta">Crear mi cuenta</button>' + 2260 '<p class="re-rec-legal">Al crear la cuenta aceptas nuestra <a href="' + PRIVACIDAD_URL + '" target="_blank" rel="noopener">polÃtica de privacidad</a>.</p>' + 2261 "</div>" 2262 : ""); 2263 } 2264 2265 // ------------------------------------------------------------------ puente 2266 2267 // El widget no cabe en un solo Embed de Webflow (tope de 50.000 caracteres), 2268 // asi que va partido en dos bloques. Esto es TODO lo que cruza de este 2269 // bloque al siguiente; es interno y no es la API publica, que sigue siendo 2270 // window.reRecomendador y la monta el bloque de comportamiento. 2271 // 2272 // `estado` cruza como referencia, no como copia: los dos bloques escriben 2273 // sobre el mismo objeto y esa es justo la idea. 2274 window.__reRecNucleo = { 2275 RUTAS_EXCLUIDAS: RUTAS_EXCLUIDAS, 2276 SEGUNDOS_ESPERA: SEGUNDOS_ESPERA, 2277 SCROLL_DISPARO: SCROLL_DISPARO, 2278 CLAVE_APAGADO: CLAVE_APAGADO, 2279 CLAVE_SESION: CLAVE_SESION,
2280 MODALIDADES: MODALIDADES, 2281 OBJETIVOS: OBJETIVOS, 2282 CUANDOS: CUANDOS, 2283 estado: estado, 2284 pasos: pasos, 2285 pasoActual: pasoActual, 2286 pideCiudad: pideCiudad, 2287 leer: leer, 2288 escribir: escribir, 2289 ciudadDeLaPagina: ciudadDeLaPagina, 2290 objetivoDeLaPagina: objetivoDeLaPagina, 2291 rest: rest, 2292 cargarCiudades: cargarCiudades, 2293 buscarTop3: buscarTop3, 2294 pasoDondeHTML: pasoDondeHTML, 2295 pasoObjetivoHTML: pasoObjetivoHTML, 2296 pasoCuandoHTML: pasoCuandoHTML, 2297 pasoDatosHTML: pasoDatosHTML, 2298 subtituloDatosHTML: subtituloDatosHTML, 2299 confirmacionHTML: confirmacionHTML, 2300 cierreHTML: cierreHTML 2301 }; 2302})(); 2303</script>
2303 2304</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: a quien se reparte --> 2305<!-- Va en el Footer V1, ENTRE el bloque 2 (JS nucleo) y el bloque 3 (JS 2306 comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `buscarTop3` 2307 a una variable local nada mas cargar, asi que quien quiera cambiarla tiene 2308 que hacerlo antes. 2309 2310 Que cambia. El nucleo reparte con `buscar_entrenadores_listado`, que es la 2311 RPC del listado publico y ordena por un score de cinco criterios (plan 35, 2312 puja 25, recomendaciones 20, calidad 12, valoracion 8). Eso vale para 2313 ordenar un listado, pero repartir no es ordenar: con 180 entrenadores el 2314 score le daba casi todos los contactos a los mismos seis. 2315 2316 Desde el 12/09/2026 el reparto son dos criterios y ni uno mas 2317 (`buscar_entrenadores_reparto`, migracion 20260913030000): 2318 2319 1. primero los de plan de pago, que pagan por eso; 2320 2. despues todos los demas, empezando por quien menos leads ha recibido. 2321 2322 Y sigue haciendo lo de antes: `p_solo_contactables` deja fuera del reparto 2323 a los perfiles de escaparate (`entrenadores.recibe_contactos = false`), que 2324 tienen que seguir saliendo en el listado y no pueden salir aqui, porque 2325 aqui no se enseña, se reparte el contacto. El servidor ya los descartaba 2326 (`solicitar-contacto-entrenador` no les manda nada), pero el pop-up 2327 enseñaba su tarjeta diciendo "ya tienen tu solicitud", que de ellos no era 2328 verdad. 2329 2330 Por que un parche y no una linea en el bloque 2. Escribir ese embed por 2331 MCP obliga a reproducir sus 34 KB enteros a mano, sobre el flujo que trae 2332 los leads. Esto son 30 lineas y se puede tirar entero el dia que se toque 2333 el nucleo: entonces, cambia alli la RPC y borra este embed. 2334 2335 La cascada de intentos es copia literal del nucleo, con el corte en cuanto 2336 hay UNO (23/08/2026, cambiado en los dos sitios a la vez). Si algun dia 2337 cambia la del nucleo, el numero de version de abajo apaga el parche en vez 2338 de imponer la version vieja en silencio. 2339 2340 ID del embed: e4e348ca-b183-eafe-444a-cd8dbed588ae (componente Footer V1). -->
2341<script> 2342(function () { 2343 "use strict"; 2344 2345 var N = window.__reRecNucleo; 2346 if (!N || !N.buscarTop3) return; 2347 2348 // Copiado del bloque 2 con esta version. Si el nucleo cambia de version, el 2349 // parche se aparta: mejor que el pop-up reparta con el score viejo (el 2350 // servidor sigue filtrando) a que se quede congelado con una busqueda vieja. 2351 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 2352 console.warn("[recomendador] parche de reparto desactivado: el nucleo es la version", window.RE_RECOMENDADOR_VERSION, "y el parche se escribio para la 1.0.0. Cambia la RPC a buscar_entrenadores_reparto en buscarTop3 y borra este embed."); 2353 return; 2354 } 2355 2356 var estado = N.estado; 2357 var rest = N.rest; 2358 2359 N.buscarTop3 = function buscarTop3() { 2360 var intentos = [ 2361 { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad }, 2362 { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad }, 2363 { objetivo: "", modalidad: "", ciudad: estado.ciudad }, 2364 { objetivo: "", modalidad: estado.modalidad, ciudad: "" } 2365 ]; 2366 2367 function intentar(i) { 2368 if (i >= intentos.length) return Promise.resolve([]); 2369 var f = intentos[i]; 2370 return rest("/rest/v1/rpc/buscar_entrenadores_reparto", { 2371 method: "POST", 2372 body: { 2373 p_objetivo: f.objetivo || null, 2374 p_modalidad: f.modalidad || null, 2375 p_ciudad: f.ciudad || null, 2376 p_limite: 3, 2377 // Va dentro de la busqueda y no filtrando el resultado porque el 2378 // LIMIT ya se ha aplicado: quitar a uno aqui fuera dejaria dos, y un 2379 // lead repartido de menos es un entrenador que no recibe nada. 2380 p_solo_contactables: true 2381 } 2382 }).then(function (filas) { 2383 // Basta con uno: el siguiente intento no seria "mejor", seria menos 2384 // fiel a lo que ha pedido. 2385 if (filas && filas.length) return filas; 2386 return intentar(i + 1); 2387 }); 2388 } 2389 2390 return intentar(0); 2391 }; 2392 2393 window.RE_RECOMENDADOR_ESCAPARATE = "2026-09-12-1"; 2394})(); 2395</script>
2395</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: no preguntar lo que ya nos han dicho --> 2396<!-- Va en el Footer V1, ENTRE el parche 2b (escaparate) y el bloque 3 (JS 2397 comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `pasos`, 2398 `pasoActual` y las cuatro funciones de pantalla a variables locales nada 2399 mas cargar, asi que quien quiera cambiarlas tiene que hacerlo antes. 2400 2401 Que arregla. El buscador de la home (widget-home-hero-buscar.html) ya le ha 2402 sacado a la persona el objetivo, la modalidad, la ciudad y cuando quiere 2403 empezar. Sin esto, el pop-up se abria igualmente por la primera pantalla 2404 ("¿donde quieres entrenar?") a preguntarle lo que acababa de escribir. 2405 2406 Como se le dice lo que ya sabemos. Quien abra el pop-up con respuestas 2407 puestas marca ademas `estado.sabidos`: 2408 2409 estado.sabidos = { donde: true, objetivo: true, cuando: true } 2410 2411 Solo se salta lo que este marcado Y ademas tenga valor de verdad. "Donde" 2412 es la unica con truco: esa pantalla pregunta modalidad y, si es presencial, 2413 tambien ciudad, asi que saltarla exige tener las dos. 2414 2415 El paso "datos" no se salta nunca: es donde se piden nombre y email, que es 2416 lo unico que no se puede adivinar de una frase. Con las cuatro respuestas 2417 puestas, el pop-up abre directamente ahi. 2418 2419 Sin `estado.sabidos` la lista es exactamente la del nucleo (["donde", 2420 "objetivo", "cuando", "datos"], con "objetivo" fuera en /objetivo/*). Quien 2421 llega por el pop-up automatico de los 25 segundos no nota ningun cambio y 2422 el embudo que ya se esta midiendo sigue igual. 2423 2424 Por que hay que parchear CUATRO cosas y no solo `pasos`: 2425 2426 - `pasoActual` la calcula el nucleo con su `pasos` interna, no con la 2427 exportada. Sin parchearla, el bloque 3 pintaria la pantalla de la lista 2428 vieja: la lista diria "datos" y la pantalla seguiria siendo "donde". 2429 - Los puntos de progreso y el "Tres preguntas rapidas" tambien salen de 2430 esa `pasos` interna, y estan dentro de `cabeceraHTML`, que no se 2431 exporta. Se corrigen sobre el HTML ya generado, que es feo pero cabe 2432 aqui; la alternativa era reescribir los 30 KB del nucleo a mano. 2433 - El boton "Atras" de la pantalla de datos esta escrito a pelo (no pasa 2434 por `navHTML`), asi que cuando datos es la unica pantalla se queda un 2435 boton que no lleva a ningun sitio. Se quita cuando no hay nada detras. 2436 2437 Desde el 23/08/2026 corrige ademas las tres frases del nucleo que daban por 2438 hecho que el reparto siempre son tres entrenadores; ya no lo es. 2439 2440 Todo esto se cae solo el dia que el nucleo incorpore `sabidos`: sube su 2441 RE_RECOMENDADOR_VERSION y este embed se apaga entero. Mismo criterio y 2442 mismo guardia de version que el parche 2b. -->
2443<script> 2444(function () { 2445 "use strict"; 2446 2447 var N = window.__reRecNucleo; 2448 if (!N || !N.pasos || !N.estado) return; 2449 2450 // Igual que en el parche 2b: si el nucleo cambia de version, este parche se 2451 // aparta y vuelve el recorrido completo. Preguntar de mas es recuperable; 2452 // saltarse una pantalla que ha cambiado de contenido, no. 2453 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 2454 console.warn("[recomendador] parche de pasos desactivado: el nucleo es la version", window.RE_RECOMENDADOR_VERSION, "y el parche se escribio para la 1.0.0. Metele `sabidos` a pasos() y borra este embed."); 2455 return; 2456 } 2457 2458 var estado = N.estado; 2459 2460 // La pantalla "donde" son dos preguntas en una. Saltarla solo vale si ninguna 2461 // de las dos queda en el aire: online no necesita ciudad, presencial si. 2462 function sabemosDonde() { 2463 if (!estado.modalidad) return false; 2464 if (estado.modalidad === "Online") return true; 2465 return !!estado.ciudad || !!estado.ciudadDePagina; 2466 } 2467 2468 N.pasos = function pasos() { 2469 var sabidos = estado.sabidos || {}; 2470 var lista = []; 2471 if (!sabidos.donde || !sabemosDonde()) lista.push("donde"); 2472 if (!estado.objetivoDePagina && !(sabidos.objetivo && estado.objetivo)) lista.push("objetivo"); 2473 if (!(sabidos.cuando && estado.cuando)) lista.push("cuando"); 2474 lista.push("datos"); 2475 return lista; 2476 }; 2477 2478 N.pasoActual = function pasoActual() { 2479 return N.pasos()[estado.indice]; 2480 }; 2481 2482 var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" }; 2483 2484 function dotsHTML() { 2485 var total = N.pasos().length; 2486 var html = ""; 2487 for (var i = 0; i < total; i++) { 2488 html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>'; 2489 } 2490 return '<div class="re-rec-dots">' + html + "</div>"; 2491 } 2492 2493 // Cuantos entrenadores van a recibir la solicitud de verdad. En la primera 2494 // pintada de la pantalla de datos todavia no se sabe (la busqueda va en 2495 // camino), y ahi no se inventa un numero. 2496 function conQuien() { 2497 var n = (estado.top3 || []).length; 2498 if (n === 1) return "este entrenador"; 2499 if (n) return "estos " + n + " entrenadores"; 2500 return "los entrenadores que mejor encajen contigo"; 2501 } 2502 2503 // Cirugia sobre el HTML que devuelve el nucleo. Los reemplazos son literales 2504 // suyos: si alguno deja de aparecer, no rompe nada (el replace no encuentra 2505 // y devuelve el mismo HTML), solo deja de corregir esa parte. 2506 function corregir(html) { 2507 html = html.replace(/<div class="re-rec-dots">[\s\S]*?<\/div>/, dotsHTML()); 2508 2509 var preguntas = N.pasos().length - 1; // la ultima pantalla no es pregunta 2510 html = html.replace( 2511 /(?:Una|Dos|Tres|Unas) preguntas rápidas/, 2512 preguntas === 1 ? "Una pregunta rápida" : (PALABRAS[preguntas] || "Unas") + " preguntas rápidas" 2513 ); 2514 2515 // El reparto dejo de forzar tres (parche 2b, 23/08/2026): si en la ciudad 2516 // solo encaja uno, se le manda a uno. Estas tres frases del nucleo daban 2517 // por hecho que siempre eran tres, asi que la primera pantalla prometia 2518 // algo que no se iba a cumplir y el aviso legal decia con cuanta gente se 2519 // comparte el dato sin que fuera verdad. 2520 html = html.replace("con los 3 entrenadores", "con los entrenadores"); 2521 html = html.replace("Enviaremos tu solicitud a los 3 entrenadores", "Enviaremos tu solicitud a los entrenadores"); 2522 html = html.replace("con estos 3 entrenadores", "con " + conQuien()); 2523 2524 if (estado.indice === 0) { 2525 html = html.replace('<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>', ""); 2526 } 2527 return html; 2528 } 2529 2530 ["pasoDondeHTML", "pasoObjetivoHTML", "pasoCuandoHTML", "pasoDatosHTML"].forEach(function (nombre) { 2531 var original = N[nombre]; 2532 if (typeof original !== "function") return; 2533 N[nombre] = function () { return corregir(original.apply(null, arguments)); }; 2534 }); 2535})(); 2536</script>
2536</div><div class="w-embed w-script"><!-- ============================================================ 2537 RedEntrenadores â Parche del pop-up recomendador: gancho manual en una ruta excluida 2538 2539 QUà ARREGLA: en /pregunta-al-experto el recomendador está en `RUTAS_EXCLUIDAS`, y 2540 el bloque 3 entiende "excluida" como "ni siquiera arranques": sale de `init()` antes 2541 de montar nada, asà que allà no existe `window.reRecomendador` y no hay forma de 2542 ofrecerlo a mano. Lo queremos ofrecer justo después de enviar la pregunta, que es 2543 cuando quien pregunta tiene el problema fresco y toda la atención puesta. 2544 2545 QUà HACE: en esa ruta, y solo en esa, 2546 2547 1. saca `/pregunta-al-experto` de `RUTAS_EXCLUIDAS` para que el bloque 3 monte el 2548 pop-up y deje el gancho `window.reRecomendador.abrir()` disponible; 2549 2. le hace creer que ya salió en esta sesión, envolviendo `leer` para la clave de 2550 sesión. `puedeSalir()` la consulta antes de cada disparo por tiempo o scroll, 2551 asà que el pop-up no sale por su cuenta; `abrir()` no la mira, asà que el 2552 gancho manual sigue funcionando. 2553 2554 Quien lo abre es el pop-up de confirmación de esa landing: 2555 `widget-pregunta-al-experto-2-popup.html`. 2556 2557 POR QUà UN PARCHE Y NO TOCAR EL BLOQUE 3: aquel embed son 28 KB que habrÃa que 2558 reproducir enteros para cambiarle dos lÃneas. Es el mismo motivo por el que ya 2559 existen `-escaparate` y `-saltar-sabidos`. El dÃa que se reescriba el bloque 3, 2560 el cambio limpio es que `excluida()` apague solo los disparos automáticos y deje 2561 el resto del `init()` en pie; entonces este embed se borra. 2562 2563 DÃNDE VA: cuarto Embed del componente Footer V1, ENTRE el núcleo (bloque 2, del que 2564 lee `window.__reRecNucleo`) y el comportamiento (bloque 3, al que tiene que 2565 adelantarse). Junto a los otros dos parches, delante del bloque 3. 2566 ============================================================ --> 2567
2568<script> 2569(function () { 2570 var N = window.__reRecNucleo; 2571 if (!N) return; // sin el núcleo no hay nada que parchear; el bloque 3 ya avisa por consola 2572 2573 // Guardia de versión, como los otros parches: si el núcleo cambia de versión, este 2574 // bloque se calla en vez de operar sobre algo que ya no es lo que espera. 2575 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 2576 console.warn("[re-recomendador] parche del gancho manual: version distinta, no se aplica"); 2577 return; 2578 } 2579 2580 var RUTA = "/pregunta-al-experto"; 2581 if (location.pathname.indexOf(RUTA) !== 0) return; 2582 2583 N.RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS.filter(function (r) { return r !== RUTA; }); 2584 2585 // El bloque 3 copia `N.leer` a una variable local en cuanto arranca, asà que esto 2586 // tiene que estar puesto antes que él. Solo miente sobre la clave de sesión: todo lo 2587 // demás (el apagado permanente, entre otros) sigue leyéndose de verdad. 2588 var leerOriginal = N.leer; 2589 N.leer = function (clave, almacen) { 2590 if (clave === N.CLAVE_SESION) return "1"; 2591 return leerOriginal(clave, almacen); 2592 }; 2593 2594 window.RE_RECOMENDADOR_GANCHO_MANUAL = true; 2595})(); 2596</script>
2596 2597</div><div class="w-embed w-script">
2597<script> 2598(function () { 2599 "use strict"; 2600 2601 // Segunda mitad del pop-up: cuando se abre, que pasa al pulsar y que se 2602 // mide. Depende del bloque anterior; si por lo que sea no ha cargado, aqui 2603 // no se hace nada en vez de reventar con un TypeError por cada pagina. 2604 var N = window.__reRecNucleo; 2605 if (!N) { 2606 console.error("[re-recomendador] falta el bloque 1, el pop-up no arranca"); 2607 return; 2608 } 2609 2610 // Se sacan a variables locales para que el resto del bloque se lea igual 2611 // que cuando todo esto era un unico fichero. 2612 var RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS; 2613 var SEGUNDOS_ESPERA = N.SEGUNDOS_ESPERA; 2614 var SCROLL_DISPARO = N.SCROLL_DISPARO; 2615 var CLAVE_APAGADO = N.CLAVE_APAGADO; 2616 var CLAVE_SESION = N.CLAVE_SESION; 2617 var MODALIDADES = N.MODALIDADES; 2618 var OBJETIVOS = N.OBJETIVOS; 2619 var CUANDOS = N.CUANDOS; 2620 var estado = N.estado; 2621 var pasos = N.pasos; 2622 var pasoActual = N.pasoActual; 2623 var pideCiudad = N.pideCiudad; 2624 var leer = N.leer; 2625 var escribir = N.escribir; 2626 var ciudadDeLaPagina = N.ciudadDeLaPagina; 2627 var objetivoDeLaPagina = N.objetivoDeLaPagina; 2628 var rest = N.rest; 2629 var cargarCiudades = N.cargarCiudades; 2630 var buscarTop3 = N.buscarTop3; 2631 var pasoDondeHTML = N.pasoDondeHTML; 2632 var pasoObjetivoHTML = N.pasoObjetivoHTML; 2633 var pasoCuandoHTML = N.pasoCuandoHTML; 2634 var pasoDatosHTML = N.pasoDatosHTML; 2635 var subtituloDatosHTML = N.subtituloDatosHTML; 2636 var confirmacionHTML = N.confirmacionHTML; 2637 var cierreHTML = N.cierreHTML; 2638 2639 var overlay = null; 2640 var caja = null; 2641 2642 // ------------------------------------------------ alto real de la pantalla 2643 2644 // Lo que el CSS no puede arreglar solo: ni `100dvh` ni un `position: fixed` 2645 // se enteran de que ha subido el teclado. El modal se sigue dibujando contra 2646 // el viewport de siempre y el teclado le tapa la parte de abajo, que en el 2647 // paso de datos es justo donde están el último campo y el botón de enviar. 2648 // La única medida que sà encoge con el teclado es `visualViewport`, asà que 2649 // mientras el pop-up está abierto el overlay se pega a ella. Hace falta 2650 // colocarlo además de medirlo (`top`/`left`) porque iOS no encoge el 2651 // viewport visible: lo desplaza. 2652 var vv = window.visualViewport || null; 2653 var ultimoEncaje = ""; 2654 2655 function ajustarAlViewport() { 2656 if (!overlay || !vv) return; 2657 var encaje = [vv.height, vv.width, vv.offsetTop, vv.offsetLeft].join("|"); 2658 if (encaje === ultimoEncaje) return; 2659 ultimoEncaje = encaje; 2660 // Con `top` + `height` y `left` + `width` puestos, el `inset: 0` del CSS 2661 // deja de mandar en el borde de abajo y en el de la derecha. 2662 overlay.style.top = vv.offsetTop + "px"; 2663 overlay.style.left = vv.offsetLeft + "px"; 2664 overlay.style.height = vv.height + "px"; 2665 overlay.style.width = vv.width + "px"; 2666 // Si estaba escribiendo, que su campo quede en el centro de lo que queda 2667 // visible. `center` y no `nearest`: `nearest` lo deja pegado al borde de 2668 // abajo, que es justo donde está la barra de botones pegada, y el campo 2669 // acaba detrás de ella. 2670 var foco = document.activeElement; 2671 if (foco && caja && foco !== caja && caja.contains(foco)) { 2672 try { foco.scrollIntoView({ block: "center" }); } catch (e) {} 2673 } 2674 } 2675 2676 function soltarViewport() { 2677 if (!overlay) return; 2678 ultimoEncaje = ""; 2679 overlay.style.top = ""; 2680 overlay.style.left = ""; 2681 overlay.style.height = ""; 2682 overlay.style.width = ""; 2683 } 2684 2685 // ----------------------------------------------------------------- render 2686 2687 var VISTAS = { 2688 donde: pasoDondeHTML, 2689 objetivo: pasoObjetivoHTML, 2690 cuando: pasoCuandoHTML, 2691 datos: pasoDatosHTML 2692 }; 2693 2694 var LISTAS = { 2695 donde: MODALIDADES, 2696 objetivo: OBJETIVOS, 2697 cuando: CUANDOS 2698 }; 2699 2700 function pintar() { 2701 var html; 2702 if (estado.fase === "confirmacion") html = confirmacionHTML(); 2703 else html = VISTAS[pasoActual()](); 2704 2705 caja.innerHTML = html; 2706 caja.classList.toggle("re-rec-ancho", estado.fase !== "preguntas"); 2707 caja.scrollTop = 0; 2708 2709 if (document.getElementById("re-rec-ciudad")) cargarCiudades(); 2710 if (estado.fase === "preguntas" && pasoActual() === "datos") precargarTop3(); 2711 // El foco automático solo en escritorio: en móvil abre el teclado y tapa 2712 // media pantalla justo cuando el usuario aún está leyendo la pregunta. 2713 var primerInput = caja.querySelector("input"); 2714 if (primerInput && window.innerWidth > 560) primerInput.focus(); 2715 2716 medirPantalla(); 2717 } 2718
2719 function claveBusqueda() { 2720 return [estado.ciudad, estado.objetivo, estado.modalidad].join("|"); 2721 } 2722 2723 // Se busca a los tres al llegar a la pantalla de datos, no al enviarla, para 2724 // poder nombrarlos antes de pedir el email. El resultado se guarda con la 2725 // clave de las respuestas que lo produjeron: si vuelve atrás y cambia una, 2726 // la caché deja de valer sola y `enviar` no manda la solicitud a los tres 2727 // entrenadores de una búsqueda que ya no es la suya. 2728 function precargarTop3() { 2729 var clave = claveBusqueda(); 2730 if (estado.top3Clave === clave) return; 2731 estado.top3Clave = clave; 2732 estado.top3 = []; 2733 2734 buscarTop3().then(function (top3) { 2735 if (estado.top3Clave !== clave) return; // llegó tarde: ya cambió algo 2736 estado.top3 = top3; 2737 // Se reescribe solo el subtÃtulo en vez de repintar la pantalla: un 2738 // `pintar()` aquà borrarÃa lo que el usuario ya estuviera escribiendo 2739 // en el nombre o el email. 2740 var sub = caja.querySelector(".re-rec-sub"); 2741 if (sub && estado.fase === "preguntas" && pasoActual() === "datos") { 2742 sub.innerHTML = subtituloDatosHTML(); 2743 } 2744 }).catch(function (err) { 2745 console.warn("[re-recomendador] no se pudo precargar el reparto", err); 2746 estado.top3Clave = null; // que `enviar` lo reintente 2747 }); 2748 } 2749 2750 function avanzar() { 2751 estado.error = ""; 2752 estado.indice += 1; 2753 pintar(); 2754 } 2755 2756 function elegirOpcion(valor) { 2757 var paso = pasoActual(); 2758 var lista = LISTAS[paso]; 2759 if (!lista) return; 2760 var op = lista.find(function (o) { return o.value === valor; }); 2761 if (!op) return; 2762 guardarCampos(); 2763 2764 if (paso === "donde") { 2765 estado.modalidad = op.value; 2766 medir("recomendador_respuesta", { paso: "modalidad", valor: op.value }); 2767 // Elegir modalidad avanza solo, salvo cuando eso destapa el campo de 2768 // ciudad: ahà hay que quedarse en la pantalla para que lo rellene. 2769 if (pideCiudad()) { estado.error = ""; return pintar(); } 2770 } else if (paso === "objetivo") { 2771 estado.objetivo = op.value; 2772 estado.objetivoRespondido = true; 2773 // El objetivo vacÃo es la opción "Aún no lo tengo claro", no un fallo: 2774 // se le pone nombre para poder contarla como una respuesta más. 2775 medir("recomendador_respuesta", { paso: "objetivo", valor: op.value || "Aun no lo tengo claro" }); 2776 } else if (paso === "cuando") { 2777 estado.cuando = op.value; 2778 medir("recomendador_respuesta", { paso: "cuando", valor: op.value }); 2779 } 2780 2781 avanzar(); 2782 } 2783 2784 function validarYAvanzar() { 2785 var input = document.getElementById("re-rec-ciudad"); 2786 if (!input) return; 2787 var ciudad = input.value.trim(); 2788 if (!ciudad) return mostrarError("Dinos en qué ciudad buscas."); 2789 estado.ciudad = ciudad; 2790 // La ciudad va tal cual la escribe: saber que piden Albacete y que ahà no 2791 // hay nadie vale más que el propio embudo. 2792 medir("recomendador_respuesta", { paso: "ciudad", valor: ciudad }); 2793 avanzar(); 2794 } 2795 2796 function mostrarError(texto) { 2797 estado.error = texto; 2798 var el = caja.querySelector(".re-rec-error"); 2799 if (el) { el.textContent = texto; el.style.display = "block"; } 2800 } 2801 2802 // Cualquier repintado tira el contenido de los campos, asà que antes de 2803 // movernos se guarda lo que haya escrito. Sin esto, quien vuelve atrás a 2804 // cambiar el objetivo se encuentra el nombre y el email en blanco al 2805 // regresar, y eso es un abandono servido en bandeja. 2806 function guardarCampos() { 2807 var campos = { nombre: "re-rec-nombre", email: "re-rec-email", telefono: "re-rec-telefono", ciudad: "re-rec-ciudad" }; 2808 Object.keys(campos).forEach(function (clave) { 2809 var el = document.getElementById(campos[clave]); 2810 if (el) estado[clave] = el.value.trim(); 2811 }); 2812 } 2813 2814 function retroceder() { 2815 guardarCampos(); 2816 medir("recomendador_atras", { desde: pasoActual() }); 2817 estado.error = ""; 2818 if (estado.indice > 0) estado.indice -= 1; 2819 pintar(); 2820 } 2821 2822 // ----------------------------------------------------------------- envÃo 2823 2824 // re-validar: copia inline de `_shared/validacion-contacto.ts`. Los embeds de 2825 // Webflow son sueltos y no pueden importar nada, asà que cuando se toque 2826 // aquel fichero hay que tocar también esto. Deja el número en E.164 o 2827 // devuelve null: 9 dÃgitos se asumen españoles, 00 pasa a +, y lo que no 2828 // encaje se rechaza en vez de guardarse en crudo. 2829 function reNormalizarTelefono(valor) { 2830 if (typeof valor !== "string") return null; 2831 var limpio = valor.replace(/[\s\-.()]/g, ""); 2832 if (!limpio) return null; 2833 var candidato; 2834 if (limpio.charAt(0) === "+") candidato = limpio; 2835 else if (limpio.slice(0, 2) === "00") candidato = "+" + limpio.slice(2); 2836 else if (/^\d{9}$/.test(limpio)) candidato = "+34" + limpio; 2837 else return null; 2838 return /^\+\d{6,15}$/.test(candidato) ? candidato : null; 2839 } 2840 2841 function enviar() { 2842 var nombre = (document.getElementById("re-rec-nombre").value || "").trim(); 2843 var email = (document.getElementById("re-rec-email").value || "").trim(); 2844 var telefono = (document.getElementById("re-rec-telefono").value || "").trim(); 2845
2846 estado.nombre = nombre; 2847 estado.email = email; 2848 estado.telefono = telefono; 2849 2850 if (!nombre) { 2851 medir("recomendador_datos_invalidos", { campo: "nombre" }); 2852 return mostrarError("Introduce tu nombre."); 2853 } 2854 // El email es el campo obligatorio desde que existe el chat: es lo que 2855 // permite avisar de un mensaje y lo que abre la cuenta. 2856 if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { 2857 medir("recomendador_datos_invalidos", { campo: "email" }); 2858 return mostrarError(email 2859 ? "Revisa el email, parece que falta algo." 2860 : "Introduce tu email para que puedan escribirte."); 2861 } 2862 // El teléfono es opcional, pero si lo escribe tiene que servir para llamar. 2863 // Se manda ya normalizado para que el backend guarde la misma forma. 2864 var telefonoE164 = telefono ? reNormalizarTelefono(telefono) : null; 2865 if (telefono && !telefonoE164) { 2866 medir("recomendador_datos_invalidos", { campo: "telefono" }); 2867 return mostrarError("Revisa el teléfono: 9 dÃgitos, o con prefijo internacional (+34â¦)."); 2868 } 2869 2870 estado.enviando = true; 2871 estado.error = ""; 2872 pintar(); 2873 2874 // Si la precarga de esta misma búsqueda ya trajo el reparto, se manda a 2875 // esos: son exactamente los que se le acaban de nombrar en pantalla. 2876 var yaEstan = estado.top3.length && estado.top3Clave === claveBusqueda(); 2877 (yaEstan ? Promise.resolve(estado.top3) : buscarTop3()).then(function (top3) { 2878 if (!top3.length) throw new Error("sin_entrenadores"); 2879 estado.top3 = top3; 2880 2881 return rest("/functions/v1/solicitar-contacto-entrenador", { 2882 method: "POST", 2883 body: { 2884 entrenador_slugs: top3.map(function (e) { return e.slug; }), 2885 nombre: nombre, 2886 email: email, 2887 telefono: telefonoE164, 2888 zona: estado.ciudad || null, 2889 objetivo: estado.objetivo || null, 2890 modalidad: estado.modalidad || null, 2891 cuando_empieza: estado.cuando || null, 2892 tipo_solicitud: "multiple", 2893 origen: "popup-recomendador" 2894 } 2895 }); 2896 }).then(function (datos) { 2897 escribir(CLAVE_APAGADO, "enviado", localStorage); 2898 estado.claimToken = (datos && datos.claim_token) || null; 2899 estado.enviando = false; 2900 estado.fase = "confirmacion"; 2901 pintar(); 2902 medir("recomendador_solicitud_enviada", { 2903 entrenadores: estado.top3.length, 2904 ciudad: estado.ciudad || null, 2905 objetivo: estado.objetivo || "Aun no lo tengo claro", 2906 modalidad: estado.modalidad || null, 2907 cuando: estado.cuando || null, 2908 // El email es obligatorio, asà que no se mide: serÃa siempre true. El 2909 // teléfono sÃ, que ahora es el opcional y su tasa es justo lo que dice 2910 // si el cambio ha costado el dato con el que el entrenador llama. 2911 con_telefono: !!telefonoE164, 2912 segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null 2913 }); 2914 }).catch(function (err) { 2915 console.error("[re-recomendador] envÃo", err); 2916 // "sin_entrenadores" no es una averÃa: es demanda en una ciudad sin 2917 // cobertura. Separarlo del fallo de red es la diferencia entre arreglar 2918 // el widget y salir a captar entrenadores en esa ciudad. 2919 medir("recomendador_envio_fallido", { 2920 motivo: err.message === "sin_entrenadores" ? "sin_entrenadores" : "error", 2921 ciudad: estado.ciudad || null 2922 }); 2923 estado.enviando = false; 2924 estado.error = err.message === "sin_entrenadores" 2925 ? "No hemos encontrado entrenadores para esa búsqueda. Prueba con otra ciudad." 2926 : "No hemos podido enviar tu solicitud. Inténtalo de nuevo en un momento."; 2927 pintar(); 2928 }); 2929 } 2930 2931 function guardarPerfil() { 2932 var btn = caja.querySelector('[data-accion="guardar-perfil"]'); 2933 var errEl = document.getElementById("re-rec-p2-error"); 2934 var numero = function (id) { 2935 var v = (document.getElementById(id).value || "").trim(); 2936 return v === "" ? null : Number(v); 2937 }; 2938 2939 btn.disabled = true; 2940 btn.textContent = "Guardandoâ¦"; 2941 if (errEl) errEl.style.display = "none"; 2942 2943 rest("/functions/v1/completar-perfil-usuario", { 2944 method: "POST", 2945 body: { 2946 claim_token: estado.claimToken, 2947 // El email ya viajó con la solicitud en el paso 1. Mandarlo otra vez 2948 // no aportarÃa nada: el RPC respeta el que la ficha ya tiene. 2949 email: null, 2950 objetivo: estado.objetivo || null, 2951 modalidad: estado.modalidad || null, 2952 zona: estado.ciudad || null, 2953 edad: numero("re-rec-edad"), 2954 peso_kg: numero("re-rec-peso"), 2955 altura_cm: numero("re-rec-altura"), 2956 disponibilidad: (document.getElementById("re-rec-disponibilidad").value || "").trim() || null, 2957 nivel_actividad: document.getElementById("re-rec-actividad").value || null, 2958 comentarios: (document.getElementById("re-rec-notas").value || "").trim() || null 2959 } 2960 }).then(function (datos) { 2961 // La cuenta se ofrece porque la ficha tiene email: lo dio en el paso 1 y 2962 // la solicitud se creó con él. Ya no se mira `email_guardado`, que desde 2963 // este cambio viene siempre a false âsignifica "no he escrito nada nuevo 2964 // porque la ficha ya tenÃa email", que es exactamente lo que pasa ahoraâ
2965 // y usarlo escondÃa la oferta de cuenta a todo el mundo. 2966 var ofreceCuenta = !!estado.email; 2967 medir("recomendador_perfil_guardado", {}); 2968 caja.innerHTML = cierreHTML(true, ofreceCuenta); 2969 }).catch(function (err) { 2970 console.error("[re-recomendador] paso 2", err); 2971 if (errEl) { 2972 errEl.textContent = "No hemos podido guardarlo. Tu solicitud sà se envió, no te preocupes."; 2973 errEl.style.display = "block"; 2974 } 2975 btn.disabled = false; 2976 btn.textContent = "Enviar mis datos"; 2977 }); 2978 } 2979 2980 function crearCuenta() { 2981 var btn = caja.querySelector('[data-accion="crear-cuenta"]'); 2982 var errEl = document.getElementById("re-rec-cuenta-error"); 2983 btn.disabled = true; 2984 btn.textContent = "Creandoâ¦"; 2985 if (errEl) errEl.style.display = "none"; 2986 2987 rest("/functions/v1/crear-cuenta-usuario", { 2988 method: "POST", 2989 body: { claim_token: estado.claimToken } 2990 }).then(function (datos) { 2991 medir("recomendador_cuenta_creada", { requiere_email: !!datos.requiere_email }); 2992 // Cuenta nueva: se entra en el acto. Si ya existÃa, la función manda el 2993 // enlace al buzón, que ahà es lo único que prueba que el email es suyo. 2994 if (datos.requiere_email) { 2995 btn.parentElement.innerHTML = 2996 "<h4>Ya tienes cuenta</h4>" + 2997 '<p class="re-rec-sub" style="font-size:14px;margin:4px 0 0">Te hemos mandado un enlace a tu email para entrar y ver esta solicitud junto a las anteriores.</p>'; 2998 } else { 2999 btn.textContent = "Entrandoâ¦"; 3000 window.location.href = datos.url; 3001 } 3002 }).catch(function (err) { 3003 console.error("[re-recomendador] crear cuenta", err); 3004 if (errEl) { 3005 errEl.textContent = "No hemos podido crear la cuenta. Tu solicitud sà se envió; inténtalo de nuevo en un momento."; 3006 errEl.style.display = "block"; 3007 } 3008 btn.disabled = false; 3009 btn.textContent = "Crear mi cuenta"; 3010 }); 3011 } 3012 3013 // Amplitude no envÃa nada hasta que cookieyes acepte analytics; hasta 3014 // entonces los eventos se acumulan en su cola. Nunca se cuelga una conversión de 3015 // Ads de este pop-up: el activador de GTM de las landings de paid va por el 3016 // selector `.re-modal-success`, y por eso aquà no se usa esa clase. 3017 // 3018 // No hace falta encolar nada aquÃ: el bloque de Amplitude del head del sitio 3019 // deja un stub `{ _q: [], track }` desde el primer momento y vacÃa esa cola 3020 // cuando el módulo arranca, asà que un `track` disparado antes del 3021 // consentimiento no se pierde. Si el consentimiento no llega nunca, la cola 3022 // se descarta sin enviarse, que es justo lo que debe pasar. 3023 function medir(evento, props) { 3024 if (!estado.midiendo) return; 3025 var datos = props || {}; 3026 // Segmentar por página es lo que distingue "el pop-up funciona" de "el 3027 // pop-up funciona en las páginas de ciudad y en la home no". 3028 datos.pagina = location.pathname; 3029 try { 3030 if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, datos); 3031 } catch (e) {} 3032 } 3033 3034 // Una misma pantalla se repinta varias veces âal destapar "Ver más", al 3035 // abrirse el campo de ciudad, al enseñar un errorâ y ninguno de esos 3036 // repintados es una pantalla nueva. Contando solo la primera, el embudo mide 3037 // personas y no renders. 3038 function medirPantalla() { 3039 var lista = pasos(); 3040 var clave = estado.fase === "confirmacion" ? "confirmacion" : pasoActual(); 3041 if (estado.vistas[clave]) return; 3042 estado.vistas[clave] = true; 3043 medir("recomendador_paso_visto", { 3044 paso: clave, 3045 numero: estado.fase === "confirmacion" ? lista.length + 1 : estado.indice + 1, 3046 total: lista.length 3047 }); 3048 } 3049 3050 // ------------------------------------------------------------ abrir/cerrar 3051 3052 function abrir(motivo) { 3053 if (!overlay || overlay.classList.contains("re-rec-abierto")) return; 3054 escribir(CLAVE_SESION, "1", sessionStorage); 3055 estado.indice = 0; 3056 estado.fase = "preguntas"; 3057 estado.vistas = {}; 3058 estado.abiertoEn = Date.now(); 3059 estado.midiendo = motivo !== "preview" && motivo !== "debug"; 3060 // Va antes de `pintar()` para que la apertura quede por delante del primer 3061 // paso en el orden de eventos, y no detrás. 3062 medir("recomendador_popup_abierto", { 3063 motivo: motivo || "manual", 3064 ciudad_de_pagina: estado.ciudadDePagina, 3065 objetivo_de_pagina: estado.objetivoDePagina, 3066 pasos: pasos().length 3067 }); 3068 pintar(); 3069 overlay.classList.add("re-rec-abierto"); 3070 document.body.style.overflow = "hidden"; 3071 ajustarAlViewport(); 3072 if (vv) { 3073 vv.addEventListener("resize", ajustarAlViewport); 3074 vv.addEventListener("scroll", ajustarAlViewport); 3075 } 3076 } 3077 3078 function cerrar() { 3079 if (!overlay) return; 3080 var estabaAbierto = overlay.classList.contains("re-rec-abierto"); 3081 overlay.classList.remove("re-rec-abierto"); 3082 document.body.style.overflow = ""; 3083 if (vv) { 3084 vv.removeEventListener("resize", ajustarAlViewport); 3085 vv.removeEventListener("scroll", ajustarAlViewport); 3086 } 3087 soltarViewport(); 3088 // El evento más útil de todos: dice en qué pantalla exacta se cae la gente. 3089 if (estabaAbierto) { 3090 medir("recomendador_cerrado", { 3091 paso: estado.fase === "preguntas" ? pasoActual() : estado.fase,
3092 habia_enviado: estado.fase !== "preguntas", 3093 segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null 3094 }); 3095 } 3096 // Solo se apaga para siempre si ya habÃa enviado; cerrar en la primera 3097 // pregunta no deberÃa castigarle a no volver a verlo nunca en el sitio. 3098 if (estado.fase !== "preguntas") escribir(CLAVE_APAGADO, "enviado", localStorage); 3099 } 3100 3101 // Con otro modal abierto no se dispara: en la ficha del entrenador convive 3102 // con el pop-up de contacto ("Nuevo contacto v2"), y dos modales apilados 3103 // es la forma más rápida de perder el lead que ya estaba escribiendo. 3104 function hayOtroModalAbierto() { 3105 // El banner de cookies entra en la lista: a un visitante nuevo le sale 3106 // CookieYes nada más entrar, y taparlo con este pop-up es a la vez una 3107 // mala experiencia y un problema de consentimiento. 3108 var candidatos = [ 3109 ".pop-up-contacto", 3110 "#re-overlay.re-open-modal-overlay", 3111 ".re-modal-overlay", 3112 "[data-re-modal-abierto]", 3113 ".cky-consent-container", 3114 ".cky-modal" 3115 ]; 3116 return candidatos.some(function (sel) { 3117 var el = document.querySelector(sel); 3118 if (!el) return false; 3119 // No basta con mirar `display`: los pop-ups de Webflow los abre y cierra 3120 // IX2, que muchas veces deja el elemento en el flujo y solo le anima la 3121 // opacidad. Dando por abierto todo lo que no sea `display:none`, este 3122 // pop-up no volverÃa a salir nunca en las 95 fichas de entrenador â y 3123 // sin un error en consola que lo delate. Se mide lo que de verdad se ve. 3124 var estilo = window.getComputedStyle(el); 3125 if (estilo.display === "none" || estilo.visibility === "hidden") return false; 3126 if (Number(estilo.opacity) === 0) return false; 3127 var caja = el.getBoundingClientRect(); 3128 return caja.width > 0 && caja.height > 0; 3129 }); 3130 } 3131 3132 function puedeSalir() { 3133 if (leer(CLAVE_APAGADO, localStorage)) return false; 3134 if (leer(CLAVE_SESION, sessionStorage)) return false; 3135 if (hayOtroModalAbierto()) return false; 3136 // Si está escribiendo en cualquier formulario de la página, no se le 3137 // interrumpe: quien ya está rellenando algo no necesita que le recomienden nada. 3138 var activo = document.activeElement; 3139 if (activo && /^(INPUT|TEXTAREA|SELECT)$/.test(activo.tagName)) return false; 3140 return true; 3141 } 3142 3143 // ------------------------------------------------------------------ init 3144 3145 function excluida() { 3146 var ruta = location.pathname; 3147 return RUTAS_EXCLUIDAS.some(function (r) { return ruta.indexOf(r) === 0; }); 3148 } 3149 3150 function init() { 3151 if (excluida()) return; 3152 3153 overlay = document.createElement("div"); 3154 overlay.id = "re-rec-overlay"; 3155 overlay.innerHTML = '<div class="re-rec-modal" role="dialog" aria-modal="true" aria-label="Te ayudamos a decidir"></div>'; 3156 document.body.appendChild(overlay); 3157 caja = overlay.querySelector(".re-rec-modal"); 3158 3159 estado.ciudad = ciudadDeLaPagina(); 3160 estado.ciudadDePagina = !!estado.ciudad; 3161 var obj = objetivoDeLaPagina(); 3162 if (obj) { estado.objetivo = obj; estado.objetivoDePagina = true; } 3163 3164 overlay.addEventListener("click", function (ev) { 3165 if (ev.target === overlay) return cerrar(); 3166 var btn = ev.target.closest("[data-accion]"); 3167 if (!btn) return; 3168 var accion = btn.getAttribute("data-accion"); 3169 if (accion === "cerrar") cerrar(); 3170 else if (accion === "opcion") elegirOpcion(btn.getAttribute("data-valor")); 3171 else if (accion === "ver-mas") { 3172 // Si mucha gente lo pulsa, las opciones visibles que fija `VISIBLES` no 3173 // son las que cubren a la gente que llega de verdad. 3174 medir("recomendador_ver_mas", { paso: pasoActual() }); 3175 estado.abiertas[pasoActual()] = true; 3176 pintar(); 3177 } 3178 else if (accion === "siguiente") validarYAvanzar(); 3179 else if (accion === "atras") retroceder(); 3180 else if (accion === "enviar") enviar(); 3181 else if (accion === "guardar-perfil") guardarPerfil(); 3182 else if (accion === "crear-cuenta") crearCuenta(); 3183 }); 3184 3185 document.addEventListener("keydown", function (ev) { 3186 if (ev.key === "Escape" && overlay.classList.contains("re-rec-abierto")) cerrar
3186(); 3187 }); 3188 3189 // Disparo por tiempo o por scroll, lo que llegue antes. 3190 var disparado = false; 3191 var entrada = Date.now(); 3192 3193 function disparar(motivo) { 3194 if (disparado) return false; 3195 if (!puedeSalir()) return false; 3196 disparado = true; 3197 abrir(motivo); 3198 return true; 3199 } 3200 3201 // 30/08/2026: sale nada más entrar, no a los 25 segundos. Lo único que se 3202 // espera es a que CookieYes pinte y despache su banner â`puedeSalir` ya lo 3203 // cuenta como modal abierto, pero tarda unas décimas en aparecer y sin esa 3204 // espera mÃnima el pop-up se le adelantaba y se quedaba debajoâ, asà que la 3205 // espera baja de 25 segundos a segundo y medio. 3206 // 3207 // El disparo se reintenta en vez de perderse: en el primer intento puede 3208 // haber un motivo temporal para no salir (el banner de cookies sin 3209 // responder, otro modal abierto, el usuario escribiendo en un campo) y con 3210 // un `setTimeout` suelto esa única oportunidad se gastaba ahÃ. El sondeo 3211 // baja a 400 ms âantes 5 sâ para que en cuanto se aceptan las cookies el 3212 // pop-up salga acto seguido y no hasta cinco segundos después. 3213 // 3214 // `SEGUNDOS_ESPERA` del bloque 2 se queda sin usar a propósito: reescribir 3215 // aquel embed de 34 KB solo para bajar una constante no compensa. 3216 var ESPERA_MS = 1500; 3217 var reintento = setInterval(function () { 3218 var transcurrido = Date.now() - entrada; 3219 if (disparado || transcurrido > 180000) return clearInterval(reintento); 3220 if (transcurrido >= ESPERA_MS) disparar("tiempo"); 3221 }, 400); 3222 3223 window.addEventListener("scroll", function () { 3224 var alto = document.documentElement.scrollHeight - window.innerHeight; 3225 if (alto <= 0) return; 3226 if (window.scrollY / alto >= SCROLL_DISPARO) disparar("scroll"); 3227 }, { passive: true }); 3228 3229 // Gancho manual: para un botón "¿No lo tienes claro?" en cualquier sitio 3230 // de la página, y para poder probarlo sin esperar al disparo automático. 3231 window.reRecomendador = { 3232 abrir: function () { abrir("manual"); }, 3233 cerrar: cerrar, 3234 reset: function () { 3235 try { localStorage.removeItem(CLAVE_APAGADO); sessionStorage.removeItem(CLAVE_SESION); } catch (e) {} 3236 }, 3237 // Pinta la confirmación con el reparto real de una búsqueda, pero SIN 3238 // enviar nada: sirve para revisar el copy y las fichas sin avisar a tres 3239 // entrenadores de verdad cada vez que se quiere mirar esa pantalla. 3240 // `tokenFalso` sirve para ver el bloque de perfil y el de crear cuenta, 3241 // que solo se pintan cuando hay `claim_token`. Guardar desde ahà falla 3242 // (el token no existe), que es justo lo que debe pasar. 3243 previsualizarConfirmacion: function (ciudad, objetivo, modalidad, tokenFalso) { 3244 estado.ciudad = ciudad || estado.ciudad; 3245 estado.objetivo = objetivo || ""; 3246 estado.modalidad = modalidad || "En un gimnasio"; 3247 return buscarTop3().then(function (top3) { 3248 // abrir() reinicia el estado al primer paso, asà que va antes de 3249 // dejar la fase en "confirmacion" y no después. 3250 abrir("preview"); 3251 estado.top3 = top3; 3252 estado.claimToken = tokenFalso ? "token-de-prueba" : null; 3253 estado.fase = "confirmacion"; 3254 pintar(); 3255 return top3.map(function (e) { return e.slug; }); 3256 }); 3257 } 3258 }; 3259 3260 if (new URLSearchParams(location.search).get("recomendador") === "1") abrir("debug"); 3261 } 3262 3263 // Si en consola hay VERSION pero no LISTO, es que falta el segundo embed. 3264 window.RE_RECOMENDADOR_LISTO = true; 3265 3266 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); 3267 else init(); 3268})(); 3269</script>
3269 3270</div><div class="w-embed w-script"><!-- RedEntrenadores - Origen de campana (embed en el componente Footer V1) --> 3271<!-- Va en el Footer V1 porque tiene que correr en las 115 paginas: el custom 3272 code de sitio haria lo mismo, pero ese campo son 20 KB y reescribirlo 3273 entero por API es justo el tamano que se corta a medias. 3274 3275 El problema que resuelve: cada formulario leia `document.referrer` en el 3276 momento de enviarse, y para entonces ya era interno. Quien llegaba por un 3277 enlace de campana, aterrizaba en una landing y contactaba desde la ficha 3278 de un entrenador nacia en `usuarios` sin origen ninguno. Y las utm de la 3279 URL de llegada se perdian en el primer salto. 3280 3281 Hace tres cosas: 3282 1. Guarda la primera llegada de la sesion y la sirve en `window.rtOrigen()`. 3283 2. Envuelve `fetch` para que toda solicitud a `solicitar-contacto-entrenador` 3284 lleve el origen. Asi el formulario de la ficha (22 KB) y el de la landing 3285 de paid quedan cubiertos sin tocarles una linea. 3286 3. Arrastra las utm a los enlaces que apuntan a /encuentra-entrenador, que 3287 es el unico sitio del que el wizard las lee. 3288 3289 Por que (3) en vez de envolver tambien la RPC del wizard: supabase-js 3290 resuelve `fetch` al construir el cliente, y ese cliente nace al parsearse 3291 su embed, antes que este. La envoltura no le llegaria. 3292 3293 Fuente versionada: widgets/widget-origen-campana.html -->
3294<script> 3295(function () { 3296 var CLAVE = 're-origen'; 3297 var ENDPOINT_CONTACTO = '/functions/v1/solicitar-contacto-entrenador'; 3298 var DESTINO_WIZARD = '/encuentra-entrenador'; 3299 3300 // Copia en memoria: en navegacion privada o con el almacenamiento capado, 3301 // sessionStorage lanza al escribir. Sin esto el origen se perderia en el 3302 // primer salto interno justo en los navegadores mas restrictivos. 3303 var enMemoria = null; 3304 3305 function leer() { 3306 try { 3307 var crudo = sessionStorage.getItem(CLAVE); 3308 if (crudo) return JSON.parse(crudo); 3309 } catch (e) { /* almacenamiento capado: tiramos de la copia en memoria */ } 3310 return enMemoria; 3311 } 3312 3313 function guardar(o) { 3314 enMemoria = o; 3315 try { sessionStorage.setItem(CLAVE, JSON.stringify(o)); } catch (e) {} 3316 } 3317 3318 function referenteExterno() { 3319 try { 3320 if (!document.referrer) return null; 3321 var r = new URL(document.referrer); 3322 if (r.hostname === location.hostname) return null; 3323 // Sin query: el referente de un buscador lleva la consulta del usuario. 3324 return r.origin + r.pathname; 3325 } catch (e) { return null; } 3326 } 3327 3328 var params = new URLSearchParams(location.search); 3329 var llegada = { 3330 pagina: location.pathname, 3331 referrer: referenteExterno(), 3332 utm_source: params.get('utm_source'), 3333 utm_medium: params.get('utm_medium'), 3334 utm_campaign: params.get('utm_campaign') 3335 }; 3336 3337 // Manda el primer toque de la sesion, con una excepcion: una llegada nueva 3338 // CON utm_source la pisa. Asi quien vuelve desde un enlace de campana se 3339 // atribuye a esa campana y no al referente con el que abrio el navegador 3340 // hace media hora. 3341 if (!leer() || llegada.utm_source) guardar(llegada); 3342 3343 // Devuelve siempre las cinco claves: quien lo consume puede volcarlo en el 3344 // payload sin comprobar nada. 3345 window.rtOrigen = function () { 3346 var o = leer() || {}; 3347 return { 3348 pagina: o.pagina || location.pathname, 3349 referrer: o.referrer || null, 3350 utm_source: o.utm_source || null, 3351 utm_medium: o.utm_medium || null, 3352 utm_campaign: o.utm_campaign || null 3353 }; 3354 }; 3355 3356 // 2. El origen viaja con cada solicitud de contacto 3357 // 3358 // Envolver `fetch` en vez de editar los formularios: son embeds de 22 y 40 3359 // KB y reescribirlos por API no cabe. La envoltura es defensiva de arriba 3360 // abajo: cualquier fallo cae al fetch original con los argumentos tal cual, 3361 // porque aqui pasa el envio del formulario de contacto y romperlo significa 3362 // perder leads. 3363 var fetchOriginal = window.fetch; 3364 3365 if (typeof fetchOriginal === 'function') { 3366 window.fetch = function (entrada, opciones) { 3367 try { 3368 var url = typeof entrada === 'string' ? entrada : (entrada && entrada.url) || ''; 3369 if (url.indexOf(ENDPOINT_CONTACTO) !== -1 && opciones && typeof opciones.body === 'string') { 3370 var cuerpo = JSON.parse(opciones.body); 3371 if (cuerpo && typeof cuerpo === 'object' && !Array.isArray(cuerpo)) { 3372 var o = window.rtOrigen(); 3373 // Solo se rellena lo que falta: un formulario que ya mande su 3374 // origen manda el. 3375 if (!cuerpo.pagina_origen) cuerpo.pagina_origen = location.pathname; 3376 if (!cuerpo.referrer_externo && o.referrer) cuerpo.referrer_externo = o.referrer; 3377 if (!cuerpo.utm_source && o.utm_source) cuerpo.utm_source = o.utm_source; 3378 if (!cuerpo.utm_medium && o.utm_medium) cuerpo.utm_medium = o.utm_medium; 3379 if (!cuerpo.utm_campaign && o.utm_campaign) cuerpo.utm_campaign = o.utm_campaign; 3380 3381 var copia = {}; 3382 for (var k in opciones) { if (Object.prototype.hasOwnProperty.call(opciones, k)) copia[k] = opciones[k]; } 3383 copia.body = JSON.stringify(cuerpo); 3384 return fetchOriginal.call(this, entrada, copia); 3385 } 3386 } 3387 } catch (e) { /* cuerpo ilegible o lo que sea: va el original */ } 3388 return fetchOriginal.apply(this, arguments); 3389 }; 3390 } 3391 3392 // 3. Las utm llegan al wizard 3393 // 3394 // El wizard de /encuentra-entrenador lee las utm de SU url. Quien llega 3395 // desde una landing interna las habria perdido por el camino, asi que se 3396 // las pegamos a los enlaces que apuntan alli. Solo a esos: llenar de utm 3397 // todos los enlaces del sitio ensuciaria las URLs indexables y contaria 3398 // cada pagina interna como una visita de campana. 3399 function propagarAlWizard() { 3400 var o = window.rtOrigen(); 3401 if (!o.utm_source && !o.utm_medium && !o.utm_campaign) return; 3402 3403 var enlaces = document.querySelectorAll('a[href*="' + DESTINO_WIZARD + '"]'); 3404 Array.prototype.forEach.call(enlaces, function (a) {
3405 try { 3406 if (a.getAttribute('data-re-origen')) return; 3407 var u = new URL(a.href, location.origin); 3408 if (u.hostname !== location.hostname) return; 3409 if (u.pathname.indexOf(DESTINO_WIZARD) !== 0) return; 3410 if (o.utm_source && !u.searchParams.get('utm_source')) u.searchParams.set('utm_source', o.utm_source); 3411 if (o.utm_medium && !u.searchParams.get('utm_medium')) u.searchParams.set('utm_medium', o.utm_medium); 3412 if (o.utm_campaign && !u.searchParams.get('utm_campaign')) u.searchParams.set('utm_campaign', o.utm_campaign); 3413 a.setAttribute('href', u.pathname + u.search + u.hash); 3414 a.setAttribute('data-re-origen', '1'); 3415 } catch (e) {} 3416 }); 3417 } 3418 3419 if (document.readyState === 'loading') { 3420 document.addEventListener('DOMContentLoaded', propagarAlWizard); 3421 } else { 3422 propagarAlWizard(); 3423 } 3424 // Los listados y el pop-up recomendador pintan enlaces despues de cargar. 3425 setTimeout(propagarAlWizard, 1500); 3426})(); 3427</script>
3427</div></footer>
3427<script src="https://d3e54v103j8qbb.cloudfront.net/js/jquery-3.5.1.min.dc5e7f18c8.js?site=68c5a729c71fa3f37858fbbe" type="text/javascript" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
3427<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/js/webflow.975777c1.283252e848e5c0e3.js" type="text/javascript" integrity="sha384-Zo/MMcWLLsMN+SlQPBtgBQDPWtbRCmp8ijx9WnkKyzy4q4eYV7FVR0+rAXA6bZmH" crossorigin="anonymous"></script>
3427<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 3427-3428
3427 3428<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
3428GTM-KBD83QKS
vendor: 83 bytes, lines 3428-3429
3428" 3429height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
3430<!-- End Google Tag Manager (noscript) --> 3431 3432<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3433 PREGUNTA AL EXPERTO â filtro por categoria + contador 3434 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 3435<style> 3436 .re-cats__link.is-active { background:#089F3B !important; border-color:#089F3B !important; color:#fff !important; } 3437 .re-cats__link.is-active [class*="re-cats__icon-"] { filter: brightness(0) invert(1); } 3438 .re-cats__check { position:absolute; top:6px; right:8px; width:18px; height:18px; border-radius:50%; background:#fff; color:#089F3B; font-size:12px; font-weight:800; line-height:1; display:flex; align-items:center; justify-content:center; } 3439 .re-cats__reset { display:block; width:fit-content; margin:16px auto 0; padding:10px 20px; border:1.5px solid #1B1E22; border-radius:999px; font-size:13px; font-weight:700; color:#1B1E22; text-decoration:none; cursor:pointer; } 3440 .re-cats__reset:hover { background:#1B1E22; color:#fff; } 3441 .re-oq-title.is-filtered { font-size:20px !important; text-transform:none !important; line-height:1.25 !important; text-align:center; } 3442 .otras_preguntas-header:has(.re-oq-title.is-filtered) { display:block !important; } 3443 #re-oq-clear { display:block !important; width:100% !important; text-align:center !important; margin-top:8px !important; float:none !important; } 3444 /* Boton de continuidad al final del bloque */ 3445 .re-oq-explorar { display:block; width:fit-content; margin:32px auto 0; padding:14px 28px; border-radius:8px; background:#089F3B; color:#fff !important; font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:.02em; text-decoration:none; text-align:center; } 3446 .re-oq-explorar:hover { background:#067A2D; } 3447</style>
3448<script> 3449(function () { 3450 if (window.location.pathname.indexOf('/pregunta-al-experto') !== 0) return; 3451 3452 var CAT = new URLSearchParams(window.location.search).get('categoria'); 3453 var contadoresPintados = false; 3454 var yaColocado = false; 3455 3456 function textoVer(n) { 3457 if (!n) return 'Leer respuesta del entrenador â'; 3458 if (n === 1) return 'Leer respuesta del entrenador â'; 3459 return 'Leer las ' + n + ' respuestas â'; 3460 } 3461 3462 function quitarFiltro(e) { 3463 if (e) { e.preventDefault(); e.stopPropagation(); } 3464 CAT = null; 3465 try { history.replaceState(null, '', window.location.pathname); } catch (err) {} 3466 3467 Array.prototype.forEach.call(document.querySelectorAll('.re-cats__link'), function (chip) { 3468 chip.classList.remove('is-active'); 3469 var c = chip.querySelector('.re-cats__check'); 3470 if (c) c.parentNode.removeChild(c); 3471 var orig = chip.getAttribute('data-re-href'); 3472 if (orig) chip.setAttribute('href', orig); 3473 }); 3474 3475 Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) { 3476 var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card; 3477 wrap.style.setProperty('display', '', 'important'); 3478 }); 3479 3480 var header = document.querySelector('.re-oq-title'); 3481 if (header) { 3482 header.removeAttribute('data-re-done'); 3483 header.classList.remove('is-filtered'); 3484 header.textContent = '¿Tienes prisa? Encuentra tu respuesta al instante'; 3485 } 3486 var clear = document.getElementById('re-oq-clear'); 3487 if (clear) clear.parentNode.removeChild(clear); 3488 var reset = document.getElementById('re-cats-reset'); 3489 if (reset) reset.parentNode.removeChild(reset); 3490 var expl = document.getElementById('re-oq-explorar'); 3491 if (expl) expl.style.display = 'none'; 3492 } 3493 3494 function aplicar() { 3495 var chips = document.querySelectorAll('.re-cats__link'); 3496 var cards = document.querySelectorAll('.re-oq-card'); 3497 3498 Array.prototype.forEach.call(chips, function (chip) { 3499 if (!chip.getAttribute('data-re-href')) { 3500 chip.setAttribute('data-re-href', chip.getAttribute('href') || ''); 3501 } 3502 }); 3503 3504 if (CAT) { 3505 Array.prototype.forEach.call(chips, function (chip) { 3506 var orig = chip.getAttribute('data-re-href') || ''; 3507 if (orig.indexOf('categoria=' + CAT) !== -1) { 3508 chip.classList.add('is-active'); 3509 chip.setAttribute('title', 'Quitar filtro'); 3510 if (!chip.getAttribute('data-re-toggle')) { 3511 chip.setAttribute('data-re-toggle', '1'); 3512 chip.addEventListener('click', function (e) { 3513 if (chip.classList.contains('is-active')) quitarFiltro(e); 3514 }); 3515 } 3516 if (!chip.querySelector('.re-cats__check')) { 3517 var c = document.createElement('span'); 3518 c.className = 're-cats__check'; 3519 c.textContent = 'â'; 3520 chip.appendChild(c); 3521 } 3522 } 3523 }); 3524 3525 var listaCats = document.querySelector('.re-cats__list'); 3526 if (listaCats && !document.getElementById('re-cats-reset')) { 3527 var r = document.createElement('a'); 3528 r.id = 're-cats-reset'; 3529 r.className = 're-cats__reset'; 3530 r.href = '#'; 3531 r.textContent = 'Ver todas las categorias'; 3532 r.addEventListener('click', quitarFiltro); 3533 listaCats.insertAdjacentElement('afterend', r); 3534 } 3535 3536 if (!yaColocado) { 3537 yaColocado = true; 3538 var seccionCats = document.querySelector('.re-cats'); 3539 if (seccionCats && window.scrollY < 40) { 3540 var top = seccionCats.getBoundingClientRect().top + window.scrollY - 70; 3541 window.scrollTo(0, top > 0 ? top : 0); 3542 } 3543 } 3544 } 3545 3546 if (!cards.length) return false; 3547 3548 // Boton de continuidad al final del bloque de preguntas 3549 var seccionOQ = document.querySelector('.re-oq-section'); 3550 var listaOQ = document.querySelector('.re-oq-section .w-dyn-list'); 3551 if (seccionOQ && listaOQ && !document.getElementById('re-oq-explorar')) { 3552 var ex = document.createElement('a'); 3553 ex.id = 're-oq-explorar'; 3554 ex.className = 're-oq-explorar'; 3555 ex.href = '/pregunta-al-experto'; 3556 ex.textContent = 'Explorar toda la biblioteca de preguntas'; 3557 ex.addEventListener('click', function (e) { 3558 if (CAT) { quitarFiltro(e); } 3559 }); 3560 listaOQ.insertAdjacentElement('afterend', ex); 3561 } 3562 var explBtn = document.getElementById('re-oq-explorar'); 3563 if (explBtn) explBtn.style.display = CAT ? 'block' : 'none'; 3564 3565 if (CAT) { 3566 var filtro = CAT.trim().toLowerCase(); 3567 var visibles = 0; 3568 Array.prototype.forEach.call(cards, function (card) { 3569 var h = card.querySelector('.re-oq-hidden'); 3570 var cat = h ? (h.textContent || '').trim().toLowerCase() : ''; 3571 var ok = (cat === filtro); 3572 var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card; 3573 wrap.style.setProperty('display', ok ? '' : 'none', 'important'); 3574 if (ok) visibles++; 3575 }); 3576 3577 var header = document.querySelector('.re-oq-title'); 3578 if (header && !header.getAttribute('data-re-done')) { 3579 header.setAttribute('data-re-done', '1'); 3580 header.classList.add('is-filtered'); 3581 var nombre = filtro.replace(/-/g, ' ');
3582 nombre = nombre.charAt(0).toUpperCase() + nombre.slice(1); 3583 header.textContent = visibles > 0 3584 ? nombre + ' (' + visibles + ')' 3585 : 'Todavia no hay preguntas sobre ' + nombre.toLowerCase(); 3586 3587 if (!document.getElementById('re-oq-clear')) { 3588 var a = document.createElement('a'); 3589 a.id = 're-oq-clear'; 3590 a.href = '#'; 3591 a.textContent = 'Ver todas las preguntas â'; 3592 a.style.cssText = 'display:block;width:100%;text-align:center;margin-top:8px;font-size:13px;font-weight:700;color:#089F3B;text-decoration:none;'; 3593 a.addEventListener('click', quitarFiltro); 3594 var contenedor = header.parentElement || header; 3595 contenedor.insertAdjacentElement('afterend', a); 3596 } 3597 } 3598 } 3599 3600 Array.prototype.forEach.call(cards, function (card) { 3601 var detail = card.querySelector('.re-oq-detail'); 3602 var toggle = card.querySelector('[data-oq-toggle]'); 3603 if (!detail || !toggle || toggle.getAttribute('data-re-bound')) return; 3604 toggle.setAttribute('data-re-bound', '1'); 3605 toggle.style.display = (detail.scrollHeight > detail.clientHeight + 2) ? 'block' : 'none'; 3606 toggle.addEventListener('click', function (e) { 3607 e.preventDefault(); 3608 e.stopPropagation(); 3609 var exp = detail.classList.toggle('re-oq-detail--expanded'); 3610 toggle.textContent = exp ? 'Ver menos' : 'Ver mas'; 3611 }); 3612 }); 3613 3614 if (!contadoresPintados) { 3615 contadoresPintados = true; 3616 cargarSupabase(function (db) { 3617 // Nada de respuestas(count): PostgREST lo traduce a una referencia de 3618 // fila entera y la clave publicable tiene los permisos de `respuestas` 3619 // por columna (migracion 20260819150000), asi que devuelve 403. Se 3620 // piden los id de una columna concedida y se cuentan aqui. 3621 db.from('preguntas').select('slug, respuestas(id)').then(function (res) { 3622 if (res.error) { console.warn('[PAE] contador de respuestas:', res.error.message); return; } 3623 var mapa = {}; 3624 (res.data || []).forEach(function (r) { 3625 mapa[r.slug] = (r.respuestas || []).length; 3626 }); 3627 Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) { 3628 var slug = (card.getAttribute('href') || '').split('/').filter(Boolean).pop(); 3629 var v = card.querySelector('[data-oq-ver]'); 3630 if (!v) return; 3631 v.textContent = textoVer(mapa[slug]); 3632 }); 3633 }); 3634 }); 3635 } 3636 3637 return true; 3638 } 3639 3640 function cargarSupabase(cb) { 3641 function go() { 3642 try { 3643 cb(window.supabase.createClient( 3644 'https://yrzanatzshdpvypazgeb.supabase.co', 3645 'sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD' 3646 )); 3647 } catch (e) {} 3648 } 3649 if (window.supabase && window.supabase.createClient) { go(); return; } 3650 var s = document.createElement('script'); 3651 s.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2'; 3652 s.onload = go; 3653 document.head.appendChild(s); 3654 } 3655 3656 function arrancar() { 3657 aplicar(); 3658 var intentos = 0; 3659 var t = setInterval(function () { 3660 intentos++; 3661 aplicar(); 3662 if (intentos > 20) clearInterval(t); 3663 }, 300); 3664 3665 var lista = document.querySelector('.w-dyn-list'); 3666 if (lista && window.MutationObserver) { 3667 new MutationObserver(function () { aplicar(); }).observe(lista, { childList: true, subtree: true }); 3668 } 3669 } 3670 3671 if (document.readyState === 'loading') { 3672 document.addEventListener('DOMContentLoaded', arrancar); 3673 } else { 3674 arrancar(); 3675 } 3676})(); 3677</script>
3677 3678 3679<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3680 BOTÃN FLOTANTE DE WHATSAPP 3681 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 3682 3683<a 3684 href="https://wa.me/34633590498?text=Hola%2C%20me%20gustar%C3%ADa%20m%C3%A1s%20informaci%C3%B3n" 3685 target="_blank" 3686 rel="noopener noreferrer" 3687 class="wa-float-btn" 3688 aria-label="Contactar por WhatsApp" 3689> 3690 <svg viewBox="0 0 24 24" width="30" height="30" fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg"> 3691 <path d="M12 2a10 10 0 0 0-8.61 15.06L2 22l5.06-1.33A10 10 0 1 0 12 2zm0 18.13a8.1 8.1 0 0 1-4.14-1.14l-.3-.18-3.08.81.82-3-.2-.31A8.13 8.13 0 1 1 12 20.13z"/> 3692 <path d="M16.57 13.72c-.25-.13-1.47-.72-1.7-.81s-.4-.13-.57.13-.65.81-.8.97-.3.19-.55.06a6.76 6.76 0 0 1-2-1.23 7.47 7.47 0 0 1-1.38-1.72c-.14-.25 0-.38.11-.5s.25-.3.37-.44a1.7 1.7 0 0 0 .25-.42.46.46 0 0 0 0-.44c-.06-.13-.57-1.37-.78-1.87s-.42-.43-.57-.44h-.49a.94.94 0 0 0-.68.32 2.87 2.87 0 0 0-.9 2.13 5 5 0 0 0 1.05 2.64 11.4 11.4 0 0 0 4.36 3.85c.61.26 1.08.42 1.45.54a3.48 3.48 0 0 0 1.6.1 2.62 2.62 0 0 0 1.72-1.21 2.13 2.13 0 0 0 .15-1.21c-.06-.11-.23-.17-.48-.3z"/> 3693 </svg> 3694</a> 3695 3696<style> 3697 .wa-float-btn { 3698 position: fixed; 3699 bottom: 24px; 3700 right: 24px; 3701 width: 60px; 3702 height: 60px; 3703 background-color: #25D366; 3704 border-radius: 50%; 3705 display: flex; 3706 align-items: center; 3707 justify-content: center; 3708 box-shadow: 0 4px 14px rgba(0,0,0,0.25); 3709 z-index: 9999; 3710 text-decoration: none; 3711 transition: transform 0.2s ease, box-shadow 0.2s ease; 3712 } 3713 .wa-float-btn:hover { 3714 transform: scale(1.08); 3715 box-shadow: 0 6px 18px rgba(0,0,0,0.3); 3716 } 3717 @media (max-width: 480px) { 3718 .wa-float-btn { 3719 width: 52px; 3720 height: 52px; 3721 bottom: 16px; 3722 right: 16px; 3723 } 3724 } 3725</style> 3726 3727<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3728 OCULTAR SECCIONES CON LISTAS CMS VACÃAS (ciudad + objetivo) 3729 Solo listas de primer nivel: una lista anidada vacÃa dentro 3730 de una tarjeta (p. ej. las especialidades de un entrenador) 3731 no debe esconder la sección entera. 3732 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
3733<script> 3734document.querySelectorAll('.w-dyn-empty').forEach(function (emptyEl) { 3735 var lista = emptyEl.closest('.w-dyn-list'); 3736 if (!lista) return; 3737 3738 // Lista anidada dentro de otra lista CMS (una tarjeta): no decide por la sección. 3739 if (lista.parentElement && lista.parentElement.closest('.w-dyn-list')) return; 3740 3741 // Webflow deja el estado vacio en el HTML aunque la lista tenga elementos: 3742 // lo tapa con w-dyn-hide. Sin esta comprobacion se escondia un bloque lleno. 3743 if (lista.querySelector('.w-dyn-item')) return; 3744 3745 // En la ficha del entrenador cada bloque es un .entrenador_sobre-m dentro de 3746 // una unica <section> que ademas lleva el formulario de contacto, asi que la 3747 // regla de abajo no llegaba y se quedaba a la vista el "No items found." de 3748 // Modalidad (Ismael Diaz, 20/08/2026). Aqui manda el bloque, no la seccion. 3749 var bloque = emptyEl.closest('.entrenador_sobre-m'); 3750 if (bloque) { 3751 bloque.style.display = 'none'; 3752 return; 3753 } 3754 3755 var section = emptyEl.closest('section'); 3756 if (section && !section.querySelector('form')) { 3757 section.style.display = 'none'; 3758 } 3759}); 3760</script>
3760 3761 3762<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3763 EMAIL â aviso de errata en el dominio (sugiere, no bloquea) 3764 Vale para TODOS los formularios del sitio, presentes y futuros: 3765 engancha por delegacion a cualquier <input> de email, no hay 3766 que tocar los widgets uno a uno. 3767 La regla vive en supabase/functions/_shared/validacion-contacto.ts 3768 (`sugerenciaDominioEmail`); esto es su copia de navegador. 3769 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 3770<style> 3771 .re-mailerrata { margin-top:6px; padding:8px 10px; border-radius:8px; background:#FEF3C7; color:#92400E; font-size:12.5px; line-height:1.35; } 3772 .re-mailerrata__fix { padding:0; border:0; background:none; font:inherit; font-weight:700; color:#92400E; text-decoration:underline; cursor:pointer; } 3773</style>
3774<script> 3775(function () { 3776 /* Dominios que damos por buenos. Un dominio de esta lista no se marca nunca, 3777 y un dominio a 1-2 letras de uno de ellos es el que se sugiere. */ 3778 var DOMINIOS = [ 3779 'gmail.com','googlemail.com', 3780 'hotmail.com','hotmail.es','hotmail.fr','hotmail.it','hotmail.co.uk','hotmail.com.ar','hotmail.com.mx', 3781 'outlook.com','outlook.es','outlook.fr','outlook.it', 3782 'live.com','live.com.mx','live.co.uk','msn.com', 3783 'yahoo.com','yahoo.es','yahoo.it','yahoo.fr','yahoo.co.uk','yahoo.com.ar','yahoo.com.mx', 3784 'icloud.com','me.com','mac.com', 3785 'aol.com','protonmail.com','proton.me','gmx.es','gmx.com','gmx.net','zoho.com','mail.com', 3786 'terra.es','telefonica.net','movistar.es','orange.es','wanadoo.es','ya.com','arrakis.es','euskalnet.net' 3787 ]; 3788 3789 function distancia(a, b) { 3790 var m = a.length, n = b.length, i, j, fila = [], ant, tmp; 3791 for (j = 0; j <= n; j++) fila[j] = j; 3792 for (i = 1; i <= m; i++) { 3793 ant = fila[0]; fila[0] = i; 3794 for (j = 1; j <= n; j++) { 3795 tmp = fila[j]; 3796 fila[j] = Math.min(fila[j] + 1, fila[j - 1] + 1, ant + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1)); 3797 ant = tmp; 3798 } 3799 } 3800 return fila[n]; 3801 } 3802 3803 /* Devuelve el email corregido o null. Solo mira el dominio, y solo cuando el 3804 email ya tiene forma valida: el "falta algo" lo dice el widget, no esto. 3805 El tope de 2 letras se reserva a dominios largos; en uno corto (ya.com, 3806 me.com) dos cambios ya son otro dominio distinto, no una errata. */ 3807 function sugerencia(valor) { 3808 var limpio = String(valor == null ? '' : valor).trim().toLowerCase(); 3809 if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(limpio)) return null; 3810 var corte = limpio.lastIndexOf('@'); 3811 var local = limpio.slice(0, corte), dominio = limpio.slice(corte + 1); 3812 if (DOMINIOS.indexOf(dominio) !== -1) return null; 3813 var tope = dominio.length >= 8 ? 2 : 1; 3814 var mejor = null, mejorD = 3, i, d; 3815 for (i = 0; i < DOMINIOS.length; i++) { 3816 d = distancia(dominio, DOMINIOS[i]); 3817 if (d <= tope && d < mejorD) { mejorD = d; mejor = DOMINIOS[i]; } 3818 } 3819 return mejor ? local + '@' + mejor : null; 3820 } 3821 3822 function esCampoEmail(el) { 3823 if (!el || el.tagName !== 'INPUT') return false; 3824 var t = (el.getAttribute('type') || '').toLowerCase(); 3825 if (t === 'email') return true; 3826 if (t && t !== 'text') return false; 3827 if ((el.getAttribute('inputmode') || '').toLowerCase() === 'email') return true; 3828 return /e-?mail|correo/i.test((el.name || '') + ' ' + (el.id || '') + ' ' + (el.getAttribute('autocomplete') || '')); 3829 } 3830 3831 function quitar(input) { 3832 var sig = input.nextElementSibling; 3833 if (sig && sig.classList && sig.classList.contains('re-mailerrata')) sig.parentNode.removeChild(sig); 3834 } 3835 3836 function medir(evento, dominio) { 3837 try { if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, { dominio: dominio }); } catch (e) {} 3838 } 3839 3840 function revisar(input) { 3841 quitar(input); 3842 if (!input.isConnected) return; 3843 var propuesta = sugerencia(input.value); 3844 if (!propuesta) return; 3845 3846 var caja = document.createElement('div'); 3847 caja.className = 're-mailerrata'; 3848 caja.setAttribute('role', 'status'); 3849 caja.appendChild(document.createTextNode('¿Quisiste decir ')); 3850 3851 var btn = document.createElement('button'); 3852 btn.type = 'button'; 3853 btn.className = 're-mailerrata__fix'; 3854 btn.textContent = propuesta; 3855 btn.addEventListener('click', function () { 3856 input.value = propuesta;
3857 try { 3858 input.dispatchEvent(new Event('input', { bubbles: true })); 3859 input.dispatchEvent(new Event('change', { bubbles: true })); 3860 } catch (e) {} 3861 quitar(input); 3862 input.focus(); 3863 medir('email_errata_corregida', propuesta.split('@')[1]); 3864 }); 3865 3866 caja.appendChild(btn); 3867 caja.appendChild(document.createTextNode('?')); 3868 input.insertAdjacentElement('afterend', caja); 3869 medir('email_errata_sugerida', input.value.split('@').pop().toLowerCase()); 3870 } 3871 3872 /* Al escribir se propone con retardo (asi el aviso ya esta en pantalla antes 3873 de que llegue al boton de enviar) y al salir del campo, de inmediato. */ 3874 var reloj = null; 3875 document.addEventListener('input', function (e) { 3876 if (!esCampoEmail(e.target)) return; 3877 quitar(e.target); 3878 clearTimeout(reloj); 3879 var input = e.target; 3880 reloj = setTimeout(function () { revisar(input); }, 500); 3881 }); 3882 document.addEventListener('focusout', function (e) { 3883 if (!esCampoEmail(e.target)) return; 3884 clearTimeout(reloj); 3885 revisar(e.target); 3886 }); 3887})(); 3888</script>
3888 3889 3890<!-- Master Tag add just before the closing </body> tag --> 3891
3891<script src="https://www.dwin1.com/130189.js" type="text/javascript" defer="defer"></script>
vendor: 1 bytes, line 3891
3891
3892<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a815b2ecfc98b6d18a9afab%2Fvisitas_ficha_publica-1.0.0.js" type="text/javascript"></script>
3892<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a9562a243b6dd3757592da5%2Flimpiarcamposcontacto-1.0.0.js" type="text/javascript"></script>
3892</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.