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