Skip to content

Instantly share code, notes, and snippets.

@camilotorresmestra
Created May 5, 2026 00:42
Show Gist options
  • Select an option

  • Save camilotorresmestra/9e320f7f6b1a1391019e3a0ae929c2ed to your computer and use it in GitHub Desktop.

Select an option

Save camilotorresmestra/9e320f7f6b1a1391019e3a0ae929c2ed to your computer and use it in GitHub Desktop.
betplay_fecha19.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liga BetPlay 2026-I · Fecha 19 · Jornada de Infarto</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow+Condensed:ital,wght@0,400;0,600;0,700;1,600&family=Barlow:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #06090D;
--s1: #0C1521;
--s2: #111D2B;
--border: #1A2B3D;
--gold: #F6C200;
--gold2: #FFD84F;
--green: #00A651;
--red: #D42535;
--blue: #1A6FD4;
--text: #DAE4EE;
--muted: #4E6478;
--muted2: #7A96A8;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
background:var(--bg);
color:var(--text);
font-family:'Barlow',system-ui,sans-serif;
min-height:100vh;
overflow-x:hidden;
}
body::before{
content:'';
position:fixed;inset:0;
background:
radial-gradient(ellipse 60% 50% at 15% 60%,rgba(0,166,81,.035) 0%,transparent 70%),
radial-gradient(ellipse 50% 40% at 85% 20%,rgba(246,194,0,.03) 0%,transparent 70%);
pointer-events:none;z-index:0;
}
/* ─── HEADER ─── */
.header{
position:relative;z-index:10;
background:var(--s1);
border-bottom:1px solid var(--border);
padding:12px 20px;
display:flex;align-items:center;justify-content:space-between;
}
.hdr-left{display:flex;align-items:center;gap:12px}
.hdr-logo{
width:42px;height:42px;background:var(--gold);border-radius:50%;
display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;
box-shadow:0 0 20px rgba(246,194,0,.25);
}
.hdr-league{
font-family:'Bebas Neue',sans-serif;font-size:19px;
letter-spacing:2px;color:var(--gold);line-height:1;
}
.hdr-sub{font-size:10px;color:var(--muted);letter-spacing:1.5px;text-transform:uppercase}
.hdr-right{text-align:right}
.hdr-fecha{
font-family:'Barlow Condensed',sans-serif;font-size:22px;
font-weight:700;line-height:1;letter-spacing:.5px;
}
.hdr-date{font-size:11px;color:var(--muted);margin-top:1px}
/* ─── LAYOUT ─── */
.layout{
position:relative;z-index:1;
display:grid;
grid-template-columns:1fr 272px;
gap:0;max-width:1280px;margin:0 auto;padding:20px 20px 40px;
}
main{padding-right:20px}
@media(max-width:820px){
.layout{grid-template-columns:1fr}
main{padding-right:0}
.sidebar{margin-top:16px}
}
/* ─── HERO ─── */
.hero{margin-bottom:20px}
.hero-title{
font-family:'Bebas Neue',sans-serif;
font-size:clamp(40px,6vw,68px);
letter-spacing:3px;line-height:.92;
}
.hero-title em{color:var(--gold);font-style:normal}
.hero-sub{
font-size:11px;color:var(--muted2);
letter-spacing:2px;text-transform:uppercase;
margin-top:6px;
}
/* ─── SECTION SHELL ─── */
.section{
background:var(--s1);border:1px solid var(--border);
border-radius:12px;padding:18px 20px;margin-bottom:14px;
}
/* ─── TIMELINE ─── */
.tl-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.tl-label{
font-family:'Barlow Condensed',sans-serif;font-size:11px;
font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--muted);
}
.min-badge{
font-family:'Bebas Neue',sans-serif;font-size:17px;
background:var(--gold);color:#000;padding:2px 10px;border-radius:4px;
letter-spacing:1px;min-width:52px;text-align:center;
}
.slider-marks{
display:flex;justify-content:space-between;margin-bottom:5px;padding:0 2px;
}
.slider-marks span{font-size:9px;color:var(--muted);font-family:'Barlow Condensed',sans-serif}
.slider-wrap{position:relative;padding-bottom:18px}
input[type=range]{
width:100%;-webkit-appearance:none;appearance:none;
height:5px;
background:linear-gradient(to right,var(--gold) var(--pct,0%),var(--border) var(--pct,0%));
border-radius:3px;cursor:pointer;outline:none;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:20px;height:20px;
background:var(--gold);border-radius:50%;
border:3px solid var(--bg);
box-shadow:0 0 14px rgba(246,194,0,.55);
}
input[type=range]::-moz-range-thumb{
width:20px;height:20px;background:var(--gold);border-radius:50%;
border:3px solid var(--bg);
}
.ht-mark{
position:absolute;bottom:0;left:calc(45/95*100%);
transform:translateX(-50%);font-size:9px;color:var(--muted);
letter-spacing:1px;text-transform:uppercase;text-align:center;
}
.ht-mark::before{
content:'';display:block;width:1px;height:8px;
background:var(--border);margin:0 auto 2px;
}
/* ─── CONTROLS ─── */
.controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.btn{
padding:7px 15px;border-radius:7px;border:1px solid var(--border);
background:var(--s2);color:var(--text);
font-family:'Barlow Condensed',sans-serif;font-size:12px;
font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
cursor:pointer;transition:all .2s;
}
.btn:hover{border-color:var(--gold);color:var(--gold)}
.btn.primary{background:var(--gold);color:#000;border-color:var(--gold)}
.btn.primary:hover{background:var(--gold2);border-color:var(--gold2)}
.speed-wrap{display:flex;align-items:center;gap:6px;margin-left:auto}
.speed-label{font-size:11px;color:var(--muted)}
.speed-select{
background:var(--s2);border:1px solid var(--border);color:var(--text);
font-family:'Barlow Condensed',sans-serif;font-size:12px;
padding:4px 8px;border-radius:5px;cursor:pointer;
}
/* ─── KEY MOMENTS ─── */
.moments-label{
font-family:'Barlow Condensed',sans-serif;font-size:10px;
font-weight:700;letter-spacing:2px;text-transform:uppercase;
color:var(--muted);margin-bottom:8px;margin-top:14px;
}
.moments-row{
display:flex;gap:5px;overflow-x:auto;padding-bottom:3px;
scrollbar-width:thin;scrollbar-color:var(--border) transparent;
}
.moment-chip{
flex-shrink:0;padding:4px 9px;border-radius:16px;
border:1px solid var(--border);background:var(--s2);
cursor:pointer;transition:all .18s;display:flex;align-items:center;gap:4px;
}
.moment-chip:hover,.moment-chip.active{
border-color:var(--gold);background:rgba(246,194,0,.08);
}
.chip-min{font-family:'Bebas Neue',sans-serif;font-size:13px;color:var(--gold);line-height:1}
.chip-lbl{font-size:10px;color:var(--text);white-space:nowrap}
/* ─── MATCH CARDS ─── */
.matches{display:flex;flex-direction:column;gap:9px;margin-bottom:14px}
.match-card{
background:var(--s1);border:1px solid var(--border);
border-radius:10px;padding:13px 16px 10px;
transition:border-color .3s,box-shadow .3s;
}
.match-card.hot{
border-color:rgba(246,194,0,.25);
box-shadow:0 0 20px rgba(246,194,0,.04);
}
.match-card.just-scored{animation:goalFlash .7s ease-out}
@keyframes goalFlash{
0%{background:rgba(246,194,0,.15);border-color:var(--gold)}
100%{background:var(--s1)}
}
.mc-top{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.team-side{display:flex;align-items:center;gap:8px;flex:1}
.team-side.away{flex-direction:row-reverse;text-align:right}
.t-emoji{font-size:24px;line-height:1;flex-shrink:0}
.t-short{
font-family:'Barlow Condensed',sans-serif;font-size:17px;
font-weight:700;letter-spacing:.5px;line-height:1;
}
.t-tag{font-size:9px;letter-spacing:1px;text-transform:uppercase;margin-top:2px}
.tag-in{color:var(--green)}.tag-out{color:var(--red)}
.tag-bubble{color:var(--gold)}.tag-muted{color:var(--muted)}
.score-mid{text-align:center;min-width:88px}
.score-num{
font-family:'Bebas Neue',sans-serif;font-size:38px;
line-height:1;letter-spacing:5px;transition:all .2s;
}
.score-venue{font-size:9px;color:var(--muted);letter-spacing:.5px;margin-top:1px}
/* ─── MINI TIMELINE ─── */
.mini-tl{position:relative;height:18px;margin-top:2px}
.mini-track{
position:absolute;inset:0;top:50%;height:2px;
transform:translateY(-50%);background:var(--border);border-radius:1px;
}
.mini-progress{
position:absolute;left:0;top:0;bottom:0;
background:rgba(246,194,0,.15);border-radius:1px;
transition:width .12s;
}
.mini-cursor{
position:absolute;top:0;bottom:0;width:2px;
background:rgba(246,194,0,.5);transform:translateX(-50%);
transition:left .12s;
}
.epip{
position:absolute;width:9px;height:9px;border-radius:50%;
top:50%;transform:translate(-50%,-50%);
transition:opacity .25s,transform .2s;cursor:pointer;
}
.epip.past{opacity:1}
.epip.future{opacity:.18}
.epip.at-now{transform:translate(-50%,-50%) scale(1.9);box-shadow:0 0 6px var(--gold)}
/* ─── EVENT FEED ─── */
.feed-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.feed-label{
font-family:'Barlow Condensed',sans-serif;font-size:11px;
font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--muted);
}
.feed-count{font-family:'Bebas Neue',sans-serif;font-size:15px;color:var(--gold)}
.feed-list{display:flex;flex-direction:column;gap:5px;max-height:210px;overflow-y:auto}
.feed-item{
display:flex;align-items:flex-start;gap:9px;padding:7px 10px;
background:var(--s2);border-radius:7px;
border-left:3px solid var(--border);
animation:fadeUp .3s ease-out;
}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.fi-min{
font-family:'Bebas Neue',sans-serif;font-size:15px;
color:var(--gold);min-width:28px;line-height:1.2;
}
.fi-main{font-size:12px;font-weight:500;line-height:1.4}
.fi-sub{font-size:9px;color:var(--muted2);margin-top:1px}
.feed-empty{text-align:center;padding:18px;color:var(--muted);font-size:12px;font-style:italic}
/* ─── SIDEBAR ─── */
.standings-card{
background:var(--s1);border:1px solid var(--border);border-radius:12px;
padding:16px;position:sticky;top:16px;
}
.sc-title{
font-family:'Bebas Neue',sans-serif;font-size:22px;
letter-spacing:2px;color:var(--gold);line-height:1;
}
.sc-sub{
font-size:10px;color:var(--muted);letter-spacing:1px;
text-transform:uppercase;margin-bottom:14px;margin-top:1px;
}
.sc-note{
font-size:9px;color:var(--muted);font-style:italic;
margin-bottom:10px;line-height:1.5;
}
.st-row{
display:flex;align-items:center;gap:7px;padding:6px 8px;
border-radius:6px;margin-bottom:2px;
border:1px solid transparent;transition:background .4s,border-color .4s,opacity .4s;
}
.st-row.in{background:rgba(0,166,81,.1);border-color:rgba(0,166,81,.2)}
.st-row.out{background:rgba(212,37,53,.05);border-color:rgba(212,37,53,.12);opacity:.6}
.st-row.bubble{background:rgba(246,194,0,.07);border-color:rgba(246,194,0,.18)}
.st-row.safe{background:transparent}
.st-pos{font-family:'Bebas Neue',sans-serif;font-size:14px;min-width:18px;color:var(--muted)}
.st-row.in .st-pos{color:var(--green)}
.st-emoji{font-size:14px}
.st-name{flex:1;font-family:'Barlow Condensed',sans-serif;font-size:13px;font-weight:600}
.st-gain{font-size:9px;min-width:22px;text-align:right;color:var(--muted)}
.st-gain.pos{color:var(--green)}.st-gain.neg{color:var(--red)}
.st-pts{font-family:'Bebas Neue',sans-serif;font-size:15px;min-width:20px;text-align:right}
.st-row.in .st-pts{color:var(--green)}
.st-row.out .st-pts{color:var(--red)}
.cutoff-div{display:flex;align-items:center;gap:7px;margin:5px 0}
.cutoff-line{flex:1;border:none;border-top:1.5px dashed rgba(246,194,0,.35)}
.cutoff-txt{font-size:9px;color:var(--gold);letter-spacing:1px;text-transform:uppercase;white-space:nowrap}
@keyframes stFlash{
0%{background:rgba(246,194,0,.3)}100%{}
}
.st-row.flash{animation:stFlash .7s ease-out}
/* ─── SCROLLBAR ─── */
::-webkit-scrollbar{width:3px;height:3px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="hdr-left">
<div class="hdr-logo"></div>
<div>
<div class="hdr-league">Liga BetPlay Dimayor</div>
<div class="hdr-sub">2026-I · Torneo Apertura · Fútbol Colombiano</div>
</div>
</div>
<div class="hdr-right">
<div class="hdr-fecha">Fecha 19</div>
<div class="hdr-date">3 de mayo de 2026</div>
</div>
</div>
<div class="layout">
<main>
<!-- HERO -->
<div class="hero">
<div class="hero-title">Jornada de<br><em>Infarto</em></div>
<div class="hero-sub">5 partidos simultáneos · 2 cupos en juego · drama hasta el final</div>
</div>
<!-- TIMELINE CONTROL -->
<div class="section">
<div class="tl-top">
<div class="tl-label">🕐 Línea de Tiempo</div>
<div class="min-badge" id="minBadge">0'</div>
</div>
<div class="slider-wrap">
<div class="slider-marks">
<span>0'</span><span>15'</span><span>30'</span><span>45'</span>
<span>60'</span><span>75'</span><span>90'</span><span>95'+</span>
</div>
<input type="range" id="slider" min="0" max="95" value="0" step="1">
<div class="ht-mark">ET</div>
</div>
<div class="controls">
<button class="btn primary" id="playBtn" onclick="togglePlay()">▶ Reproducir</button>
<button class="btn" onclick="resetTL()">↩ Inicio</button>
<div class="speed-wrap">
<span class="speed-label">Velocidad</span>
<select class="speed-select" id="speedSel">
<option value="600">Lento</option>
<option value="250" selected>Normal</option>
<option value="80">Rápido</option>
</select>
</div>
</div>
<div class="moments-label">⚡ Momentos Clave</div>
<div class="moments-row" id="momentsRow"></div>
</div>
<!-- MATCH CARDS -->
<div class="matches" id="matchCards"></div>
<!-- EVENT FEED -->
<div class="section">
<div class="feed-hdr">
<div class="feed-label">📡 Relato del partido</div>
<div class="feed-count" id="feedCount">0 goles</div>
</div>
<div class="feed-list" id="feedList">
<div class="feed-empty">Mueve el deslizador para ver qué pasó…</div>
</div>
</div>
</main>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="standings-card">
<div class="sc-title">Tabla en Vivo</div>
<div class="sc-sub">Posiciones proyectadas · Fecha 19</div>
<div class="sc-note">* Proyección si los marcadores actuales fueran definitivos. Desempate real por diferencia de goles no aplicado.</div>
<div id="standingsList"></div>
</div>
</aside>
</div>
<script>
// ═══════════════ DATA ═══════════════
const MATCHES = [
{
id:'sfe_int',
hot:true,
home:{key:'sfe',name:'Santa Fe', short:'SFE',emoji:'❤️', color:'#D42535',pts:26},
away:{key:'int',name:'Inter Bogotá', short:'INT',emoji:'🌐',color:'#1A6FD4',pts:28},
venue:'El Campín · Bogotá',
events:[
{min:49, team:'away', player:'F. Sanguinetti', note:'Inter se adelanta — Santa Fe en peligro', emoji:'😱'},
{min:70, team:'home', player:'H. Rodallega', note:'¡RODALLEGA! El Campín explota — iguala el legendario', emoji:'🔥'},
{min:74, team:'home', player:'E. Olivera', note:'Olivera la vuelta — ¡Santa Fe delante!', emoji:'🚀'},
{min:83, team:'home', player:'N. Bustos', note:'¡Bustos y Santa Fe clasifica a playoffs!', emoji:'🎉'},
]
},
{
id:'tol_cal',
hot:false,
home:{key:'tol',name:'Deportes Tolima', short:'TOL',emoji:'🍷',color:'#8B0000',pts:30},
away:{key:'cal',name:'Deportivo Cali', short:'CAL',emoji:'🍃',color:'#228B22',pts:26},
venue:'M. Murillo Toro · Ibagué',
events:[
{min:35, team:'home', player:'E. Valencia', note:'Tolima abre el marcador de penalti', emoji:'⚽'},
{min:63, team:'away', player:'E. Valencia', note:'Cali empata de penalti — pero sigue eliminado', emoji:'😤'},
]
},
{
id:'med_agu',
hot:false,
home:{key:'med',name:'Ind. Medellín', short:'MED',emoji:'🔴',color:'#CC0000',pts:26},
away:{key:'agu',name:'Águilas Doradas', short:'AGU',emoji:'🦅',color:'#DAA520',pts:21},
venue:'Atanasio Girardot · Medellín',
events:[
{min:28, team:'away', player:'A. Ricaurte', note:'¡Águilas sorprende en el Atanasio!', emoji:'😲'},
{min:55, team:'away', player:'J. Rivaldo', note:'Doble golpe — Medellín queda eliminado', emoji:'💀'},
{min:88, team:'home', player:'D. Moreno', note:'Solo el honor para el Poderoso', emoji:'😢'},
]
},
{
id:'ali_mil',
hot:false,
home:{key:'ali',name:'Alianza FC', short:'ALI',emoji:'🟠',color:'#E06000',pts:20},
away:{key:'mil',name:'Millonarios', short:'MIL',emoji:'💙',color:'#003087',pts:25},
venue:'Municipal · Valledupar',
events:[
{min:44, team:'home', player:'Y. Londoño', note:'Alianza se adelanta antes del descanso', emoji:'⚽'},
{min:52, team:'home', player:'P. Franco', note:'2-0 — Millonarios en crisis total', emoji:'😰'},
{min:58, team:'away', player:'R. Contreras',note:'Contreras recorta — el Embajador despierta', emoji:'💪'},
{min:91, team:'away', player:'R. Contreras',note:'Contreras empata al 91\'… demasiado tarde para clasificar', emoji:'😭'},
]
},
{
id:'for_buc',
hot:false,
home:{key:'for',name:'Fortaleza FC', short:'FOR',emoji:'🏰',color:'#006400',pts:18},
away:{key:'buc',name:'Bucaramanga', short:'BUC',emoji:'🟡',color:'#D4A800',pts:22},
venue:'La Independencia · Tunja',
events:[
{min:39, team:'home', player:'J.S. Herrera', note:'Fortaleza se adelanta en Tunja', emoji:'⚽'},
{min:78, team:'home', player:'A. Arroyo', note:'Arroyo liquida a Bucaramanga', emoji:'✅'},
{min:85, team:'away', player:'F. Sambueza', note:'El veterano Sambueza descuenta', emoji:'⚽'},
]
},
];
const KEY_MOMENTS = [
{min:0, label:'Inicio', emoji:'🟢'},
{min:28, label:'Águilas 1-0 MED', emoji:'🦅'},
{min:35, label:'TOL 1-0 CAL', emoji:'⚽'},
{min:39, label:'FOR 1-0 BUC', emoji:'🏰'},
{min:44, label:'ALI 1-0 MIL', emoji:'⚽'},
{min:45, label:'Medio Tiempo', emoji:'⏸️'},
{min:49, label:'INT 1-0 SFE !', emoji:'😱'},
{min:55, label:'Águilas 2-0 MED', emoji:'💀'},
{min:63, label:'TOL 1-1 CAL', emoji:'⚽'},
{min:70, label:'Rodallega gol', emoji:'🔥'},
{min:74, label:'SFE 2-1 INT', emoji:'🚀'},
{min:83, label:'SFE clasifica', emoji:'🎉'},
{min:91, label:'Contreras 91\'', emoji:'😭'},
{min:95, label:'Final', emoji:'🏁'},
];
// Teams NOT playing today (safe top-8 already secured before this gameday)
const SAFE_TEAMS = [
{key:'nac',name:'Nacional', emoji:'💚',pts:40},
{key:'jun',name:'Junior', emoji:'🔵',pts:35},
{key:'pas',name:'D. Pasto', emoji:'⚫',pts:34},
{key:'onc',name:'Once Caldas', emoji:'⚪',pts:33},
{key:'ame',name:'América', emoji:'🔴',pts:30},
];
// ═══════════════ STATE ═══════════════
let cur = 0;
let playing = false;
let playTimer = null;
const prevStatus = {}; // track flashing
// ═══════════════ HELPERS ═══════════════
function getScore(match, min) {
let h=0,a=0;
for(const e of match.events) {
if(e.min<=min){e.team==='home'?h++:a++}
}
return [h,a];
}
function gainPts(h,a){
if(h>a)return[3,0];
if(a>h)return[0,3];
return[1,1];
}
// ═══════════════ STANDINGS ═══════════════
function computeStandings(min) {
const live = {};
for(const t of SAFE_TEAMS) live[t.key]={...t,livePoints:t.pts,safe:true};
for(const m of MATCHES) {
live[m.home.key]={key:m.home.key,name:m.home.name,emoji:m.home.emoji,pts:m.home.pts,livePoints:m.home.pts,safe:false};
live[m.away.key]={key:m.away.key,name:m.away.name,emoji:m.away.emoji,pts:m.away.pts,livePoints:m.away.pts,safe:false};
}
for(const m of MATCHES) {
const [h,a]=getScore(m,min);
const [hp,ap]=gainPts(h,a);
live[m.home.key].livePoints+=hp;
live[m.away.key].livePoints+=ap;
}
return Object.values(live).sort((a,b)=>b.livePoints-a.livePoints||a.name.localeCompare(b.name));
}
function renderStandings(min) {
const sorted = computeStandings(min);
const el = document.getElementById('standingsList');
let html='';
sorted.forEach((t,i)=>{
const pos=i+1;
const isIn=pos<=8;
const cls=t.safe?'safe':(isIn?'in':(pos<=10?'bubble':'out'));
const gain=t.livePoints-t.pts;
const gainStr=gain>0?'+'+gain:(gain<0?gain:'');
const gainCls=gain>0?'pos':gain<0?'neg':'';
const statusKey=t.key+':'+isIn;
const shouldFlash = prevStatus[t.key] && prevStatus[t.key]!==statusKey;
prevStatus[t.key]=statusKey;
if(pos===9){
html+=`<div class="cutoff-div"><div class="cutoff-line"></div><div class="cutoff-txt">Clasificados</div><div class="cutoff-line"></div></div>`;
}
html+=`<div class="st-row ${cls}${shouldFlash?' flash':''}" id="st-${t.key}">
<div class="st-pos">${pos}</div>
<div class="st-emoji">${t.emoji}</div>
<div class="st-name">${t.name}</div>
<div class="st-gain ${gainCls}">${gainStr}</div>
<div class="st-pts">${t.livePoints}</div>
</div>`;
});
el.innerHTML=html;
}
// ═══════════════ MATCH CARDS ═══════════════
function buildMatchCards() {
const el=document.getElementById('matchCards');
el.innerHTML=MATCHES.map(m=>{
const pips=m.events.map(e=>
`<div class="epip future" id="pip-${m.id}-${e.min}"
style="left:calc(${e.min}/95*100%);background:${e.team==='home'?m.home.color:m.away.color}"
title="${e.min}' ${e.player}"></div>`
).join('');
return `<div class="match-card${m.hot?' hot':''}" id="card-${m.id}">
<div class="mc-top">
<div class="team-side">
<div class="t-emoji">${m.home.emoji}</div>
<div>
<div class="t-short">${m.home.short}</div>
<div class="t-tag tag-bubble" id="htag-${m.id}"></div>
</div>
</div>
<div class="score-mid">
<div class="score-num" id="score-${m.id}">0 – 0</div>
<div class="score-venue">${m.venue}</div>
</div>
<div class="team-side away">
<div class="t-emoji">${m.away.emoji}</div>
<div>
<div class="t-short">${m.away.short}</div>
<div class="t-tag tag-bubble" id="atag-${m.id}"></div>
</div>
</div>
</div>
<div class="mini-tl">
<div class="mini-track">
<div class="mini-progress" id="prog-${m.id}"></div>
</div>
<div class="mini-cursor" id="cursor-${m.id}"></div>
${pips}
</div>
</div>`;
}).join('');
}
// ═══════════════ FEED ═══════════════
function renderFeed(min) {
const all=[];
for(const m of MATCHES) {
for(const e of m.events) {
if(e.min<=min) all.push({...e, matchHome:m.home.short, matchAway:m.away.short, matchId:m.id,
color:e.team==='home'?m.home.color:m.away.color});
}
}
all.sort((a,b)=>b.min-a.min);
const el=document.getElementById('feedList');
document.getElementById('feedCount').textContent=all.length+' gol'+(all.length!==1?'es':'');
if(!all.length){el.innerHTML='<div class="feed-empty">Mueve el deslizador para ver qué pasó…</div>';return;}
el.innerHTML=all.map(e=>`
<div class="feed-item" style="border-left-color:${e.color}">
<div class="fi-min">${e.min}'</div>
<div style="font-size:16px">${e.emoji}</div>
<div>
<div class="fi-main">${e.player}${e.note}</div>
<div class="fi-sub">${e.matchHome} vs ${e.matchAway}</div>
</div>
</div>`).join('');
}
// ═══════════════ KEY MOMENTS ═══════════════
function buildMoments() {
const el=document.getElementById('momentsRow');
el.innerHTML=KEY_MOMENTS.map(m=>`
<div class="moment-chip" data-min="${m.min}" onclick="jumpTo(${m.min})">
<span class="chip-min">${m.min}'</span>
<span class="chip-lbl">${m.label}</span>
</div>`).join('');
}
// ═══════════════ CORE UPDATE ═══════════════
function update(min) {
cur=min;
const pct=(min/95*100).toFixed(1)+'%';
// Slider fill
document.getElementById('slider').style.setProperty('--pct',pct);
document.getElementById('minBadge').textContent=min===95?"90+5'":min+"'";
for(const m of MATCHES) {
const [h,a]=getScore(m,min);
document.getElementById(`score-${m.id}`).textContent=`${h}${a}`;
document.getElementById(`prog-${m.id}`).style.width=pct;
document.getElementById(`cursor-${m.id}`).style.left=pct;
// pips
for(const e of m.events) {
const pip=document.getElementById(`pip-${m.id}-${e.min}`);
if(!pip)continue;
const past=e.min<=min;
const atNow=Math.abs(e.min-min)<=1;
pip.className=`epip ${past?'past':'future'}${atNow?' at-now':''}`;
}
}
renderFeed(min);
renderStandings(min);
// Highlight active moment chip
document.querySelectorAll('.moment-chip').forEach(c=>{
c.classList.toggle('active',parseInt(c.dataset.min)===min);
});
}
function jumpTo(min) {
stopPlay();
document.getElementById('slider').value=min;
update(min);
}
function resetTL(){jumpTo(0)}
// ═══════════════ PLAY ═══════════════
function togglePlay(){
if(playing){stopPlay();return;}
playing=true;
document.getElementById('playBtn').textContent='⏸ Pausar';
if(cur>=95)cur=0;
const speed=()=>parseInt(document.getElementById('speedSel').value);
function step(){
if(cur>=95){stopPlay();return;}
cur++;
document.getElementById('slider').value=cur;
update(cur);
playTimer=setTimeout(step,speed());
}
playTimer=setTimeout(step,speed());
}
function stopPlay(){
playing=false;
clearTimeout(playTimer);
document.getElementById('playBtn').textContent='▶ Reproducir';
}
// ═══════════════ EVENTS ═══════════════
document.getElementById('slider').addEventListener('input',e=>{
stopPlay();
update(parseInt(e.target.value));
});
// ═══════════════ INIT ═══════════════
buildMatchCards();
buildMoments();
update(0);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment