Compare commits
20 Commits
e318a2c058
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1c494ade | ||
| d9b2c5ee22 | |||
| 57c5f4e054 | |||
| 3c0ca7843f | |||
| a135746e37 | |||
| 56ecfaad54 | |||
| 1ad1d96cb2 | |||
| 1a2dc295e1 | |||
| 9d34d648d8 | |||
| 7e52e1768b | |||
| 3dc9bdcc21 | |||
| 22c4d28b42 | |||
| b5031c4f71 | |||
| b9d3b6115a | |||
| a9ccb42008 | |||
| 20f0122f59 | |||
| 548a7e29f0 | |||
| a01b584d10 | |||
| 191d66e6d4 | |||
| 462979e6f7 |
31
.replit
Normal file
31
.replit
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
modules = ["bun-1.3"]
|
||||||
|
[agent]
|
||||||
|
expertMode = true
|
||||||
|
|
||||||
|
[workflows]
|
||||||
|
runButton = "Project"
|
||||||
|
|
||||||
|
[[workflows.workflow]]
|
||||||
|
name = "Project"
|
||||||
|
mode = "parallel"
|
||||||
|
author = "agent"
|
||||||
|
|
||||||
|
[[workflows.workflow.tasks]]
|
||||||
|
task = "workflow.run"
|
||||||
|
args = "Start application"
|
||||||
|
|
||||||
|
[[workflows.workflow]]
|
||||||
|
name = "Start application"
|
||||||
|
author = "agent"
|
||||||
|
|
||||||
|
[[workflows.workflow.tasks]]
|
||||||
|
task = "shell.exec"
|
||||||
|
args = "PORT=5000 bun --hot run src/server.ts"
|
||||||
|
waitForPort = 5000
|
||||||
|
|
||||||
|
[workflows.workflow.metadata]
|
||||||
|
outputType = "webview"
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 5000
|
||||||
|
externalPort = 80
|
||||||
@@ -177,6 +177,12 @@
|
|||||||
onclick="this.select()" />
|
onclick="this.select()" />
|
||||||
<span class="side-hint">SEC</span>
|
<span class="side-hint">SEC</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tog-row">
|
||||||
|
<div class="lbl">LINK TIMER</div>
|
||||||
|
<label class="tog">
|
||||||
|
<input type="checkbox" id="timer-tog" onchange="toggleTimerFromSwitch()" /><span class="tog-track"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +192,6 @@
|
|||||||
<div class="side-btn-group">
|
<div class="side-btn-group">
|
||||||
<button class="btn btn-g btn-full" id="mod-round-btn" onclick="toggleRound()">▶ OPEN ROUND</button>
|
<button class="btn btn-g btn-full" id="mod-round-btn" onclick="toggleRound()">▶ OPEN ROUND</button>
|
||||||
<button class="btn btn-ghost btn-full" id="mod-resume-btn" style="display:none;" onclick="resumeRound()">▶ RESUME ROUND</button>
|
<button class="btn btn-ghost btn-full" id="mod-resume-btn" style="display:none;" onclick="resumeRound()">▶ RESUME ROUND</button>
|
||||||
<button class="btn btn-yellow btn-full" onclick="resetBuzzer()">↺ RESET BUZZER</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,7 +232,7 @@
|
|||||||
<div id="tab-buzzer">
|
<div id="tab-buzzer">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">BUZZ ORDER</div>
|
<div class="panel-title">BUZZ ORDER</div>
|
||||||
<div class="empty" id="mod-bz-empty">No buzzes — open a round to begin.</div>
|
<div class="empty" id="mod-bz-empty">No buzzes</div>
|
||||||
<div class="buzz-list" id="mod-bz-list"></div>
|
<div class="buzz-list" id="mod-bz-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,29 +79,28 @@ function handle(msg){
|
|||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();renderModSettings();}else renderPlayer();
|
if(role==='mod'){renderMod();renderRoundButtons();renderModSettings();}else renderPlayer();
|
||||||
break;
|
break;
|
||||||
|
case 'new_round':
|
||||||
|
toast('NEW ROUND STARTED','ok');
|
||||||
|
break;
|
||||||
case 'round_open':
|
case 'round_open':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}
|
if(role==='mod'){renderMod();renderRoundButtons();}
|
||||||
else{renderPlayerBuzzer();startPlayerTimer();toast('ROUND OPEN','ok');}
|
else{renderPlayerBuzzer();startPlayerTimer();}
|
||||||
|
// If timer is enabled and this is a fresh open, load the timer
|
||||||
|
if(room.settings.timerSeconds>0 && modTimerRemaining===0 && !modTimerRunning){
|
||||||
|
modTimerRemaining=room.settings.timerSeconds;
|
||||||
|
renderModTimerDisplay();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'round_closed':
|
case 'round_closed':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}
|
if(role==='mod'){renderMod();renderRoundButtons();toast('ROUND CLOSED','warn');}
|
||||||
else{renderPlayerBuzzer();stopPlayerTimer();toast('ROUND CLOSED','warn');}
|
else{renderPlayerBuzzer();stopPlayerTimer();}
|
||||||
break;
|
break;
|
||||||
case 'buzzer_reset':
|
case 'buzzer_reset':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
|
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
|
||||||
break;
|
break;
|
||||||
case 'round_closed':
|
|
||||||
room=msg.room;
|
|
||||||
if(role==='mod')renderMod();
|
|
||||||
else{renderPlayerBuzzer();stopPlayerTimer();toast('ROUND CLOSED','warn');}
|
|
||||||
break;
|
|
||||||
case 'buzzer_reset':
|
|
||||||
room=msg.room;
|
|
||||||
if(role==='mod')renderMod();else renderPlayerBuzzer();
|
|
||||||
break;
|
|
||||||
case 'buzz_event':
|
case 'buzz_event':
|
||||||
room=msg.room;
|
room=msg.room;
|
||||||
if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
|
if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
|
||||||
@@ -323,6 +322,41 @@ function modTimerToggle(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTimer(){
|
||||||
|
modTimerToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function modTimerToggle(){
|
||||||
|
if(modTimerRunning){
|
||||||
|
clearInterval(modTimerInterval);modTimerRunning=false;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='START';
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,false);
|
||||||
|
} else {
|
||||||
|
if(modTimerRemaining<=0)modTimerLoad();
|
||||||
|
modTimerRunning=true;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='PAUSE';
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,true);
|
||||||
|
modTimerInterval=setInterval(()=>{
|
||||||
|
modTimerRemaining--;
|
||||||
|
renderModTimerDisplay();
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,true);
|
||||||
|
if(modTimerRemaining<=0){
|
||||||
|
clearInterval(modTimerInterval);modTimerRunning=false;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='START';
|
||||||
|
ws_send({type:'close_round'});
|
||||||
|
toast('TIME UP — round closed','warn');
|
||||||
|
if(typeof gsap!=='undefined'){
|
||||||
|
gsap.to('#mod-timer-disp',{scale:1.08,duration:0.1,yoyo:true,repeat:3,ease:'power2.inOut'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTimerFromSwitch(){
|
||||||
|
modTimerToggle();
|
||||||
|
}
|
||||||
|
|
||||||
function renderModTimerDisplay(){
|
function renderModTimerDisplay(){
|
||||||
const el=document.getElementById('mod-timer-disp');
|
const el=document.getElementById('mod-timer-disp');
|
||||||
const s=modTimerRemaining;
|
const s=modTimerRemaining;
|
||||||
@@ -330,6 +364,23 @@ function renderModTimerDisplay(){
|
|||||||
el.className='timer-digits'+(s<=5?' danger':s<=10?' warn':'');
|
el.className='timer-digits'+(s<=5?' danger':s<=10?' warn':'');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modTimerLoad(){
|
||||||
|
const sec=parseInt(document.getElementById('mod-timer-set').value)||30;
|
||||||
|
modTimerRemaining=sec;
|
||||||
|
modTimerRunning=false;
|
||||||
|
clearInterval(modTimerInterval);
|
||||||
|
document.getElementById('btn-timer-ss').textContent='START';
|
||||||
|
renderModTimerDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function modTimerReset(){
|
||||||
|
clearInterval(modTimerInterval);
|
||||||
|
modTimerRunning=false;
|
||||||
|
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
|
||||||
|
renderModTimerDisplay();
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,false);
|
||||||
|
}
|
||||||
|
|
||||||
function broadcastTimerToPlayers(sec,running){
|
function broadcastTimerToPlayers(sec,running){
|
||||||
try{
|
try{
|
||||||
const bc=new BroadcastChannel('buzzer_timer');
|
const bc=new BroadcastChannel('buzzer_timer');
|
||||||
@@ -344,27 +395,69 @@ function broadcastTimerToPlayers(sec,running){
|
|||||||
function toggleRound(){
|
function toggleRound(){
|
||||||
if(room.buzzerState.roundOpen){
|
if(room.buzzerState.roundOpen){
|
||||||
ws_send({type:'close_round'});
|
ws_send({type:'close_round'});
|
||||||
|
// If linked (toggle ON) and timer running, pause it
|
||||||
|
if(document.getElementById('timer-tog').checked && modTimerRunning){
|
||||||
|
modTimerToggle();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ws_send({type:'open_round'});
|
ws_send({type:'open_round'});
|
||||||
|
// If linked (toggle ON) and timer stopped, reset and start it
|
||||||
|
if(document.getElementById('timer-tog').checked && !modTimerRunning){
|
||||||
|
modTimerLoad();
|
||||||
|
modTimerRunning=true;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='PAUSE';
|
||||||
|
modTimerInterval=setInterval(()=>{
|
||||||
|
modTimerRemaining--;
|
||||||
|
renderModTimerDisplay();
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,true);
|
||||||
|
if(modTimerRemaining<=0){
|
||||||
|
clearInterval(modTimerInterval);modTimerRunning=false;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='START';
|
||||||
|
ws_send({type:'close_round'});
|
||||||
|
toast('TIME UP — round closed','warn');
|
||||||
|
}
|
||||||
|
},1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resumeRound(){
|
function resumeRound(){
|
||||||
ws_send({type:'open_round'});
|
ws_send({type:'resume_round'});
|
||||||
|
// If linked (toggle ON) and timer stopped, resume it (preserve current value)
|
||||||
|
if(document.getElementById('timer-tog').checked && !modTimerRunning){
|
||||||
|
modTimerRunning=true;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='PAUSE';
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,true);
|
||||||
|
modTimerInterval=setInterval(()=>{
|
||||||
|
modTimerRemaining--;
|
||||||
|
renderModTimerDisplay();
|
||||||
|
broadcastTimerToPlayers(modTimerRemaining,true);
|
||||||
|
if(modTimerRemaining<=0){
|
||||||
|
clearInterval(modTimerInterval);modTimerRunning=false;
|
||||||
|
document.getElementById('btn-timer-ss').textContent='START';
|
||||||
|
ws_send({type:'close_round'});
|
||||||
|
toast('TIME UP — round closed','warn');
|
||||||
|
}
|
||||||
|
},1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// TIMER
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
|
||||||
function renderRoundButtons(){
|
function renderRoundButtons(){
|
||||||
const openBtn=document.getElementById('mod-round-btn');
|
const btn=document.getElementById('mod-round-btn');
|
||||||
const resumeBtn=document.getElementById('mod-resume-btn');
|
const resumeBtn=document.getElementById('mod-resume-btn');
|
||||||
|
|
||||||
if(room.buzzerState.roundOpen){
|
if(room.buzzerState.roundOpen){
|
||||||
openBtn.innerHTML='■ CLOSE ROUND';
|
btn.innerHTML='■ PAUSE ROUND';
|
||||||
openBtn.className='btn btn-red btn-full';
|
btn.className='btn btn-red btn-full';
|
||||||
resumeBtn.style.display='block';
|
|
||||||
} else {
|
|
||||||
openBtn.innerHTML='▶ OPEN ROUND';
|
|
||||||
openBtn.className='btn btn-g btn-full';
|
|
||||||
resumeBtn.style.display='none';
|
resumeBtn.style.display='none';
|
||||||
|
} else {
|
||||||
|
btn.innerHTML='▶ OPEN ROUND';
|
||||||
|
btn.className='btn btn-g btn-full';
|
||||||
|
resumeBtn.style.display='block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +586,7 @@ function renderModSettings(){
|
|||||||
segActivate('ls-seg-mode',s.mode);
|
segActivate('ls-seg-mode',s.mode);
|
||||||
renderLiveTeamNames();
|
renderLiveTeamNames();
|
||||||
renderRoundButtons();
|
renderRoundButtons();
|
||||||
|
renderModTimerDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function segActivate(groupId,val){
|
function segActivate(groupId,val){
|
||||||
|
|||||||
@@ -135,6 +135,15 @@ export function handleMessage(ws: WS, raw: string) {
|
|||||||
room.buzzerState = freshBuzzer();
|
room.buzzerState = freshBuzzer();
|
||||||
room.buzzerState.roundOpen = true;
|
room.buzzerState.roundOpen = true;
|
||||||
broadcast(room, { type: "round_open", room: publicRoom(room) });
|
broadcast(room, { type: "round_open", room: publicRoom(room) });
|
||||||
|
// Send new_round notification to all players (with toast)
|
||||||
|
const d = JSON.stringify({ type: "new_round" });
|
||||||
|
for (const p of room.players.values()) if (p.ws && p.isConnected) try { p.ws.send(d); } catch {}
|
||||||
|
if (room.modWs) try { room.modWs.send(d); } catch {}
|
||||||
|
break;
|
||||||
|
case "resume_round":
|
||||||
|
// Open round WITHOUT clearing existing buzzes (for accidental close recovery)
|
||||||
|
room.buzzerState.roundOpen = true;
|
||||||
|
broadcast(room, { type: "round_open", room: publicRoom(room) });
|
||||||
break;
|
break;
|
||||||
case "close_round":
|
case "close_round":
|
||||||
room.buzzerState.roundOpen = false;
|
room.buzzerState.roundOpen = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user