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 { useEffect, useCallback } from 'react'
import { useGameStore, saveSession, clearSession } from '../stores/gameStore' import { useGameStore, saveSession, clearSession, saveGameResult } 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'
@@ -193,7 +193,7 @@ export function useSocket() {
soundPlayer.play('defeat', volume) soundPlayer.play('defeat', volume)
} }
setGameResult({ const gameResultData = {
winner: data.winner, winner: data.winner,
finalScores: data.final_scores, finalScores: data.final_scores,
replayCode: data.replay_code, replayCode: data.replay_code,
@@ -202,6 +202,14 @@ export function useSocket() {
team: a.team, team: a.team,
achievement: a.achievement as Achievement 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 { useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useSound } from '../hooks/useSound' import { useSound } from '../hooks/useSound'
import { useGameStore } from '../stores/gameStore' import { useGameStore, getSavedGameResult, clearGameResult } from '../stores/gameStore'
import { useThemeStyles } from '../themes/ThemeProvider' import { useThemeStyles } from '../themes/ThemeProvider'
export default function Results() { export default function Results() {
const navigate = useNavigate() const navigate = useNavigate()
const { roomCode } = useParams<{ roomCode: string }>()
const { play } = useSound() const { play } = useSound()
const { gameResult, resetGame, playerName, room } = useGameStore() const { gameResult, resetGame, playerName, room, setGameResult } = useGameStore()
const { config, styles } = useThemeStyles() 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 // Determine if current player won
const myTeam = room?.teams.A.find(p => p.name === playerName) ? 'A' : 'B' const myTeam = room?.teams.A.find(p => p.name === playerName) ? 'A' : 'B'
const won = gameResult?.winner === myTeam const won = effectiveGameResult?.winner === myTeam
const tied = gameResult?.winner === null const tied = effectiveGameResult?.winner === null
// Play victory/defeat sound // Play victory/defeat sound
useEffect(() => { useEffect(() => {
if (gameResult) { if (effectiveGameResult) {
if (won) { if (won) {
play('victory') play('victory')
} else if (!tied) { } else if (!tied) {
play('defeat') play('defeat')
} }
} }
}, [gameResult, won, tied, play]) }, [effectiveGameResult, won, tied, play])
if (!gameResult) { if (!effectiveGameResult) {
return ( return (
<div className="min-h-screen flex items-center justify-center" style={styles.bgPrimary}> <div className="min-h-screen flex items-center justify-center" style={styles.bgPrimary}>
<p style={styles.textSecondary}>No hay resultados disponibles</p> <p style={styles.textSecondary}>No hay resultados disponibles</p>
@@ -36,6 +78,7 @@ export default function Results() {
} }
const handlePlayAgain = () => { const handlePlayAgain = () => {
clearGameResult()
resetGame() resetGame()
navigate('/') navigate('/')
} }
@@ -54,15 +97,15 @@ export default function Results() {
transition={{ type: 'spring', bounce: 0.5 }} transition={{ type: 'spring', bounce: 0.5 }}
className="mb-8" className="mb-8"
> >
{gameResult.winner ? ( {effectiveGameResult.winner ? (
<h1 <h1
className={`text-5xl font-bold mb-2 ${styles.glowEffect}`} className={`text-5xl font-bold mb-2 ${styles.glowEffect}`}
style={{ 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, fontFamily: config.fonts.heading,
}} }}
> >
¡Equipo {gameResult.winner} Gana! ¡Equipo {effectiveGameResult.winner} Gana!
</h1> </h1>
) : ( ) : (
<h1 <h1
@@ -80,7 +123,7 @@ export default function Results() {
initial={{ x: -50, opacity: 0 }} initial={{ x: -50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.2 }} 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={{ style={{
backgroundColor: config.colors.primary + '20', backgroundColor: config.colors.primary + '20',
border: `2px solid ${config.colors.primary}`, 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-sm mb-2" style={styles.textSecondary}>Equipo A</div>
<div className="text-4xl font-bold" style={{ color: config.colors.primary }}> <div className="text-4xl font-bold" style={{ color: config.colors.primary }}>
{gameResult.finalScores.A} {effectiveGameResult.finalScores.A}
</div> </div>
</motion.div> </motion.div>
@@ -101,7 +144,7 @@ export default function Results() {
initial={{ x: 50, opacity: 0 }} initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.2 }} 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={{ style={{
backgroundColor: config.colors.secondary + '20', backgroundColor: config.colors.secondary + '20',
border: `2px solid ${config.colors.secondary}`, 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-sm mb-2" style={styles.textSecondary}>Equipo B</div>
<div className="text-4xl font-bold" style={{ color: config.colors.secondary }}> <div className="text-4xl font-bold" style={{ color: config.colors.secondary }}>
{gameResult.finalScores.B} {effectiveGameResult.finalScores.B}
</div> </div>
</motion.div> </motion.div>
</div> </div>
{/* Achievements Unlocked */} {/* Achievements Unlocked */}
{gameResult.achievementsUnlocked && gameResult.achievementsUnlocked.length > 0 && ( {effectiveGameResult.achievementsUnlocked && effectiveGameResult.achievementsUnlocked.length > 0 && (
<div className="mb-8"> <div className="mb-8">
<h2 className="text-xl font-bold mb-4" style={{ color: config.colors.accent }}> <h2 className="text-xl font-bold mb-4" style={{ color: config.colors.accent }}>
Logros Desbloqueados Logros Desbloqueados
</h2> </h2>
<div className="grid gap-4"> <div className="grid gap-4">
{gameResult.achievementsUnlocked.map((unlock, i) => ( {effectiveGameResult.achievementsUnlocked.map((unlock, i) => (
<motion.div <motion.div
key={i} key={i}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -159,9 +202,9 @@ export default function Results() {
transition={{ delay: 0.5 }} transition={{ delay: 0.5 }}
className="flex gap-4 justify-center" className="flex gap-4 justify-center"
> >
{gameResult.replayCode && ( {effectiveGameResult.replayCode && (
<button <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" className="px-8 py-3 rounded-lg font-bold transition-all hover:scale-105"
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',

View File

@@ -3,6 +3,7 @@ import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
// Session persistence helpers // Session persistence helpers
const SESSION_KEY = 'trivy_session' const SESSION_KEY = 'trivy_session'
const RESULT_KEY = 'trivy_game_result'
interface SavedSession { interface SavedSession {
roomCode: string roomCode: string
@@ -43,6 +44,54 @@ export function clearSession() {
localStorage.removeItem(SESSION_KEY) 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 { export interface Reaction {
id: string id: string
player_name: string player_name: string