Add kick player and leave game functionality

- Host can kick players from lobby (removes from game, clears presenter if needed)
- Client can voluntarily leave game
- Fix browser-compatible base64 decoding for document upload (atob vs Buffer)
This commit is contained in:
Joey Yakimowich-Payne 2026-01-19 14:52:57 -07:00
commit 79820f5298
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
7 changed files with 1640 additions and 10 deletions

View file

@ -1446,6 +1446,31 @@ export const useGame = () => {
if (data.type === 'PRESENTER_CHANGED') {
setPresenterId(data.payload.presenterId);
}
if (data.type === 'KICKED') {
if (hostConnectionRef.current) {
hostConnectionRef.current.close();
hostConnectionRef.current = null;
}
if (peerRef.current) {
peerRef.current.destroy();
peerRef.current = null;
}
clearStoredSession();
if (timerRef.current) clearInterval(timerRef.current);
setError(data.payload.reason || 'You were kicked from the game');
setGamePin(null);
setQuiz(null);
setPlayers([]);
setCurrentPlayerId(null);
setCurrentPlayerName(null);
setGameState('LANDING');
navigate('/', { replace: true });
}
if (data.type === 'PLAYER_LEFT') {
setPlayers(prev => prev.filter(p => p.id !== data.payload.playerId));
}
};
const handleAnswer = (arg: boolean | AnswerOption) => {
@ -1535,6 +1560,54 @@ export const useGame = () => {
broadcast({ type: 'PRESENTER_CHANGED', payload: { presenterId: playerId } });
};
const kickPlayer = (playerId: string) => {
if (role !== 'HOST') return;
if (playerId === 'host') return;
const conn = connectionsRef.current.get(playerId);
if (conn?.open) {
conn.send({ type: 'KICKED', payload: { reason: 'You were kicked by the host' } });
conn.close();
}
connectionsRef.current.delete(playerId);
const updatedPlayers = playersRef.current.filter(p => p.id !== playerId);
playersRef.current = updatedPlayers;
setPlayers(updatedPlayers);
if (presenterIdRef.current === playerId) {
setPresenterId(null);
broadcast({ type: 'PRESENTER_CHANGED', payload: { presenterId: null } });
}
broadcast({ type: 'PLAYER_LEFT', payload: { playerId } });
};
const leaveGame = () => {
if (role !== 'CLIENT') return;
if (hostConnectionRef.current?.open) {
hostConnectionRef.current.close();
}
hostConnectionRef.current = null;
if (peerRef.current) {
peerRef.current.destroy();
peerRef.current = null;
}
clearStoredSession();
if (timerRef.current) clearInterval(timerRef.current);
setGamePin(null);
setQuiz(null);
setPlayers([]);
setCurrentPlayerId(null);
setCurrentPlayerName(null);
setGameState('LANDING');
navigate('/', { replace: true });
};
const sendAdvance = (action: 'START' | 'NEXT' | 'SCOREBOARD') => {
if (role !== 'CLIENT' || !hostConnectionRef.current) return;
hostConnectionRef.current.send({ type: 'ADVANCE', payload: { action } });
@ -1544,6 +1617,6 @@ export const useGame = () => {
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig,
pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId,
startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard,
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame
};
};