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:
89
frontend/src/components/chat/EmojiReactions.tsx
Normal file
89
frontend/src/components/chat/EmojiReactions.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user