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:
@@ -5,6 +5,10 @@ import { useSocket } from '../hooks/useSocket'
|
||||
import { useSound } from '../hooks/useSound'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { useThemeStyles } from '../themes/ThemeProvider'
|
||||
import EmojiReactions from '../components/chat/EmojiReactions'
|
||||
import ReactionOverlay from '../components/chat/ReactionOverlay'
|
||||
import TeamChat from '../components/chat/TeamChat'
|
||||
import SoundControl from '../components/ui/SoundControl'
|
||||
import type { Question } from '../types'
|
||||
|
||||
const categories = [
|
||||
@@ -21,9 +25,9 @@ const categories = [
|
||||
export default function Game() {
|
||||
useParams<{ roomCode: string }>()
|
||||
const navigate = useNavigate()
|
||||
const { selectQuestion, submitAnswer, stealDecision, sendEmojiReaction } = useSocket()
|
||||
const { selectQuestion, submitAnswer, stealDecision, sendTeamMessage } = useSocket()
|
||||
const { play } = useSound()
|
||||
const { room, playerName, currentQuestion, showStealPrompt, setShowStealPrompt } = useGameStore()
|
||||
const { room, playerName, currentQuestion, showStealPrompt, setShowStealPrompt, teamMessages } = useGameStore()
|
||||
const { config, styles } = useThemeStyles()
|
||||
|
||||
const [answer, setAnswer] = useState('')
|
||||
@@ -37,7 +41,7 @@ export default function Game() {
|
||||
}
|
||||
}, [room?.status, room?.code, navigate])
|
||||
|
||||
// Timer logic
|
||||
// Timer logic with sound effects
|
||||
useEffect(() => {
|
||||
if (!currentQuestion || !showingQuestion) return
|
||||
|
||||
@@ -48,7 +52,13 @@ export default function Game() {
|
||||
clearInterval(interval)
|
||||
return 0
|
||||
}
|
||||
if (prev === 6) play('timer_urgent')
|
||||
// Play urgent sound when time is running low (5 seconds or less)
|
||||
if (prev <= 6 && prev > 1) {
|
||||
play('timer_urgent')
|
||||
} else if (prev > 6) {
|
||||
// Play tick sound for normal countdown
|
||||
play('timer_tick')
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
@@ -95,7 +105,15 @@ export default function Game() {
|
||||
setShowStealPrompt(false)
|
||||
}
|
||||
|
||||
const emojis = ['👏', '😮', '😂', '🔥', '💀', '🎉', '😭', '🤔']
|
||||
// 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'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4" style={styles.bgPrimary}>
|
||||
@@ -316,20 +334,30 @@ export default function Game() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Emoji Reactions */}
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{emojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
onClick={() => sendEmojiReaction(emoji)}
|
||||
className="text-2xl p-2 rounded-lg transition-transform hover:scale-125"
|
||||
style={{ backgroundColor: config.colors.bg + '80' }}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
{/* Emoji Reactions Bar - Fixed at bottom */}
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-30">
|
||||
<EmojiReactions />
|
||||
</div>
|
||||
|
||||
{/* Sound Control - Fixed at top right */}
|
||||
<div className="fixed top-4 right-4 z-30">
|
||||
<SoundControl compact popupPosition="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reaction Overlay - Full screen overlay for floating reactions */}
|
||||
<ReactionOverlay />
|
||||
|
||||
{/* Team Chat - Only visible during the game */}
|
||||
{isGameActive && (
|
||||
<TeamChat
|
||||
roomCode={room.code}
|
||||
playerName={playerName}
|
||||
team={myTeam}
|
||||
sendTeamMessage={handleSendTeamMessage}
|
||||
teamMessages={teamMessages}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user