feat: 5 categorías rotativas por partida + pool de 200 preguntas + mejoras UI

Cambios principales:
- Tablero ahora muestra 5 categorías aleatorias (de 8 disponibles)
- Pool de 200 preguntas (8 cats × 5 diffs × 5 opciones)
- Preguntas rotan aleatoriamente entre partidas
- Diseño mejorado estilo Jeopardy con efectos visuales
- Socket singleton para conexión persistente
- Nuevos sonidos: game_start, player_join, question_reveal, hover, countdown
- Control de volumen vertical
- Barra de progreso del timer en modal de preguntas
- Animaciones mejoradas con Framer Motion

Backend:
- question_service: selección aleatoria de 5 categorías
- room_manager: fix retorno de create_room
- game_events: carga board desde DB, await en enter_room

Frontend:
- Game.tsx: tablero dinámico, efectos hover, mejor scoreboard
- useSocket: singleton service, eventos con sonidos
- SoundControl: slider vertical
- soundStore: 5 nuevos efectos de sonido

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:44:55 +00:00
parent e5a2b016a0
commit ab201e113a
8 changed files with 631 additions and 264 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { useSocket } from '../hooks/useSocket'
@@ -11,16 +11,17 @@ import TeamChat from '../components/chat/TeamChat'
import SoundControl from '../components/ui/SoundControl'
import type { Question } from '../types'
const categories = [
{ id: 1, name: 'Nintendo', icon: '🍄', color: '#E60012' },
{ id: 2, name: 'Xbox', icon: '🎮', color: '#107C10' },
{ id: 3, name: 'PlayStation', icon: '🎯', color: '#003791' },
{ id: 4, name: 'Anime', icon: '⛩️', color: '#FF6B9D' },
{ id: 5, name: 'Música', icon: '🎵', color: '#1DB954' },
{ id: 6, name: 'Películas', icon: '🎬', color: '#F5C518' },
{ id: 7, name: 'Libros', icon: '📚', color: '#8B4513' },
{ id: 8, name: 'Historia-Cultura', icon: '🏛️', color: '#6B5B95' },
]
// All available categories with their styling
const allCategories: Record<number, { name: string; icon: string; color: string }> = {
1: { name: 'Nintendo', icon: '🍄', color: '#E60012' },
2: { name: 'Xbox', icon: '🎮', color: '#107C10' },
3: { name: 'PlayStation', icon: '🎯', color: '#003791' },
4: { name: 'Anime', icon: '⛩️', color: '#FF6B9D' },
5: { name: 'Música', icon: '🎵', color: '#1DB954' },
6: { name: 'Películas', icon: '🎬', color: '#F5C518' },
7: { name: 'Libros', icon: '📚', color: '#8B4513' },
8: { name: 'Historia', icon: '🏛️', color: '#6B5B95' },
}
export default function Game() {
useParams<{ roomCode: string }>()
@@ -32,7 +33,7 @@ export default function Game() {
const [answer, setAnswer] = useState('')
const [timeLeft, setTimeLeft] = useState(0)
const [showingQuestion, setShowingQuestion] = useState(false)
const [hoveredCell, setHoveredCell] = useState<string | null>(null)
// Redirect if game finished
useEffect(() => {
@@ -41,9 +42,16 @@ export default function Game() {
}
}, [room?.status, room?.code, navigate])
// Play sound when question is revealed
useEffect(() => {
if (currentQuestion) {
play('question_reveal')
}
}, [currentQuestion, play])
// Timer logic with sound effects
useEffect(() => {
if (!currentQuestion || !showingQuestion) return
if (!currentQuestion) return
setTimeLeft(currentQuestion.time_seconds)
const interval = setInterval(() => {
@@ -52,11 +60,11 @@ export default function Game() {
clearInterval(interval)
return 0
}
// Play urgent sound when time is running low (5 seconds or less)
if (prev <= 6 && prev > 1) {
if (prev <= 4 && prev > 1) {
play('countdown')
} else if (prev <= 6 && prev > 4) {
play('timer_urgent')
} else if (prev > 6) {
// Play tick sound for normal countdown
play('timer_tick')
}
return prev - 1
@@ -64,12 +72,25 @@ export default function Game() {
}, 1000)
return () => clearInterval(interval)
}, [currentQuestion, showingQuestion, play])
}, [currentQuestion, play])
// Hover sound handler
const handleCellHover = useCallback((cellId: string, canSelect: boolean) => {
if (canSelect && hoveredCell !== cellId) {
setHoveredCell(cellId)
play('hover')
}
}, [hoveredCell, play])
if (!room) {
return (
<div className="min-h-screen flex items-center justify-center" style={styles.bgPrimary}>
<p style={styles.textSecondary}>Cargando...</p>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-8 h-8 border-4 rounded-full"
style={{ borderColor: config.colors.primary, borderTopColor: 'transparent' }}
/>
</div>
)
}
@@ -85,164 +106,379 @@ export default function Game() {
if (!amICurrentPlayer || question.answered) return
play('select')
selectQuestion(question.id, categoryId)
setShowingQuestion(true)
}
const handleSubmitAnswer = () => {
if (!currentQuestion || !answer.trim()) return
submitAnswer(answer, currentQuestion as unknown as Record<string, unknown>, room.can_steal)
setAnswer('')
setShowingQuestion(false)
}
const handleStealDecision = (attempt: boolean) => {
if (!currentQuestion) return
if (attempt) {
setShowingQuestion(true)
} else {
if (!attempt) {
stealDecision(false, currentQuestion.id)
}
setShowStealPrompt(false)
}
// Handler for sending team messages
const handleSendTeamMessage = (message: string) => {
if (room && playerName && myTeam) {
sendTeamMessage(message, room.code, myTeam, playerName)
}
}
// Determine if the game is active (playing status)
const isGameActive = room.status === 'playing'
const timerProgress = currentQuestion ? (timeLeft / currentQuestion.time_seconds) * 100 : 100
// Get active categories from the board (dynamic based on what backend sends)
const activeCategories = useMemo(() => {
if (!room.board) return []
return Object.keys(room.board).map(id => ({
id: parseInt(id),
...allCategories[parseInt(id)] || { name: `Cat ${id}`, icon: '❓', color: '#666' }
}))
}, [room.board])
const numCategories = activeCategories.length || 5
return (
<div className="min-h-screen p-4" style={styles.bgPrimary}>
<div className="max-w-6xl mx-auto">
{/* Scoreboard */}
<div className="flex justify-between items-center mb-6">
<div
className="text-center px-6 py-2 rounded-lg"
style={{
backgroundColor: config.colors.primary + '20',
border: `2px solid ${config.colors.primary}`,
}}
<div className="min-h-screen p-2 md:p-4 overflow-hidden" style={styles.bgPrimary}>
<div className="max-w-7xl mx-auto">
{/* Header with Room Code */}
<div className="text-center mb-4">
<motion.h1
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="text-2xl md:text-3xl font-bold tracking-wider"
style={{ color: config.colors.primary, fontFamily: config.fonts.heading }}
>
<div className="text-sm" style={styles.textSecondary}>Equipo A</div>
<div className="text-3xl font-bold" style={{ color: config.colors.primary }}>
{room.scores.A}
</div>
</div>
<div className="text-center">
<div className="text-sm" style={styles.textSecondary}>
Turno de {room.current_team === 'A' ? 'Equipo A' : 'Equipo B'}
</div>
{amICurrentPlayer && (
<div className="text-lg font-bold" style={{ color: config.colors.accent }}>
¡Tu turno!
</div>
)}
</div>
<div
className="text-center px-6 py-2 rounded-lg"
style={{
backgroundColor: config.colors.secondary + '20',
border: `2px solid ${config.colors.secondary}`,
}}
>
<div className="text-sm" style={styles.textSecondary}>Equipo B</div>
<div className="text-3xl font-bold" style={{ color: config.colors.secondary }}>
{room.scores.B}
</div>
TRIVY
</motion.h1>
<div className="text-xs opacity-60" style={{ color: config.colors.textMuted }}>
Sala: {room.code}
</div>
</div>
{/* Game Board */}
<div className="grid grid-cols-8 gap-2 mb-6">
{/* Category Headers */}
{categories.map((cat) => (
<div
key={cat.id}
className="text-center p-2 rounded-t-lg"
style={{ backgroundColor: cat.color }}
>
<div className="text-2xl">{cat.icon}</div>
<div className="text-xs text-white font-bold truncate">{cat.name}</div>
{/* Scoreboard */}
<div className="flex justify-between items-stretch gap-4 mb-4">
{/* Team A Score */}
<motion.div
initial={{ x: -50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className={`flex-1 text-center p-3 md:p-4 rounded-xl relative overflow-hidden ${
room.current_team === 'A' ? 'ring-2 ring-offset-2' : ''
}`}
style={{
background: `linear-gradient(135deg, ${config.colors.primary}30 0%, ${config.colors.primary}10 100%)`,
border: `3px solid ${config.colors.primary}`,
boxShadow: room.current_team === 'A' ? `0 0 20px ${config.colors.primary}50` : 'none',
'--tw-ring-color': config.colors.primary,
'--tw-ring-offset-color': config.colors.bg,
} as React.CSSProperties}
>
{room.current_team === 'A' && (
<motion.div
className="absolute inset-0"
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity }}
style={{ background: `radial-gradient(circle, ${config.colors.primary}20 0%, transparent 70%)` }}
/>
)}
<div className="relative z-10">
<div className="text-xs md:text-sm font-medium opacity-80" style={{ color: config.colors.primary }}>
EQUIPO A
</div>
<motion.div
key={room.scores.A}
initial={{ scale: 1.3 }}
animate={{ scale: 1 }}
className="text-3xl md:text-5xl font-black"
style={{ color: config.colors.primary, textShadow: `0 0 10px ${config.colors.primary}50` }}
>
{room.scores.A}
</motion.div>
<div className="text-xs opacity-60 mt-1" style={{ color: config.colors.textMuted }}>
{room.teams.A.map(p => p.name).join(', ')}
</div>
</div>
))}
</motion.div>
{/* Turn Indicator */}
<div className="flex flex-col items-center justify-center px-2">
<motion.div
animate={amICurrentPlayer ? { scale: [1, 1.1, 1] } : {}}
transition={{ duration: 0.5, repeat: amICurrentPlayer ? Infinity : 0 }}
className="text-center"
>
<div className="text-xs opacity-60 mb-1" style={{ color: config.colors.textMuted }}>TURNO</div>
<div
className="text-lg md:text-xl font-bold px-3 py-1 rounded-full"
style={{
backgroundColor: room.current_team === 'A' ? config.colors.primary : config.colors.secondary,
color: config.colors.bg
}}
>
{room.current_team}
</div>
{amICurrentPlayer && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
className="text-xs mt-1 font-bold"
style={{ color: config.colors.accent }}
>
¡TU TURNO!
</motion.div>
)}
</motion.div>
</div>
{/* Team B Score */}
<motion.div
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className={`flex-1 text-center p-3 md:p-4 rounded-xl relative overflow-hidden ${
room.current_team === 'B' ? 'ring-2 ring-offset-2' : ''
}`}
style={{
background: `linear-gradient(135deg, ${config.colors.secondary}30 0%, ${config.colors.secondary}10 100%)`,
border: `3px solid ${config.colors.secondary}`,
boxShadow: room.current_team === 'B' ? `0 0 20px ${config.colors.secondary}50` : 'none',
'--tw-ring-color': config.colors.secondary,
'--tw-ring-offset-color': config.colors.bg,
} as React.CSSProperties}
>
{room.current_team === 'B' && (
<motion.div
className="absolute inset-0"
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity }}
style={{ background: `radial-gradient(circle, ${config.colors.secondary}20 0%, transparent 70%)` }}
/>
)}
<div className="relative z-10">
<div className="text-xs md:text-sm font-medium opacity-80" style={{ color: config.colors.secondary }}>
EQUIPO B
</div>
<motion.div
key={room.scores.B}
initial={{ scale: 1.3 }}
animate={{ scale: 1 }}
className="text-3xl md:text-5xl font-black"
style={{ color: config.colors.secondary, textShadow: `0 0 10px ${config.colors.secondary}50` }}
>
{room.scores.B}
</motion.div>
<div className="text-xs opacity-60 mt-1" style={{ color: config.colors.textMuted }}>
{room.teams.B.map(p => p.name).join(', ')}
</div>
</div>
</motion.div>
</div>
{/* Game Board - Jeopardy Style */}
<motion.div
initial={{ y: 30, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="rounded-xl overflow-hidden mb-16"
style={{
border: `3px solid ${config.colors.primary}40`,
boxShadow: `0 10px 40px ${config.colors.primary}20`
}}
>
{/* Category Headers */}
<div
className="grid"
style={{ gridTemplateColumns: `repeat(${numCategories}, minmax(0, 1fr))` }}
>
{activeCategories.map((cat, index) => (
<motion.div
key={cat.id}
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: index * 0.05 }}
className="text-center p-2 md:p-4 relative overflow-hidden"
style={{
background: `linear-gradient(180deg, ${cat.color} 0%, ${cat.color}CC 100%)`,
borderRight: index < numCategories - 1 ? '1px solid rgba(255,255,255,0.2)' : 'none'
}}
>
<div className="text-2xl md:text-4xl mb-1 drop-shadow-lg">{cat.icon}</div>
<div className="text-xs md:text-sm text-white font-bold uppercase tracking-wide truncate drop-shadow">
{cat.name}
</div>
</motion.div>
))}
</div>
{/* Questions Grid */}
{[1, 2, 3, 4, 5].map((difficulty) =>
categories.map((cat) => {
const questions = room.board[String(cat.id)] || []
const question = questions.find(q => q.difficulty === difficulty)
const isAnswered = question?.answered
{[1, 2, 3, 4, 5].map((difficulty, rowIndex) => (
<div
key={difficulty}
className="grid"
style={{ gridTemplateColumns: `repeat(${numCategories}, minmax(0, 1fr))` }}
>
{activeCategories.map((cat, colIndex) => {
const questions = room.board[String(cat.id)] || []
const question = questions.find(q => q.difficulty === difficulty)
const isAnswered = question?.answered
const cellId = `${cat.id}-${difficulty}`
const canSelect = !isAnswered && amICurrentPlayer && question
return (
<motion.button
key={`${cat.id}-${difficulty}`}
whileHover={!isAnswered && amICurrentPlayer ? { scale: 1.05 } : {}}
whileTap={!isAnswered && amICurrentPlayer ? { scale: 0.95 } : {}}
onClick={() => question && handleSelectQuestion(question, cat.id)}
disabled={isAnswered || !amICurrentPlayer}
className={`p-4 rounded transition-all ${
isAnswered ? 'opacity-30' : amICurrentPlayer ? 'cursor-pointer' : 'cursor-not-allowed opacity-70'
}`}
style={{
backgroundColor: isAnswered ? config.colors.bg : cat.color + '40',
border: `2px solid ${cat.color}`,
}}
>
<span className="text-xl font-bold" style={{ color: config.colors.text }}>
{difficulty * 100}
</span>
</motion.button>
)
})
)}
</div>
return (
<motion.button
key={cellId}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: (rowIndex * 8 + colIndex) * 0.02 + 0.3 }}
whileHover={canSelect ? {
scale: 1.08,
zIndex: 10,
boxShadow: `0 0 25px ${cat.color}80`
} : {}}
whileTap={canSelect ? { scale: 0.95 } : {}}
onClick={() => question && handleSelectQuestion(question, cat.id)}
onMouseEnter={() => handleCellHover(cellId, !!canSelect)}
onMouseLeave={() => setHoveredCell(null)}
disabled={isAnswered || !amICurrentPlayer}
className={`relative aspect-[4/3] md:aspect-square flex items-center justify-center transition-all duration-200 ${
isAnswered
? 'cursor-default'
: canSelect
? 'cursor-pointer'
: 'cursor-not-allowed'
}`}
style={{
background: isAnswered
? `linear-gradient(135deg, ${config.colors.bg} 0%, ${config.colors.bg}90 100%)`
: `linear-gradient(135deg, ${cat.color}50 0%, ${cat.color}30 100%)`,
borderRight: colIndex < numCategories - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none',
borderBottom: '1px solid rgba(255,255,255,0.1)',
opacity: isAnswered ? 0.3 : (!amICurrentPlayer ? 0.6 : 1),
}}
>
{/* Glow effect on hover */}
{canSelect && hoveredCell === cellId && (
<motion.div
layoutId="cellHighlight"
className="absolute inset-0 pointer-events-none"
style={{
background: `radial-gradient(circle, ${cat.color}40 0%, transparent 70%)`,
}}
/>
)}
{/* Points display */}
<span
className={`text-lg md:text-2xl lg:text-3xl font-black relative z-10 transition-all ${
isAnswered ? 'line-through opacity-40' : ''
}`}
style={{
color: isAnswered ? config.colors.textMuted : '#FFD700',
textShadow: isAnswered ? 'none' : '2px 2px 4px rgba(0,0,0,0.5), 0 0 10px rgba(255,215,0,0.3)'
}}
>
${difficulty * 100}
</span>
{/* Answered checkmark */}
{isAnswered && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute inset-0 flex items-center justify-center"
>
<span className="text-2xl opacity-40"></span>
</motion.div>
)}
</motion.button>
)
})}
</div>
))}
</motion.div>
{/* Question Modal */}
<AnimatePresence>
{showingQuestion && currentQuestion && (
{currentQuestion && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 flex items-center justify-center p-4 z-50"
className="fixed inset-0 bg-black/90 flex items-center justify-center p-4 z-50"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="w-full max-w-lg p-6 rounded-lg"
initial={{ scale: 0.8, y: 50, rotateX: -15 }}
animate={{ scale: 1, y: 0, rotateX: 0 }}
exit={{ scale: 0.8, y: 50, opacity: 0 }}
transition={{ type: "spring", damping: 20 }}
className="w-full max-w-2xl p-6 md:p-8 rounded-2xl relative overflow-hidden"
style={{
backgroundColor: config.colors.bg,
border: `3px solid ${config.colors.primary}`,
background: `linear-gradient(145deg, ${config.colors.bg} 0%, ${config.colors.bg}F0 100%)`,
border: `4px solid ${config.colors.primary}`,
boxShadow: `0 0 60px ${config.colors.primary}40, inset 0 0 60px ${config.colors.primary}10`
}}
>
{/* Timer */}
<div className="flex justify-between items-center mb-4">
<span className="text-sm" style={styles.textSecondary}>
{currentQuestion.points} puntos
</span>
<div
className={`text-2xl font-bold ${timeLeft <= 5 ? 'text-red-500 animate-pulse' : ''}`}
style={{ color: timeLeft > 5 ? config.colors.primary : undefined }}
{/* Timer Bar */}
<div className="absolute top-0 left-0 right-0 h-2 bg-black/30 overflow-hidden">
<motion.div
initial={{ width: '100%' }}
animate={{ width: `${timerProgress}%` }}
transition={{ duration: 0.5 }}
className="h-full"
style={{
backgroundColor: timerProgress > 30 ? config.colors.primary : timerProgress > 15 ? '#FFA500' : '#FF4444',
boxShadow: `0 0 10px ${timerProgress > 30 ? config.colors.primary : '#FF4444'}`
}}
/>
</div>
{/* Points & Timer */}
<div className="flex justify-between items-center mb-6 mt-2">
<motion.div
className="px-4 py-2 rounded-full font-bold"
style={{
backgroundColor: '#FFD700',
color: '#000',
boxShadow: '0 0 15px rgba(255,215,0,0.5)'
}}
>
{timeLeft}s
</div>
${currentQuestion.points}
</motion.div>
<motion.div
animate={timeLeft <= 5 ? { scale: [1, 1.1, 1] } : {}}
transition={{ duration: 0.3, repeat: timeLeft <= 5 ? Infinity : 0 }}
className={`text-4xl md:text-5xl font-black ${timeLeft <= 5 ? 'text-red-500' : ''}`}
style={{
color: timeLeft > 5 ? config.colors.primary : undefined,
textShadow: timeLeft <= 5 ? '0 0 20px #FF0000' : `0 0 20px ${config.colors.primary}50`
}}
>
{timeLeft}
</motion.div>
</div>
{/* Question */}
<p className="text-xl mb-6 text-center" style={{ color: config.colors.text }}>
{currentQuestion.question_text || 'Pregunta de ejemplo: ¿En qué año se lanzó la NES?'}
</p>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-xl md:text-2xl mb-8 text-center leading-relaxed"
style={{ color: config.colors.text }}
>
{currentQuestion.question_text}
</motion.p>
{/* Answer Input */}
{amICurrentPlayer && (
<div className="space-y-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="space-y-4"
>
<input
type="text"
value={answer}
@@ -250,30 +486,49 @@ export default function Game() {
onKeyDown={(e) => e.key === 'Enter' && handleSubmitAnswer()}
placeholder="Escribe tu respuesta..."
autoFocus
className="w-full px-4 py-3 rounded-lg bg-transparent outline-none text-lg"
className="w-full px-6 py-4 rounded-xl bg-black/30 outline-none text-xl text-center transition-all focus:ring-2"
style={{
border: `2px solid ${config.colors.primary}`,
border: `3px solid ${config.colors.primary}50`,
color: config.colors.text,
}}
'--tw-ring-color': config.colors.primary,
} as React.CSSProperties}
/>
<button
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleSubmitAnswer}
disabled={!answer.trim()}
className="w-full py-3 rounded-lg font-bold transition-all hover:scale-105 disabled:opacity-50"
className="w-full py-4 rounded-xl font-bold text-xl transition-all disabled:opacity-30"
style={{
backgroundColor: config.colors.primary,
background: `linear-gradient(135deg, ${config.colors.primary} 0%, ${config.colors.accent} 100%)`,
color: config.colors.bg,
boxShadow: answer.trim() ? `0 5px 30px ${config.colors.primary}50` : 'none'
}}
>
Responder
</button>
</div>
RESPONDER
</motion.button>
</motion.div>
)}
{!amICurrentPlayer && (
<p className="text-center" style={styles.textSecondary}>
Esperando respuesta de {currentPlayer?.name}...
</p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-4"
>
<div className="inline-flex items-center gap-2 px-6 py-3 rounded-full"
style={{ backgroundColor: config.colors.primary + '20' }}>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-4 h-4 border-2 rounded-full"
style={{ borderColor: config.colors.primary, borderTopColor: 'transparent' }}
/>
<span style={{ color: config.colors.textMuted }}>
Esperando respuesta de <strong style={{ color: config.colors.primary }}>{currentPlayer?.name}</strong>
</span>
</div>
</motion.div>
)}
</motion.div>
</motion.div>
@@ -287,68 +542,82 @@ export default function Game() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 flex items-center justify-center p-4 z-50"
className="fixed inset-0 bg-black/90 flex items-center justify-center p-4 z-50"
>
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
className="p-6 rounded-lg text-center"
initial={{ scale: 0.5, rotate: -5 }}
animate={{ scale: 1, rotate: 0 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ type: "spring", damping: 15 }}
className="p-8 rounded-2xl text-center max-w-md"
style={{
backgroundColor: config.colors.bg,
border: `3px solid ${config.colors.accent}`,
background: `linear-gradient(145deg, ${config.colors.bg} 0%, ${config.colors.bg}F0 100%)`,
border: `4px solid ${config.colors.accent}`,
boxShadow: `0 0 60px ${config.colors.accent}60`
}}
>
<h3 className="text-2xl font-bold mb-4" style={{ color: config.colors.accent }}>
¡Oportunidad de Robo!
<motion.div
animate={{ rotate: [0, -10, 10, 0] }}
transition={{ duration: 0.5, repeat: Infinity }}
className="text-6xl mb-4"
>
🎯
</motion.div>
<h3 className="text-3xl font-black mb-4" style={{ color: config.colors.accent }}>
¡OPORTUNIDAD DE ROBO!
</h3>
<p className="mb-6" style={styles.textSecondary}>
El equipo contrario falló. ¿Quieres intentar robar los puntos?
<br />
<span className="text-red-500">Advertencia: Si fallas, perderás puntos</span>
<p className="mb-2" style={{ color: config.colors.textMuted }}>
El equipo contrario falló
</p>
<p className="mb-6 text-red-400 text-sm">
Si fallas, perderás puntos
</p>
<div className="flex gap-4 justify-center">
<button
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleStealDecision(true)}
className="px-6 py-3 rounded-lg font-bold"
className="px-8 py-4 rounded-xl font-bold text-lg"
style={{
backgroundColor: config.colors.accent,
color: config.colors.bg,
background: `linear-gradient(135deg, ${config.colors.accent} 0%, #FF6B6B 100%)`,
color: '#FFF',
boxShadow: `0 5px 30px ${config.colors.accent}50`
}}
>
¡Robar!
</button>
<button
¡ROBAR!
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleStealDecision(false)}
className="px-6 py-3 rounded-lg font-bold"
className="px-8 py-4 rounded-xl font-bold text-lg"
style={{
backgroundColor: 'transparent',
color: config.colors.text,
border: `2px solid ${config.colors.text}`,
border: `2px solid ${config.colors.text}50`,
}}
>
Pasar
</button>
</motion.button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* Emoji Reactions Bar - Fixed at bottom */}
{/* Emoji Reactions Bar */}
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-30">
<EmojiReactions />
</div>
{/* Sound Control - Fixed at top right */}
{/* Sound Control */}
<div className="fixed top-4 right-4 z-30">
<SoundControl compact popupPosition="bottom" />
<SoundControl compact />
</div>
</div>
{/* Reaction Overlay - Full screen overlay for floating reactions */}
<ReactionOverlay />
{/* Team Chat - Only visible during the game */}
{isGameActive && (
<TeamChat
roomCode={room.code}