CuteBoard/static/js/main.js
Nick d8ebfcda13 Add reduced-styles toggle, decrement, and kiosk improvements
- Add reduced_styles config to switch between Tailwind CDN and inline CSS
- Restore liquid wave animations in full-styles mode
- Add score decrement endpoint and staged hold-to-reset (decrement/arm/reset)
- Improve start.sh: prefer .venv, cleanup stale processes, X display fallback
- Scope inline utility classes to body.reduced-styles
2026-08-11 18:58:59 -05:00

369 lines
10 KiB
JavaScript

(function () {
'use strict';
const scores = {
left: parseInt(document.getElementById('score-left').textContent) || 0,
right: parseInt(document.getElementById('score-right').textContent) || 0,
};
let debounce = false;
// ---- Auto-fit text ----
function fitText(el, maxWidth, maxHeight) {
el.style.fontSize = '10px';
if (el.scrollWidth === 0) return;
var lo = 8, hi = 600;
while (lo < hi) {
var mid = Math.ceil((lo + hi) / 2);
el.style.fontSize = mid + 'px';
if (el.scrollWidth <= maxWidth && el.scrollHeight <= maxHeight) {
lo = mid;
} else {
hi = mid - 1;
}
}
el.style.fontSize = lo + 'px';
}
function fitTextHeight(el, maxHeight) {
el.style.fontSize = '10px';
if (el.scrollHeight === 0) return;
var lo = 8, hi = 600;
while (lo < hi) {
var mid = Math.ceil((lo + hi) / 2);
el.style.fontSize = mid + 'px';
if (el.scrollHeight <= maxHeight) {
lo = mid;
} else {
hi = mid - 1;
}
}
el.style.fontSize = lo + 'px';
}
function fitAll() {
scoreFontCache = {};
var headerRow = document.querySelector('.header-row');
if (headerRow) {
var span = headerRow.querySelector('span');
var hfs = parseFloat(headerRow.dataset.headerFontSize);
if (hfs > 0) {
span.style.fontSize = hfs + 'rem';
} else {
var w = window.innerWidth * 0.9;
fitText(span, w, 80);
}
}
document.querySelectorAll('.side').forEach(function (side) {
var rect = side.getBoundingClientRect();
var w = rect.width * 0.88;
var h = rect.height;
var name = side.querySelector('.team-name');
if (name) {
var fs = parseFloat(name.dataset.nameFontSize);
if (fs > 0) {
name.style.fontSize = fs + 'rem';
} else {
fitText(name, w, h * 0.14);
}
}
var subtitle = side.querySelector('.team-subtitle');
if (subtitle) {
var sfs = parseFloat(subtitle.dataset.subtitleFontSize);
if (sfs > 0) {
subtitle.style.fontSize = sfs + 'rem';
} else {
subtitle.style.fontSize = '';
}
}
var score = side.querySelector('.score');
if (score) {
var sfs = parseFloat(score.dataset.scoreFontSize);
if (sfs > 0) {
score.style.fontSize = sfs + 'rem';
} else {
fitTextHeight(score, h * 0.55);
}
}
var content = side.querySelector('.side-content');
if (content) {
var cpt = parseFloat(document.querySelector('.scoreboard').dataset.contentPaddingTop);
content.style.paddingTop = cpt > 0 ? cpt + 'rem' : '';
}
});
}
window.addEventListener('load', fitAll);
window.addEventListener('resize', fitAll);
// ---- Tap / Hold handlers ----
document.querySelectorAll('.side').forEach(function (side) {
var sideName = side.dataset.side;
var holdFadeTimer = null;
var holdDecTimer = null;
var holdResetArmTimer = null;
var holdResetTimer = null;
var holdFired = false;
function clearHoldTimers() {
if (holdFadeTimer) { clearTimeout(holdFadeTimer); holdFadeTimer = null; }
if (holdDecTimer) { clearTimeout(holdDecTimer); holdDecTimer = null; }
if (holdResetArmTimer) { clearTimeout(holdResetArmTimer); holdResetArmTimer = null; }
if (holdResetTimer) { clearTimeout(holdResetTimer); holdResetTimer = null; }
}
function onStart(e) {
e.preventDefault();
if (debounce) return;
holdFired = false;
side.classList.remove('hold-cancel');
side.classList.remove('hold-reset-arm');
holdFadeTimer = setTimeout(function () {
void side.offsetWidth;
side.classList.add('holding');
}, 300);
holdDecTimer = setTimeout(function () {
holdFired = true;
decrement(sideName);
}, 2000);
holdResetArmTimer = setTimeout(function () {
side.classList.add('hold-reset-arm');
}, 2500);
holdResetTimer = setTimeout(function () {
holdFired = true;
side.classList.remove('holding');
side.classList.remove('hold-reset-arm');
resetSide(sideName);
}, 4000);
}
function onEnd(e) {
clearHoldTimers();
side.classList.remove('holding');
side.classList.remove('hold-reset-arm');
if (holdFired) {
side.classList.remove('hold-cancel');
return;
}
side.classList.add('hold-cancel');
if (debounce) return;
debounce = true;
setTimeout(function () { debounce = false; }, 400);
selectedSide = sideName;
increment(sideName);
}
function onCancel() {
clearHoldTimers();
side.classList.remove('holding');
side.classList.remove('hold-cancel');
side.classList.remove('hold-reset-arm');
}
side.addEventListener('pointerdown', onStart);
side.addEventListener('pointerup', onEnd);
side.addEventListener('pointercancel', onCancel);
side.addEventListener('pointerleave', onCancel);
});
var selectedSide = 'left';
var counting = false;
var countdownTimer = null;
var switchCount = 0;
var originalWaveSide = 'left';
var confirming = false;
var confirmTimer = null;
function stopCountdown() {
if (countdownTimer) { clearTimeout(countdownTimer); countdownTimer = null; }
counting = false;
document.querySelectorAll('.side').forEach(function (s) {
s.classList.remove('counting');
var bar = s.querySelector('.hw-bar');
if (bar) {
bar.classList.remove('active');
bar.style.transform = 'scaleX(0)';
}
});
}
function startCountdown(side) {
stopCountdown();
counting = true;
selectedSide = side;
var el = document.querySelector('.side-' + side);
if (el) el.classList.add('counting');
var bar = document.querySelector('.side-' + side + ' .hw-bar');
if (bar) {
bar.style.transform = 'scaleX(0)';
void bar.offsetWidth;
bar.classList.add('active');
}
countdownTimer = setTimeout(function () {
stopCountdown();
switchCount = 0;
increment(side);
}, 5000);
}
function handleWave() {
if (confirming) {
clearTimeout(confirmTimer);
confirmTimer = null;
confirming = false;
document.getElementById('reset-confirm').classList.remove('show');
switchCount = 0;
fetch('/reset', { method: 'POST' }).catch(function () {});
return;
}
if (counting) {
switchCount++;
if (switchCount >= 6) {
stopCountdown();
switchCount = 0;
confirming = true;
document.getElementById('reset-confirm').classList.add('show');
confirmTimer = setTimeout(function () {
confirming = false;
document.getElementById('reset-confirm').classList.remove('show');
}, 10000);
return;
}
var mode = (switchCount - 1) % 3;
if (mode === 0) {
// Show opposite side
var next = selectedSide === 'left' ? 'right' : 'left';
startCountdown(next);
} else if (mode === 1) {
// Hidden — mistake cancel, timer keeps running silently
var next = selectedSide === 'left' ? 'right' : 'left';
selectedSide = next;
stopCountdown();
counting = true;
countdownTimer = setTimeout(function () {
stopCountdown();
switchCount = 0;
increment(selectedSide);
}, 5000);
} else {
// Show original side again
startCountdown(originalWaveSide);
}
} else {
switchCount = 0;
originalWaveSide = selectedSide;
startCountdown(selectedSide);
}
}
document.addEventListener('keydown', function (e) {
if (e.code !== 'Space') return;
e.preventDefault();
handleWave();
});
var eventSource = new EventSource('/events');
eventSource.onmessage = function (e) {
var data = JSON.parse(e.data);
if (data.type === 'score') {
var prevLeft = scores.left;
var prevRight = scores.right;
scores.left = data.scores.left;
scores.right = data.scores.right;
if (scores.left !== prevLeft) {
selectedSide = 'left';
updateDisplay('left', scores.left);
triggerSweep('left');
}
if (scores.right !== prevRight) {
selectedSide = 'right';
updateDisplay('right', scores.right);
triggerSweep('right');
}
debounce = false;
} else if (data.type === 'config') {
location.reload();
}
};
// ---- Increment ----
function increment(side) {
if (confirming) {
clearTimeout(confirmTimer);
confirmTimer = null;
confirming = false;
document.getElementById('reset-confirm').classList.remove('show');
switchCount = 0;
}
fetch('/increment/' + side, { method: 'POST' }).catch(function () {});
}
// ---- Decrement ----
function decrement(side) {
fetch('/decrement/' + side, { method: 'POST' }).catch(function () {});
}
// ---- Update display ----
var scoreFontCache = {};
function updateDisplay(side, value) {
var el = document.getElementById('score-' + side);
el.textContent = value;
var sfs = parseFloat(el.dataset.scoreFontSize);
if (sfs > 0) {
el.style.fontSize = sfs + 'rem';
} else {
var key = side + '-' + String(value).length;
if (scoreFontCache[key]) {
el.style.fontSize = scoreFontCache[key] + 'px';
} else {
var sideEl = document.querySelector('.side-' + side);
var rect = sideEl.getBoundingClientRect();
fitTextHeight(el, rect.height * 0.55);
scoreFontCache[key] = parseFloat(el.style.fontSize) || 0;
}
}
el.classList.remove('score-pop');
void el.offsetWidth;
el.classList.add('score-pop');
}
// ---- Trigger sweep animation ----
var sweepTimers = {};
function triggerSweep(side) {
var overlay = document.getElementById('sweep-' + side);
if (!overlay) return;
overlay.classList.remove('sweeping');
overlay.style.animation = 'none';
void overlay.offsetHeight;
overlay.style.animation = '';
overlay.classList.add('sweeping');
if (sweepTimers[side]) clearTimeout(sweepTimers[side]);
sweepTimers[side] = setTimeout(function () {
overlay.classList.remove('sweeping');
sweepTimers[side] = null;
}, 1100);
}
// ---- Reset single side ----
function resetSide(side) {
fetch('/reset/' + side, { method: 'POST' }).catch(function () {});
}
})();