-
setCurrentEventIndex(Number(e.target.value))}
- className="w-full"
- />
-
- Evento {currentEventIndex + 1}
- de {replayData.events.length}
-
-
-
- {/* Events List */}
+ {/* Sidebar - Events List */}
-
- {replayData.events.map((event, index) => (
+
+ Eventos ({replay.event_count})
+
+
+ {replay.events.map((event, index) => (
setCurrentEventIndex(index)}
+ className={`p-3 border-b cursor-pointer transition-all hover:bg-opacity-80 ${
+ index === currentEventIndex ? 'ring-2 ring-inset' : ''
+ }`}
style={{
borderColor: config.colors.primary + '30',
- backgroundColor: index <= currentEventIndex ? config.colors.bg : config.colors.bg + '50',
- ringColor: config.colors.accent,
- }}
+ backgroundColor:
+ index === currentEventIndex
+ ? config.colors.primary + '20'
+ : index <= currentEventIndex
+ ? config.colors.bg
+ : config.colors.bg + '50',
+ '--tw-ring-color': config.colors.accent,
+ } as React.CSSProperties}
>
{event.team}
- {event.player_name}
+
+ {event.player_name}
+
-
- {event.was_correct ? (
-
✓ +{event.points_earned}
- ) : (
-
✗ {event.was_steal ? `-${Math.abs(event.points_earned || 0)}` : ''}
+
+ {event.event_type === 'answer_submitted' && (
+ <>
+ {event.was_correct ? (
+ +{event.points_earned}
+ ) : (
+
+ {event.points_earned < 0 ? event.points_earned : 'X'}
+
+ )}
+ >
)}
+
+ {getEventTypeLabel(event.event_type)}
+ {event.was_steal && ' (Robo)'}
+
{event.answer_given && (
-
- Respuesta: "{event.answer_given}"
+
+ "{event.answer_given}"
)}
))}
-
- {/* Final Scores */}
-
-
Resultado Final
-
- {replayData.session.team_a_score}
- -
- {replayData.session.team_b_score}
-
-
)
diff --git a/frontend/src/pages/Results.tsx b/frontend/src/pages/Results.tsx
index 69e1f00..8dad459 100644
--- a/frontend/src/pages/Results.tsx
+++ b/frontend/src/pages/Results.tsx
@@ -1,33 +1,33 @@
import { useEffect } from 'react'
-import { useParams, useNavigate, Link } from 'react-router-dom'
+import { useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { useSound } from '../hooks/useSound'
-import { useAchievements } from '../hooks/useAchievements'
import { useGameStore } from '../stores/gameStore'
import { useThemeStyles } from '../themes/ThemeProvider'
export default function Results() {
- const { roomCode } = useParams<{ roomCode: string }>()
const navigate = useNavigate()
const { play } = useSound()
- const { achievements } = useAchievements()
- const { room, playerName, resetGame } = useGameStore()
+ const { gameResult, resetGame, playerName, room } = useGameStore()
const { config, styles } = useThemeStyles()
+ // Determine if current player won
const myTeam = room?.teams.A.find(p => p.name === playerName) ? 'A' : 'B'
- const won = room ? room.scores[myTeam] > room.scores[myTeam === 'A' ? 'B' : 'A'] : false
- const tied = room ? room.scores.A === room.scores.B : false
+ const won = gameResult?.winner === myTeam
+ const tied = gameResult?.winner === null
// Play victory/defeat sound
useEffect(() => {
- if (won) {
- play('victory')
- } else if (!tied) {
- play('defeat')
+ if (gameResult) {
+ if (won) {
+ play('victory')
+ } else if (!tied) {
+ play('defeat')
+ }
}
- }, [won, tied, play])
+ }, [gameResult, won, tied, play])
- if (!room) {
+ if (!gameResult) {
return (
No hay resultados disponibles
@@ -35,9 +35,6 @@ export default function Results() {
)
}
- const winnerTeam = room.scores.A > room.scores.B ? 'A' : room.scores.B > room.scores.A ? 'B' : null
- const newAchievements = achievements.filter(a => a.unlocked && a.unlockedAt)
-
const handlePlayAgain = () => {
resetGame()
navigate('/')
@@ -52,135 +49,130 @@ export default function Results() {
>
{/* Result Header */}
- {tied ? (
+ {gameResult.winner ? (
+
+ ¡Equipo {gameResult.winner} Gana!
+
+ ) : (
- ¡EMPATE!
+ ¡Empate!
- ) : won ? (
- <>
-
- ¡VICTORIA!
-
-
- Tu equipo ha ganado
-
- >
- ) : (
- <>
-
- DERROTA
-
-
- Mejor suerte la próxima vez
-
- >
)}
- {/* Scores */}
-
-
+
Equipo A
-
- {room.scores.A}
+
+ {gameResult.finalScores.A}
-
- {room.teams.A.map(p => p.name).join(', ')}
-
-
+
VS
-
Equipo B
-
- {room.scores.B}
+
+ {gameResult.finalScores.B}
-
- {room.teams.B.map(p => p.name).join(', ')}
-
-
-
+
+
- {/* New Achievements */}
- {newAchievements.length > 0 && (
-
+ {/* Achievements Unlocked */}
+ {gameResult.achievementsUnlocked && gameResult.achievementsUnlocked.length > 0 && (
+
- ¡Nuevos Logros Desbloqueados!
+ Logros Desbloqueados
-
- {newAchievements.map((achievement) => (
+
+ {gameResult.achievementsUnlocked.map((unlock, i) => (
- {achievement.icon}
-
- {achievement.name}
-
-
- {achievement.description}
-
+ {unlock.achievement.icon}
+
+ {unlock.achievement.name}
+
+ -
+
+ {unlock.player_name}
+
))}
-
+
)}
- {/* Actions */}
+ {/* Action Buttons */}
+ {gameResult.replayCode && (
+
+ )}
+
-
-
- Ver Replay
-
diff --git a/frontend/src/stores/gameStore.ts b/frontend/src/stores/gameStore.ts
index 9a7fefa..6679064 100644
--- a/frontend/src/stores/gameStore.ts
+++ b/frontend/src/stores/gameStore.ts
@@ -1,5 +1,5 @@
import { create } from 'zustand'
-import type { GameRoom, Player, Question, ChatMessage, Achievement } from '../types'
+import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
interface GameState {
// Room state
@@ -44,6 +44,19 @@ interface GameState {
showStealPrompt: boolean
setShowStealPrompt: (show: boolean) => void
+ // Game result
+ gameResult: {
+ winner: 'A' | 'B' | null
+ finalScores: { A: number; B: number }
+ replayCode: string | null
+ achievementsUnlocked: Array<{
+ player_name: string
+ team: 'A' | 'B'
+ achievement: Achievement
+ }>
+ } | null
+ setGameResult: (result: GameState['gameResult']) => void
+
// Reset
resetGame: () => void
}
@@ -92,6 +105,9 @@ export const useGameStore = create((set) => ({
showStealPrompt: false,
setShowStealPrompt: (showStealPrompt) => set({ showStealPrompt }),
+ gameResult: null,
+ setGameResult: (gameResult) => set({ gameResult }),
+
resetGame: () =>
set({
room: null,
@@ -100,5 +116,6 @@ export const useGameStore = create((set) => ({
messages: [],
stats: initialStats,
showStealPrompt: false,
+ gameResult: null,
}),
}))
diff --git a/frontend/src/styles/effects.css b/frontend/src/styles/effects.css
new file mode 100644
index 0000000..c8f351c
--- /dev/null
+++ b/frontend/src/styles/effects.css
@@ -0,0 +1,146 @@
+/* Neon Glow Effect (DRRR, Retro, RGB themes) */
+.text-shadow-neon {
+ text-shadow:
+ 0 0 5px currentColor,
+ 0 0 10px currentColor,
+ 0 0 20px currentColor,
+ 0 0 40px currentColor;
+}
+
+/* CRT Scanlines (Retro Arcade theme) */
+.crt-scanlines {
+ position: relative;
+}
+.crt-scanlines::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: repeating-linear-gradient(
+ 0deg,
+ rgba(0, 0, 0, 0.15),
+ rgba(0, 0, 0, 0.15) 1px,
+ transparent 1px,
+ transparent 2px
+ );
+ pointer-events: none;
+ z-index: 10;
+}
+
+/* Glitch Effect (DRRR theme) */
+.glitch-text {
+ position: relative;
+ animation: glitch 2s infinite;
+}
+.glitch-text::before,
+.glitch-text::after {
+ content: attr(data-text);
+ position: absolute;
+ left: 0;
+ width: 100%;
+ overflow: hidden;
+}
+.glitch-text::before {
+ animation: glitch-top 2s infinite;
+ clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%);
+ -webkit-clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%);
+}
+.glitch-text::after {
+ animation: glitch-bottom 2s infinite;
+ clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%);
+ -webkit-clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%);
+}
+
+@keyframes glitch {
+ 0%, 100% { transform: translate(0); }
+ 20% { transform: translate(-2px, 2px); }
+ 40% { transform: translate(-2px, -2px); }
+ 60% { transform: translate(2px, 2px); }
+ 80% { transform: translate(2px, -2px); }
+}
+
+@keyframes glitch-top {
+ 0%, 100% { transform: translate(0); color: #00ffff; }
+ 50% { transform: translate(-3px); }
+}
+
+@keyframes glitch-bottom {
+ 0%, 100% { transform: translate(0); color: #ff00ff; }
+ 50% { transform: translate(3px); }
+}
+
+/* Sparkle Effect (Anime 90s theme) */
+.sparkle {
+ position: relative;
+}
+.sparkle::before {
+ content: '\2728';
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ animation: sparkle-float 1.5s ease-in-out infinite;
+ font-size: 1rem;
+}
+.sparkle::after {
+ content: '\2B50';
+ position: absolute;
+ bottom: -5px;
+ left: -5px;
+ animation: sparkle-float 1.5s ease-in-out infinite 0.5s;
+ font-size: 0.8rem;
+}
+
+@keyframes sparkle-float {
+ 0%, 100% { opacity: 1; transform: scale(1) rotate(0deg); }
+ 50% { opacity: 0.5; transform: scale(1.2) rotate(180deg); }
+}
+
+/* RGB Shift Effect (Gaming RGB theme) */
+.animate-rgb-shift {
+ animation: rgb-shift 3s linear infinite;
+}
+
+@keyframes rgb-shift {
+ 0% { filter: hue-rotate(0deg); }
+ 100% { filter: hue-rotate(360deg); }
+}
+
+/* RGB Border Animation */
+.rgb-border {
+ position: relative;
+}
+.rgb-border::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff, #ff0000);
+ background-size: 400% 400%;
+ animation: rgb-gradient 3s linear infinite;
+ z-index: -1;
+ border-radius: inherit;
+}
+
+@keyframes rgb-gradient {
+ 0% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+ 100% { background-position: 0% 50%; }
+}
+
+/* Pulse Animation */
+.animate-pulse-glow {
+ animation: pulse-glow 2s ease-in-out infinite;
+}
+
+@keyframes pulse-glow {
+ 0%, 100% { box-shadow: 0 0 5px currentColor; }
+ 50% { box-shadow: 0 0 20px currentColor, 0 0 30px currentColor; }
+}
+
+/* Floating Animation */
+.animate-float {
+ animation: float 3s ease-in-out infinite;
+}
+
+@keyframes float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index f23f053..642ac58 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -93,17 +93,21 @@ export interface GameEvent {
timestamp: string
}
+export interface ReplayMetadata {
+ session_id: number
+ room_code: string
+ status: string
+ created_at: string
+ finished_at: string
+ duration_seconds: number
+}
+
export interface ReplayData {
- session: {
- id: number
- room_code: string
- team_a_score: number
- team_b_score: number
- status: string
- created_at: string
- finished_at: string
- }
+ metadata: ReplayMetadata
+ final_scores: { A: number; B: number }
+ winner: 'A' | 'B' | 'tie'
events: GameEvent[]
+ event_count: number
}
// Theme Types
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..7c8ed3c
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL: string
+ readonly VITE_WS_URL: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}