fix: persistencia de resultados del juego

- Guarda gameResult en localStorage al terminar partida
- Results.tsx recupera resultados de localStorage o del room
- Expira después de 1 hora
- Resuelve "No hay resultados disponibles" tras recargar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 02:07:03 +00:00
parent 112f489e40
commit e0106502b1
3 changed files with 122 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useCallback } from 'react'
import { useGameStore, saveSession, clearSession } from '../stores/gameStore'
import { useGameStore, saveSession, clearSession, saveGameResult } from '../stores/gameStore'
import { soundPlayer } from './useSound'
import { useThemeStore } from '../stores/themeStore'
import { useSoundStore } from '../stores/soundStore'
@@ -193,7 +193,7 @@ export function useSocket() {
soundPlayer.play('defeat', volume)
}
setGameResult({
const gameResultData = {
winner: data.winner,
finalScores: data.final_scores,
replayCode: data.replay_code,
@@ -202,6 +202,14 @@ export function useSocket() {
team: a.team,
achievement: a.achievement as Achievement
}))
}
setGameResult(gameResultData)
// Persist game result to localStorage
saveGameResult({
...gameResultData,
roomCode: data.room.code
})
})

View File

@@ -1,33 +1,75 @@
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useEffect, useMemo } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { motion } from 'framer-motion'
import { useSound } from '../hooks/useSound'
import { useGameStore } from '../stores/gameStore'
import { useGameStore, getSavedGameResult, clearGameResult } from '../stores/gameStore'
import { useThemeStyles } from '../themes/ThemeProvider'
export default function Results() {
const navigate = useNavigate()
const { roomCode } = useParams<{ roomCode: string }>()
const { play } = useSound()
const { gameResult, resetGame, playerName, room } = useGameStore()
const { gameResult, resetGame, playerName, room, setGameResult } = useGameStore()
const { config, styles } = useThemeStyles()
// Try to recover game result from localStorage if not in store
const effectiveGameResult = useMemo(() => {
if (gameResult) return gameResult
// Try localStorage
const saved = getSavedGameResult(roomCode)
if (saved) {
return {
winner: saved.winner,
finalScores: saved.finalScores,
replayCode: saved.replayCode,
achievementsUnlocked: saved.achievementsUnlocked
}
}
// Fallback: use room data if available and game is finished
if (room && room.status === 'finished') {
const teamAScore = room.scores?.A ?? 0
const teamBScore = room.scores?.B ?? 0
let winner: 'A' | 'B' | null = null
if (teamAScore > teamBScore) winner = 'A'
else if (teamBScore > teamAScore) winner = 'B'
return {
winner,
finalScores: { A: teamAScore, B: teamBScore },
replayCode: null,
achievementsUnlocked: []
}
}
return null
}, [gameResult, roomCode, room])
// Restore game result to store if recovered from localStorage
useEffect(() => {
if (!gameResult && effectiveGameResult) {
setGameResult(effectiveGameResult)
}
}, [gameResult, effectiveGameResult, setGameResult])
// Determine if current player won
const myTeam = room?.teams.A.find(p => p.name === playerName) ? 'A' : 'B'
const won = gameResult?.winner === myTeam
const tied = gameResult?.winner === null
const won = effectiveGameResult?.winner === myTeam
const tied = effectiveGameResult?.winner === null
// Play victory/defeat sound
useEffect(() => {
if (gameResult) {
if (effectiveGameResult) {
if (won) {
play('victory')
} else if (!tied) {
play('defeat')
}
}
}, [gameResult, won, tied, play])
}, [effectiveGameResult, won, tied, play])
if (!gameResult) {
if (!effectiveGameResult) {
return (
<div className="min-h-screen flex items-center justify-center" style={styles.bgPrimary}>
<p style={styles.textSecondary}>No hay resultados disponibles</p>
@@ -36,6 +78,7 @@ export default function Results() {
}
const handlePlayAgain = () => {
clearGameResult()
resetGame()
navigate('/')
}
@@ -54,15 +97,15 @@ export default function Results() {
transition={{ type: 'spring', bounce: 0.5 }}
className="mb-8"
>
{gameResult.winner ? (
{effectiveGameResult.winner ? (
<h1
className={`text-5xl font-bold mb-2 ${styles.glowEffect}`}
style={{
color: gameResult.winner === 'A' ? config.colors.primary : config.colors.secondary,
color: effectiveGameResult.winner === 'A' ? config.colors.primary : config.colors.secondary,
fontFamily: config.fonts.heading,
}}
>
¡Equipo {gameResult.winner} Gana!
¡Equipo {effectiveGameResult.winner} Gana!
</h1>
) : (
<h1
@@ -80,7 +123,7 @@ export default function Results() {
initial={{ x: -50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className={`p-6 rounded-lg text-center ${gameResult.winner === 'A' ? 'ring-4' : ''}`}
className={`p-6 rounded-lg text-center ${effectiveGameResult.winner === 'A' ? 'ring-4' : ''}`}
style={{
backgroundColor: config.colors.primary + '20',
border: `2px solid ${config.colors.primary}`,
@@ -89,7 +132,7 @@ export default function Results() {
>
<div className="text-sm mb-2" style={styles.textSecondary}>Equipo A</div>
<div className="text-4xl font-bold" style={{ color: config.colors.primary }}>
{gameResult.finalScores.A}
{effectiveGameResult.finalScores.A}
</div>
</motion.div>
@@ -101,7 +144,7 @@ export default function Results() {
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className={`p-6 rounded-lg text-center ${gameResult.winner === 'B' ? 'ring-4' : ''}`}
className={`p-6 rounded-lg text-center ${effectiveGameResult.winner === 'B' ? 'ring-4' : ''}`}
style={{
backgroundColor: config.colors.secondary + '20',
border: `2px solid ${config.colors.secondary}`,
@@ -110,19 +153,19 @@ export default function Results() {
>
<div className="text-sm mb-2" style={styles.textSecondary}>Equipo B</div>
<div className="text-4xl font-bold" style={{ color: config.colors.secondary }}>
{gameResult.finalScores.B}
{effectiveGameResult.finalScores.B}
</div>
</motion.div>
</div>
{/* Achievements Unlocked */}
{gameResult.achievementsUnlocked && gameResult.achievementsUnlocked.length > 0 && (
{effectiveGameResult.achievementsUnlocked && effectiveGameResult.achievementsUnlocked.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-bold mb-4" style={{ color: config.colors.accent }}>
Logros Desbloqueados
</h2>
<div className="grid gap-4">
{gameResult.achievementsUnlocked.map((unlock, i) => (
{effectiveGameResult.achievementsUnlocked.map((unlock, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
@@ -159,9 +202,9 @@ export default function Results() {
transition={{ delay: 0.5 }}
className="flex gap-4 justify-center"
>
{gameResult.replayCode && (
{effectiveGameResult.replayCode && (
<button
onClick={() => navigate(`/replay/${gameResult.replayCode}`)}
onClick={() => navigate(`/replay/${effectiveGameResult.replayCode}`)}
className="px-8 py-3 rounded-lg font-bold transition-all hover:scale-105"
style={{
backgroundColor: 'transparent',

View File

@@ -3,6 +3,7 @@ import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
// Session persistence helpers
const SESSION_KEY = 'trivy_session'
const RESULT_KEY = 'trivy_game_result'
interface SavedSession {
roomCode: string
@@ -43,6 +44,54 @@ export function clearSession() {
localStorage.removeItem(SESSION_KEY)
}
// Game result persistence
export interface SavedGameResult {
winner: 'A' | 'B' | null
finalScores: { A: number; B: number }
replayCode: string | null
achievementsUnlocked: Array<{
player_name: string
team: 'A' | 'B'
achievement: Achievement
}>
roomCode: string
timestamp: number
}
export function saveGameResult(result: Omit<SavedGameResult, 'timestamp'>) {
const data: SavedGameResult = {
...result,
timestamp: Date.now()
}
localStorage.setItem(RESULT_KEY, JSON.stringify(data))
}
export function getSavedGameResult(roomCode?: string): SavedGameResult | null {
try {
const data = localStorage.getItem(RESULT_KEY)
if (!data) return null
const result: SavedGameResult = JSON.parse(data)
// Result expires after 1 hour
const oneHour = 60 * 60 * 1000
if (Date.now() - result.timestamp > oneHour) {
clearGameResult()
return null
}
// If roomCode provided, only return if it matches
if (roomCode && result.roomCode !== roomCode) {
return null
}
return result
} catch {
return null
}
}
export function clearGameResult() {
localStorage.removeItem(RESULT_KEY)
}
export interface Reaction {
id: string
player_name: string