contextProvider

This commit is contained in:
magdo
2025-11-18 00:09:08 +01:00
parent 13871b2dcc
commit 8647fde38f
7 changed files with 482 additions and 57 deletions
@@ -0,0 +1,371 @@
import React, { createContext, useContext, useRef, useState, useCallback, useMemo, useEffect } from 'react';
import { io } from 'socket.io-client';
import { API_CONFIG } from '../api/userApi';
const GameWebSocketContext = createContext(null);
const isDev = import.meta.env.DEV;
const log = (...args) => isDev && console.log(...args);
const warn = (...args) => isDev && console.warn(...args);
const logError = (...args) => console.error(...args);
/**
* Provider that maintains WebSocket connection across page navigation
*/
export const GameWebSocketProvider = ({ children }) => {
const socketRef = useRef(null);
const gameTokenRef = useRef(null);
const [isConnected, setIsConnected] = useState(false);
const [gameState, setGameState] = useState(null);
const [boardData, setBoardData] = useState(null);
const [error, setError] = useState(null);
const [isGamemaster, setIsGamemaster] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [pendingPlayers, setPendingPlayers] = useState([]);
const [approvalStatus, setApprovalStatus] = useState(null);
// Memoized derived values
const players = useMemo(() => {
const connectedPlayers = gameState?.connectedPlayers || [];
const currentPlayers = gameState?.currentPlayers || [];
if (currentPlayers.length > 0) {
return currentPlayers;
}
if (connectedPlayers.length > 0) {
return connectedPlayers.map((nameOrObj, index) => {
const playerName = typeof nameOrObj === 'string'
? nameOrObj
: (nameOrObj.playerName || nameOrObj.name || `Player ${index + 1}`);
return {
id: `player-${index}`,
name: playerName,
isOnline: true,
isReady: gameState?.readyPlayers?.includes(playerName) || false,
};
});
}
return [];
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
/**
* Connect to game WebSocket with a game token
* This maintains the connection even when navigating between pages
*/
const connect = useCallback((gameToken) => {
if (!gameToken) {
warn('⚠️ Cannot connect without game token');
return;
}
// If already connected with same token, don't reconnect
if (socketRef.current?.connected && gameTokenRef.current === gameToken) {
log('✅ Already connected with same token');
return;
}
// Disconnect old socket if exists
if (socketRef.current) {
log('🔌 Disconnecting old socket');
socketRef.current.removeAllListeners();
socketRef.current.disconnect();
}
log('🔌 Connecting to game WebSocket...');
gameTokenRef.current = gameToken;
// Connect to /game namespace
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 5000,
});
const socket = socketRef.current;
// Connection handlers
socket.on('connect', () => {
log('✅ Connected to game WebSocket');
setIsConnected(true);
setError(null);
socket.emit('game:join', { gameToken });
});
socket.on('connect_error', (err) => {
logError('❌ Connection error:', err);
setIsConnected(false);
setError(err.message);
});
socket.on('disconnect', (reason) => {
log('🔌 Disconnected:', reason);
setIsConnected(false);
});
// Game state handlers
socket.on('game:state', (state) => {
log('📊 Game state received:', state);
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
});
socket.on('game:state-update', (state) => {
log('📊 Game state update:', state);
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
});
socket.on('game:joined', (data) => {
log('✅ Joined game:', data);
if (data.isGamemaster !== undefined) {
setIsGamemaster(data.isGamemaster);
}
});
socket.on('game:player-joined', (data) => {
log('👤 Player joined:', data.playerName);
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
}
return prev;
});
});
socket.on('game:start', (data) => {
log('🎮 Game started:', data);
setGameStarted(true);
// Store board data if provided
if (data.boardData) {
setBoardData(data.boardData);
log('✅ Board data stored from game:start event');
}
// Update game state with turn info
if (data.playerOrder) {
setGameState(prev => ({
...prev,
playerOrder: data.playerOrder,
currentPlayer: data.currentPlayer,
turnSequence: data.playerOrder
}));
}
});
socket.on('game:started', (data) => {
log('🎮 Game started (legacy event):', data);
setGameStarted(true);
});
socket.on('game:player-moved', (moveData) => {
log('🏃 Player moved:', moveData.playerName);
setGameState(prev => {
if (!prev?.currentPlayers) return prev;
return {
...prev,
currentPlayers: prev.currentPlayers.map(p =>
p.playerId === moveData.playerId
? { ...p, boardPosition: moveData.newPosition }
: p
),
};
});
});
socket.on('game:turn-changed', (data) => {
log('🔄 Turn changed to:', data.currentPlayerName);
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
});
socket.on('game:error', (err) => {
logError('❌ Game error:', err);
setError(err.message);
});
// Approval system handlers
socket.on('game:pending-approval', (data) => {
log('⏳ Pending gamemaster approval:', data);
setApprovalStatus('pending');
setError('Waiting for gamemaster approval...');
});
socket.on('game:player-requesting-join', (data) => {
log('🔔 Player requesting to join:', data.playerName);
setPendingPlayers(prev => {
if (prev.some(p => p.playerName === data.playerName)) return prev;
return [...prev, {
playerName: data.playerName,
isAuthenticated: data.isAuthenticated,
timestamp: data.timestamp
}];
});
});
socket.on('game:approval-granted', (data) => {
log('✅ Join request approved:', data);
setApprovalStatus('approved');
setError(null);
socket.emit('game:join-approved', { gameToken });
});
socket.on('game:approval-denied', (data) => {
logError('❌ Join request denied:', data.reason || data.message);
setApprovalStatus('denied');
setError(data.reason || 'Your request to join was denied');
});
socket.on('game:player-approved', (data) => {
log('✅ Player approved by gamemaster:', data.playerName);
setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName));
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
}
return prev;
});
});
}, []);
/**
* Disconnect from WebSocket
*/
const disconnect = useCallback(() => {
if (socketRef.current) {
log('🔌 Disconnecting from game WebSocket');
socketRef.current.removeAllListeners();
socketRef.current.disconnect();
socketRef.current = null;
gameTokenRef.current = null;
setIsConnected(false);
setGameState(null);
setBoardData(null);
setError(null);
setIsGamemaster(false);
setGameStarted(false);
setPendingPlayers([]);
setApprovalStatus(null);
}
}, []);
// Action methods
const rollDice = useCallback((diceValue) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot roll dice: not connected');
return false;
}
log('🎲 Rolling dice:', diceValue);
socket.emit('game:dice-roll', { gameCode: gameState?.gameCode, diceValue });
return true;
}, [isConnected, gameState?.gameCode]);
const sendMessage = useCallback((message) => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:chat', { gameCode: gameState?.gameCode, message });
return true;
}, [isConnected, gameState?.gameCode]);
const setReady = useCallback((ready = true) => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:ready', { gameCode: gameState?.gameCode, ready });
return true;
}, [isConnected, gameState?.gameCode]);
const leaveGame = useCallback(() => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:leave', { gameCode: gameState?.gameCode });
return true;
}, [isConnected, gameState?.gameCode]);
const approvePlayer = useCallback((playerName) => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) return false;
socket.emit('game:approve-player', { gameCode: gameState?.gameCode, playerName });
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) return false;
socket.emit('game:reject-player', { gameCode: gameState?.gameCode, playerName, reason });
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const addEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (socket) socket.on(event, handler);
}, []);
const removeEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (socket) socket.off(event, handler);
}, []);
const value = {
socket: socketRef.current,
isConnected,
gameState,
players,
boardData,
currentTurn,
error,
isGamemaster,
gameStarted,
pendingPlayers,
approvalStatus,
// Connection management
connect,
disconnect,
// Methods
rollDice,
sendMessage,
setReady,
leaveGame,
approvePlayer,
rejectPlayer,
addEventListener,
removeEventListener,
};
return (
<GameWebSocketContext.Provider value={value}>
{children}
</GameWebSocketContext.Provider>
);
};
/**
* Hook to access the shared WebSocket connection
*/
export const useGameWebSocketContext = () => {
const context = useContext(GameWebSocketContext);
if (!context) {
throw new Error('useGameWebSocketContext must be used within GameWebSocketProvider');
}
return context;
};