From f0c327617b120b676ff7b3a9cf437e60c3827dfa Mon Sep 17 00:00:00 2001
From: AyoubBenziza
Date: Thu, 11 Jun 2026 17:33:10 +0200
Subject: [PATCH] feat: leave-room button
- room:leave event: removes the player from the room (not just disconnected),
reassigns host if the host leaves, deletes the room when empty
- store leaveRoom() emits it and clears the session (reset)
- room header gets a "Quitter" button (always available); end-screen "Quitter"
now also leaves cleanly
Verified e2e: leaver removed from snapshot, host reassigned to the next player.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/server/src/handlers.ts | 16 +++++++++++++++
apps/server/src/rooms.ts | 22 ++++++++++++++++++++
apps/web/src/components/game-end-view.tsx | 4 ++--
apps/web/src/pages/room.tsx | 25 ++++++++++++++++++-----
apps/web/src/store/room.ts | 6 ++++++
packages/shared/src/events.ts | 1 +
6 files changed, 67 insertions(+), 7 deletions(-)
diff --git a/apps/server/src/handlers.ts b/apps/server/src/handlers.ts
index 8aa2cd4..8ab1faf 100644
--- a/apps/server/src/handlers.ts
+++ b/apps/server/src/handlers.ts
@@ -324,6 +324,22 @@ export function registerRoomHandlers(
broadcastState(room)
})
+ socket.on("room:leave", () => {
+ const code = socket.data.roomCode
+ const playerId = socket.data.playerId
+ if (!code || !playerId) {
+ return
+ }
+ socket.leave(code)
+ socket.data.roomCode = undefined
+ socket.data.playerId = undefined
+ const room = rooms.leave(code, playerId)
+ if (room) {
+ io.to(room.code).emit("player:left", { playerId })
+ broadcastState(room)
+ }
+ })
+
socket.on("disconnect", () => {
const affected = rooms.handleDisconnect(socket.id)
if (!affected) {
diff --git a/apps/server/src/rooms.ts b/apps/server/src/rooms.ts
index 1aa79f3..d32dd67 100644
--- a/apps/server/src/rooms.ts
+++ b/apps/server/src/rooms.ts
@@ -94,6 +94,28 @@ export class RoomManager {
return this.rooms.get(code)
}
+ /**
+ * Retire un joueur d'une room (départ volontaire). Réassigne l'hôte si besoin,
+ * supprime la room si plus personne. Renvoie la room (ou null si supprimée).
+ */
+ leave(code: string, playerId: string): ServerRoom | null {
+ const room = this.rooms.get(code)
+ if (!room) {
+ return null
+ }
+ room.players.delete(playerId)
+ room.scores.delete(playerId)
+ if (room.players.size === 0) {
+ this.rooms.delete(code)
+ return null
+ }
+ if (room.hostId === playerId) {
+ const named = [...room.players.values()].find((p) => p.name !== "")
+ room.hostId = (named ?? [...room.players.values()][0]).id
+ }
+ return room
+ }
+
/**
* Marque le joueur d'un socket comme déconnecté.
* Retourne la room affectée et le playerId, ou null si non rattaché.
diff --git a/apps/web/src/components/game-end-view.tsx b/apps/web/src/components/game-end-view.tsx
index 5e5bd39..ce64d4d 100644
--- a/apps/web/src/components/game-end-view.tsx
+++ b/apps/web/src/components/game-end-view.tsx
@@ -133,7 +133,7 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
const finalScores = useRoomStore((s) => s.finalScores)
const gameTracks = useRoomStore((s) => s.gameTracks)
const gameRecap = useRoomStore((s) => s.gameRecap)
- const reset = useRoomStore((s) => s.reset)
+ const leaveRoom = useRoomStore((s) => s.leaveRoom)
const returnToLobby = useRoomStore((s) => s.returnToLobby)
const isHost = snapshot.hostId === playerId
@@ -284,7 +284,7 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
-