feat: power play indicator with live countdown clock
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:
@@ -73,6 +73,7 @@ function renderLiveGame(game) {
|
|||||||
</div>
|
</div>
|
||||||
${teamRow(game, 'Away', 'live')}
|
${teamRow(game, 'Away', 'live')}
|
||||||
${teamRow(game, 'Home', 'live')}
|
${teamRow(game, 'Home', 'live')}
|
||||||
|
${ppIndicator(game)}
|
||||||
${hype}
|
${hype}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -112,12 +113,10 @@ function teamRow(game, side, state) {
|
|||||||
const logo = game[`${side} Logo`];
|
const logo = game[`${side} Logo`];
|
||||||
const score = game[`${side} Score`];
|
const score = game[`${side} Score`];
|
||||||
const sog = game[`${side} Shots`];
|
const sog = game[`${side} Shots`];
|
||||||
const pp = game[`${side} Power Play`];
|
|
||||||
const record = game[`${side} Record`];
|
const record = game[`${side} Record`];
|
||||||
|
|
||||||
const sogHtml = (state === 'live' || state === 'final') && sog !== undefined
|
const sogHtml = (state === 'live' || state === 'final') && sog !== undefined
|
||||||
? `<span class="team-sog">${sog} SOG</span>` : '';
|
? `<span class="team-sog">${sog} SOG</span>` : '';
|
||||||
const ppHtml = pp ? `<span class="team-pp">${pp}</span>` : '';
|
|
||||||
|
|
||||||
const right = state === 'pre'
|
const right = state === 'pre'
|
||||||
? `<span class="team-record">${record}</span>`
|
? `<span class="team-record">${record}</span>`
|
||||||
@@ -128,12 +127,31 @@ function teamRow(game, side, state) {
|
|||||||
<img src="${logo}" alt="${name} logo" class="team-logo">
|
<img src="${logo}" alt="${name} logo" class="team-logo">
|
||||||
<div class="team-meta">
|
<div class="team-meta">
|
||||||
<span class="team-name">${name}</span>
|
<span class="team-name">${name}</span>
|
||||||
${sogHtml}${ppHtml}
|
${sogHtml}
|
||||||
</div>
|
</div>
|
||||||
${right}
|
${right}
|
||||||
</div>`;
|
</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 ────────────────────────────────────────────
|
// ── Gauge ────────────────────────────────────────────
|
||||||
|
|
||||||
function updateGauges() {
|
function updateGauges() {
|
||||||
@@ -166,7 +184,7 @@ function secondsToTime(s) {
|
|||||||
function snapshotClocks(grid) {
|
function snapshotClocks(grid) {
|
||||||
const snapshot = new Map();
|
const snapshot = new Map();
|
||||||
grid.querySelectorAll('[data-game-key]').forEach(card => {
|
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;
|
if (!badge) return;
|
||||||
const seconds = parseInt(badge.dataset.seconds, 10);
|
const seconds = parseInt(badge.dataset.seconds, 10);
|
||||||
const receivedAt = parseInt(badge.dataset.receivedAt, 10);
|
const receivedAt = parseInt(badge.dataset.receivedAt, 10);
|
||||||
@@ -181,7 +199,7 @@ function restoreClocks(grid, snapshot) {
|
|||||||
grid.querySelectorAll('[data-game-key]').forEach(card => {
|
grid.querySelectorAll('[data-game-key]').forEach(card => {
|
||||||
const prior = snapshot.get(card.dataset.gameKey);
|
const prior = snapshot.get(card.dataset.gameKey);
|
||||||
if (!prior) return;
|
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;
|
if (!badge) return;
|
||||||
// Only restore if we're outside the final sync window
|
// Only restore if we're outside the final sync window
|
||||||
if (prior.current > CLOCK_SYNC_THRESHOLD) {
|
if (prior.current > CLOCK_SYNC_THRESHOLD) {
|
||||||
|
|||||||
@@ -209,6 +209,47 @@ main {
|
|||||||
white-space: nowrap;
|
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 ─────────────────────────────────── */
|
||||||
|
|
||||||
.hype-meter {
|
.hype-meter {
|
||||||
|
|||||||
Reference in New Issue
Block a user