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 }) {

)} - diff --git a/apps/web/src/pages/room.tsx b/apps/web/src/pages/room.tsx index 81bfb56..12e82a4 100644 --- a/apps/web/src/pages/room.tsx +++ b/apps/web/src/pages/room.tsx @@ -1,5 +1,6 @@ -import { Link } from "wouter" +import { Link, useLocation } from "wouter" import { AnimatePresence, motion } from "framer-motion" +import { LogOut } from "lucide-react" import { Button } from "@workspace/ui/components/button" import { useRoomStore } from "@/store/room" import { LobbyView } from "@/components/lobby-view" @@ -13,8 +14,10 @@ import { RoundTransition } from "@/components/round-transition" import { PseudoScreen } from "@/components/pseudo-screen" export function RoomPage({ code }: { code: string }) { + const [, navigate] = useLocation() const snapshot = useRoomStore((s) => s.snapshot) const roomCode = useRoomStore((s) => s.roomCode) + const leaveRoom = useRoomStore((s) => s.leaveRoom) const playerId = useRoomStore((s) => s.playerId) const connected = useRoomStore((s) => s.connected) const pseudoSet = useRoomStore((s) => s.pseudoSet) @@ -80,10 +83,22 @@ export function RoomPage({ code }: { code: string }) { >
- +
+ + +
diff --git a/apps/web/src/store/room.ts b/apps/web/src/store/room.ts index 5cb55f0..5b72134 100644 --- a/apps/web/src/store/room.ts +++ b/apps/web/src/store/room.ts @@ -77,6 +77,7 @@ interface RoomState { submitTrack: (youtubeUrl: string) => Promise removeTrack: (trackId: string) => Promise returnToLobby: () => void + leaveRoom: () => void vote: (choiceIndex: number) => void voteText: (text: string) => void voteBlindtest: (answer: BlindtestAnswer) => void @@ -192,6 +193,11 @@ export const useRoomStore = create((set, get) => ({ socket.emit("lobby:return") }, + leaveRoom: () => { + socket.emit("room:leave") + get().reset() + }, + vote: (choiceIndex) => { if (get().hasVoted) { return diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts index eabf5d3..f0dfe38 100644 --- a/packages/shared/src/events.ts +++ b/packages/shared/src/events.ts @@ -149,6 +149,7 @@ export interface ClientToServerEvents { "room:join": (payload: RoomJoinPayload, ack: Ack) => void "room:rejoin": (payload: RejoinPayload, ack: Ack) => void "player:setName": (payload: SetNamePayload) => void + "room:leave": () => void "lobby:updateSettings": (payload: UpdateSettingsPayload) => void "lobby:return": () => void "game:start": (ack: Ack) => void