1/** 2 * CHS Pollón de Oro â Widget JS v4.1 3 * 4 * Datos en chspwData.jornadas = [{f, g, c:[{g,h,h2,n,p}]}] 5 * f = fecha (YYYY-MM-DD) 6 * g = grupo ganador de la jornada (G1/G2/G3) 7 * c = array de carreras individuales [{g: grupo, h: caballo_1, h2: caballo_2 (empate), n: NUMERO_CARRERA, p: nombre_premio}] 8 * 9 * Grilla = celdas por carrera (no por etapa). 10 * Sin llamada AJAX adicional por jornada â todos los datos vienen en chspwData. 11 * 12 * v4.1 â Eliminado el auto-refresco por intervalo (generaba carga sostenida sobre 13 * admin-ajax.php con cada visitante). Los datos se pintan desde chspwData al 14 * cargar; el refresco solo ocurre si el usuario pulsa "Actualizar". 15 */ 16(function ($) { 17 'use strict'; 18 19 /* ââ State âââââââââââââââââââââââââââââââââââââââââââââââ */ 20 var state = { 21 open: false, 22 loading: false, 23 initialized: false, 24 jornadas: [], // [{f, g, c:[{g,h,h2,n,p}]}] â solo jornadas con resultados reales 25 jornadaIdx: -1, 26 lastFetch: null, 27 }; 28 29 /* ââ DOM refs âââââââââââââââââââââââââââââââââââââââââââââ */ 30 var $widget, $panel, $toggle, $grid, $subtitle, 31 $badge, $refreshBtn, $lastUpdate, $closeBtn, $progressFill, 32 $podium, $selPrev, $selNext, $selValue, $selLatest, 33 $tip; 34 35 /* ââ Grupos âââââââââââââââââââââââââââââââââââââââââââââââ */ 36 var GRUPOS = { 37 G1: { label: 'Grupo 1', cls: 'chspw-g1' }, 38 G2: { label: 'Grupo 2', cls: 'chspw-g2' }, 39 G3: { label: 'Grupo 3', cls: 'chspw-g3' }, 40 }; 41 42 /* ââ Boot âââââââââââââââââââââââââââââââââââââââââââââââââ */ 43 $(function () { 44 $widget = $('#chspw-widget'); 45 if (!$widget.length) return; 46 47 $panel = $('#chspw-panel'); 48 $toggle = $('#chspw-toggle'); 49 $grid = $('#chspw-grid'); 50 $subtitle = $('#chspw-subtitle'); 51 $badge = $('#chspw-badge'); 52 $refreshBtn = $('#chspw-refresh'); 53 $lastUpdate = $('#chspw-last-update'); 54 $closeBtn = $('#chspw-close'); 55 $progressFill = $('#chspw-progress-fill'); 56 $podium = $('#chspw-podium'); 57 $selPrev = $('#chspw-sel-prev'); 58 $selNext = $('#chspw-sel-next'); 59 $selValue = $('#chspw-sel-value'); 60 $selLatest = $('#chspw-sel-latest'); 61 62 if (chspwData.sinEndpoint === '1') { 63 setGridMsg('Configurar endpoint en: Pollón de Oro â Configuración', 'chspw-no-endpoint'); 64 setSubtitle('Sin endpoint configurado', 'idle'); 65 return; 66 } 67 68 /* Leer jornadas pre-cargadas por PHP en chspwData */ 69 state.jornadas = Array.isArray(chspwData.jornadas) ? chspwData.jornadas : []; 70 71 bindEvents(); 72 73 if (state.jornadas.length > 0) { 74 state.jornadaIdx = lastPlayedIdx(); 75 state.initialized = true; 76 updatePodium(); 77 updateProgress(); 78 updateBadge(); 79 updateSelector(); 80 updateSubtitle(); 81 renderJornadaGrid(state.jornadaIdx); 82 } else { 83 fetchData(); 84 } 85 /* Auto-refresco eliminado en v4.1: sin polling por intervalo. */ 86 }); 87 88 /* ââ Events âââââââââââââââââââââââââââââââââââââââââââââââ */ 89 function bindEvents() { 90 $toggle.on('click', togglePanel); 91 $closeBtn.on('click', closePanel); 92 $refreshBtn.on('click', function () { fetchData(); }); 93 $selPrev.on('click', function () { navigateJornada(-1); }); 94 $selNext.on('click', function () { navigateJornada(+1); }); 95 96 $(document).on('click.chspw', function (e) { 97 if (state.open && !$(e.target).closest('#chspw-widget').length) { 98 closePanel(); 99 } 100 }); 101 102 $(document).on('keydown.chspw', function (e) { 103 if (e.key === 'Escape' && state.open) closePanel(); 104 }); 105 106 /* ââ Tooltip: position:fixed appended to <body> para escapar overflow:hidden del panel ââ */ 107 $grid.on('mouseenter', '.chspw-cell.chspw-played', function (e) { 108 var $c = $(this); 109 var nro = $c.data('carrera') || ''; 110 var etapaN = $c.data('etapa') || 0; 111 var grupo = String($c.data('grupo') || '').toUpperCase().replace(/\s+/g, ''); 112 var caballo = String($c.data('caballo') || '').trim(); 113 var caballo2 = String($c.data('caballo2') || '').trim(); 114 var premio = String($c.data('premio') || '').trim();
115 var fecha = String($c.data('fecha') || '').trim(); 116 var label = grupoLabel(grupo); 117 var gc = grupoClass(grupo); 118 var isEmpate = grupo.indexOf('-') !== -1; 119 120 /* Barra de color superior */ 121 var accent = '<div class="chspw-tip-accent' + (gc ? ' ' + gc : '') + '"></div>'; 122 123 /* Fila: número de carrera + fecha */ 124 var nroLabel = (etapaN > 0 ? 'E' + etapaN + ' · ' : '') + nro + 'ª carrera'; 125 var topRow = '<div class="chspw-tip-row-top">' 126 + '<span class="chspw-tip-nro">' + escHtml(nroLabel) + '</span>' 127 + (fecha ? '<span class="chspw-tip-fecha">' + escHtml(tipFechaDisplay(fecha)) + '</span>' : '') 128 + '</div>'; 129 130 /* Grupo ganador (con badge de empate si aplica) */ 131 var grupoLine = label 132 ? '<span class="chspw-tip-grupo' + (gc ? ' ' + gc : '') + '">' 133 + escHtml(label) 134 + (isEmpate ? '<span class="chspw-tip-empate">Empate</span>' : '') 135 + '</span>' 136 : ''; 137 138 /* Caballos ganadores (uno o dos en caso de empate, separados por /) */ 139 var horsesHtml = ''; 140 if (caballo) { 141 var horseText = escHtml(caballo); 142 if (caballo2 && caballo2 !== caballo) { 143 horseText += ' <span class="chspw-tip-sep">/</span> ' + escHtml(caballo2); 144 } 145 horsesHtml = '<span class="chspw-tip-horse">' + horseText + '</span>'; 146 } 147 148 /* Cuerpo */ 149 var body = '<div class="chspw-tip-body">' 150 + topRow 151 + grupoLine 152 + horsesHtml 153 + (premio ? '<span class="chspw-tip-prize"><span class="chspw-tip-prize-lbl">Premio:</span> ' + escHtml(premio) + '</span>' : '') 154 + '</div>'; 155 156 if (!$tip) { $tip = $('<div class="chspw-tip" aria-hidden="true"></div>').appendTo('body'); } 157 $tip.html(accent + body); 158 tipPosition(e); 159 $tip.addClass('chspw-tip--visible'); 160 }).on('mousemove', '.chspw-cell.chspw-played', function (e) { 161 if ($tip) tipPosition(e); 162 }).on('mouseleave', '.chspw-cell.chspw-played', function () { 163 if ($tip) $tip.removeClass('chspw-tip--visible'); 164 }); 165 } 166 167 /* ââ Toggle âââââââââââââââââââââââââââââââââââââââââââââââ */ 168 function togglePanel() { state.open ? closePanel() : openPanel(); } 169 170 function openPanel() { 171 state.open = true; 172 $panel.addClass('chspw-open'); 173 $toggle.attr('aria-expanded', 'true'); 174 $badge.hide(); 175 } 176 177 function closePanel() { 178 state.open = false; 179 $panel.removeClass('chspw-open'); 180 $toggle.attr('aria-expanded', 'false'); 181 updateBadge(); 182 } 183 184 /* ââ Navegación âââââââââââââââââââââââââââââââââââââââââââ */ 185 function navigateJornada(dir) { 186 var newIdx = state.jornadaIdx + dir; 187 if (newIdx < 0 || newIdx >= state.jornadas.length) return; 188 state.jornadaIdx = newIdx; 189 updateSelector(); 190 updateSubtitle(); 191 renderJornadaGrid(state.jornadaIdx); 192 } 193 194 /* ââ Render grilla de la jornada seleccionada âââââââââââââ */ 195 function renderJornadaGrid(idx) { 196 var cols = parseInt($widget.data('columnas') || chspwData.columnas || 5, 10); 197 $grid.css('--pw-cols', cols); 198 199 if (!state.jornadas.length) { 200 $grid.html('<div class="chspw-grid-message">Sin datos de temporada</div>'); 201 return; 202 } 203 204 var j = (idx >= 0 && idx < state.jornadas.length) ? state.jornadas[idx] : null; 205 if (!j) { 206 $grid.html('<div class="chspw-grid-message">Sin datos</div>'); 207 return; 208 } 209 210 var carreras = j.c || []; 211 var html = ''; 212 213 if (carreras.length > 0) { 214 /* Jornada jugada: una celda por carrera, coloreada por grupo */ 215 for (var ci = 0; ci < carreras.length; ci++) { 216 var cr = carreras[ci]; 217 var nro = (cr.n && cr.n > 0) ? cr.n : (ci + 1); // NUMERO_CARRERA (número fÃsico del programa) 218 var etapa = cr.e || 0; 219 var gc = grupoClass(cr.g || ''); 220 var lbl = cr.g ? escHtml(cr.g) : ''; 221 var cellLbl = (etapa > 0 ? 'E' + etapa + ' · ' : '') + nro + 'ª'; 222 var h1 = String(cr.h || '').trim(); 223 var h2 = String(cr.h2 || '').trim(); 224 var horseHtml = ''; 225 html += 226 '<div class="chspw-cell chspw-played' + (gc ? ' ' + gc : '') + '"' 227 + ' data-carrera="' + nro + '"' 228 + ' data-etapa="' + etapa + '"' 229 + ' data-grupo="' + escAttr(cr.g || '') + '"' 230 + ' data-caballo="' + escAttr(h1) + '"' 231 + ' data-caballo2="' + escAttr(h2) + '"' 232 + ' data-premio="' + escAttr(cr.p || '') + '"' 233 + ' data-fecha="' + escAttr(j.f || '') + '"' 234 + ' role="gridcell">' 235 + '<span class="chspw-cell-label">' + cellLbl + '</span>' 236 + '<span class="chspw-cell-sub">' + lbl + '</span>' 237 + horseHtml 238 + '</div>'; 239 } 240 } else { 241 /* Jornada pendiente: celdas grises con conteo estimado */ 242 var pendingCount = getLastKnownCarreraCount(); 243 for (var i = 1; i <= pendingCount; i++) { 244 html += 245 '<div class="chspw-cell chspw-pending"' 246 + ' data-carrera="' + i + '"' 247 + ' role="gridcell"' 248 + ' title="Carrera ' + i + ' â pendiente">' 249 + '<span class="chspw-cell-label">C' + i + '</span>' 250 + '</div>'; 251 } 252 } 253 254 $grid.html(html || '<div class="chspw-grid-message">Sin datos</div>'); 255 } 256 257 /* Devuelve el número de carreras de la última jornada jugada */ 258 function getLastKnownCarreraCount() { 259 for (var k = state.jornadas.length - 2; k >= 0; k--) { 260 if (state.jornadas[k].c && state.jornadas[k].c.length > 0) { 261 return state.jornadas[k].c.length; 262 } 263 } 264 return 14; 265 } 266 267 /* ââ Fetch: recargar desde endpoint y actualizar ââââââââââ */ 268 function fetchData() { 269 if (state.loading) return; 270 state.loading = true; 271 setSubtitle('Actualizandoâ¦', 'loading'); 272 $refreshBtn.addClass('chspw-loading'); 273 274 $.ajax({ 275 url: chspwData.ajaxUrl, 276 type: 'POST', 277 dataType: 'json', 278 timeout: 14000, 279 data: { 280 action: 'chspw_get_ganadores', 281 nonce: chspwData.nonce, 282 }, 283 success: function (resp) { 284 if (resp && resp.success && Array.isArray(resp.data)) { 285 var prevLen = state.jornadas.length; 286 287 /* resp.data ya viene en formato [{f, g, c:[{g,h,n,p}]}] */ 288 state.jornadas = resp.data.map(function (j) { 289 return { 290 f: j.f || j.fecha || '', 291 g: j.g || j.grupo || '', 292 c: Array.isArray(j.c) ? j.c : (Array.isArray(j.carreras) ? j.carreras : []), 293 }; 294 }); 295 296 if (state.jornadaIdx < 0 || state.jornadaIdx >= state.jornadas.length 297 || state.jornadas.length > prevLen) { 298 state.jornadaIdx = lastPlayedIdx(); 299 } 300 301 state.initialized = true; 302 state.lastFetch = new Date(); 303 304 renderJornadaGrid(state.jornadaIdx); 305 updatePodium(); 306 updateProgress(); 307 updateBadge(); 308 updateSelector(); 309 updateSubtitle(); 310 updateLastUpdate(); 311 312 if (state.jornadas.length > prevLen && !state.open) { 313 $toggle.addClass('chspw-pulse'); 314 setTimeout(function () { $toggle.removeClass('chspw-pulse'); }, 5000); 315 } 316 } else { 317 var msg = (resp && resp.data && resp.data.message) ? resp.data.message : 'Error al cargar'; 318 setSubtitle(msg, 'error'); 319 } 320 }, 321 error: function (xhr, status) { 322 setSubtitle(status === 'timeout' ? 'Tiempo agotado' : 'Error de conexión', 'error'); 323 }, 324 complete: function () { 325 state.loading = false;
326 $refreshBtn.removeClass('chspw-loading'); 327 }, 328 }); 329 } 330 331 /* ââ Pódium â victorias por carrera en toda la temporada ââ */ 332 function updatePodium() { 333 if (!$podium || !$podium.length) return; 334 335 /* Contar cada carrera individual, no solo jornadas */ 336 var wins = { G1: 0, G2: 0, G3: 0 }; 337 for (var i = 0; i < state.jornadas.length; i++) { 338 var carreras = state.jornadas[i].c || []; 339 for (var ci = 0; ci < carreras.length; ci++) { 340 var g = String(carreras[ci].g || '').toUpperCase().replace(/\s+/g, ''); 341 if (g in wins) wins[g]++; 342 } 343 } 344 345 if (wins.G1 + wins.G2 + wins.G3 === 0) { 346 $podium.html('<div class="chspw-podium-title">Pódium â sin datos aún</div>'); 347 return; 348 } 349 350 var ranked = Object.keys(wins) 351 .map(function (g) { return { g: g, v: wins[g] }; }) 352 .sort(function (a, b) { return b.v - a.v; }); 353 354 var leader = ranked[0] ? ranked[0].g : null; 355 var podiumOrder = [ranked[1], ranked[0], ranked[2]]; 356 var positions = ['2°', '1°', '3°']; 357 var posCls = ['chspw-p2', 'chspw-p1', 'chspw-p3']; 358 359 var html = '<div class="chspw-podium-title">Pódium â victorias por grupo</div>' 360 + '<div class="chspw-podium-stage">'; 361 362 for (var p = 0; p < 3; p++) { 363 var item = podiumOrder[p]; 364 if (!item) { 365 html += '<div class="chspw-podium-col chspw-p-empty ' + posCls[p] + '"></div>'; 366 continue; 367 } 368 var gcfg = GRUPOS[item.g] || { label: item.g, cls: '' }; 369 html += 370 '<div class="chspw-podium-col ' + posCls[p] + ' ' + gcfg.cls + '">' 371 + '<div class="chspw-podium-wins">' + item.v + '</div>' 372 + '<div class="chspw-podium-bar"></div>' 373 + '<div class="chspw-podium-medal">' + positions[p] + '</div>' 374 + '<div class="chspw-podium-group">' + escHtml(gcfg.label) + '</div>' 375 + '</div>'; 376 } 377 html += '</div>'; 378 379 html += '<div class="chspw-legend">'; 380 var grupos = ['G1', 'G2', 'G3']; 381 for (var gi = 0; gi < grupos.length; gi++) { 382 var gk = grupos[gi]; 383 var gcf = GRUPOS[gk]; 384 var ld = (gk === leader && wins[gk] > 0) ? ' chspw-legend-leader' : ''; 385 html += 386 '<span class="chspw-legend-item ' + gcf.cls + ld + '">' 387 + '<span class="chspw-legend-dot"></span>' 388 + escHtml(gcf.label) + ': ' + wins[gk] 389 + '</span>'; 390 } 391 html += '</div>'; 392 393 $podium.html(html); 394 } 395 396 /* ââ Progress bar âââââââââââââââââââââââââââââââââââââââââ */ 397 function updateProgress() { 398 if (!$progressFill.length) return; 399 var jugadas = state.jornadas.length; 400 var total = parseInt(chspwData.etapas, 10) || 15; 401 var pct = total > 0 ? Math.min(100, Math.round(jugadas / total * 100)) : 0; 402 $progressFill.css('width', pct + '%'); 403 } 404 405 /* ââ Selector âââââââââââââââââââââââââââââââââââââââââââââ */ 406 function updateSelector() { 407 var n = state.jornadas.length; 408 var idx = state.jornadaIdx; 409 var j = n > 0 && idx >= 0 ? state.jornadas[idx] : null; 410 411 if ($selValue.length) { 412 var label = j && j.f ? fechaDisplay(j.f) : (j ? 'Jornada ' + (idx + 1) : 'â'); 413 $selValue.text(label); 414 } 415 if ($selLatest.length) { 416 var jorN = idx >= 0 ? idx + 1 : n; 417 var jorEtapas = j && j.c ? j.c.length : 0; 418 $selLatest.text('Jor. N°' + jorN + ' / ' + jorEtapas + ' Etp.'); 419 } 420 if ($selPrev.length) $selPrev.prop('disabled', idx <= 0); 421 if ($selNext.length) $selNext.prop('disabled', idx >= n - 1); 422 } 423 424 /* ââ Badge ââââââââââââââââââââââââââââââââââââââââââââââââ */ 425 function updateBadge() { 426 var count = state.jornadas.length; 427 if (count > 0 && !state.open) { 428 var prev = parseInt($badge.text(), 10) || 0; 429 $badge.text(count).show(); 430 if (prev !== count && state.initialized && prev > 0) { 431 $badge.removeClass('chspw-badge-bump'); 432 void $badge[0].offsetWidth; 433 $badge.addClass('chspw-badge-bump'); 434 setTimeout(function () { $badge.removeClass('chspw-badge-bump'); }, 400); 435 } 436 } else { 437 $badge.hide(); 438 } 439 } 440 441 /* ââ Subtitle âââââââââââââââââââââââââââââââââââââââââââââ */ 442 function updateSubtitle() { 443 var n = state.jornadas.length; 444 var total = parseInt(chspwData.etapas, 10) || 15; 445 var j = (n > 0 && state.jornadaIdx >= 0) ? state.jornadas[state.jornadaIdx] : null; 446 var isLast = (state.jornadaIdx === n - 1); 447 var text; 448 if (j) { 449 var dateStr = j.f ? fechaDisplay(j.f) : 'Jornada ' + (state.jornadaIdx + 1); 450 var pending = !j.g; 451 var carreraCount = j.c ? j.c.length : 0; 452 text = dateStr 453 + (pending ? ' â pendiente' : (' â ' + escHtml(j.g || ''))) 454 + (carreraCount > 0 ? ' · ' + carreraCount + ' carreras' : '') 455 + (isLast ? ' (última)' : ''); 456 } else { 457 text = n > 0 ? n + ' jornadas' : 'Sin datos de temporada'; 458 } 459 setSubtitle(text, n > 0 ? 'live' : 'idle'); 460 } 461 462 function setSubtitle(text, type) { 463 var dotCls = 'chspw-dot'; 464 if (type === 'live') dotCls += ' chspw-dot-live'; 465 if (type === 'error') dotCls += ' chspw-dot-error'; 466 $subtitle.html( 467 '<span class="' + dotCls + '" aria-hidden="true"></span>' 468 + '<span>' + escHtml(text) + '</span>' 469 ); 470 } 471 472 function setGridMsg(text, extraCls) { 473 $grid.html( 474 '<div class="chspw-grid-message' + (extraCls ? ' ' + extraCls : '') + '">' 475 + escHtml(text) + '</div>' 476 ); 477 } 478 479 /* ââ Timestamp ââââââââââââââââââââââââââââââââââââââââââââ */ 480 function updateLastUpdate() { 481 if (!state.lastFetch) return; 482 var d = state.lastFetch; 483 $lastUpdate.text( 484 pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) 485 ); 486 } 487 488 /* ââ Ãndice de la última jornada con resultados reales âââââ */
489 /* Solo jornadas con carreras (c.length > 0) son navegables */ 490 function lastPlayedIdx() { 491 for (var k = state.jornadas.length - 1; k >= 0; k--) { 492 if (state.jornadas[k].c && state.jornadas[k].c.length > 0) { 493 return k; 494 } 495 } 496 return state.jornadas.length - 1; // fallback si no hay ninguna (no deberÃa ocurrir) 497 } 498 499 /* ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââ */ 500 function tipPosition(e) { 501 var x = e.clientX + 14, y = e.clientY + 14; 502 var tw = $tip.outerWidth(), th = $tip.outerHeight(); 503 if (x + tw > window.innerWidth - 8) x = e.clientX - tw - 10; 504 if (y + th > window.innerHeight - 8) y = e.clientY - th - 10; 505 $tip.css({ left: x, top: y }); 506 } 507 508 function grupoClass(grupo) { 509 var g = String(grupo || '').toUpperCase().replace(/\s+/g, ''); 510 if (g === 'G1' || g === '1') return 'chspw-g1'; 511 if (g === 'G2' || g === '2') return 'chspw-g2'; 512 if (g === 'G3' || g === '3') return 'chspw-g3'; 513 if (g.indexOf('-') !== -1) return 'chspw-gmix'; 514 return ''; 515 } 516 517 function grupoLabel(grupo) { 518 var g = String(grupo || '').toUpperCase().replace(/\s+/g, ''); 519 if (GRUPOS[g]) return GRUPOS[g].label; 520 if (g.indexOf('-') !== -1) return 'Grupo ' + g.replace(/^G/, '').replace(/-G/g, '-'); 521 return grupo; 522 } 523 524 function fechaDisplay(iso) { 525 var p = String(iso || '').split('-'); 526 return p.length === 3 ? p[2] + ' / ' + p[1] + ' / ' + p[0] : (iso || 'â'); 527 } 528 529 /* Formatea fecha para tooltip: "2026-05-25" â "25 may. 2026" */ 530 function tipFechaDisplay(iso) { 531 var months = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic']; 532 var p = String(iso || '').split('-'); 533 if (p.length !== 3) return iso || ''; 534 var d = parseInt(p[2], 10); 535 var m = parseInt(p[1], 10) - 1; 536 var y = parseInt(p[0], 10); 537 return d + ' ' + (months[m] || p[1]) + '. ' + y; 538 } 539 540 function pad(n) { return String(n).padStart(2, '0'); } 541 542 function escHtml(s) { 543 if (typeof s !== 'string') return ''; 544 return s 545 .replace(/&/g, '&') 546 .replace(/</g, '<') 547 .replace(/>/g, '>') 548 .replace(/"/g, '"') 549 .replace(/'/g, '''); 550 } 551 552 function escAttr(s) { 553 if (typeof s !== 'string') return ''; 554 return s 555 .replace(/&/g, '&') 556 .replace(/"/g, '"') 557 .replace(/'/g, ''') 558 .replace(/</g, '<') 559 .replace(/>/g, '>'); 560 } 561 562})(jQuery);
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.