Add connection status component and improve socket reconnection handling
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s

This commit is contained in:
2025-05-14 20:10:46 +02:00
parent f2712bdcec
commit 301e08b6e6
9 changed files with 651 additions and 106 deletions

View File

@ -7,6 +7,7 @@ import HomeScreen from "./components/HomeScreen";
import SongSubmissionScreen from "./components/SongSubmissionScreen";
import VotingScreen from "./components/VotingScreen";
import ResultsScreen from "./components/ResultsScreen";
import ConnectionStatus from "./components/ConnectionStatus";
const App = () => {
const [cursorPos, setCursorPos] = useState({x: 0, y: 0});
@ -84,12 +85,6 @@ const App = () => {
</div>
<div className="content-container">
{!isConnected && (
<div className="connection-status">
<p>Connecting to server...</p>
</div>
)}
{error && (
<div className="error-message">
<p>{error}</p>
@ -97,6 +92,9 @@ const App = () => {
)}
{renderGameScreen()}
{/* Show connection status component */}
<ConnectionStatus />
</div>
</>
)

View File

@ -0,0 +1,55 @@
// connection-status.sass - Component for displaying network connection status
.connection-status
position: fixed
bottom: 0
left: 0
right: 0
padding: 0.5rem 1rem
display: flex
align-items: center
justify-content: center
gap: 0.5rem
z-index: 1000
transition: all 0.3s ease
animation: slideUp 0.3s forwards
font-family: 'Press Start 2P', monospace
font-size: 0.7rem
&.connected
background-color: rgba($success, 0.9)
color: #fff
animation: slideUp 0.3s forwards, fadeOut 0.5s 2.5s forwards
&.disconnected
background-color: rgba(#f44336, 0.9)
color: #fff
&.reconnecting
background-color: rgba(#ff9800, 0.9)
color: #fff
&.offline
background-color: rgba(#f44336, 0.9)
color: #fff
.connection-icon
display: flex
align-items: center
justify-content: center
.connection-message
text-align: center
@keyframes slideUp
from
transform: translateY(100%)
to
transform: translateY(0)
@keyframes fadeOut
from
opacity: 1
to
opacity: 0
transform: translateY(100%)

View File

@ -253,11 +253,46 @@
.voting-actions
display: flex
justify-content: center
flex-direction: column
align-items: center
margin: 2rem 0
.btn
min-width: 180px
font-size: 1rem
&.offline
background-color: $secondary
position: relative
.offline-notice
margin-top: 1rem
padding: 0.5rem
background-color: rgba($secondary, 0.2)
border: 2px solid $secondary
max-width: 400px
text-align: center
font-size: 0.7rem
svg
margin-right: 0.5rem
color: $secondary
.offline-vote-status
margin-top: 1rem
padding: 0.5rem
background-color: rgba($success, 0.2)
border: 2px solid $success
max-width: 400px
text-align: center
font-size: 0.7rem
color: $success
animation: pulse-opacity 2s infinite
&.error
background-color: rgba(#f44336, 0.2)
border-color: #f44336
color: #f44336
// Voting status and information
.voting-status
@ -274,6 +309,17 @@
font-size: 0.9rem
color: $text
.reconnecting-notice
margin: 0.5rem auto 1rem auto
padding: 0.5rem
background-color: rgba($secondary, 0.2)
border: 2px solid $secondary
max-width: 400px
text-align: center
font-size: 0.7rem
color: $secondary
animation: pulse-opacity 2s infinite
.auto-advance-notice
margin: 1rem auto
max-width: 400px
@ -297,6 +343,11 @@
color: $primary
font-weight: bold
.offline-badge
color: $secondary
margin-left: 0.5rem
font-size: 0.7rem
// Player votes list styling
.player-votes
background-color: rgba(0, 0, 0, 0.2)
@ -368,6 +419,21 @@
background-color: rgba(255, 255, 255, 0.05)
border-color: rgba(255, 255, 255, 0.1)
opacity: 0.7
&.offline-voted
background-color: rgba($secondary, 0.1)
border-color: rgba($secondary, 0.5)
.offline-icon
color: $secondary
margin-left: 0.5rem
animation: pixel-pulse 1.5s infinite
@keyframes pulse-opacity
0%, 100%
opacity: 1
50%
opacity: 0.5
@keyframes pixel-blink
0%, 100%

View File

@ -12,6 +12,7 @@
@import './components/youtube-embed'
@import './components/youtube-search'
@import './components/song-form-overlay'
@import './components/connection-status'
// Global styles
html, body

View File

@ -0,0 +1,70 @@
// ConnectionStatus.jsx
import { useState, useEffect } from 'react';
import { useSocket } from '../context/GameContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faWifi, faExclamationTriangle, faSpinner } from '@fortawesome/free-solid-svg-icons';
function ConnectionStatus() {
const { socket, isConnected, isReconnecting, reconnectionAttempts } = useSocket();
const [showStatus, setShowStatus] = useState(false);
const [offline, setOffline] = useState(!navigator.onLine);
// Monitor browser's online/offline status
useEffect(() => {
const handleOnline = () => setOffline(false);
const handleOffline = () => setOffline(true);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Always show status when disconnected, hide after successful connection
useEffect(() => {
if (!isConnected || isReconnecting || offline) {
setShowStatus(true);
} else {
// Hide the status indicator after a delay
const timer = setTimeout(() => {
setShowStatus(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [isConnected, isReconnecting, offline]);
if (!showStatus) return null;
let statusClass = 'connection-status';
let icon, message;
if (offline) {
statusClass += ' offline';
icon = <FontAwesomeIcon icon={faExclamationTriangle} />;
message = 'Keine Internetverbindung. Spielstand wird lokal gespeichert.';
} else if (!isConnected) {
statusClass += ' disconnected';
icon = <FontAwesomeIcon icon={faExclamationTriangle} />;
message = 'Verbindung zum Server verloren. Versuche neu zu verbinden...';
} else if (isReconnecting) {
statusClass += ' reconnecting';
icon = <FontAwesomeIcon icon={faSpinner} spin />;
message = `Verbindungsversuch ${reconnectionAttempts}...`;
} else {
statusClass += ' connected';
icon = <FontAwesomeIcon icon={faWifi} />;
message = 'Verbunden mit dem Server';
}
return (
<div className={statusClass}>
<div className="connection-icon">{icon}</div>
<div className="connection-message">{message}</div>
</div>
);
}
export default ConnectionStatus;

View File

@ -2,15 +2,16 @@
import { useState, useEffect, useMemo } from 'react';
import { useGame } from '../context/GameContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown } from '@fortawesome/free-solid-svg-icons';
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import YouTubeEmbed from './YouTubeEmbed';
function VotingScreen() {
const { lobby, currentPlayer, submitVote, isHost } = useGame();
const { lobby, currentPlayer, submitVote, isHost, isOffline, isReconnecting } = useGame();
const [hasVoted, setHasVoted] = useState(false);
const [selectedSong, setSelectedSong] = useState(null);
const [countdown, setCountdown] = useState(null);
const [processingByeAdvance, setProcessingByeAdvance] = useState(false);
const [offlineVoteStatus, setOfflineVoteStatus] = useState(null);
// Hole aktuellen Kampf
const battle = lobby?.currentBattle || null;
@ -39,10 +40,10 @@ function VotingScreen() {
useEffect(() => {
if (battle && battle.votes && currentPlayer) {
// Prüfe, ob die ID des Spielers im Stimmen-Objekt existiert
// Da Stimmen als Objekt, nicht als Map gesendet werden
const votesObj = battle.votes || {};
setHasVoted(Object.prototype.hasOwnProperty.call(votesObj, currentPlayer.id) ||
Object.keys(votesObj).includes(currentPlayer.id));
setHasVoted(
Object.prototype.hasOwnProperty.call(battle.votes, currentPlayer.id) ||
battle.votes[currentPlayer.id] !== undefined
);
} else {
setHasVoted(false);
}
@ -59,8 +60,41 @@ function VotingScreen() {
const handleSubmitVote = async () => {
if (!selectedSong || hasVoted) return;
await submitVote(selectedSong);
// setHasVoted wird jetzt durch den useEffect behandelt, der die Stimmen prüft
try {
// If offline, show status but still try to submit (it will be queued)
if (isOffline) {
setOfflineVoteStatus('pending');
}
await submitVote(selectedSong);
if (isOffline) {
// In offline mode, optimistically update UI
setHasVoted(true);
setOfflineVoteStatus('queued');
// Store vote locally for later sync
try {
const savedVotes = JSON.parse(localStorage.getItem('pendingVotes') || '{}');
if (battle) {
savedVotes[`battle_${battle.round}_${battle.song1.id}`] = {
songId: selectedSong,
battleRound: battle.round,
timestamp: Date.now()
};
localStorage.setItem('pendingVotes', JSON.stringify(savedVotes));
}
} catch (e) {
console.error("Fehler beim Speichern der Offline-Stimme:", e);
}
}
// setHasVoted wird jetzt durch den useEffect behandelt, der die Stimmen prüft
} catch (error) {
console.error("Fehler bei der Stimmabgabe:", error);
setOfflineVoteStatus('error');
}
};
// Handle bye round advancement - für automatisches Weiterkommen
@ -69,12 +103,18 @@ function VotingScreen() {
setProcessingByeAdvance(true);
try {
// Nur der Host kann im Bye-Modus weiterschaltenNNULL
// If offline, show notification that the action will be queued
if (isOffline) {
setOfflineVoteStatus('byeQueued');
}
// Nur der Host kann im Bye-Modus weiterschalten
if (battle && battle.song1 && !battle.song2 && battle.song1.id) {
await submitVote(battle.song1.id);
}
} catch (error) {
console.error("Fehler beim Fortfahren:", error);
setOfflineVoteStatus('error');
} finally {
// Verzögerung, um mehrere Klicks zu verhindern
setTimeout(() => setProcessingByeAdvance(false), 1000);
@ -258,19 +298,46 @@ function VotingScreen() {
{!hasVoted && (
<div className="voting-actions">
<button
className="btn primary"
className={`btn primary ${isOffline ? 'offline' : ''}`}
onClick={handleSubmitVote}
disabled={!selectedSong}
>
Abstimmen
{isOffline ? 'Offline Abstimmen' : 'Abstimmen'}
</button>
{isOffline && (
<div className="offline-notice">
<FontAwesomeIcon icon={faExclamationTriangle} />
<span>Deine Stimme wird gespeichert und gesendet, sobald die Verbindung wiederhergestellt ist.</span>
</div>
)}
{offlineVoteStatus === 'queued' && (
<div className="offline-vote-status">
<span>Stimme gespeichert! Wird gesendet, wenn online.</span>
</div>
)}
{offlineVoteStatus === 'error' && (
<div className="offline-vote-status error">
<span>Fehler beim Speichern der Stimme.</span>
</div>
)}
</div>
)}
<div className="voting-status">
<p>{hasVoted ? 'Warte auf andere Spieler...' : 'Wähle deinen Favoriten!'}</p>
{isReconnecting && (
<div className="reconnecting-notice">
<span>Versuche die Verbindung wiederherzustellen...</span>
</div>
)}
<div className="votes-count">
<span>{battle.voteCount || 0}</span> von <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> Stimmen
{isOffline && <span className="offline-badge"> (Offline-Modus)</span>}
</div>
{/* Liste der Spielerstimmen */}
@ -282,12 +349,20 @@ function VotingScreen() {
const hasPlayerVoted = battle.votes &&
Object.keys(battle.votes).includes(player.id);
// Highlight current player to show they've voted offline
const isCurrentPlayerOfflineVoted = player.id === currentPlayer.id &&
isOffline &&
hasVoted;
return (
<li key={player.id} className={hasPlayerVoted ? 'voted' : 'not-voted'}>
<li key={player.id} className={`${hasPlayerVoted ? 'voted' : 'not-voted'} ${isCurrentPlayerOfflineVoted ? 'offline-voted' : ''}`}>
{player.name} {player.id === currentPlayer.id && '(Du)'}
{hasPlayerVoted &&
<FontAwesomeIcon icon={faCheck} className="vote-icon" />
}
{isCurrentPlayerOfflineVoted &&
<FontAwesomeIcon icon={faExclamationTriangle} className="offline-icon" />
}
</li>
);
})}

View File

@ -11,35 +11,61 @@ 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
// 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: 5000,
reconnectionAttempts: 5
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 } = JSON.parse(savedGameData);
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 }, (response) => {
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);
localStorage.removeItem('songBattleGame');
// 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);
}
});
}
@ -50,25 +76,200 @@ export function SocketProvider({ children }) {
}
});
socketInstance.on('disconnect', () => {
socketInstance.on('disconnect', (reason) => {
setIsConnected(false);
console.log('Disconnected from server');
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);
// Clean up socket connection on unmount
// 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 }}>
<SocketContext.Provider value={{
socket,
isConnected,
isReconnecting,
reconnectionAttempts,
isOffline,
safeEmit,
queueOfflineAction
}}>
{children}
</SocketContext.Provider>
);
@ -78,7 +279,7 @@ export function SocketProvider({ children }) {
* Game state provider - manages game state and provides methods for game actions
*/
export function GameProvider({ children }) {
const { socket, isConnected } = useContext(SocketContext);
const { socket, isConnected, isOffline, isReconnecting, safeEmit } = useContext(SocketContext);
const [lobby, setLobby] = useState(null);
const [error, setError] = useState(null);
const [currentPlayer, setCurrentPlayer] = useState(null);
@ -86,12 +287,38 @@ export function GameProvider({ children }) {
// Save game info when lobby is joined
useEffect(() => {
if (lobby && currentPlayer) {
sessionStorage.setItem('songBattleGame', JSON.stringify({
const savedData = {
lobbyId: lobby.id,
playerName: currentPlayer.name
}));
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(() => {
@ -263,7 +490,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('create_lobby', { playerName }, (response) => {
safeEmit('create_lobby', { playerName }, (response) => {
if (response.error) {
setError(response.error);
} else {
@ -284,7 +511,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('join_lobby', { lobbyId, playerName }, (response) => {
safeEmit('join_lobby', { lobbyId, playerName }, (response) => {
if (response.error) {
setError(response.error);
} else {
@ -305,7 +532,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('update_settings', { settings }, (response) => {
safeEmit('update_settings', { settings }, (response) => {
if (response.error) {
setError(response.error);
}
@ -319,7 +546,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('start_game', {}, (response) => {
safeEmit('start_game', {}, (response) => {
if (response.error) {
setError(response.error);
}
@ -338,7 +565,7 @@ export function GameProvider({ children }) {
console.log('Current lobby state before adding song:', lobby);
return new Promise((resolve, reject) => {
socket.emit('add_song', { song }, (response) => {
safeEmit('add_song', { song }, (response) => {
console.log('Song addition response:', response);
if (response.error) {
console.error('Error adding song:', response.error);
@ -368,6 +595,9 @@ export function GameProvider({ children }) {
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 {
@ -378,8 +608,7 @@ export function GameProvider({ children }) {
});
});
};
// Search for songs on YouTube
// Search for songs on YouTube
const searchYouTube = (query) => {
return new Promise((resolve, reject) => {
if (!socket || !isConnected) {
@ -388,7 +617,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('search_youtube', { query }, (response) => {
safeEmit('search_youtube', { query }, (response) => {
if (response.error) {
setError(response.error);
reject(response.error);
@ -398,7 +627,7 @@ export function GameProvider({ children }) {
});
});
};
// Get metadata for a YouTube video by ID
const getYouTubeMetadata = (videoId) => {
return new Promise((resolve, reject) => {
@ -408,7 +637,7 @@ export function GameProvider({ children }) {
return;
}
socket.emit('get_video_metadata', { videoId }, (response) => {
safeEmit('get_video_metadata', { videoId }, (response) => {
if (response.error) {
setError(response.error);
reject(response.error);
@ -426,9 +655,12 @@ export function GameProvider({ children }) {
return;
}
socket.emit('remove_song', { songId }, (response) => {
safeEmit('remove_song', { songId }, (response) => {
if (response.error) {
setError(response.error);
} else if (response.lobby) {
setLobby(response.lobby);
updateSavedGameData(response.lobby);
}
});
};
@ -440,9 +672,12 @@ export function GameProvider({ children }) {
return;
}
socket.emit('player_ready', {}, (response) => {
safeEmit('player_ready', {}, (response) => {
if (response.error) {
setError(response.error);
} else if (response.lobby) {
setLobby(response.lobby);
updateSavedGameData(response.lobby);
}
});
};
@ -454,9 +689,12 @@ export function GameProvider({ children }) {
return;
}
socket.emit('submit_vote', { songId }, (response) => {
safeEmit('submit_vote', { songId }, (response) => {
if (response.error) {
setError(response.error);
} else if (response.lobby) {
setLobby(response.lobby);
updateSavedGameData(response.lobby);
}
});
};
@ -474,6 +712,8 @@ export function GameProvider({ children }) {
error,
currentPlayer,
isHost: currentPlayer && lobby && currentPlayer.id === lobby.hostId,
isOffline: isOffline,
isReconnecting,
createLobby,
joinLobby,
updateSettings,