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:
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user