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>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { useGameStore, saveSession, clearSession } from '../stores/gameStore'
|
||||
import { soundPlayer } from './useSound'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import { useSoundStore } from '../stores/soundStore'
|
||||
@@ -58,6 +58,25 @@ export function useSocket() {
|
||||
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)
|
||||
@@ -268,10 +287,19 @@ export function useSocket() {
|
||||
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,
|
||||
|
||||
@@ -21,6 +21,12 @@ const allCategories: Record<number, { name: string; icon: string; color: string
|
||||
6: { name: 'Películas', icon: '🎬', color: '#F5C518' },
|
||||
7: { name: 'Libros', icon: '📚', color: '#8B4513' },
|
||||
8: { name: 'Historia', icon: '🏛️', color: '#6B5B95' },
|
||||
9: { name: 'Series de TV', icon: '📺', color: '#E50914' },
|
||||
10: { name: 'Marvel/DC', icon: '🦸', color: '#ED1D24' },
|
||||
11: { name: 'Disney', icon: '🏰', color: '#113CCF' },
|
||||
12: { name: 'Memes', icon: '🐸', color: '#7CFC00' },
|
||||
13: { name: 'Pokémon', icon: '🔴', color: '#FFCB05' },
|
||||
14: { name: 'Mitología', icon: '⚡', color: '#9B59B6' },
|
||||
}
|
||||
|
||||
export default function Game() {
|
||||
@@ -116,7 +122,13 @@ export default function Game() {
|
||||
|
||||
const handleStealDecision = (attempt: boolean) => {
|
||||
if (!currentQuestion) return
|
||||
if (!attempt) {
|
||||
if (attempt) {
|
||||
// Notify server that we're attempting to steal
|
||||
stealDecision(true, currentQuestion.id)
|
||||
// Keep the question modal open for the steal attempt
|
||||
// The modal is already controlled by currentQuestion state
|
||||
} else {
|
||||
// Pass on steal
|
||||
stealDecision(false, currentQuestion.id)
|
||||
}
|
||||
setShowStealPrompt(false)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useSocket } from '../hooks/useSocket'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { useGameStore, getSavedSession, saveSession, clearSession } from '../stores/gameStore'
|
||||
import { useThemeStore, themes } from '../stores/themeStore'
|
||||
import { useThemeStyles } from '../themes/ThemeProvider'
|
||||
import type { ThemeName } from '../types'
|
||||
@@ -12,16 +12,63 @@ export default function Home() {
|
||||
const [roomCode, setRoomCode] = useState('')
|
||||
const [mode, setMode] = useState<'select' | 'create' | 'join'>('select')
|
||||
const [error, setError] = useState('')
|
||||
const [savedSession, setSavedSession] = useState<ReturnType<typeof getSavedSession>>(null)
|
||||
const [reconnecting, setReconnecting] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { createRoom, joinRoom } = useSocket()
|
||||
const { createRoom, joinRoom, rejoinRoom } = useSocket()
|
||||
const { setPlayerName: storeSetPlayerName, room } = useGameStore()
|
||||
const { currentTheme, setTheme } = useThemeStore()
|
||||
const { config, styles } = useThemeStyles()
|
||||
|
||||
// Check for saved session on mount
|
||||
useEffect(() => {
|
||||
const session = getSavedSession()
|
||||
if (session) {
|
||||
setSavedSession(session)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Navigate when room is created/joined
|
||||
if (room) {
|
||||
navigate(`/lobby/${room.code}`)
|
||||
useEffect(() => {
|
||||
if (room) {
|
||||
// Save session when we have a room
|
||||
const currentName = useGameStore.getState().playerName
|
||||
const myTeam = room.teams.A.find(p => p.name === currentName) ? 'A' : 'B'
|
||||
saveSession(room.code, currentName, myTeam)
|
||||
setReconnecting(false)
|
||||
|
||||
// Navigate based on game status
|
||||
if (room.status === 'playing') {
|
||||
navigate(`/game/${room.code}`)
|
||||
} else if (room.status === 'finished') {
|
||||
navigate(`/results/${room.code}`)
|
||||
} else {
|
||||
navigate(`/lobby/${room.code}`)
|
||||
}
|
||||
}
|
||||
}, [room, navigate])
|
||||
|
||||
const handleReconnect = () => {
|
||||
if (!savedSession) return
|
||||
setReconnecting(true)
|
||||
storeSetPlayerName(savedSession.playerName)
|
||||
rejoinRoom(savedSession.roomCode, savedSession.playerName, savedSession.team)
|
||||
|
||||
// Timeout for reconnection
|
||||
setTimeout(() => {
|
||||
if (!room) {
|
||||
setReconnecting(false)
|
||||
setError('No se pudo reconectar. La sala puede haber expirado.')
|
||||
clearSession()
|
||||
setSavedSession(null)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const handleClearSession = () => {
|
||||
clearSession()
|
||||
setSavedSession(null)
|
||||
}
|
||||
|
||||
const handleCreateRoom = () => {
|
||||
@@ -106,6 +153,50 @@ export default function Home() {
|
||||
: '0 4px 6px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
{/* Reconnect Banner */}
|
||||
{savedSession && mode === 'select' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-4 p-4 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: config.colors.accent + '20',
|
||||
border: `1px solid ${config.colors.accent}`,
|
||||
}}
|
||||
>
|
||||
<p className="text-sm mb-2" style={{ color: config.colors.text }}>
|
||||
Partida en progreso detectada
|
||||
</p>
|
||||
<p className="text-xs mb-3" style={{ color: config.colors.textMuted }}>
|
||||
Sala: <strong>{savedSession.roomCode}</strong> • Jugador: <strong>{savedSession.playerName}</strong>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleReconnect}
|
||||
disabled={reconnecting}
|
||||
className="flex-1 py-2 rounded-lg font-bold text-sm transition-all hover:scale-105 disabled:opacity-50"
|
||||
style={{
|
||||
backgroundColor: config.colors.accent,
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{reconnecting ? 'Reconectando...' : 'Reconectar'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearSession}
|
||||
className="px-3 py-2 rounded-lg text-sm transition-all hover:scale-105"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: config.colors.textMuted,
|
||||
border: `1px solid ${config.colors.textMuted}`,
|
||||
}}
|
||||
>
|
||||
Ignorar
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{mode === 'select' ? (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
|
||||
@@ -10,11 +10,13 @@ class SocketService {
|
||||
if (!this.socket) {
|
||||
console.log('Creating new socket connection to:', SOCKET_URL)
|
||||
this.socket = io(SOCKET_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
transports: ['polling', 'websocket'],
|
||||
autoConnect: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 10,
|
||||
reconnectionDelay: 1000,
|
||||
upgrade: true,
|
||||
rememberUpgrade: true,
|
||||
})
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
import { create } from 'zustand'
|
||||
import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
|
||||
|
||||
// Session persistence helpers
|
||||
const SESSION_KEY = 'trivy_session'
|
||||
|
||||
interface SavedSession {
|
||||
roomCode: string
|
||||
playerName: string
|
||||
team: 'A' | 'B'
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export function saveSession(roomCode: string, playerName: string, team: 'A' | 'B') {
|
||||
const session: SavedSession = {
|
||||
roomCode,
|
||||
playerName,
|
||||
team,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
|
||||
}
|
||||
|
||||
export function getSavedSession(): SavedSession | null {
|
||||
try {
|
||||
const data = localStorage.getItem(SESSION_KEY)
|
||||
if (!data) return null
|
||||
|
||||
const session: SavedSession = JSON.parse(data)
|
||||
// Session expires after 3 hours (same as room TTL)
|
||||
const threeHours = 3 * 60 * 60 * 1000
|
||||
if (Date.now() - session.timestamp > threeHours) {
|
||||
clearSession()
|
||||
return null
|
||||
}
|
||||
return session
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
localStorage.removeItem(SESSION_KEY)
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
id: string
|
||||
player_name: string
|
||||
|
||||
@@ -5,7 +5,8 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true
|
||||
host: true,
|
||||
allowedHosts: ['trivy.consultoria-as.com', 'localhost', '192.168.10.217']
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user