fix: smooth intermission clock by preserving local anchor across renders
Snapshot the locally-computed clock state before each re-render and restore it afterwards, so the API response doesn't cause a visible jump. Only resync to the API value in the final 60 seconds, where accuracy matters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,14 @@ function updateScoreboard(data) {
|
|||||||
const grid = document.getElementById(gridId);
|
const grid = document.getElementById(gridId);
|
||||||
const hasGames = games && games.length > 0;
|
const hasGames = games && games.length > 0;
|
||||||
section.classList.toggle('hidden', !hasGames);
|
section.classList.toggle('hidden', !hasGames);
|
||||||
|
|
||||||
|
// Snapshot current clock state before blowing away the DOM
|
||||||
|
const clockSnapshot = snapshotClocks(grid);
|
||||||
|
|
||||||
grid.innerHTML = hasGames ? games.map(render).join('') : '';
|
grid.innerHTML = hasGames ? games.map(render).join('') : '';
|
||||||
|
|
||||||
|
// Restore smooth local anchors unless we're in the final 60s
|
||||||
|
if (hasGames) restoreClocks(grid, clockSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGauges();
|
updateGauges();
|
||||||
@@ -56,7 +63,7 @@ function renderLiveGame(game) {
|
|||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="game-box ${intermission ? 'game-box-intermission' : 'game-box-live'}">
|
<div class="game-box ${intermission ? 'game-box-intermission' : 'game-box-live'}" data-game-key="${game['Away Team']}|${game['Home Team']}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
${periodLabel}
|
${periodLabel}
|
||||||
@@ -139,6 +146,8 @@ function updateGauges() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CLOCK_SYNC_THRESHOLD = 60; // seconds — only resync from API in final 60s
|
||||||
|
|
||||||
// ── Clock ─────────────────────────────────────────────
|
// ── Clock ─────────────────────────────────────────────
|
||||||
|
|
||||||
function timeToSeconds(str) {
|
function timeToSeconds(str) {
|
||||||
@@ -154,6 +163,35 @@ function secondsToTime(s) {
|
|||||||
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshotClocks(grid) {
|
||||||
|
const snapshot = new Map();
|
||||||
|
grid.querySelectorAll('[data-game-key]').forEach(card => {
|
||||||
|
const badge = card.querySelector('[data-seconds][data-received-at]');
|
||||||
|
if (!badge) return;
|
||||||
|
const seconds = parseInt(badge.dataset.seconds, 10);
|
||||||
|
const receivedAt = parseInt(badge.dataset.receivedAt, 10);
|
||||||
|
const elapsed = Math.floor((Date.now() - receivedAt) / 1000);
|
||||||
|
const current = Math.max(0, seconds - elapsed);
|
||||||
|
snapshot.set(card.dataset.gameKey, { current, ts: Date.now() });
|
||||||
|
});
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]');
|
||||||
|
if (!badge) return;
|
||||||
|
// Only restore if we're outside the final sync window
|
||||||
|
if (prior.current > CLOCK_SYNC_THRESHOLD) {
|
||||||
|
badge.dataset.seconds = prior.current;
|
||||||
|
badge.dataset.receivedAt = prior.ts;
|
||||||
|
badge.textContent = secondsToTime(prior.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function tickClocks() {
|
function tickClocks() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
document.querySelectorAll('[data-seconds][data-received-at]').forEach(el => {
|
document.querySelectorAll('[data-seconds][data-received-at]').forEach(el => {
|
||||||
|
|||||||
Reference in New Issue
Block a user