Files
Trivy/frontend/src/hooks/useSocket.ts
consultoria-as 112f489e40 feat: reconexión de sesión + 6 nuevas categorías + corrección de bugs
- Añade sistema de reconexión tras refresh/cierre del navegador
  - Persistencia de sesión en localStorage (3h TTL)
  - Banner de reconexión en Home
  - Evento rejoin_room en backend

- Nuevas categorías: Series TV, Marvel/DC, Disney, Memes, Pokémon, Mitología

- Correcciones de bugs:
  - Fix: juego bloqueado al fallar robo (steal decision)
  - Fix: jugador duplicado al cambiar de equipo
  - Fix: rotación incorrecta de turno tras fallo

- Config: soporte para Cloudflare tunnel (allowedHosts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 01:53:32 +00:00

315 lines
9.2 KiB
TypeScript

import { useEffect, useCallback } from 'react'
import { useGameStore, saveSession, clearSession } from '../stores/gameStore'
import { soundPlayer } from './useSound'
import { useThemeStore } from '../stores/themeStore'
import { useSoundStore } from '../stores/soundStore'
import { socketService } from '../services/socket'
import type { GameRoom, ChatMessage, AnswerResult, Achievement } from '../types'
import type { Reaction } from '../stores/gameStore'
// Team message type
export interface TeamMessage {
player_name: string
team: 'A' | 'B'
message: string
timestamp: string
}
export function useSocket() {
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setGameResult, addReaction, addTeamMessage } =
useGameStore()
// Initialize sound player with current theme
const currentTheme = useThemeStore.getState().currentTheme
soundPlayer.loadTheme(currentTheme)
useEffect(() => {
// Get singleton socket connection
const socket = socketService.connect()
// Only set up listeners once globally
if (socketService.isInitialized) {
return // No cleanup - socket persists
}
socketService.setInitialized()
// Error handler
socket.on('error', (data: { message: string }) => {
console.error('Socket error:', data.message)
})
// Room events
socket.on('room_created', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('player_joined', (data: { room: GameRoom }) => {
setRoom(data.room)
// Play sound when a player joins
const volume = useSoundStore.getState().volume
soundPlayer.play('player_join', volume)
})
socket.on('player_left', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('team_changed', (data: { room: GameRoom }) => {
setRoom(data.room)
})
// Reconnection events
socket.on('rejoin_success', (data: { room: GameRoom; player_name: string; team: 'A' | 'B' }) => {
console.log('Rejoin successful:', data.player_name)
setRoom(data.room)
useGameStore.getState().setPlayerName(data.player_name)
// Update saved session with possibly new team
saveSession(data.room.code, data.player_name, data.team)
})
socket.on('rejoin_failed', (data: { message: string }) => {
console.log('Rejoin failed:', data.message)
clearSession()
})
socket.on('player_reconnected', (data: { player_name: string; team: string; room: GameRoom }) => {
console.log('Player reconnected:', data.player_name)
setRoom(data.room)
})
// Game events
socket.on('game_started', (data: { room: GameRoom }) => {
setRoom(data.room)
// Play game start sound
const volume = useSoundStore.getState().volume
soundPlayer.play('game_start', volume)
})
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
setRoom(data.room)
// Find the question in the board and set it as current
const questionId = data.question_id
for (const categoryQuestions of Object.values(data.room.board || {})) {
const question = (categoryQuestions as Array<{ id: number }>).find(q => q.id === questionId)
if (question) {
setCurrentQuestion(question as unknown as import('../types').Question)
break
}
}
})
socket.on('answer_result', (data: AnswerResult) => {
setRoom(data.room)
// Play appropriate sound based on answer result
const volume = useSoundStore.getState().volume
if (data.valid) {
soundPlayer.play('correct', volume)
// Clear current question after correct answer
setCurrentQuestion(null)
} else {
soundPlayer.play('incorrect', volume)
}
if (!data.valid && !data.was_steal && data.room.can_steal) {
setShowStealPrompt(true)
} else if (data.was_steal) {
// Clear question after steal attempt (success or fail)
setCurrentQuestion(null)
}
})
socket.on('steal_attempted', (data: { room: GameRoom; success?: boolean }) => {
setRoom(data.room)
setShowStealPrompt(false)
// Play steal sound when a steal is attempted
const volume = useSoundStore.getState().volume
soundPlayer.play('steal', volume)
})
socket.on('steal_passed', (data: { room: GameRoom }) => {
setRoom(data.room)
setShowStealPrompt(false)
setCurrentQuestion(null)
})
socket.on('time_up', (data: { room: GameRoom; was_steal: boolean }) => {
setRoom(data.room)
if (!data.was_steal && data.room.can_steal) {
setShowStealPrompt(true)
} else {
setShowStealPrompt(false)
setCurrentQuestion(null)
}
})
// Chat events
socket.on('chat_message', (data: ChatMessage) => {
addMessage(data)
})
socket.on('emoji_reaction', (data: { player_name: string; team: string; emoji: string }) => {
// Legacy handler - redirect to new reaction system
addReaction({
player_name: data.player_name,
team: data.team as 'A' | 'B',
emoji: data.emoji,
timestamp: new Date().toISOString(),
})
})
socket.on('receive_reaction', (data: Omit<Reaction, 'id'>) => {
// Add reaction to the store for display in overlay
addReaction(data)
})
// Team chat events
socket.on('receive_team_message', (data: TeamMessage) => {
addTeamMessage(data)
})
socket.on('game_finished', (data: {
room: GameRoom
winner: 'A' | 'B' | null
final_scores: { A: number; B: number }
replay_code: string | null
achievements_unlocked: Array<{
player_name: string
team: 'A' | 'B'
achievement: unknown
}>
}) => {
setRoom(data.room)
// Determine if current player is on the winning team
const currentPlayerName = useGameStore.getState().playerName
const myTeam = data.room.teams.A.find(p => p.name === currentPlayerName) ? 'A' : 'B'
const volume = useSoundStore.getState().volume
if (data.winner === myTeam) {
soundPlayer.play('victory', volume)
} else if (data.winner !== null) {
soundPlayer.play('defeat', volume)
}
setGameResult({
winner: data.winner,
finalScores: data.final_scores,
replayCode: data.replay_code,
achievementsUnlocked: data.achievements_unlocked.map(a => ({
player_name: a.player_name,
team: a.team,
achievement: a.achievement as Achievement
}))
})
})
// No cleanup - socket connection persists across components
}, [])
// Socket methods - use singleton service
const createRoom = useCallback((playerName: string) => {
socketService.emit('create_room', { player_name: playerName })
}, [])
const joinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
socketService.emit('join_room', {
room_code: roomCode,
player_name: playerName,
team,
})
}, [])
const changeTeam = useCallback((team: 'A' | 'B') => {
socketService.emit('change_team', { team })
}, [])
const startGame = useCallback((board: Record<string, unknown>) => {
socketService.emit('start_game', { board })
}, [])
const selectQuestion = useCallback((questionId: number, categoryId: number) => {
socketService.emit('select_question', {
question_id: questionId,
category_id: categoryId,
})
}, [])
const submitAnswer = useCallback(
(answer: string, question: Record<string, unknown>, isSteal: boolean = false) => {
socketService.emit('submit_answer', {
answer,
question,
is_steal: isSteal,
})
},
[]
)
const stealDecision = useCallback((attempt: boolean, questionId: number, answer?: string) => {
socketService.emit('steal_decision', {
attempt,
question_id: questionId,
answer,
})
}, [])
const sendChatMessage = useCallback((message: string) => {
socketService.emit('chat_message', { message })
}, [])
const sendEmojiReaction = useCallback((emoji: string) => {
socketService.emit('emoji_reaction', { emoji })
}, [])
const sendReaction = useCallback((emoji: string, roomCode?: string, playerName?: string) => {
socketService.emit('send_reaction', {
emoji,
room_code: roomCode,
player_name: playerName,
})
}, [])
const sendTeamMessage = useCallback(
(message: string, roomCode: string, team: 'A' | 'B', playerName: string) => {
socketService.emit('team_message', {
room_code: roomCode,
team,
player_name: playerName,
message,
})
},
[]
)
const notifyTimerExpired = useCallback(() => {
socketService.emit('timer_expired', {})
}, [])
const rejoinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
socketService.emit('rejoin_room', {
room_code: roomCode,
player_name: playerName,
team,
})
}, [])
return {
socket: socketService.connect(),
createRoom,
joinRoom,
rejoinRoom,
changeTeam,
startGame,
selectQuestion,
submitAnswer,
stealDecision,
sendChatMessage,
sendEmojiReaction,
sendReaction,
sendTeamMessage,
notifyTimerExpired,
}
}