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>
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
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>
|
|
)
|
|
}
|