Compare commits

..

36 Commits

Author SHA1 Message Date
keshavmathguy
1a1c494ade Saved progress at the end of the loop
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 22590254-16a6-4e67-8db6-70bf23ec5efa
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 480bdc5f-3460-4340-bad8-55c04b01e09d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3f3f1d57-54c5-43b3-bdea-ac659ef0a32c/22590254-16a6-4e67-8db6-70bf23ec5efa/PuWDZtN
Replit-Helium-Checkpoint-Created: true
2026-04-10 18:38:21 +00:00
d9b2c5ee22 Change timer toggle label to 'LINK TIMER', remove 'open a round' text from empty buzzer state 2026-04-08 20:34:50 -05:00
57c5f4e054 Open Round: start timer if toggle ON, regardless of current value 2026-04-08 20:31:08 -05:00
3c0ca7843f Open Round: properly reset and start timer when toggle ON 2026-04-08 20:30:56 -05:00
a135746e37 Resume Round: preserve current timer value instead of restarting from set value 2026-04-08 20:29:42 -05:00
56ecfaad54 Timer toggle: manual ON/OFF control, buttons auto-link when ON, NEVER auto-uncheck 2026-04-08 20:28:17 -05:00
1ad1d96cb2 Timer toggle: manual ON/OFF control, buttons auto-link when ON, never auto-uncheck 2026-04-08 20:24:44 -05:00
1a2dc295e1 Timer toggle: manual ON/OFF control, links to Open/Pause/Resume buttons, auto-syncs state 2026-04-08 19:28:07 -05:00
9d34d648d8 Round closed: uncheck timer toggle when round ends 2026-04-08 19:25:26 -05:00
7e52e1768b Fix timer toggle: manual control, starts from set value, syncs with modTimerRunning 2026-04-08 19:25:22 -05:00
3dc9bdcc21 Link timer toggle to Open/Pause/Resume buttons — timer auto-starts/pauses/resumes with round 2026-04-08 19:21:32 -05:00
22c4d28b42 Fix renderMod() duplicate code that broke script execution 2026-04-08 19:18:31 -05:00
b5031c4f71 Add timer toggle switch beneath timer block — links to START/STOP buttons 2026-04-08 19:17:44 -05:00
b9d3b6115a Fix resumeRound: send resume_round type instead of open_round to preserve buzzes 2026-04-08 19:11:02 -05:00
a9ccb42008 Add resume_round message and NEW ROUND toast notification for fresh round starts 2026-04-08 19:09:28 -05:00
20f0122f59 Fix toggleRound logic: Open Round opens, Pause Round closes 2026-04-08 19:07:34 -05:00
548a7e29f0 Fix toggleRound: Pause Round now actually closes the round (not open) 2026-04-08 19:06:26 -05:00
a01b584d10 Mod: update round_closed handler to show resume button for accidental closes 2026-04-08 19:05:46 -05:00
191d66e6d4 Mod buttons: show Resume Round button when round is closed/paused 2026-04-08 19:05:41 -05:00
462979e6f7 Remove reset buzzer button — Open Round now resets and becomes Pause Round 2026-04-08 19:05:36 -05:00
e318a2c058 Mod settings panel: call renderRoundButtons() for consistent button state 2026-04-08 19:02:49 -05:00
c9795f816c Mod round buttons: clean button state management without extra animations 2026-04-08 19:02:44 -05:00
01cd50abf7 Redesign mod UI — combine round controls into toggle system with pause/resume 2026-04-08 19:02:37 -05:00
9c9a95206d Improve team picker and buzz button for senior-friendly sizing and clarity 2026-04-08 18:57:49 -05:00
ed46de26b3 Simplify player page — remove ripple effects, animations, and decorations for cleaner senior-friendly UI 2026-04-08 18:57:40 -05:00
b69c442c90 Mod UI: display numeric IDs in player list, buzz order, and team members instead of names 2026-04-08 18:54:15 -05:00
cddbdfaae8 Client: show numeric IDs in player roster and buzz feed instead of names 2026-04-08 18:53:43 -05:00
c00db744c5 Server: assign sequential numeric IDs (1-2-3...) to players on join instead of random strings 2026-04-08 18:53:24 -05:00
b76bcbffb2 Update join logic — remove playerName from join request, store numeric ID only 2026-04-08 18:53:05 -05:00
67bd8d9e77 Remove name input from landing page — players assigned numeric IDs instead 2026-04-08 18:52:51 -05:00
7696b6005b Increase font sizes for senior citizen accessibility
- Raised base HTML font size from 18px to 22px
- Increased header logo from 22px to 32px
- Enhanced all text labels from 13-15px to 16-17px for readability
- Made timer display larger (72px to 84px for players, 56px to 52px for mod)
- Increased room codes from 34-44px to 42-52px
- Made tab navigation labels larger (14px to 17px)
- Updated all panel titles and section labels
- Enlarged player names from 16px to 18px
- Increased buzz status and hints from 14-16px to 16-18px
- Made modal text and toast notifications more readable
- Adjusted responsive breakpoints for mobile devices
- All interactive elements (toggles, buttons) preserved unchanged
2026-04-08 18:41:31 -05:00
c08c0831b1 fixed run 2026-03-26 00:17:59 -05:00
6afb853874 Update application aesthetics with a pastel color scheme and larger text
Introduce the Catppuccin Mocha color palette and increase font sizes for improved readability, while also adding a visual hint for spacebar functionality.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f3ac8eb3-f610-4678-ab6e-ebf900098be4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 098ca8d5-1a49-468d-abec-89858708710d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/d4b7863b-f7b2-425c-a9b5-ad7bd1885e9d/f3ac8eb3-f610-4678-ab6e-ebf900098be4/N1qS6qo
Replit-Helium-Checkpoint-Created: true
2026-03-25 21:14:15 +00:00
c0e7c08106 Integrate buzzer functionality into Bun server and update workflows
Reverts Express server setup for the buzzer, migrates all functionality to a Bun server, and updates the workflow to execute the Bun server directly.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f3ac8eb3-f610-4678-ab6e-ebf900098be4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c1942b0a-0cf4-4ca9-9b5f-f80287c102f2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/d4b7863b-f7b2-425c-a9b5-ad7bd1885e9d/f3ac8eb3-f610-4678-ab6e-ebf900098be4/N1qS6qo
Replit-Helium-Checkpoint-Created: true
2026-03-25 21:08:30 +00:00
83bd893d93 Update buzzer app to serve static files and handle WebSocket connections
Integrates static file serving for HTML, CSS, and JS, and sets up a WebSocket server for real-time communication.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f3ac8eb3-f610-4678-ab6e-ebf900098be4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 182c9671-a459-4b94-a7eb-1c7a0cefe768
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/d4b7863b-f7b2-425c-a9b5-ad7bd1885e9d/f3ac8eb3-f610-4678-ab6e-ebf900098be4/N1qS6qo
Replit-Helium-Checkpoint-Created: true
2026-03-25 21:07:18 +00:00
ce13860cd1 Extracted stack files 2026-03-25 20:56:28 +00:00
5 changed files with 444 additions and 199 deletions

31
.replit Normal file
View 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

View File

@@ -50,16 +50,12 @@
<div class="land-card"> <div class="land-card">
<div class="land-card-icon"></div> <div class="land-card-icon"></div>
<h2>// JOIN A SESSION</h2> <h2>// JOIN A SESSION</h2>
<p>Enter a room code and your name to join an existing session.</p> <p>Enter a room code to join an existing session. You'll be assigned a numeric ID.</p>
<div class="field"> <div class="field">
<label>ROOM CODE</label> <label>ROOM CODE</label>
<input id="ji-code" maxlength="8" placeholder="XXXXXX" style="letter-spacing:5px;text-transform:uppercase;font-size:20px;" <input id="ji-code" maxlength="8" placeholder="XXXXXX" style="letter-spacing:5px;text-transform:uppercase;font-size:20px;"
oninput="this.value=this.value.toUpperCase()" /> oninput="this.value=this.value.toUpperCase()" />
</div> </div>
<div class="field">
<label>YOUR NAME</label>
<input id="ji-name" maxlength="24" placeholder="Enter name…" />
</div>
<button class="btn btn-g btn-full" onclick="joinRoom()">JOIN ROOM →</button> <button class="btn btn-g btn-full" onclick="joinRoom()">JOIN ROOM →</button>
</div> </div>
</div> </div>
@@ -181,16 +177,21 @@
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>
<!-- BUZZER CONTROLS --> <!-- BUZZER CONTROLS -->
<div class="side-sec"> <div class="side-sec">
<div class="side-label">BUZZER CONTROLS</div> <div class="side-label">ROUND CONTROL</div>
<div class="side-btn-group"> <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-g btn-full" id="mod-round-btn" onclick="toggleRound()">▶ OPEN ROUND</button>
<button class="btn btn-ghost btn-full" onclick="ws_send({type:'close_round'})">■ CLOSE 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="ws_send({type:'reset_buzzer'})">↺ RESET BUZZER</button>
</div> </div>
</div> </div>
@@ -231,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>
@@ -331,14 +332,8 @@
</div> </div>
<div class="buzz-wrap"> <div class="buzz-wrap">
<div class="buzz-ripple" id="buzz-ripple" style="display:none;">
<div class="buzz-ring"></div>
<div class="buzz-ring"></div>
<div class="buzz-ring"></div>
</div>
<button id="buzz-btn" class="s-closed" disabled onclick="doBuzz()">BUZZ</button> <button id="buzz-btn" class="s-closed" disabled onclick="doBuzz()">BUZZ</button>
<div class="buzz-status" id="buzz-status">WAITING FOR ROUND</div> <div class="buzz-status" id="buzz-status">WAITING FOR ROUND</div>
<div class="spacebar-hint" id="spacebar-hint">SPACEBAR TO BUZZ</div>
</div> </div>
<div class="p-panel"> <div class="p-panel">

View File

@@ -24,7 +24,7 @@ let playerTimerRemaining=0,playerTimerInterval=null;
const saveMod=(id,s)=>localStorage.setItem('mod',JSON.stringify({id,s})); const saveMod=(id,s)=>localStorage.setItem('mod',JSON.stringify({id,s}));
const loadMod=()=>{try{return JSON.parse(localStorage.getItem('mod')||'null');}catch{return null;}}; const loadMod=()=>{try{return JSON.parse(localStorage.getItem('mod')||'null');}catch{return null;}};
const clearMod=()=>{localStorage.removeItem('mod');document.getElementById('rejoin-bar').style.display='none';}; const clearMod=()=>{localStorage.removeItem('mod');document.getElementById('rejoin-bar').style.display='none';};
const savePlay=(rid,pid,name)=>localStorage.setItem('play',JSON.stringify({rid,pid,name})); const savePlay=(rid,pid)=>localStorage.setItem('play',JSON.stringify({rid,pid}));
const loadPlay=()=>{try{return JSON.parse(localStorage.getItem('play')||'null');}catch{return null;}}; const loadPlay=()=>{try{return JSON.parse(localStorage.getItem('play')||'null');}catch{return null;}};
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
@@ -68,34 +68,42 @@ function handle(msg){
break; break;
case 'joined': case 'joined':
myId=msg.playerId;room=msg.room;role='player'; myId=msg.playerId;room=msg.room;role='player';
savePlay(room.id,myId,document.getElementById('ji-name').value||loadPlay()?.name||''); savePlay(room.id,myId);
showScr('s-player');renderPlayer(); showScr('s-player');renderPlayer();
break; break;
case 'room_update': case 'room_update':
room=msg.room; room=msg.room;
if(role==='mod')renderMod();else renderPlayer(); if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayer();
break; break;
case 'settings_updated': case 'settings_updated':
room=msg.room; 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; break;
case 'round_open': case 'round_open':
room=msg.room; room=msg.room;
if(role==='mod')renderMod(); 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(); 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();else renderPlayerBuzzer(); if(role==='mod'){renderMod();renderRoundButtons();}else renderPlayerBuzzer();
break; break;
case 'buzz_event': case 'buzz_event':
room=msg.room; room=msg.room;
if(role==='mod')renderModBuzz(msg);else{renderPlayerBuzzer();addFeed(msg);} if(role==='mod'){renderMod();renderRoundButtons();}else{renderPlayerBuzzer();addFeed(msg);}
break; break;
case 'buzz_rejected': case 'buzz_rejected':
toast(msg.reason,'warn');break; toast(msg.reason,'warn');break;
@@ -171,10 +179,8 @@ function setConn(on){
function goSetup(){renderSetupTeamNames();showScr('s-setup');} function goSetup(){renderSetupTeamNames();showScr('s-setup');}
function joinRoom(){ function joinRoom(){
const code=document.getElementById('ji-code').value.trim().toUpperCase(); const code=document.getElementById('ji-code').value.trim().toUpperCase();
const name=document.getElementById('ji-name').value.trim();
if(!code){toast('Enter room code','err');return;} if(!code){toast('Enter room code','err');return;}
if(!name){toast('Enter your name','err');return;} connect(()=>ws_send({type:'join_room',roomId:code}));
connect(()=>ws_send({type:'join_room',roomId:code,playerName:name}));
} }
function openRejoin(){ function openRejoin(){
const m=loadMod();if(!m)return; const m=loadMod();if(!m)return;
@@ -274,7 +280,6 @@ function createRoom(){
// MOD TIMER // MOD TIMER
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);} function fmtTime(s){return Math.floor(s/60)+':'+(s%60<10?'0':'')+(s%60);}
function modTimerLoad(){ function modTimerLoad(){
modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30); modTimerRemaining=Math.max(5,parseInt(document.getElementById('mod-timer-set').value)||30);
modTimerRunning=false; modTimerRunning=false;
@@ -317,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;
@@ -324,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');
@@ -332,6 +389,78 @@ function broadcastTimerToPlayers(sec,running){
}catch{} }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 // MOD RENDER
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════
@@ -353,6 +482,7 @@ function renderMod(){
renderModBuzz(null); renderModBuzz(null);
renderModPlayerList(); renderModPlayerList();
renderModTeams(); renderModTeams();
renderRoundButtons();
} }
function renderModBuzz(evt){ function renderModBuzz(evt){
@@ -374,7 +504,7 @@ function renderModBuzz(evt){
div.innerHTML=` div.innerHTML=`
<div class="bz-rank ${idx===0?'first':''}">${idx+1}</div> <div class="bz-rank ${idx===0?'first':''}">${idx+1}</div>
<div class="bz-info"> <div class="bz-info">
<div class="bz-name">${esc(p.name)}</div> <div class="bz-name">${pid}</div>
${teamName?`<div class="bz-team" style="color:${color}">${esc(teamName)}</div>`:''} ${teamName?`<div class="bz-team" style="color:${color}">${esc(teamName)}</div>`:''}
${ms?`<div class="bz-ms">${ms} after first</div>`:''} ${ms?`<div class="bz-ms">${ms} after first</div>`:''}
</div> </div>
@@ -411,7 +541,7 @@ function renderModPlayerList(){
row.className='pl-row'+(p.isConnected?'':' offline'); row.className='pl-row'+(p.isConnected?'':' offline');
row.innerHTML=` row.innerHTML=`
<div class="pl-info" style="flex:1;min-width:0;"> <div class="pl-info" style="flex:1;min-width:0;">
<div class="pl-name">${esc(p.name)} ${p.isConnected?'':`<span class="tag tag-red" style="font-size:9px;padding:2px 6px;">OFFLINE</span>`}</div> <div class="pl-name">${p.id} ${p.isConnected?'':`<span class="tag tag-red" style="font-size:9px;padding:2px 6px;">OFFLINE</span>`}</div>
${teamName?`<div class="pl-meta" style="color:${color}">${esc(teamName)}</div>`:'<div class="pl-meta">No team</div>'} ${teamName?`<div class="pl-meta" style="color:${color}">${esc(teamName)}</div>`:'<div class="pl-meta">No team</div>'}
${teamSel} ${teamSel}
</div> </div>
@@ -437,7 +567,7 @@ function renderModTeams(){
card.innerHTML=` card.innerHTML=`
<div class="tc-n" style="color:${color}">${esc(name)}</div> <div class="tc-n" style="color:${color}">${esc(name)}</div>
<div class="tc-c" style="color:${color}">${members.length}</div> <div class="tc-c" style="color:${color}">${members.length}</div>
<div class="tc-m">${members.map(p=>esc(p.name)).join('<br>')||'—'}</div> <div class="tc-m">${members.map(p=>p.id).join('<br>')||'—'}</div>
`; `;
grid.appendChild(card); grid.appendChild(card);
if(typeof gsap!=='undefined'){ if(typeof gsap!=='undefined'){
@@ -455,6 +585,8 @@ function renderModSettings(){
document.getElementById('ls-numteams').value=s.numTeams; document.getElementById('ls-numteams').value=s.numTeams;
segActivate('ls-seg-mode',s.mode); segActivate('ls-seg-mode',s.mode);
renderLiveTeamNames(); renderLiveTeamNames();
renderRoundButtons();
renderModTimerDisplay();
} }
function segActivate(groupId,val){ function segActivate(groupId,val){
@@ -527,7 +659,7 @@ function renderPlayer(){
if(!room)return; if(!room)return;
document.getElementById('p-code').textContent=room.id; document.getElementById('p-code').textContent=room.id;
const me=room.players.find(p=>p.id===myId); const me=room.players.find(p=>p.id===myId);
document.getElementById('p-namelbl').textContent=me?.name??''; document.getElementById('p-namelbl').textContent=me?.id||'';
renderTeamPicker(); renderTeamPicker();
renderPlayerBuzzer(); renderPlayerBuzzer();
renderRoster(); renderRoster();
@@ -550,21 +682,20 @@ function renderTeamPicker(){
btn.className='team-btn'+(isMine?' mine':''); btn.className='team-btn'+(isMine?' mine':'');
btn.style.borderColor=isMine?color:'var(--border2)'; btn.style.borderColor=isMine?color:'var(--border2)';
btn.style.color=isMine?color:'var(--text)'; btn.style.color=isMine?color:'var(--text)';
btn.innerHTML=`<div>${esc(s.teamNames[i]??greekName(i))}</div><div class="tb-count">${members.length} player${members.length!==1?'s':''}</div>`; btn.style.padding='16px';
btn.style.fontSize='20px';
btn.style.fontWeight='600';
btn.style.cursor='pointer';
btn.innerHTML=`<div style="font-size:24px;">${esc(s.teamNames[i]??greekName(i))}</div><div style="font-size:14px;color:var(--dim);margin-top:4px;">${members.length} player${members.length!==1?'s':''}</div>`;
btn.onclick=()=>ws_send({type:'pick_team',teamIndex:i}); btn.onclick=()=>ws_send({type:'pick_team',teamIndex:i});
grid.appendChild(btn); grid.appendChild(btn);
if(typeof gsap!=='undefined'){
gsap.fromTo(btn,{opacity:0,y:10},{opacity:1,y:0,duration:0.3,ease:'power2.out',delay:i*0.04});
}
} }
} }
let _lastBuzzState='';
function renderPlayerBuzzer(){ function renderPlayerBuzzer(){
if(!room)return; if(!room)return;
const btn=document.getElementById('buzz-btn'); const btn=document.getElementById('buzz-btn');
const sts=document.getElementById('buzz-status'); const sts=document.getElementById('buzz-status');
const ripple=document.getElementById('buzz-ripple');
const bz=room.buzzerState; const bz=room.buzzerState;
const already=bz.buzzOrder.includes(myId); const already=bz.buzzOrder.includes(myId);
const isFirst=bz.buzzOrder[0]===myId; const isFirst=bz.buzzOrder[0]===myId;
@@ -574,49 +705,33 @@ function renderPlayerBuzzer(){
if(!bz.roundOpen){ if(!bz.roundOpen){
newState='closed'; newState='closed';
btn.className='s-closed';btn.disabled=true;sts.textContent='WAITING FOR ROUND'; btn.className='s-closed';btn.disabled=true;sts.textContent='WAITING FOR ROUND';
ripple.style.display='none';
if(hint)hint.classList.remove('visible'); if(hint)hint.classList.remove('visible');
} else if(isFirst){ } else if(isFirst){
newState='first'; newState='first';
btn.className='s-first';btn.disabled=false;sts.textContent='YOU BUZZED FIRST!'; btn.className='s-first';btn.disabled=false;sts.textContent='YOU BUZZED FIRST!';
ripple.style.display='flex';
if(hint)hint.classList.remove('visible'); if(hint)hint.classList.remove('visible');
} else if(already){ } else if(already){
const pos=bz.buzzOrder.indexOf(myId)+1; const pos=bz.buzzOrder.indexOf(myId)+1;
newState='buzzed'; newState='buzzed';
btn.className='s-buzzed';btn.disabled=true;sts.textContent='BUZZED — #'+pos+' IN ORDER'; btn.className='s-buzzed';btn.disabled=true;sts.textContent='BUZZED — #'+pos+' IN ORDER';
ripple.style.display='none';
if(hint)hint.classList.remove('visible'); if(hint)hint.classList.remove('visible');
} else if(room.settings.buzzerLockout&&bz.buzzOrder.length>0){ } else if(room.settings.buzzerLockout&&bz.buzzOrder.length>0){
newState='locked'; newState='locked';
btn.className='s-locked';btn.disabled=true;sts.textContent='BUZZER LOCKED OUT'; btn.className='s-locked';btn.disabled=true;sts.textContent='BUZZER LOCKED OUT';
ripple.style.display='none';
if(hint)hint.classList.remove('visible'); if(hint)hint.classList.remove('visible');
} else { } else {
newState='open'; newState='open';
btn.className='s-open';btn.disabled=false;sts.textContent='ROUND OPEN — BUZZ!'; btn.className='s-open';btn.disabled=false;sts.textContent='ROUND OPEN — BUZZ!';
ripple.style.display='flex';
if(hint)hint.classList.add('visible'); if(hint)hint.classList.add('visible');
} }
// Animate state transitions
if(newState!==_lastBuzzState && typeof gsap!=='undefined'){
if(newState==='first'){
gsap.fromTo(btn,{scale:0.88},{scale:1,duration:0.5,ease:'back.out(2.5)'});
gsap.fromTo(sts,{opacity:0,y:6},{opacity:1,y:0,duration:0.4,ease:'power2.out'});
} else if(newState==='open' && _lastBuzzState==='closed'){
gsap.fromTo(btn,{scale:0.95},{scale:1,duration:0.4,ease:'back.out(1.5)'});
} else if(newState==='closed'){
gsap.to(btn,{scale:0.97,duration:0.15,yoyo:true,repeat:1,ease:'power2.inOut'});
}
}
_lastBuzzState=newState; _lastBuzzState=newState;
} }
function renderRoster(){ function renderRoster(){
if(!room)return; if(!room)return;
const el=document.getElementById('p-roster'); const el=document.getElementById('p-roster');
if(room.players.length===0){el.innerHTML='<div style="font-size:13px;color:var(--dim);padding:12px;letter-spacing:1px;">No players yet.</div>';return;} if(room.players.length===0){el.innerHTML='<div style="font-size:18px;color:var(--dim);padding:20px;">No players yet.</div>';return;}
el.innerHTML=''; el.innerHTML='';
room.players.forEach(p=>{ room.players.forEach(p=>{
const isMe=p.id===myId; const isMe=p.id===myId;
@@ -625,9 +740,9 @@ function renderRoster(){
const row=document.createElement('div'); const row=document.createElement('div');
row.className='roster-row'+(isMe?' roster-me':''); row.className='roster-row'+(isMe?' roster-me':'');
row.innerHTML=` row.innerHTML=`
<div class="roster-dot" style="background:${p.isConnected?(isMe?'var(--g)':color):'var(--border2)'}"></div> <div class="roster-dot" style="width:16px;height:16px;border-radius:50%;background:${p.isConnected?(isMe?'var(--g)':color):'var(--border2)'}"></div>
<div style="flex:1">${esc(p.name)}${isMe?' <span style="font-size:11px;color:var(--dim);letter-spacing:1px;">(YOU)</span>':''}</div> <div style="font-size:20px;font-weight:700;">#${p.id}</div>
${teamName?`<div style="font-size:12px;color:${color};letter-spacing:0.5px;">${esc(teamName)}</div>`:''} ${teamName?`<div style="font-size:14px;color:${color};margin-top:2px;">${esc(teamName)}</div>`:''}
`; `;
el.appendChild(row); el.appendChild(row);
}); });
@@ -641,26 +756,13 @@ function addFeed(evt){
const div=document.createElement('div'); const div=document.createElement('div');
div.className='feed-entry'+(isFirst?' first':''); div.className='feed-entry'+(isFirst?' first':'');
div.style.borderLeftColor=isFirst?'var(--yellow)':color; div.style.borderLeftColor=isFirst?'var(--yellow)':color;
div.innerHTML=`<strong>${esc(evt.playerName)}</strong>${teamStr} buzzed${isFirst?' <span style="color:var(--yellow);font-weight:700;"> — FIRST!</span>':''}`; div.innerHTML=`<strong>#${evt.playerId}</strong>${teamStr} buzzed${isFirst?' <span style="color:var(--yellow);font-weight:700;"> — FIRST!</span>':''}`;
feed.prepend(div); feed.prepend(div);
if(typeof gsap!=='undefined'){
gsap.fromTo(div,
{opacity:0,x:-20,borderLeftWidth:'3px'},
{opacity:1,x:0,duration:0.38,ease:'power3.out'}
);
}
while(feed.children.length>30)feed.removeChild(feed.lastChild); while(feed.children.length>30)feed.removeChild(feed.lastChild);
} }
function doBuzz(){ function doBuzz(){
ws_send({type:'buzz'}); ws_send({type:'buzz'});
// Immediate haptic-like feedback
if(typeof gsap!=='undefined'){
const btn=document.getElementById('buzz-btn');
gsap.to(btn,{scale:0.91,duration:0.08,ease:'power2.in',onComplete:()=>{
gsap.to(btn,{scale:1,duration:0.25,ease:'back.out(2.5)'});
}});
}
} }
// ══════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════

View File

@@ -9,7 +9,7 @@
} }
html { html {
font-size: 18px; font-size: 22px;
scroll-behavior: smooth; scroll-behavior: smooth;
} }
@@ -108,17 +108,17 @@ header {
} }
.logo { .logo {
font-size: 22px; font-size: 32px;
font-weight: 800; font-weight: 800;
letter-spacing: 6px; letter-spacing: 8px;
color: var(--lav); color: var(--lav);
line-height: 1; line-height: 1;
} }
.logo-sub { .logo-sub {
font-size: 11px; font-size: 16px;
color: var(--ov1); color: var(--ov1);
letter-spacing: 2px; letter-spacing: 3px;
} }
.hdr-r { .hdr-r {
@@ -129,13 +129,13 @@ header {
} }
.room-chip { .room-chip {
font-size: 14px; font-size: 18px;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--lav); color: var(--lav);
background: rgba(180,190,254,0.1); background: rgba(180,190,254,0.1);
border: 1px solid rgba(180,190,254,0.3); border: 1px solid rgba(180,190,254,0.3);
padding: 6px 14px; padding: 10px 20px;
border-radius: 6px; border-radius: 8px;
display: none; display: none;
font-weight: 600; font-weight: 600;
} }
@@ -143,14 +143,14 @@ header {
.conn-pill { .conn-pill {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
font-size: 13px; font-size: 16px;
color: var(--ov1); color: var(--ov1);
letter-spacing: 1px; letter-spacing: 1.5px;
background: var(--s0); background: var(--s0);
border: 1px solid var(--s1); border: 1px solid var(--s1);
padding: 7px 14px; padding: 10px 20px;
border-radius: 20px; border-radius: 24px;
} }
.conn-dot { .conn-dot {
@@ -201,35 +201,55 @@ header {
.hero-decoration { display: none; } .hero-decoration { display: none; }
.hero h1 { .hero h1 {
font-size: clamp(60px, 14vw, 108px); font-size: clamp(72px, 16vw, 130px);
font-weight: 800; font-weight: 800;
letter-spacing: 16px; letter-spacing: 20px;
color: var(--lav); color: var(--lav);
line-height: 1; line-height: 1;
text-shadow: 0 0 40px rgba(180,190,254,0.4), 0 0 80px rgba(180,190,254,0.15); text-shadow: 0 0 40px rgba(180,190,254,0.4), 0 0 80px rgba(180,190,254,0.15);
} }
.hero p { .hero p {
font-size: 14px; font-size: 18px;
color: var(--sub0); color: var(--sub0);
letter-spacing: 4px; letter-spacing: 5px;
margin-top: 20px; margin-top: 24px;
} }
.hero-badge { .hero-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
margin-top: 16px; margin-top: 20px;
padding: 7px 16px; padding: 10px 20px;
border: 1px solid var(--s2); border: 1px solid var(--s2);
border-radius: 20px; border-radius: 24px;
font-size: 12px; font-size: 16px;
letter-spacing: 2px; letter-spacing: 2.5px;
color: var(--ov2); color: var(--ov2);
background: var(--s0); background: var(--s0);
} }
.rejoin-bar {
font-size: 16px;
color: var(--ov1);
margin-top: -20px;
}
.rejoin-bar a {
color: var(--blue);
text-decoration: none;
letter-spacing: 1.5px;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 24px;
border: 1px solid var(--s1);
border-radius: 10px;
transition: all .2s;
font-size: 16px;
}
.hero-badge-dot { .hero-badge-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -283,17 +303,17 @@ header {
} }
.land-card h2 { .land-card h2 {
font-size: 15px; font-size: 20px;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--lav); color: var(--lav);
margin-bottom: -4px; margin-bottom: -6px;
font-weight: 700; font-weight: 700;
} }
.land-card p { .land-card p {
font-size: 15px; font-size: 17px;
color: var(--sub0); color: var(--sub0);
line-height: 1.8; line-height: 2;
} }
.rejoin-bar { .rejoin-bar {
@@ -344,17 +364,17 @@ header {
} }
.setup-title { .setup-title {
font-size: 17px; font-size: 22px;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--lav); color: var(--lav);
font-weight: 700; font-weight: 700;
} }
.setup-sub { .setup-sub {
font-size: 14px; font-size: 16px;
color: var(--sub0); color: var(--sub0);
letter-spacing: 0.5px; letter-spacing: 0.8px;
line-height: 1.7; line-height: 1.8;
} }
/* ── FORM ATOMS ────────────────────────────────────── */ /* ── FORM ATOMS ────────────────────────────────────── */
@@ -366,8 +386,8 @@ header {
label, label,
.lbl { .lbl {
font-size: 14px; font-size: 17px;
letter-spacing: 1.5px; letter-spacing: 2px;
color: var(--sub0); color: var(--sub0);
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
@@ -452,22 +472,22 @@ select option {
} }
.tog-row .lbl { .tog-row .lbl {
font-size: 15px; font-size: 17px;
color: var(--text); color: var(--text);
letter-spacing: 0.3px; letter-spacing: 0.5px;
line-height: 1.4; line-height: 1.5;
text-transform: none; text-transform: none;
font-weight: 600; font-weight: 600;
} }
.lbl-sub { .lbl-sub {
font-size: 13px; font-size: 15px;
color: var(--ov2); color: var(--ov2);
margin-top: 4px; margin-top: 6px;
letter-spacing: 0; letter-spacing: 0;
text-transform: none; text-transform: none;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.6;
} }
.tog { .tog {
@@ -615,18 +635,29 @@ select option {
} }
.panel-title { .panel-title {
font-size: 14px; font-size: 17px;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--lav); color: var(--lav);
margin-bottom: 22px; margin-bottom: 26px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 12px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
} }
.tag {
display: inline-block;
padding: 6px 16px;
font-size: 15px;
letter-spacing: 1.5px;
border: 1px solid;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.panel-title::after { .panel-title::after {
content: ''; content: '';
flex: 1; flex: 1;
@@ -704,24 +735,24 @@ select option {
} }
.side-label { .side-label {
font-size: 13px; font-size: 16px;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--ov1); color: var(--ov1);
margin-bottom: 14px; margin-bottom: 18px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
} }
.side-hint { .side-hint {
font-size: 13px; font-size: 15px;
color: var(--ov1); color: var(--ov1);
letter-spacing: 0.5px; letter-spacing: 0.8px;
} }
.side-room-code { .side-room-code {
font-size: 34px; font-size: 42px;
font-weight: 800; font-weight: 800;
letter-spacing: 8px; letter-spacing: 10px;
color: var(--lav); color: var(--lav);
line-height: 1; line-height: 1;
text-shadow: 0 0 20px rgba(180,190,254,0.3); text-shadow: 0 0 20px rgba(180,190,254,0.3);
@@ -734,10 +765,10 @@ select option {
} }
.side-status { .side-status {
font-size: 14px; font-size: 16px;
color: var(--sub0); color: var(--sub0);
line-height: 2; line-height: 2.2;
letter-spacing: 0.5px; letter-spacing: 0.8px;
} }
/* ── TIMER BLOCK ───────────────────────────────────── */ /* ── TIMER BLOCK ───────────────────────────────────── */
@@ -814,10 +845,10 @@ select option {
} }
.tab { .tab {
padding: 16px 22px; padding: 18px 26px;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 14px; font-size: 17px;
letter-spacing: 1.5px; letter-spacing: 2px;
cursor: pointer; cursor: pointer;
color: var(--ov1); color: var(--ov1);
transition: all .2s; transition: all .2s;
@@ -860,15 +891,15 @@ select option {
} }
.bz-rank { .bz-rank {
width: 42px; width: 52px;
height: 42px; height: 52px;
border-radius: 50%; border-radius: 50%;
background: var(--s1); background: var(--s1);
border: 2px solid var(--s2); border: 3px solid var(--s2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 18px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: var(--sub0); color: var(--sub0);
flex-shrink: 0; flex-shrink: 0;
@@ -878,17 +909,12 @@ select option {
background: rgba(249,226,175,0.15); background: rgba(249,226,175,0.15);
border-color: var(--yellow); border-color: var(--yellow);
color: var(--yellow); color: var(--yellow);
font-size: 20px; font-size: 24px;
box-shadow: 0 0 16px rgba(249,226,175,0.25); box-shadow: 0 0 16px rgba(249,226,175,0.25);
} }
.bz-info {
flex: 1;
min-width: 0;
}
.bz-name { .bz-name {
font-size: 18px; font-size: 20px;
font-weight: 700; font-weight: 700;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -896,16 +922,16 @@ select option {
} }
.bz-team { .bz-team {
font-size: 14px; font-size: 16px;
margin-top: 4px; margin-top: 6px;
letter-spacing: 0.5px; letter-spacing: 0.8px;
color: var(--sub0); color: var(--sub0);
} }
.bz-ms { .bz-ms {
font-size: 13px; font-size: 15px;
color: var(--ov1); color: var(--ov1);
margin-top: 3px; margin-top: 5px;
} }
/* ── PLAYERS ───────────────────────────────────────── */ /* ── PLAYERS ───────────────────────────────────────── */
@@ -917,12 +943,31 @@ select option {
.pl-row { .pl-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 16px;
padding: 16px 0; padding: 18px 0;
border-bottom: 1px solid var(--s0); border-bottom: 1px solid var(--s0);
transition: opacity .2s; transition: opacity .2s;
} }
.pl-info {
flex: 1;
min-width: 0;
}
.pl-name {
font-size: 18px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pl-meta {
font-size: 15px;
color: var(--ov2);
margin-top: 5px;
}
.pl-row:last-child { .pl-row:last-child {
border-bottom: none; border-bottom: none;
} }
@@ -977,24 +1022,24 @@ select option {
} }
.team-card .tc-n { .team-card .tc-n {
font-size: 13px; font-size: 16px;
letter-spacing: 1.5px; letter-spacing: 2px;
margin-bottom: 12px; margin-bottom: 14px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
} }
.team-card .tc-c { .team-card .tc-c {
font-size: 34px; font-size: 40px;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
} }
.team-card .tc-m { .team-card .tc-m {
font-size: 13px; font-size: 15px;
color: var(--sub0); color: var(--sub0);
margin-top: 10px; margin-top: 12px;
line-height: 1.8; line-height: 2;
} }
/* ── SETTINGS ──────────────────────────────────────── */ /* ── SETTINGS ──────────────────────────────────────── */
@@ -1012,16 +1057,16 @@ select option {
} }
.sl { .sl {
font-size: 15px; font-size: 17px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.3px; letter-spacing: 0.5px;
color: var(--text); color: var(--text);
} }
.sd { .sd {
font-size: 13px; font-size: 15px;
color: var(--ov2); color: var(--ov2);
margin-top: 4px; margin-top: 6px;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -1040,35 +1085,35 @@ select option {
} }
.p-room-lbl { .p-room-lbl {
font-size: 13px; font-size: 16px;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--ov1); color: var(--ov1);
margin-bottom: 8px; margin-bottom: 10px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
} }
.p-room-code { .p-room-code {
font-size: 44px; font-size: 52px;
font-weight: 800; font-weight: 800;
letter-spacing: 12px; letter-spacing: 14px;
color: var(--lav); color: var(--lav);
text-shadow: 0 0 24px rgba(180,190,254,0.35); text-shadow: 0 0 24px rgba(180,190,254,0.35);
} }
.p-name-lbl { .p-name-lbl {
font-size: 16px; font-size: 18px;
color: var(--sub0); color: var(--sub0);
margin-top: 12px; margin-top: 14px;
letter-spacing: 0.5px; letter-spacing: 0.8px;
font-weight: 600; font-weight: 600;
} }
/* player timer */ /* player timer */
.p-timer { .p-timer {
font-size: 72px; font-size: 84px;
font-weight: 700; font-weight: 700;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--teal); color: var(--teal);
text-align: center; text-align: center;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -1076,6 +1121,31 @@ select option {
transition: color .3s, text-shadow .3s; transition: color .3s, text-shadow .3s;
} }
.team-picker-title {
font-size: 16px;
letter-spacing: 4px;
color: var(--sub0);
margin-bottom: 22px;
text-align: center;
font-weight: 600;
}
.team-btn {
padding: 26px 20px;
border: 2px solid var(--s1);
border-radius: var(--r);
background: var(--panel);
color: var(--text);
font-family: 'JetBrains Mono', monospace;
font-size: 17px;
letter-spacing: 1.5px;
cursor: pointer;
text-align: center;
transition: all .2s;
box-shadow: var(--shadow-panel);
font-weight: 600;
}
.p-timer.warn { .p-timer.warn {
color: var(--yellow); color: var(--yellow);
text-shadow: 0 0 20px rgba(249,226,175,0.5); text-shadow: 0 0 20px rgba(249,226,175,0.5);
@@ -1273,25 +1343,25 @@ select option {
} }
.buzz-status { .buzz-status {
font-size: 16px; font-size: 18px;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--sub0); color: var(--sub0);
text-align: center; text-align: center;
min-height: 26px; min-height: 32px;
transition: color .3s; transition: color .3s;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
} }
.spacebar-hint { .spacebar-hint {
font-size: 14px; font-size: 16px;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--ov0); color: var(--ov0);
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
opacity: 0; opacity: 0;
transition: opacity .3s; transition: opacity .3s;
margin-top: -12px; margin-top: -16px;
} }
.spacebar-hint.visible { .spacebar-hint.visible {
@@ -1305,14 +1375,34 @@ select option {
} }
.p-panel-title { .p-panel-title {
font-size: 14px; font-size: 17px;
letter-spacing: 3px; letter-spacing: 4px;
color: var(--sub0); color: var(--sub0);
margin-bottom: 16px; margin-bottom: 18px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
} }
.feed-entry {
padding: 16px 20px;
background: var(--s0);
border: 1px solid var(--s1);
border-left: 4px solid var(--blue);
margin-bottom: 12px;
font-size: 17px;
border-radius: 0 var(--r) var(--r) 0;
line-height: 1.7;
}
.roster-row {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0;
border-bottom: 1px solid var(--s0);
font-size: 17px;
}
.feed-entry { .feed-entry {
padding: 14px 18px; padding: 14px 18px;
background: var(--s0); background: var(--s0);
@@ -1372,22 +1462,28 @@ select option {
display: flex; display: flex;
} }
.modal p {
font-size: 17px;
color: var(--ov1);
line-height: 1.8;
}
.modal { .modal {
background: #1a1a2e; background: #1a1a2e;
border: 2px solid var(--s2); border: 2px solid var(--s2);
border-radius: var(--r2); border-radius: var(--r2);
padding: 44px 40px; padding: 52px 48px;
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 28px;
box-shadow: 0 20px 60px rgba(0,0,0,0.7); box-shadow: 0 20px 60px rgba(0,0,0,0.7);
} }
.modal h2 { .modal h2 {
font-size: 18px; font-size: 22px;
letter-spacing: 2px; letter-spacing: 3px;
color: var(--lav); color: var(--lav);
font-weight: 700; font-weight: 700;
} }
@@ -1412,11 +1508,11 @@ select option {
} }
.toast { .toast {
padding: 14px 22px; padding: 16px 24px;
background: #24243e; background: #24243e;
border: 2px solid var(--s2); border: 2px solid var(--s2);
font-size: 14px; font-size: 16px;
letter-spacing: 1px; letter-spacing: 1.5px;
border-radius: var(--r); border-radius: var(--r);
max-width: 360px; max-width: 360px;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
@@ -1424,6 +1520,15 @@ select option {
font-weight: 600; font-weight: 600;
} }
.empty {
font-size: 16px;
color: var(--ov1);
letter-spacing: 2px;
text-align: center;
padding: 40px;
text-transform: uppercase;
}
.toast.ok { .toast.ok {
border-color: rgba(166,227,161,0.5); border-color: rgba(166,227,161,0.5);
color: var(--green); color: var(--green);
@@ -1472,7 +1577,7 @@ select option {
} }
.timer-digits { .timer-digits {
font-size: 44px; font-size: 52px;
} }
.land-card { .land-card {
@@ -1494,8 +1599,8 @@ select option {
} }
.logo { .logo {
font-size: 18px; font-size: 24px;
letter-spacing: 4px; letter-spacing: 5px;
} }
#s-mod { #s-mod {

View File

@@ -65,16 +65,19 @@ export function handleMessage(ws: WS, raw: string) {
if (!room) { er(ws, "Room not found"); return; } if (!room) { er(ws, "Room not found"); return; }
if (room.locked) { er(ws, "Room is locked"); return; } if (room.locked) { er(ws, "Room is locked"); return; }
const name = sanitize(msg.playerName ?? "Player", 24) || "Player"; // Calculate numeric ID based on join order
const existingId = sanitize(msg.playerId ?? "", 12); const joinCount = room.players.size + 1;
const playerId = joinCount.toString();
const name = sanitize(msg.playerName ?? "", 24) || ""; // Keep name for compatibility but don't use it
let player: Player | undefined; let player: Player | undefined;
if (existingId && room.players.has(existingId)) { if (playerId && room.players.has(playerId)) {
player = room.players.get(existingId)!; player = room.players.get(playerId)!;
player.ws = ws; player.isConnected = true; player.name = name; player.ws = ws; player.isConnected = true; player.name = name;
} else { } else {
player = { id: genId(), name, teamIndex: null, ws, isConnected: true }; player = { id: playerId, name, teamIndex: null, ws, isConnected: true };
room.players.set(player.id, player); room.players.set(playerId, player);
} }
wsToPlayer.set(ws, { roomId: room.id, playerId: player.id }); wsToPlayer.set(ws, { roomId: room.id, playerId: player.id });
@@ -132,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;