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:
@@ -141,9 +141,17 @@ class GameManager:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Original team failed - enable steal
|
# Original team failed - enable steal
|
||||||
|
failed_team = room["current_team"]
|
||||||
room["can_steal"] = True
|
room["can_steal"] = True
|
||||||
|
|
||||||
|
# Advance failed team's player index (they had their turn)
|
||||||
|
team_players = room["teams"][failed_team]
|
||||||
|
room["current_player_index"][failed_team] = (
|
||||||
|
room["current_player_index"][failed_team] + 1
|
||||||
|
) % len(team_players)
|
||||||
|
|
||||||
# Switch to other team for potential steal
|
# Switch to other team for potential steal
|
||||||
room["current_team"] = "B" if room["current_team"] == "A" else "A"
|
room["current_team"] = "B" if failed_team == "A" else "A"
|
||||||
|
|
||||||
# Check if game is over (all questions answered)
|
# Check if game is over (all questions answered)
|
||||||
all_answered = all(
|
all_answered = all(
|
||||||
|
|||||||
@@ -168,6 +168,23 @@ class RoomManager:
|
|||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def update_player(self, socket_id: str, updates: dict) -> Optional[dict]:
|
||||||
|
"""Update player info."""
|
||||||
|
await self.connect()
|
||||||
|
data = await self.redis.get(f"player:{socket_id}")
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
player = json.loads(data)
|
||||||
|
player.update(updates)
|
||||||
|
|
||||||
|
await self.redis.setex(
|
||||||
|
f"player:{socket_id}",
|
||||||
|
3600 * 3,
|
||||||
|
json.dumps(player)
|
||||||
|
)
|
||||||
|
return player
|
||||||
|
|
||||||
async def get_player_stats(self, room_code: str, player_name: str) -> Optional[dict]:
|
async def get_player_stats(self, room_code: str, player_name: str) -> Optional[dict]:
|
||||||
"""Obtiene stats de un jugador."""
|
"""Obtiene stats de un jugador."""
|
||||||
await self.connect()
|
await self.connect()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import socketio
|
import socketio
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.services.room_manager import room_manager
|
from app.services.room_manager import room_manager
|
||||||
from app.services.game_manager import game_manager
|
from app.services.game_manager import game_manager
|
||||||
@@ -80,6 +81,105 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
# Notify all players
|
# Notify all players
|
||||||
await sio.emit("player_joined", {"room": room}, room=room_code)
|
await sio.emit("player_joined", {"room": room}, room=room_code)
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def rejoin_room(sid, data):
|
||||||
|
"""Rejoin an existing room after disconnect/refresh."""
|
||||||
|
room_code = data.get("room_code", "").upper()
|
||||||
|
player_name = data.get("player_name", "")
|
||||||
|
team = data.get("team", "A")
|
||||||
|
|
||||||
|
if not room_code or not player_name:
|
||||||
|
await sio.emit(
|
||||||
|
"rejoin_failed",
|
||||||
|
{"message": "Missing room code or player name"},
|
||||||
|
to=sid
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
room = await room_manager.get_room(room_code)
|
||||||
|
if not room:
|
||||||
|
await sio.emit(
|
||||||
|
"rejoin_failed",
|
||||||
|
{"message": "Room not found or expired"},
|
||||||
|
to=sid
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if player was in this room (by name)
|
||||||
|
player_found = False
|
||||||
|
player_team = None
|
||||||
|
for t in ["A", "B"]:
|
||||||
|
for i, p in enumerate(room["teams"][t]):
|
||||||
|
if p["name"] == player_name:
|
||||||
|
# Update socket_id for this player
|
||||||
|
room["teams"][t][i]["socket_id"] = sid
|
||||||
|
player_found = True
|
||||||
|
player_team = t
|
||||||
|
break
|
||||||
|
if player_found:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not player_found:
|
||||||
|
# Player not found, try to add them back to their preferred team
|
||||||
|
if len(room["teams"][team]) >= 4:
|
||||||
|
# Try other team
|
||||||
|
other_team = "B" if team == "A" else "A"
|
||||||
|
if len(room["teams"][other_team]) >= 4:
|
||||||
|
await sio.emit(
|
||||||
|
"rejoin_failed",
|
||||||
|
{"message": "Room is full"},
|
||||||
|
to=sid
|
||||||
|
)
|
||||||
|
return
|
||||||
|
team = other_team
|
||||||
|
|
||||||
|
room["teams"][team].append({
|
||||||
|
"name": player_name,
|
||||||
|
"team": team,
|
||||||
|
"position": len(room["teams"][team]),
|
||||||
|
"socket_id": sid
|
||||||
|
})
|
||||||
|
player_team = team
|
||||||
|
|
||||||
|
# Update room and player records
|
||||||
|
await room_manager.update_room(room_code, room)
|
||||||
|
await room_manager.update_player(sid, {
|
||||||
|
"name": player_name,
|
||||||
|
"room": room_code,
|
||||||
|
"team": player_team
|
||||||
|
})
|
||||||
|
|
||||||
|
# Also set new player record if it doesn't exist
|
||||||
|
existing = await room_manager.get_player(sid)
|
||||||
|
if not existing:
|
||||||
|
await room_manager.redis.setex(
|
||||||
|
f"player:{sid}",
|
||||||
|
3600 * 3,
|
||||||
|
json.dumps({"name": player_name, "room": room_code, "team": player_team})
|
||||||
|
)
|
||||||
|
|
||||||
|
# Join socket room
|
||||||
|
await sio.enter_room(sid, room_code)
|
||||||
|
|
||||||
|
# Send current game state to rejoining player
|
||||||
|
await sio.emit(
|
||||||
|
"rejoin_success",
|
||||||
|
{
|
||||||
|
"room": room,
|
||||||
|
"player_name": player_name,
|
||||||
|
"team": player_team
|
||||||
|
},
|
||||||
|
to=sid
|
||||||
|
)
|
||||||
|
|
||||||
|
# Notify others that player reconnected
|
||||||
|
await sio.emit(
|
||||||
|
"player_reconnected",
|
||||||
|
{"player_name": player_name, "team": player_team, "room": room},
|
||||||
|
room=room_code,
|
||||||
|
skip_sid=sid
|
||||||
|
)
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def change_team(sid, data):
|
async def change_team(sid, data):
|
||||||
"""Switch player to another team."""
|
"""Switch player to another team."""
|
||||||
@@ -89,6 +189,11 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
|
|
||||||
room_code = player["room"]
|
room_code = player["room"]
|
||||||
new_team = data.get("team")
|
new_team = data.get("team")
|
||||||
|
current_team = player["team"]
|
||||||
|
|
||||||
|
# Don't do anything if already on that team
|
||||||
|
if current_team == new_team:
|
||||||
|
return
|
||||||
|
|
||||||
room = await room_manager.get_room(room_code)
|
room = await room_manager.get_room(room_code)
|
||||||
if not room or len(room["teams"][new_team]) >= 4:
|
if not room or len(room["teams"][new_team]) >= 4:
|
||||||
@@ -99,12 +204,16 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove from current team
|
# Remove from current team (by socket_id to be safe)
|
||||||
current_team = player["team"]
|
|
||||||
room["teams"][current_team] = [
|
room["teams"][current_team] = [
|
||||||
p for p in room["teams"][current_team] if p["socket_id"] != sid
|
p for p in room["teams"][current_team] if p["socket_id"] != sid
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Also remove from new team if somehow already there (prevent duplicates)
|
||||||
|
room["teams"][new_team] = [
|
||||||
|
p for p in room["teams"][new_team] if p["socket_id"] != sid
|
||||||
|
]
|
||||||
|
|
||||||
# Add to new team
|
# Add to new team
|
||||||
room["teams"][new_team].append({
|
room["teams"][new_team].append({
|
||||||
"name": player["name"],
|
"name": player["name"],
|
||||||
@@ -113,7 +222,12 @@ def register_socket_events(sio: socketio.AsyncServer):
|
|||||||
"socket_id": sid
|
"socket_id": sid
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Update room state
|
||||||
await room_manager.update_room(room_code, room)
|
await room_manager.update_room(room_code, room)
|
||||||
|
|
||||||
|
# Update player record with new team
|
||||||
|
await room_manager.update_player(sid, {"team": new_team})
|
||||||
|
|
||||||
await sio.emit("team_changed", {"room": room}, room=room_code)
|
await sio.emit("team_changed", {"room": room}, room=room_code)
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useCallback } from 'react'
|
import { useEffect, useCallback } from 'react'
|
||||||
import { useGameStore } from '../stores/gameStore'
|
import { useGameStore, saveSession, clearSession } from '../stores/gameStore'
|
||||||
import { soundPlayer } from './useSound'
|
import { soundPlayer } from './useSound'
|
||||||
import { useThemeStore } from '../stores/themeStore'
|
import { useThemeStore } from '../stores/themeStore'
|
||||||
import { useSoundStore } from '../stores/soundStore'
|
import { useSoundStore } from '../stores/soundStore'
|
||||||
@@ -58,6 +58,25 @@ export function useSocket() {
|
|||||||
setRoom(data.room)
|
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
|
// Game events
|
||||||
socket.on('game_started', (data: { room: GameRoom }) => {
|
socket.on('game_started', (data: { room: GameRoom }) => {
|
||||||
setRoom(data.room)
|
setRoom(data.room)
|
||||||
@@ -268,10 +287,19 @@ export function useSocket() {
|
|||||||
socketService.emit('timer_expired', {})
|
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 {
|
return {
|
||||||
socket: socketService.connect(),
|
socket: socketService.connect(),
|
||||||
createRoom,
|
createRoom,
|
||||||
joinRoom,
|
joinRoom,
|
||||||
|
rejoinRoom,
|
||||||
changeTeam,
|
changeTeam,
|
||||||
startGame,
|
startGame,
|
||||||
selectQuestion,
|
selectQuestion,
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ const allCategories: Record<number, { name: string; icon: string; color: string
|
|||||||
6: { name: 'Películas', icon: '🎬', color: '#F5C518' },
|
6: { name: 'Películas', icon: '🎬', color: '#F5C518' },
|
||||||
7: { name: 'Libros', icon: '📚', color: '#8B4513' },
|
7: { name: 'Libros', icon: '📚', color: '#8B4513' },
|
||||||
8: { name: 'Historia', icon: '🏛️', color: '#6B5B95' },
|
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() {
|
export default function Game() {
|
||||||
@@ -116,7 +122,13 @@ export default function Game() {
|
|||||||
|
|
||||||
const handleStealDecision = (attempt: boolean) => {
|
const handleStealDecision = (attempt: boolean) => {
|
||||||
if (!currentQuestion) return
|
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)
|
stealDecision(false, currentQuestion.id)
|
||||||
}
|
}
|
||||||
setShowStealPrompt(false)
|
setShowStealPrompt(false)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { useSocket } from '../hooks/useSocket'
|
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 { useThemeStore, themes } from '../stores/themeStore'
|
||||||
import { useThemeStyles } from '../themes/ThemeProvider'
|
import { useThemeStyles } from '../themes/ThemeProvider'
|
||||||
import type { ThemeName } from '../types'
|
import type { ThemeName } from '../types'
|
||||||
@@ -12,16 +12,63 @@ export default function Home() {
|
|||||||
const [roomCode, setRoomCode] = useState('')
|
const [roomCode, setRoomCode] = useState('')
|
||||||
const [mode, setMode] = useState<'select' | 'create' | 'join'>('select')
|
const [mode, setMode] = useState<'select' | 'create' | 'join'>('select')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [savedSession, setSavedSession] = useState<ReturnType<typeof getSavedSession>>(null)
|
||||||
|
const [reconnecting, setReconnecting] = useState(false)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { createRoom, joinRoom } = useSocket()
|
const { createRoom, joinRoom, rejoinRoom } = useSocket()
|
||||||
const { setPlayerName: storeSetPlayerName, room } = useGameStore()
|
const { setPlayerName: storeSetPlayerName, room } = useGameStore()
|
||||||
const { currentTheme, setTheme } = useThemeStore()
|
const { currentTheme, setTheme } = useThemeStore()
|
||||||
const { config, styles } = useThemeStyles()
|
const { config, styles } = useThemeStyles()
|
||||||
|
|
||||||
|
// Check for saved session on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const session = getSavedSession()
|
||||||
|
if (session) {
|
||||||
|
setSavedSession(session)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Navigate when room is created/joined
|
// Navigate when room is created/joined
|
||||||
if (room) {
|
useEffect(() => {
|
||||||
navigate(`/lobby/${room.code}`)
|
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 = () => {
|
const handleCreateRoom = () => {
|
||||||
@@ -106,6 +153,50 @@ export default function Home() {
|
|||||||
: '0 4px 6px rgba(0,0,0,0.1)',
|
: '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' ? (
|
{mode === 'select' ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class SocketService {
|
|||||||
if (!this.socket) {
|
if (!this.socket) {
|
||||||
console.log('Creating new socket connection to:', SOCKET_URL)
|
console.log('Creating new socket connection to:', SOCKET_URL)
|
||||||
this.socket = io(SOCKET_URL, {
|
this.socket = io(SOCKET_URL, {
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['polling', 'websocket'],
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionAttempts: 10,
|
reconnectionAttempts: 10,
|
||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
|
upgrade: true,
|
||||||
|
rememberUpgrade: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
|
|||||||
@@ -1,6 +1,48 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
|
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 {
|
export interface Reaction {
|
||||||
id: string
|
id: string
|
||||||
player_name: string
|
player_name: string
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: true
|
host: true,
|
||||||
|
allowedHosts: ['trivy.consultoria-as.com', 'localhost', '192.168.10.217']
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|||||||
Reference in New Issue
Block a user