From ae14680ecab0624a57facb73300a7e77f4093128 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sat, 6 Dec 2025 19:18:15 -0800 Subject: [PATCH] [C][2nub] Use sessionStorage to persist current tab --- experimental/2nub/.claude/settings.local.json | 6 ++- experimental/2nub/src/client/App.tsx | 45 ++++++++++++++++++- .../2nub/src/client/utils/sessionStorage.ts | 33 ++++++++++++++ experimental/2nub/src/server/gameState.ts | 13 ++++++ experimental/2nub/src/server/routes.ts | 35 +++++++++++++++ 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 experimental/2nub/src/client/utils/sessionStorage.ts diff --git a/experimental/2nub/.claude/settings.local.json b/experimental/2nub/.claude/settings.local.json index 13c5ada..5284030 100644 --- a/experimental/2nub/.claude/settings.local.json +++ b/experimental/2nub/.claude/settings.local.json @@ -9,7 +9,11 @@ "Bash(npm run dev:server:*)", "Bash(npm run dev:client:*)", "Bash(timeout 10s npm run dev:server)", - "Bash(curl:*)" + "Bash(curl:*)", + "Bash(npm run dev:*)", + "Bash(npm run typecheck:*)", + "Bash(npm run:*)", + "Bash(npx tsc:*)" ], "deny": [], "ask": [] diff --git a/experimental/2nub/src/client/App.tsx b/experimental/2nub/src/client/App.tsx index 5e19e0a..e6e972c 100644 --- a/experimental/2nub/src/client/App.tsx +++ b/experimental/2nub/src/client/App.tsx @@ -5,6 +5,7 @@ import { GameList } from './components/GameList'; import { useWebSocket } from './hooks/useWebSocket'; import { GameState, WebSocketMessage } from '../types'; import { buildApiUrl } from './config/api'; +import { sessionStorage } from './utils/sessionStorage'; const App: React.FC = () => { const [games, setGames] = useState([]); @@ -83,6 +84,31 @@ const App: React.FC = () => { } }; + const attemptRejoin = async (gameId: string, playerId: string): Promise => { + try { + const response = await fetch(buildApiUrl(`api/games/${gameId}/rejoin`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playerId }) + }); + const result = await response.json(); + if (result.success) { + setCurrentGame(result.data.game); + setCurrentPlayerId(result.data.player.id); + setView('game'); + + sendMessage({ + type: 'joinGame', + data: { gameId, playerId: result.data.player.id } + }); + return true; + } + } catch (error) { + console.error('Failed to rejoin game:', error); + } + return false; + }; + const joinGame = async (gameId: string, playerName: string) => { try { const response = await fetch(buildApiUrl(`api/games/${gameId}/join`), { @@ -96,6 +122,8 @@ const App: React.FC = () => { setCurrentPlayerId(result.data.player.id); setView('game'); + sessionStorage.setPlayerSession(result.data.player.id, gameId); + sendMessage({ type: 'joinGame', data: { gameId, playerId: result.data.player.id } @@ -116,14 +144,27 @@ const App: React.FC = () => { setCurrentGame(null); setCurrentPlayerId(null); setView('list'); + sessionStorage.clearPlayerSession(); } catch (error) { console.error('Failed to leave game:', error); } }; useEffect(() => { - connect(); - fetchGames(); + const initializeApp = async () => { + connect(); + await fetchGames(); + + const session = sessionStorage.getPlayerSession(); + if (session.playerId && session.gameId) { + const rejoined = await attemptRejoin(session.gameId, session.playerId); + if (!rejoined) { + sessionStorage.clearPlayerSession(); + } + } + }; + + initializeApp(); }, []); return ( diff --git a/experimental/2nub/src/client/utils/sessionStorage.ts b/experimental/2nub/src/client/utils/sessionStorage.ts new file mode 100644 index 0000000..8d09ffd --- /dev/null +++ b/experimental/2nub/src/client/utils/sessionStorage.ts @@ -0,0 +1,33 @@ +const STORAGE_KEYS = { + CURRENT_PLAYER_ID: 'current-player-id', + CURRENT_GAME_ID: 'current-game-id' +} as const; + +export interface SessionData { + playerId: string | null; + gameId: string | null; +} + +export const sessionStorage = { + setPlayerSession(playerId: string, gameId: string): void { + window.sessionStorage.setItem(STORAGE_KEYS.CURRENT_PLAYER_ID, playerId); + window.sessionStorage.setItem(STORAGE_KEYS.CURRENT_GAME_ID, gameId); + }, + + getPlayerSession(): SessionData { + const playerId = window.sessionStorage.getItem(STORAGE_KEYS.CURRENT_PLAYER_ID); + const gameId = window.sessionStorage.getItem(STORAGE_KEYS.CURRENT_GAME_ID); + + return { playerId, gameId }; + }, + + clearPlayerSession(): void { + window.sessionStorage.removeItem(STORAGE_KEYS.CURRENT_PLAYER_ID); + window.sessionStorage.removeItem(STORAGE_KEYS.CURRENT_GAME_ID); + }, + + hasPlayerSession(): boolean { + const { playerId, gameId } = this.getPlayerSession(); + return !!(playerId && gameId); + } +}; \ No newline at end of file diff --git a/experimental/2nub/src/server/gameState.ts b/experimental/2nub/src/server/gameState.ts index f51d9ef..7efd5fa 100644 --- a/experimental/2nub/src/server/gameState.ts +++ b/experimental/2nub/src/server/gameState.ts @@ -79,6 +79,19 @@ class GameStateManager { return true; } + rejoinPlayer(gameId: string, playerId: string): { player: Player; game: GameState } | null { + const game = this.games.get(gameId); + if (!game) return null; + + const player = game.players.find(p => p.id === playerId); + if (!player) return null; + + player.connected = true; + game.lastActivity = new Date(); + + return { player, game }; + } + private generateGameId(): string { return Math.random().toString(10).substring(2, 8); } diff --git a/experimental/2nub/src/server/routes.ts b/experimental/2nub/src/server/routes.ts index 54a5228..daa3468 100644 --- a/experimental/2nub/src/server/routes.ts +++ b/experimental/2nub/src/server/routes.ts @@ -96,6 +96,41 @@ export function setupRoutes(): Router { res.status(201).json(response); }); + router.post('/games/:gameId/rejoin', (req, res) => { + const { gameId } = req.params; + const { playerId }: { playerId: string } = req.body; + + if (!playerId || playerId.trim() === '') { + const response: ApiResponse = { + success: false, + error: 'Player ID is required' + }; + return res.status(400).json(response); + } + + const result = gameStateManager.rejoinPlayer(gameId, playerId.trim()); + + if (!result) { + const response: ApiResponse = { + success: false, + error: 'Game or player not found' + }; + return res.status(404).json(response); + } + + broadcastToGame(gameId, { + type: 'playerJoined', + data: { player: result.player, game: result.game }, + gameId + }); + + const response: ApiResponse = { + success: true, + data: { player: result.player, game: result.game } + }; + res.json(response); + }); + router.delete('/games/:gameId/players/:playerId', (req, res) => { const { gameId, playerId } = req.params; -- 2.51.2