feat(phase6): Add sounds, team chat, reactions, monitor, settings, and CSV import/export

Sound System:
- Add soundStore with volume/mute persistence
- Add useSound hook with Web Audio API fallback
- Add SoundControl component for in-game volume adjustment
- Play sounds for correct/incorrect, steal, timer, victory/defeat

Team Chat:
- Add TeamChat component with collapsible panel
- Add team_message WebSocket event (team-only visibility)
- Store up to 50 messages per session

Emoji Reactions:
- Add EmojiReactions bar with 8 emojis
- Add ReactionOverlay with floating animations (Framer Motion)
- Add rate limiting (1 reaction per 3 seconds)
- Broadcast reactions to all players in room

Admin Monitor:
- Add Monitor page showing active rooms from Redis
- Display player counts, team composition, status
- Add ability to close problematic rooms

Admin Settings:
- Add Settings page for game configuration
- Configure points/times by difficulty, steal penalty, max players
- Store config in JSON file with service helpers

CSV Import/Export:
- Add export endpoint with optional filters
- Add import endpoint with validation and error reporting
- Add UI buttons and import result modal in Questions page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 08:58:33 +00:00
parent 90fa220890
commit 720432702f
23 changed files with 2753 additions and 51 deletions

View File

@@ -0,0 +1,89 @@
import { useState, useCallback, useEffect } from 'react'
import { motion } from 'framer-motion'
import { useSocket } from '../../hooks/useSocket'
import { useGameStore } from '../../stores/gameStore'
import { useThemeStyles } from '../../themes/ThemeProvider'
const EMOJIS = ['👏', '😮', '😂', '🔥', '💀', '🎉', '😭', '🤔']
const COOLDOWN_MS = 3000 // 3 seconds cooldown
export default function EmojiReactions() {
const { sendReaction } = useSocket()
const { room, playerName } = useGameStore()
const { config } = useThemeStyles()
const [isDisabled, setIsDisabled] = useState(false)
const [cooldownRemaining, setCooldownRemaining] = useState(0)
// Handle cooldown timer display
useEffect(() => {
if (!isDisabled) return
const interval = setInterval(() => {
setCooldownRemaining((prev) => {
if (prev <= 100) {
setIsDisabled(false)
return 0
}
return prev - 100
})
}, 100)
return () => clearInterval(interval)
}, [isDisabled])
const handleEmojiClick = useCallback(
(emoji: string) => {
if (isDisabled || !room?.code) return
// Send the reaction via socket
sendReaction(emoji, room.code, playerName)
// Enable cooldown
setIsDisabled(true)
setCooldownRemaining(COOLDOWN_MS)
},
[isDisabled, room?.code, playerName, sendReaction]
)
if (!room) return null
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-2 px-4 py-2 rounded-full"
style={{
backgroundColor: `${config.colors.bg}CC`,
backdropFilter: 'blur(8px)',
border: `1px solid ${config.colors.primary}40`,
}}
>
{EMOJIS.map((emoji) => (
<motion.button
key={emoji}
onClick={() => handleEmojiClick(emoji)}
disabled={isDisabled}
whileHover={!isDisabled ? { scale: 1.2 } : {}}
whileTap={!isDisabled ? { scale: 0.9 } : {}}
className={`text-2xl p-2 rounded-lg transition-all ${
isDisabled ? 'opacity-40 cursor-not-allowed grayscale' : 'cursor-pointer hover:bg-white/10'
}`}
title={isDisabled ? `Espera ${Math.ceil(cooldownRemaining / 1000)}s` : emoji}
>
{emoji}
</motion.button>
))}
{/* Cooldown indicator */}
{isDisabled && (
<motion.div
initial={{ width: '100%' }}
animate={{ width: '0%' }}
transition={{ duration: COOLDOWN_MS / 1000, ease: 'linear' }}
className="absolute bottom-0 left-0 h-1 rounded-full"
style={{ backgroundColor: config.colors.primary }}
/>
)}
</motion.div>
)
}