feat: power play indicator with live countdown clock
All checks were successful
CI / Lint (push) Successful in 5s
CI / Test (push) Successful in 5s
CI / Build & Push (push) Successful in 14s

Shows a red pill below the team rows when a PP is active, displaying
the team on the power play and a ticking countdown. PP clock always
resyncs from the API (no local anchoring) since 2-minute penalties
are short enough that accuracy matters throughout. Removed the old
inline PP text from team rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:58:31 -04:00
parent 257e2151c8
commit 8945b99782
2 changed files with 64 additions and 5 deletions

View File

@@ -73,6 +73,7 @@ function renderLiveGame(game) {
</div>
${teamRow(game, 'Away', 'live')}
${teamRow(game, 'Home', 'live')}
${ppIndicator(game)}
${hype}
</div>`;
}
@@ -112,12 +113,10 @@ function teamRow(game, side, state) {
const logo = game[`${side} Logo`];
const score = game[`${side} Score`];
const sog = game[`${side} Shots`];
const pp = game[`${side} Power Play`];
const record = game[`${side} Record`];
const sogHtml = (state === 'live' || state === 'final') && sog !== undefined
? `<span class="team-sog">${sog} SOG</span>` : '';
const ppHtml = pp ? `<span class="team-pp">${pp}</span>` : '';
const right = state === 'pre'
? `<span class="team-record">${record}</span>`
@@ -128,12 +127,31 @@ function teamRow(game, side, state) {
<img src="${logo}" alt="${name} logo" class="team-logo">
<div class="team-meta">
<span class="team-name">${name}</span>
${sogHtml}${ppHtml}
${sogHtml}
</div>
${right}
</div>`;
}
function ppIndicator(game) {
const awayPP = game['Away Power Play'];
const homePP = game['Home Power Play'];
const pp = awayPP || homePP;
if (!pp) return '';
const team = awayPP ? game['Away Team'] : game['Home Team'];
const timeStr = pp.replace('PP ', '');
const seconds = timeToSeconds(timeStr);
const attrs = `data-seconds="${seconds}" data-received-at="${Date.now()}" data-pp-clock`;
return `
<div class="pp-indicator">
<span class="pp-label">PP</span>
<span class="pp-team">${team}</span>
<span class="pp-clock" ${attrs}>${timeStr}</span>
</div>`;
}
// ── Gauge ────────────────────────────────────────────
function updateGauges() {
@@ -166,7 +184,7 @@ function secondsToTime(s) {
function snapshotClocks(grid) {
const snapshot = new Map();
grid.querySelectorAll('[data-game-key]').forEach(card => {
const badge = card.querySelector('[data-seconds][data-received-at]');
const badge = card.querySelector('[data-seconds][data-received-at]:not([data-pp-clock])');
if (!badge) return;
const seconds = parseInt(badge.dataset.seconds, 10);
const receivedAt = parseInt(badge.dataset.receivedAt, 10);
@@ -181,7 +199,7 @@ function restoreClocks(grid, snapshot) {
grid.querySelectorAll('[data-game-key]').forEach(card => {
const prior = snapshot.get(card.dataset.gameKey);
if (!prior) return;
const badge = card.querySelector('[data-seconds][data-received-at]');
const badge = card.querySelector('[data-seconds][data-received-at]:not([data-pp-clock])');
if (!badge) return;
// Only restore if we're outside the final sync window
if (prior.current > CLOCK_SYNC_THRESHOLD) {

View File

@@ -209,6 +209,47 @@ main {
white-space: nowrap;
}
/* ── Power Play Indicator ───────────────────────── */
.pp-indicator {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
}
.pp-label {
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--red);
text-transform: uppercase;
flex-shrink: 0;
}
.pp-team {
font-size: 0.72rem;
font-weight: 600;
color: var(--text);
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pp-clock {
font-size: 0.72rem;
font-weight: 700;
color: var(--red);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
/* ── Hype Meter ─────────────────────────────────── */
.hype-meter {