Some checks failed
Publish Docker image / Push Docker image to Docker Hub (push) Failing after 6m59s
774 lines
24 KiB
JavaScript
774 lines
24 KiB
JavaScript
// Socket.IO client instance for real-time communication
|
|
import { io } from 'socket.io-client';
|
|
import { createContext, useContext, useEffect, useState } from 'react';
|
|
|
|
// Create a Socket.IO context for use throughout the app
|
|
const SocketContext = createContext(null);
|
|
|
|
// Game context for sharing game state
|
|
const GameContext = createContext(null);
|
|
|
|
export function SocketProvider({ children }) {
|
|
const [socket, setSocket] = useState(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
const [reconnectionAttempts, setReconnectionAttempts] = useState(0);
|
|
const [offlineActions, setOfflineActions] = useState([]);
|
|
const [lastActivityTime, setLastActivityTime] = useState(Date.now());
|
|
|
|
useEffect(() => {
|
|
// Create socket connection on component mount with improved reconnection settings
|
|
const socketInstance = io(import.meta.env.DEV ? 'http://localhost:5237' : window.location.origin, {
|
|
autoConnect: true,
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 10000,
|
|
reconnectionAttempts: Infinity, // Keep trying to reconnect indefinitely
|
|
timeout: 20000, // Longer timeout for initial connection
|
|
randomizationFactor: 0.5 // Add some randomization to reconnection attempts to prevent thundering herd
|
|
});
|
|
|
|
// Socket event listeners
|
|
socketInstance.on('connect', () => {
|
|
setIsConnected(true);
|
|
setIsReconnecting(false);
|
|
setReconnectionAttempts(0);
|
|
console.log('Connected to server');
|
|
|
|
// Process any actions that were queued while offline
|
|
if (offlineActions.length > 0) {
|
|
console.log(`Processing ${offlineActions.length} queued actions from offline mode`);
|
|
// Wait a moment to ensure connection is stable
|
|
setTimeout(() => {
|
|
processOfflineActions(socketInstance);
|
|
}, 1000);
|
|
}
|
|
|
|
// Try to reconnect to game if we have saved data
|
|
const savedGameData = localStorage.getItem('songBattleGame');
|
|
if (savedGameData) {
|
|
try {
|
|
const { lobbyId, playerName, lastKnownState } = JSON.parse(savedGameData);
|
|
if (lobbyId && playerName) {
|
|
console.log(`Attempting to reconnect to lobby: ${lobbyId}`);
|
|
socketInstance.emit('reconnect_to_lobby', {
|
|
lobbyId,
|
|
playerName,
|
|
lastKnownState // Send last known game state for server reconciliation
|
|
}, (response) => {
|
|
if (response.error) {
|
|
console.error('Reconnection failed:', response.error);
|
|
// Don't remove data immediately, we might try again
|
|
if (response.error === 'Lobby not found') {
|
|
localStorage.removeItem('songBattleGame');
|
|
}
|
|
} else {
|
|
console.log('Successfully reconnected to lobby');
|
|
// Update the saved game data with latest state
|
|
updateSavedGameData(response.lobby);
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing saved game data:', e);
|
|
localStorage.removeItem('songBattleGame');
|
|
}
|
|
}
|
|
});
|
|
|
|
socketInstance.on('disconnect', (reason) => {
|
|
setIsConnected(false);
|
|
console.log(`Disconnected from server: ${reason}`);
|
|
|
|
// If the disconnection is expected, don't try to reconnect
|
|
if (reason === 'io client disconnect') {
|
|
console.log('Disconnection was initiated by the client');
|
|
} else {
|
|
// Start reconnection process for unexpected disconnections
|
|
setIsReconnecting(true);
|
|
}
|
|
});
|
|
|
|
socketInstance.on('connect_error', (error) => {
|
|
console.error('Connection error:', error);
|
|
setIsReconnecting(true);
|
|
});
|
|
|
|
socketInstance.on('reconnect_attempt', (attemptNumber) => {
|
|
console.log(`Reconnection attempt #${attemptNumber}`);
|
|
setReconnectionAttempts(attemptNumber);
|
|
});
|
|
|
|
socketInstance.on('reconnect', () => {
|
|
console.log('Reconnected to server after interruption');
|
|
setIsReconnecting(false);
|
|
setReconnectionAttempts(0);
|
|
});
|
|
|
|
socketInstance.on('reconnect_error', (error) => {
|
|
console.error('Reconnection error:', error);
|
|
});
|
|
|
|
socketInstance.on('reconnect_failed', () => {
|
|
console.error('Failed to reconnect to server after maximum attempts');
|
|
setIsReconnecting(false);
|
|
});
|
|
|
|
// Setup a heartbeat to detect silent disconnections
|
|
const heartbeatInterval = setInterval(() => {
|
|
if (socketInstance && isConnected) {
|
|
// Check if we've seen activity recently (within 15 seconds)
|
|
const timeSinceLastActivity = Date.now() - lastActivityTime;
|
|
if (timeSinceLastActivity > 15000) {
|
|
console.log('No recent activity, sending ping to verify connection');
|
|
socketInstance.emit('ping', () => {
|
|
// Update activity time when we get a response
|
|
setLastActivityTime(Date.now());
|
|
});
|
|
|
|
// Set a timeout to check if the ping was successful
|
|
setTimeout(() => {
|
|
const newTimeSinceLastActivity = Date.now() - lastActivityTime;
|
|
if (newTimeSinceLastActivity > 15000) {
|
|
console.warn('No response to ping, connection may be dead');
|
|
// Force a reconnection attempt
|
|
socketInstance.disconnect().connect();
|
|
}
|
|
}, 5000);
|
|
}
|
|
}
|
|
}, 30000); // Check every 30 seconds
|
|
|
|
setSocket(socketInstance);
|
|
|
|
// Function to process queued offline actions
|
|
const processOfflineActions = (socket) => {
|
|
if (!socket || !offlineActions.length) return;
|
|
|
|
// Process each action in sequence
|
|
const processAction = (index) => {
|
|
if (index >= offlineActions.length) {
|
|
// All actions processed, clear the queue
|
|
setOfflineActions([]);
|
|
return;
|
|
}
|
|
|
|
const { action, payload, callback } = offlineActions[index];
|
|
console.log(`Processing offline action: ${action}`, payload);
|
|
|
|
// Emit the action to the server
|
|
socket.emit(action, payload, (response) => {
|
|
if (response.error) {
|
|
console.error(`Error processing offline action ${action}:`, response.error);
|
|
} else {
|
|
console.log(`Successfully processed offline action: ${action}`);
|
|
if (callback) callback(response);
|
|
}
|
|
|
|
// Process the next action
|
|
processAction(index + 1);
|
|
});
|
|
};
|
|
|
|
// Start processing from the first action
|
|
processAction(0);
|
|
};
|
|
|
|
// Update activity timestamp on any received message
|
|
const originalOnEvent = socketInstance.onEvent;
|
|
socketInstance.onEvent = (packet) => {
|
|
setLastActivityTime(Date.now());
|
|
originalOnEvent.call(socketInstance, packet);
|
|
};
|
|
|
|
// Function to update saved game data with latest state
|
|
const updateSavedGameData = (lobby) => {
|
|
if (!lobby) return;
|
|
|
|
const currentData = localStorage.getItem('songBattleGame');
|
|
if (currentData) {
|
|
try {
|
|
const data = JSON.parse(currentData);
|
|
data.lastKnownState = {
|
|
gameState: lobby.state,
|
|
currentBattle: lobby.currentBattle,
|
|
timestamp: Date.now()
|
|
};
|
|
localStorage.setItem('songBattleGame', JSON.stringify(data));
|
|
} catch (e) {
|
|
console.error('Error updating saved game data:', e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Clean up on unmount
|
|
return () => {
|
|
clearInterval(heartbeatInterval);
|
|
socketInstance.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
// Offline mode detection
|
|
const [isOffline, setIsOffline] = useState(!navigator.onLine);
|
|
|
|
// Monitor browser's online/offline status
|
|
useEffect(() => {
|
|
const handleOnline = () => setIsOffline(false);
|
|
const handleOffline = () => setIsOffline(true);
|
|
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
// Add an action to the offline queue
|
|
const queueOfflineAction = (action, payload, callback) => {
|
|
console.log(`Queuing offline action: ${action}`, payload);
|
|
setOfflineActions(prev => [...prev, { action, payload, callback }]);
|
|
|
|
// Store in localStorage as fallback
|
|
try {
|
|
const offlineQueue = JSON.parse(localStorage.getItem('offlineActionQueue') || '[]');
|
|
offlineQueue.push({ action, payload, timestamp: Date.now() });
|
|
localStorage.setItem('offlineActionQueue', JSON.stringify(offlineQueue));
|
|
} catch (e) {
|
|
console.error('Error storing offline action in localStorage:', e);
|
|
}
|
|
};
|
|
|
|
// Emit an event, queue it if offline
|
|
const safeEmit = (eventName, data, callback) => {
|
|
// Update activity timestamp
|
|
setLastActivityTime(Date.now());
|
|
|
|
if (socket && isConnected && !isOffline) {
|
|
// Online - send immediately
|
|
socket.emit(eventName, data, callback);
|
|
} else {
|
|
// Offline or disconnected - queue for later
|
|
queueOfflineAction(eventName, data, callback);
|
|
|
|
// For specific actions, provide optimistic UI updates
|
|
if (eventName === 'submit_vote' && data.songId) {
|
|
// Optimistically show the vote locally
|
|
// Implementation depends on your UI update mechanism
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SocketContext.Provider value={{
|
|
socket,
|
|
isConnected,
|
|
isReconnecting,
|
|
reconnectionAttempts,
|
|
isOffline,
|
|
safeEmit,
|
|
queueOfflineAction
|
|
}}>
|
|
{children}
|
|
</SocketContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Game state provider - manages game state and provides methods for game actions
|
|
*/
|
|
export function GameProvider({ children }) {
|
|
const { socket, isConnected, isOffline, isReconnecting, safeEmit } = useContext(SocketContext);
|
|
const [lobby, setLobby] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [currentPlayer, setCurrentPlayer] = useState(null);
|
|
|
|
// Save game info when lobby is joined
|
|
useEffect(() => {
|
|
if (lobby && currentPlayer) {
|
|
const savedData = {
|
|
lobbyId: lobby.id,
|
|
playerName: currentPlayer.name,
|
|
lastKnownState: {
|
|
gameState: lobby.state,
|
|
currentBattle: lobby.currentBattle,
|
|
timestamp: Date.now()
|
|
}
|
|
};
|
|
localStorage.setItem('songBattleGame', JSON.stringify(savedData));
|
|
}
|
|
}, [lobby, currentPlayer]);
|
|
|
|
// Helper function to update saved game data
|
|
const updateSavedGameData = (updatedLobby) => {
|
|
if (!updatedLobby || !currentPlayer) return;
|
|
|
|
const savedData = {
|
|
lobbyId: updatedLobby.id,
|
|
playerName: currentPlayer.name,
|
|
lastKnownState: {
|
|
gameState: updatedLobby.state,
|
|
currentBattle: updatedLobby.currentBattle,
|
|
playerState: {
|
|
isReady: updatedLobby.players?.find(p => p.id === currentPlayer.id)?.isReady,
|
|
hasVoted: updatedLobby.currentBattle?.votes?.[currentPlayer.id] !== undefined
|
|
},
|
|
timestamp: Date.now()
|
|
}
|
|
};
|
|
localStorage.setItem('songBattleGame', JSON.stringify(savedData));
|
|
};
|
|
|
|
// Clear error after 5 seconds
|
|
useEffect(() => {
|
|
if (error) {
|
|
const timer = setTimeout(() => {
|
|
setError(null);
|
|
}, 5000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [error]);
|
|
|
|
// Socket event handlers for game updates
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
|
|
// Player joined the lobby
|
|
const handlePlayerJoined = (data) => {
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
|
|
// Update the lobby with the new player
|
|
const updatedPlayers = [...prevLobby.players, {
|
|
id: data.playerId,
|
|
name: data.playerName,
|
|
isConnected: true,
|
|
isReady: false,
|
|
songCount: 0
|
|
}];
|
|
|
|
return {
|
|
...prevLobby,
|
|
players: updatedPlayers
|
|
};
|
|
});
|
|
};
|
|
|
|
// Player reconnected to the lobby
|
|
const handlePlayerReconnected = (data) => {
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
|
|
// Update the lobby with the reconnected player
|
|
const updatedPlayers = prevLobby.players.map(player => {
|
|
if (player.name === data.playerName && !player.isConnected) {
|
|
return { ...player, id: data.playerId, isConnected: true };
|
|
}
|
|
return player;
|
|
});
|
|
|
|
return {
|
|
...prevLobby,
|
|
players: updatedPlayers
|
|
};
|
|
});
|
|
};
|
|
|
|
// Player disconnected from the lobby
|
|
const handlePlayerDisconnected = (data) => {
|
|
setLobby(data.lobby);
|
|
};
|
|
|
|
// Game settings were updated
|
|
const handleSettingsUpdated = (data) => {
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
return {
|
|
...prevLobby,
|
|
settings: data.settings
|
|
};
|
|
});
|
|
};
|
|
|
|
// Game started
|
|
const handleGameStarted = (data) => {
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
return {
|
|
...prevLobby,
|
|
state: data.state
|
|
};
|
|
});
|
|
};
|
|
|
|
// Songs were updated
|
|
const handleSongsUpdated = (data) => {
|
|
setLobby(data.lobby);
|
|
};
|
|
|
|
// Player status changed (ready/not ready)
|
|
const handlePlayerStatusChanged = (data) => {
|
|
console.log('Player status changed, new lobby state:', data.lobby.state);
|
|
setLobby(data.lobby);
|
|
|
|
// If the state is VOTING and we have a current battle, explicitly log it
|
|
if (data.lobby.state === 'VOTING' && data.lobby.currentBattle) {
|
|
console.log('Battle ready in player_status_changed:', data.lobby.currentBattle);
|
|
}
|
|
};
|
|
|
|
// Vote was submitted
|
|
const handleVoteSubmitted = (data) => {
|
|
console.log('Vote submitted, updating lobby');
|
|
setLobby(data.lobby);
|
|
};
|
|
|
|
// New battle started
|
|
const handleNewBattle = (data) => {
|
|
console.log('New battle received:', data.battle);
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
|
|
// Ensure we update both the currentBattle and the state
|
|
return {
|
|
...prevLobby,
|
|
currentBattle: data.battle,
|
|
state: 'VOTING'
|
|
};
|
|
});
|
|
};
|
|
|
|
// Handle battle ended and show result screen
|
|
const handleBattleEnded = (data) => {
|
|
console.log('Battle ended, showing result screen:', data);
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
return {
|
|
...prevLobby,
|
|
state: 'BATTLE',
|
|
previousBattle: data.previousBattle
|
|
};
|
|
});
|
|
};
|
|
|
|
// Game finished
|
|
const handleGameFinished = (data) => {
|
|
setLobby(prevLobby => {
|
|
if (!prevLobby) return prevLobby;
|
|
return {
|
|
...prevLobby,
|
|
state: 'FINISHED',
|
|
finalWinner: data.winner,
|
|
battles: data.battles
|
|
};
|
|
});
|
|
};
|
|
|
|
// Register socket event listeners
|
|
socket.on('player_joined', handlePlayerJoined);
|
|
socket.on('player_reconnected', handlePlayerReconnected);
|
|
socket.on('player_disconnected', handlePlayerDisconnected);
|
|
socket.on('settings_updated', handleSettingsUpdated);
|
|
socket.on('game_started', handleGameStarted);
|
|
socket.on('songs_updated', handleSongsUpdated);
|
|
socket.on('player_status_changed', handlePlayerStatusChanged);
|
|
socket.on('vote_submitted', handleVoteSubmitted);
|
|
socket.on('battle_ended', handleBattleEnded);
|
|
socket.on('tournament_started', data => {
|
|
console.log('Tournament started event received:', data);
|
|
setLobby(prevLobby => prevLobby ? {...prevLobby, state: data.state} : prevLobby);
|
|
});
|
|
socket.on('new_battle', handleNewBattle);
|
|
socket.on('battle_ended', handleBattleEnded);
|
|
socket.on('game_finished', handleGameFinished);
|
|
|
|
// Clean up listeners on unmount
|
|
return () => {
|
|
socket.off('player_joined', handlePlayerJoined);
|
|
socket.off('player_reconnected', handlePlayerReconnected);
|
|
socket.off('player_disconnected', handlePlayerDisconnected);
|
|
socket.off('settings_updated', handleSettingsUpdated);
|
|
socket.off('game_started', handleGameStarted);
|
|
socket.off('songs_updated', handleSongsUpdated);
|
|
socket.off('player_status_changed', handlePlayerStatusChanged);
|
|
socket.off('vote_submitted', handleVoteSubmitted);
|
|
socket.off('new_battle', handleNewBattle);
|
|
socket.off('battle_ended', handleBattleEnded);
|
|
socket.off('game_finished', handleGameFinished);
|
|
};
|
|
}, [socket]);
|
|
|
|
// Create a lobby
|
|
const createLobby = (playerName) => {
|
|
if (!socket || !isConnected) {
|
|
setError('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
safeEmit('create_lobby', { playerName }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else {
|
|
setLobby(response.lobby);
|
|
setCurrentPlayer({
|
|
id: socket.id,
|
|
name: playerName,
|
|
isHost: true
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
// Join a lobby
|
|
const joinLobby = (lobbyId, playerName) => {
|
|
if (!socket || !isConnected) {
|
|
setError('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
safeEmit('join_lobby', { lobbyId, playerName }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else {
|
|
setLobby(response.lobby);
|
|
setCurrentPlayer({
|
|
id: socket.id,
|
|
name: playerName,
|
|
isHost: response.lobby.hostId === socket.id
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
// Update lobby settings
|
|
const updateSettings = (settings) => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return;
|
|
}
|
|
|
|
safeEmit('update_settings', { settings }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Start the game
|
|
const startGame = () => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return;
|
|
}
|
|
|
|
safeEmit('start_game', {}, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Add a song
|
|
const addSong = (song) => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return Promise.reject('Not connected to server or no active lobby');
|
|
}
|
|
|
|
console.log('Attempting to add song:', song);
|
|
console.log('Current player state:', currentPlayer);
|
|
console.log('Current lobby state before adding song:', lobby);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
safeEmit('add_song', { song }, (response) => {
|
|
console.log('Song addition response:', response);
|
|
if (response.error) {
|
|
console.error('Error adding song:', response.error);
|
|
setError(response.error);
|
|
reject(response.error);
|
|
} else if (response.lobby) {
|
|
// Log detailed lobby state for debugging
|
|
console.log('Song added successfully, full lobby response:', response.lobby);
|
|
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
|
|
console.log('All players song data:',
|
|
response.lobby.players.map(p => ({
|
|
name: p.name,
|
|
id: p.id,
|
|
songCount: p.songCount,
|
|
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
|
|
}))
|
|
);
|
|
|
|
// Force a deep clone of the lobby to ensure React detects the change
|
|
const updatedLobby = JSON.parse(JSON.stringify(response.lobby));
|
|
setLobby(updatedLobby);
|
|
|
|
// Verify the state was updated correctly
|
|
setTimeout(() => {
|
|
// This won't show the updated state immediately due to React's state update mechanism
|
|
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
|
|
console.log('Updated lobby that was set:', updatedLobby);
|
|
}, 0);
|
|
|
|
// Store the latest state for offline reconciliation
|
|
updateSavedGameData(updatedLobby);
|
|
|
|
// Resolve with the updated lobby
|
|
resolve(updatedLobby);
|
|
} else {
|
|
console.error('Song addition succeeded but no lobby data was returned');
|
|
setError('Failed to update song list');
|
|
reject('Failed to update song list');
|
|
}
|
|
});
|
|
});
|
|
};
|
|
// Search for songs on YouTube
|
|
const searchYouTube = (query) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (!socket || !isConnected) {
|
|
setError('Not connected to server');
|
|
reject('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
safeEmit('search_youtube', { query }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
reject(response.error);
|
|
} else {
|
|
resolve(response.results);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
// Get metadata for a YouTube video by ID
|
|
const getYouTubeMetadata = (videoId) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (!socket || !isConnected) {
|
|
setError('Not connected to server');
|
|
reject('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
safeEmit('get_video_metadata', { videoId }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
reject(response.error);
|
|
} else {
|
|
resolve(response.metadata);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
// Remove a song
|
|
const removeSong = (songId) => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return;
|
|
}
|
|
|
|
safeEmit('remove_song', { songId }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else if (response.lobby) {
|
|
setLobby(response.lobby);
|
|
updateSavedGameData(response.lobby);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Set player ready
|
|
const setPlayerReady = () => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return;
|
|
}
|
|
|
|
safeEmit('player_ready', {}, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else if (response.lobby) {
|
|
setLobby(response.lobby);
|
|
updateSavedGameData(response.lobby);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Submit a vote
|
|
const submitVote = (songId) => {
|
|
if (!socket || !isConnected || !lobby) {
|
|
setError('Not connected to server or no active lobby');
|
|
return;
|
|
}
|
|
|
|
safeEmit('submit_vote', { songId }, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else if (response.lobby) {
|
|
setLobby(response.lobby);
|
|
updateSavedGameData(response.lobby);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Leave lobby and clear saved game state
|
|
const leaveLobby = () => {
|
|
localStorage.removeItem('songBattleGame');
|
|
setLobby(null);
|
|
setCurrentPlayer(null);
|
|
};
|
|
|
|
// Proceed to next battle after the battle result screen
|
|
const proceedToNextBattle = () => {
|
|
if (!socket || !isConnected) {
|
|
setError('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
safeEmit('proceed_to_next_battle', {}, (response) => {
|
|
if (response.error) {
|
|
setError(response.error);
|
|
} else if (response.lobby) {
|
|
setLobby(response.lobby);
|
|
updateSavedGameData(response.lobby);
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<GameContext.Provider value={{
|
|
lobby,
|
|
error,
|
|
currentPlayer,
|
|
isHost: currentPlayer && lobby && currentPlayer.id === lobby.hostId,
|
|
isOffline: isOffline,
|
|
isReconnecting,
|
|
createLobby,
|
|
joinLobby,
|
|
updateSettings,
|
|
startGame,
|
|
addSong,
|
|
removeSong,
|
|
setPlayerReady,
|
|
submitVote,
|
|
leaveLobby,
|
|
searchYouTube,
|
|
getYouTubeMetadata,
|
|
proceedToNextBattle
|
|
}}>
|
|
{children}
|
|
</GameContext.Provider>
|
|
);
|
|
}
|
|
|
|
// Custom hooks for using contexts
|
|
export const useSocket = () => useContext(SocketContext);
|
|
export const useGame = () => useContext(GameContext);
|
|
|
|
// Re-export for better module compatibility
|
|
export { SocketContext, GameContext };
|