Compare commits
23 Commits
9c9a95206d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1c494ade | ||
| d9b2c5ee22 | |||
| 57c5f4e054 | |||
| 3c0ca7843f | |||
| a135746e37 | |||
| 56ecfaad54 | |||
| 1ad1d96cb2 | |||
| 1a2dc295e1 | |||
| 9d34d648d8 | |||
| 7e52e1768b | |||
| 3dc9bdcc21 | |||
| 22c4d28b42 | |||
| b5031c4f71 | |||
| b9d3b6115a | |||
| a9ccb42008 | |||
| 20f0122f59 | |||
| 548a7e29f0 | |||
| a01b584d10 | |||
| 191d66e6d4 | |||
| 462979e6f7 | |||
| e318a2c058 | |||
| c9795f816c | |||
| 01cd50abf7 |
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,16 +177,21 @@
|
||||
onclick="this.select()" />
|
||||
<span class="side-hint">SEC</span>
|
||||
</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>
|
||||
|
||||
<!-- BUZZER CONTROLS -->
|
||||
<div class="side-sec">
|
||||
<div class="side-label">BUZZER CONTROLS</div>
|
||||
<div class="side-label">ROUND CONTROL</div>
|
||||
<div class="side-btn-group">
|
||||
<button class="btn btn-g btn-full" onclick="ws_send({type:'open_round'})">▶ OPEN ROUND</button>
|
||||
<button class="btn btn-ghost btn-full" onclick="ws_send({type:'close_round'})">■ CLOSE ROUND</button>
|
||||
<button class="btn btn-yellow btn-full" onclick="ws_send({type:'reset_buzzer'})">↺ RESET BUZZER</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +232,7 @@
|
||||
<div id="tab-buzzer">
|
||||
<div class="panel">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -73,29 +73,37 @@ function handle(msg){
|
||||
break;
|
||||
case 'room_update':
|
||||
room=msg.room;
|
||||
if(role==='mod')renderMod();else renderPlayer();
|
||||
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayer();
|
||||
break;
|
||||
case 'settings_updated':
|
||||
room=msg.room;
|
||||
if(role==='mod'){renderMod();renderModSettings();}else renderPlayer();
|
||||
if(role==='mod'){renderMod();renderRoundButtons();renderModSettings();}else renderPlayer();
|
||||
break;
|
||||
case 'new_round':
|
||||
toast('NEW ROUND STARTED','ok');
|
||||
break;
|
||||
case 'round_open':
|
||||
room=msg.room;
|
||||
if(role==='mod')renderMod();
|
||||
else{renderPlayerBuzzer();startPlayerTimer();toast('ROUND OPEN','ok');}
|
||||
if(role==='mod'){renderMod();renderRoundButtons();}
|
||||
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;
|
||||
case 'round_closed':
|
||||
room=msg.room;
|
||||
if(role==='mod')renderMod();
|
||||
else{renderPlayerBuzzer();stopPlayerTimer();toast('ROUND CLOSED','warn');}
|
||||
if(role==='mod'){renderMod();renderRoundButtons();toast('ROUND CLOSED','warn');}
|
||||
else{renderPlayerBuzzer();stopPlayerTimer();}
|
||||
break;
|
||||
case 'buzzer_reset':
|
||||
room=msg.room;
|
||||
if(role==='mod')renderMod();else renderPlayerBuzzer();
|
||||
if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
|
||||
break;
|
||||
case 'buzz_event':
|
||||
room=msg.room;
|
||||
if(role==='mod')renderModBuzz(msg);else{renderPlayerBuzzer();addFeed(msg);}
|
||||
if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
|
||||
break;
|
||||
case 'buzz_rejected':
|
||||
toast(msg.reason,'warn');break;
|
||||
@@ -272,7 +280,6 @@ function createRoom(){
|
||||
// MOD TIMER
|
||||
// ══════════════════════════════════════════════════════
|
||||
function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);}
|
||||
|
||||
function modTimerLoad(){
|
||||
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
|
||||
modTimerRunning=false;
|
||||
@@ -315,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(){
|
||||
const el=document.getElementById('mod-timer-disp');
|
||||
const s=modTimerRemaining;
|
||||
@@ -322,6 +364,23 @@ function renderModTimerDisplay(){
|
||||
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){
|
||||
try{
|
||||
const bc=new BroadcastChannel('buzzer_timer');
|
||||
@@ -330,6 +389,78 @@ function broadcastTimerToPlayers(sec,running){
|
||||
}catch{}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// MOD ROUND CONTROL
|
||||
// ══════════════════════════════════════════════════════
|
||||
function toggleRound(){
|
||||
if(room.buzzerState.roundOpen){
|
||||
ws_send({type:'close_round'});
|
||||
// If linked (toggle ON) and timer running, pause it
|
||||
if(document.getElementById('timer-tog').checked && modTimerRunning){
|
||||
modTimerToggle();
|
||||
}
|
||||
} else {
|
||||
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(){
|
||||
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(){
|
||||
const btn=document.getElementById('mod-round-btn');
|
||||
const resumeBtn=document.getElementById('mod-resume-btn');
|
||||
|
||||
if(room.buzzerState.roundOpen){
|
||||
btn.innerHTML='■ PAUSE ROUND';
|
||||
btn.className='btn btn-red btn-full';
|
||||
resumeBtn.style.display='none';
|
||||
} else {
|
||||
btn.innerHTML='▶ OPEN ROUND';
|
||||
btn.className='btn btn-g btn-full';
|
||||
resumeBtn.style.display='block';
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// MOD RENDER
|
||||
// ══════════════════════════════════════════════════════
|
||||
@@ -351,6 +482,7 @@ function renderMod(){
|
||||
renderModBuzz(null);
|
||||
renderModPlayerList();
|
||||
renderModTeams();
|
||||
renderRoundButtons();
|
||||
}
|
||||
|
||||
function renderModBuzz(evt){
|
||||
@@ -453,6 +585,8 @@ function renderModSettings(){
|
||||
document.getElementById('ls-numteams').value=s.numTeams;
|
||||
segActivate('ls-seg-mode',s.mode);
|
||||
renderLiveTeamNames();
|
||||
renderRoundButtons();
|
||||
renderModTimerDisplay();
|
||||
}
|
||||
|
||||
function segActivate(groupId,val){
|
||||
|
||||
@@ -135,6 +135,15 @@ export function handleMessage(ws: WS, raw: string) {
|
||||
room.buzzerState = freshBuzzer();
|
||||
room.buzzerState.roundOpen = true;
|
||||
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;
|
||||
case "close_round":
|
||||
room.buzzerState.roundOpen = false;
|
||||
|
||||
Reference in New Issue
Block a user