Compare commits

...

34 Commits

Author SHA1 Message Date
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
8 changed files with 1606 additions and 959 deletions

34
.gitignore vendored
View File

@@ -1,34 +1,6 @@
# dependencies (bun install)
node_modules node_modules
# output
out
dist dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store .DS_Store
server/public
vite.config.ts.*
*.tar.gz

View File

@@ -1,15 +1,15 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 1, "configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "buzzer", "name": "rest-express",
"dependencies": { "dependencies": {
"hono": "^4.7.7", "hono": "^4.7.7",
}, },
}, },
}, },
"packages": { "packages": {
"hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
} }
} }

View File

@@ -7,8 +7,7 @@
<title>Buzzer Platform</title> <title>Buzzer Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700;800&display=swap" <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
rel="stylesheet" />
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
@@ -17,9 +16,9 @@
<!-- HEADER --> <!-- HEADER -->
<header> <header>
<div> <div class="logo-wrap">
<div class="logo">BUZZER</div> <div class="logo">BUZZER</div>
<div class="logo-sub">// QUIZ CONTROL</div> <div class="logo-sub">// QUIZ CONTROL PLATFORM</div>
</div> </div>
<div class="hdr-r"> <div class="hdr-r">
<div class="room-chip" id="hdr-room"></div> <div class="room-chip" id="hdr-room"></div>
@@ -33,27 +32,30 @@
<!-- ══════════ LANDING ══════════ --> <!-- ══════════ LANDING ══════════ -->
<div class="scr" id="s-land"> <div class="scr" id="s-land">
<div class="hero"> <div class="hero">
<div class="hero-decoration"></div>
<h1 class="glow">BUZZ</h1> <h1 class="glow">BUZZ</h1>
<p>REAL-TIME QUIZ BUZZER SYSTEM</p> <p>REAL-TIME QUIZ BUZZER SYSTEM</p>
<div class="hero-badge">
<div class="hero-badge-dot"></div>
WEBSOCKET POWERED
</div>
</div> </div>
<div class="land-cards"> <div class="land-cards">
<div class="land-card"> <div class="land-card">
<div class="land-card-icon"></div>
<h2>// HOST A SESSION</h2> <h2>// HOST A SESSION</h2>
<p>Create a room, configure teams and rules, then control the buzzer live.</p> <p>Create a room, configure teams and rules, then control the buzzer live.</p>
<button class="btn btn-g" onclick="goSetup()">CREATE ROOM →</button> <button class="btn btn-g" onclick="goSetup()">CREATE ROOM →</button>
</div> </div>
<div class="land-card"> <div class="land-card">
<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:4px;text-transform:uppercase" <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>
@@ -65,7 +67,7 @@
<!-- ══════════ SETUP ══════════ --> <!-- ══════════ SETUP ══════════ -->
<div class="scr" id="s-setup"> <div class="scr" id="s-setup">
<div class="setup-wrap"> <div class="setup-wrap">
<div> <div class="setup-header">
<div class="setup-title">// NEW ROOM SETUP</div> <div class="setup-title">// NEW ROOM SETUP</div>
<div class="setup-sub">Configure before creating — all settings are adjustable in-game too.</div> <div class="setup-sub">Configure before creating — all settings are adjustable in-game too.</div>
</div> </div>
@@ -81,28 +83,26 @@
</div> </div>
</div> </div>
<div id="team-opts"> <div id="team-opts">
<div class="field" style="margin-top:16px;"> <div class="field" style="margin-top:20px;">
<label>NUMBER OF TEAMS</label> <label>NUMBER OF TEAMS</label>
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:14px;">
<input type="number" id="st-numteams" value="2" min="2" max="64" style="width:90px;" <input type="number" id="st-numteams" value="2" min="2" max="64" style="width:100px;"
oninput="renderSetupTeamNames()" /> oninput="renderSetupTeamNames()" />
<span style="font-size:12px;color:var(--dim);">2 64</span> <span style="font-size:12px;color:var(--dim);letter-spacing:1px;">2 64</span>
</div> </div>
</div> </div>
<div class="field" style="margin-top:12px;"> <div class="field" style="margin-top:14px;">
<label>TEAM NAMES <span style="color:var(--dim);font-size:9px;">(AUTO-FILLED WITH GREEK <label>TEAM NAMES <span style="color:var(--dim);font-size:9px;letter-spacing:1px;">(AUTO-FILLED WITH GREEK ALPHABET)</span></label>
ALPHABET)</span></label>
<div id="setup-team-names" <div id="setup-team-names"
style="display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto;padding-right:4px;"> style="display:flex;flex-direction:column;gap:9px;max-height:240px;overflow-y:auto;padding-right:4px;">
</div> </div>
</div> </div>
<div class="tog-row" style="margin-top:12px;"> <div class="tog-row" style="margin-top:14px;">
<div> <div>
<div class="lbl">PLAYERS PICK OWN TEAM</div> <div class="lbl">PLAYERS PICK OWN TEAM</div>
<div class="lbl-sub">If off, moderator assigns teams manually</div> <div class="lbl-sub">If off, moderator assigns teams manually</div>
</div> </div>
<label class="tog"><input type="checkbox" id="st-playerpick" checked /><span <label class="tog"><input type="checkbox" id="st-playerpick" checked /><span class="tog-track"></span></label>
class="tog-track"></span></label>
</div> </div>
</div> </div>
</div> </div>
@@ -121,8 +121,7 @@
<div class="lbl">SHOW FULL BUZZ ORDER TO PLAYERS</div> <div class="lbl">SHOW FULL BUZZ ORDER TO PLAYERS</div>
<div class="lbl-sub">If off, players only see if they were first</div> <div class="lbl-sub">If off, players only see if they were first</div>
</div> </div>
<label class="tog"><input type="checkbox" id="st-showorder" checked /><span <label class="tog"><input type="checkbox" id="st-showorder" checked /><span class="tog-track"></span></label>
class="tog-track"></span></label>
</div> </div>
</div> </div>
@@ -133,18 +132,17 @@
<div class="lbl">ENABLE COUNTDOWN TIMER</div> <div class="lbl">ENABLE COUNTDOWN TIMER</div>
<div class="lbl-sub">Auto-closes round when it hits zero</div> <div class="lbl-sub">Auto-closes round when it hits zero</div>
</div> </div>
<label class="tog"><input type="checkbox" id="st-usetimer" onchange="toggleTimerField()" /><span <label class="tog"><input type="checkbox" id="st-usetimer" onchange="toggleTimerField()" /><span class="tog-track"></span></label>
class="tog-track"></span></label>
</div> </div>
<div id="timer-field" style="display:none;margin-top:14px;"> <div id="timer-field" style="display:none;margin-top:16px;">
<div class="field"> <div class="field">
<label>SECONDS PER ROUND</label> <label>SECONDS PER ROUND</label>
<input type="number" id="st-timersec" value="30" min="5" max="600" style="max-width:160px;" /> <input type="number" id="st-timersec" value="30" min="5" max="600" style="max-width:180px;" />
</div> </div>
</div> </div>
</div> </div>
<div style="display:flex;gap:10px;justify-content:flex-end;padding-bottom:32px;"> <div style="display:flex;gap:12px;justify-content:flex-end;padding-bottom:40px;">
<button class="btn btn-ghost" onclick="showScr('s-land')">← BACK</button> <button class="btn btn-ghost" onclick="showScr('s-land')">← BACK</button>
<button class="btn btn-g" onclick="createRoom()">CREATE ROOM →</button> <button class="btn btn-g" onclick="createRoom()">CREATE ROOM →</button>
</div> </div>
@@ -155,11 +153,11 @@
<div class="scr" id="s-mod"> <div class="scr" id="s-mod">
<div class="mod-side"> <div class="mod-side">
<div class="side-sec"> <div class="side-sec">
<div class="side-label">ROOM</div> <div class="side-label">ROOM CODE</div>
<div class="side-room-code glow" id="mod-code">──────</div> <div class="side-room-code glow" id="mod-code">──────</div>
<div class="side-hint">SHARE WITH PLAYERS</div> <div class="side-hint" style="margin-top:6px;">SHARE WITH PLAYERS</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;"> <div style="display:flex;gap:7px;flex-wrap:wrap;margin-top:14px;">
<button class="btn btn-ghost btn-sm" onclick="copyCode()">COPY</button> <button class="btn btn-ghost btn-sm" onclick="copyCode()">COPY CODE</button>
<button class="btn btn-red btn-sm" onclick="openModal('m-end')">END ROOM</button> <button class="btn btn-red btn-sm" onclick="openModal('m-end')">END ROOM</button>
</div> </div>
</div> </div>
@@ -171,7 +169,7 @@
<div class="timer-digits" id="mod-timer-disp">0:30</div> <div class="timer-digits" id="mod-timer-disp">0:30</div>
<div class="timer-controls"> <div class="timer-controls">
<button class="btn btn-g btn-sm" id="btn-timer-ss" onclick="modTimerToggle()">START</button> <button class="btn btn-g btn-sm" id="btn-timer-ss" onclick="modTimerToggle()">START</button>
<button class="btn btn-ghost btn-sm" onclick="modTimerReset()"></button> <button class="btn btn-ghost btn-sm" onclick="modTimerReset()"> RESET</button>
</div> </div>
<div class="timer-set-row"> <div class="timer-set-row">
<span class="side-hint">SET</span> <span class="side-hint">SET</span>
@@ -179,22 +177,27 @@
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">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</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>
<!-- ROOM CONTROLS --> <!-- ROOM CONTROLS -->
<div class="side-sec"> <div class="side-sec">
<div class="side-label">CONTROLS</div> <div class="side-label">ROOM CONTROLS</div>
<div class="tog-row"> <div class="tog-row">
<div class="lbl">LOCK ROOM</div> <div class="lbl">LOCK ROOM</div>
<label class="tog"><input type="checkbox" id="lock-room-tog" <label class="tog"><input type="checkbox" id="lock-room-tog"
@@ -205,9 +208,8 @@
<label class="tog"><input type="checkbox" id="lock-teams-tog" <label class="tog"><input type="checkbox" id="lock-teams-tog"
onchange="ws_send({type:'lock_teams',locked:this.checked})" /><span class="tog-track"></span></label> onchange="ws_send({type:'lock_teams',locked:this.checked})" /><span class="tog-track"></span></label>
</div> </div>
<div style="margin-top:10px;display:flex;flex-direction:column;gap:5px;"> <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
<button class="btn btn-ghost btn-sm btn-full" onclick="ws_send({type:'reset_teams'})">CLEAR ALL <button class="btn btn-ghost btn-sm btn-full" onclick="ws_send({type:'reset_teams'})">CLEAR ALL TEAMS</button>
TEAMS</button>
</div> </div>
</div> </div>
@@ -238,7 +240,7 @@
<div id="tab-players" style="display:none"> <div id="tab-players" style="display:none">
<div class="panel"> <div class="panel">
<div class="panel-title"> <div class="panel-title">
<span>CONNECTED PLAYERS</span> CONNECTED PLAYERS
<span class="tag tag-g" id="pcount-badge">0</span> <span class="tag tag-g" id="pcount-badge">0</span>
</div> </div>
<div class="player-list" id="mod-plist"> <div class="player-list" id="mod-plist">
@@ -297,16 +299,16 @@
<div class="sl">NUMBER OF TEAMS</div> <div class="sl">NUMBER OF TEAMS</div>
</div> </div>
<div style="display:flex;align-items:center;gap:8px;"> <div style="display:flex;align-items:center;gap:8px;">
<input type="number" id="ls-numteams" min="2" max="64" style="width:80px;" <input type="number" id="ls-numteams" min="2" max="64" style="width:84px;"
onchange="pushSetting('numTeams',+this.value)" /> onchange="pushSetting('numTeams',+this.value)" />
<button class="btn btn-ghost btn-sm" <button class="btn btn-ghost btn-sm"
onclick="pushSetting('numTeams',+document.getElementById('ls-numteams').value)">APPLY</button> onclick="pushSetting('numTeams',+document.getElementById('ls-numteams').value)">APPLY</button>
</div> </div>
</div> </div>
<div style="margin-top:18px;"> <div style="margin-top:20px;">
<div class="lbl" style="margin-bottom:10px;">TEAM NAMES</div> <div class="lbl" style="margin-bottom:12px;">TEAM NAMES</div>
<div id="ls-team-names" <div id="ls-team-names"
style="display:flex;flex-direction:column;gap:8px;max-height:260px;overflow-y:auto;padding-right:4px;"> style="display:flex;flex-direction:column;gap:9px;max-height:280px;overflow-y:auto;padding-right:4px;">
</div> </div>
</div> </div>
</div> </div>
@@ -351,8 +353,8 @@
<div class="modal-bg" id="m-rejoin"> <div class="modal-bg" id="m-rejoin">
<div class="modal"> <div class="modal">
<h2>// REJOIN SESSION</h2> <h2>// REJOIN SESSION</h2>
<p style="font-size:13px;color:var(--dim);">You have a saved mod session. Rejoin it?</p> <p style="font-size:13px;color:var(--dim);line-height:1.7;">You have a saved mod session. Rejoin it?</p>
<div style="font-size:24px;letter-spacing:6px;color:var(--g);font-weight:700;" id="m-rejoin-code"></div> <div style="font-size:28px;letter-spacing:8px;color:var(--g);font-weight:700;text-shadow:0 0 12px var(--g);" id="m-rejoin-code"></div>
<div class="modal-btns"> <div class="modal-btns">
<button class="btn btn-ghost btn-sm" onclick="clearMod();closeModal('m-rejoin')">DISCARD</button> <button class="btn btn-ghost btn-sm" onclick="clearMod();closeModal('m-rejoin')">DISCARD</button>
<button class="btn btn-g btn-sm" onclick="doRejoin()">REJOIN →</button> <button class="btn btn-g btn-sm" onclick="doRejoin()">REJOIN →</button>
@@ -363,7 +365,7 @@
<div class="modal-bg" id="m-end"> <div class="modal-bg" id="m-end">
<div class="modal"> <div class="modal">
<h2>// END ROOM</h2> <h2>// END ROOM</h2>
<p style="font-size:13px;color:var(--dim);">All players will be disconnected and the room deleted.</p> <p style="font-size:13px;color:var(--dim);line-height:1.7;">All players will be disconnected and the room deleted.</p>
<div class="modal-btns"> <div class="modal-btns">
<button class="btn btn-ghost btn-sm" onclick="closeModal('m-end')">CANCEL</button> <button class="btn btn-ghost btn-sm" onclick="closeModal('m-end')">CANCEL</button>
<button class="btn btn-red btn-sm" onclick="ws_send({type:'end_room'});closeModal('m-end')">END ROOM</button> <button class="btn btn-red btn-sm" onclick="ws_send({type:'end_room'});closeModal('m-end')">END ROOM</button>
@@ -372,7 +374,10 @@
</div> </div>
<div id="toasts"></div> <div id="toasts"></div>
<!-- GSAP -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="/script.js"></script> <script src="/script.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,30 @@
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { handleMessage, handleClose } from "./ws-handler"; import { handleMessage, handleClose } from "./ws-handler";
const HTML = readFileSync("./src/public/index.html", "utf-8"); const PORT = parseInt(process.env.PORT || "3009", 10);
const CSS = readFileSync("./src/public/styles.css", "utf-8");
const JS = readFileSync("./src/public/script.js", "utf-8");
const server = Bun.serve({ const server = Bun.serve({
port: 3009, port: PORT,
hostname: "0.0.0.0",
fetch(req, server) { fetch(req, server) {
const url = new URL(req.url); const url = new URL(req.url);
if (url.pathname === "/ws") { if (url.pathname === "/ws") {
if (!server.upgrade(req)) return new Response("WS upgrade failed", { status: 400 }); if (!server.upgrade(req)) return new Response("WS upgrade failed", { status: 400 });
return undefined as any; return undefined as any;
} }
if (url.pathname === "/styles.css") return new Response(CSS, { headers: { "Content-Type": "text/css" } }); if (url.pathname === "/styles.css") {
if (url.pathname === "/script.js") return new Response(JS, { headers: { "Content-Type": "text/javascript" } }); return new Response(readFileSync("./src/public/styles.css", "utf-8"), {
return new Response(HTML, { headers: { "Content-Type": "text/html; charset=utf-8" } }); headers: { "Content-Type": "text/css" },
});
}
if (url.pathname === "/script.js") {
return new Response(readFileSync("./src/public/script.js", "utf-8"), {
headers: { "Content-Type": "text/javascript" },
});
}
return new Response(readFileSync("./src/public/index.html", "utf-8"), {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}, },
websocket: { websocket: {
perMessageDeflate: true, perMessageDeflate: true,
@@ -26,4 +35,4 @@ const server = Bun.serve({
}, },
}); });
console.log(`\x1b[32m[BUZZER]\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`); console.log(`\x1b[32m[BUZZER]\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`);

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;

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["bun-types"]
}
}