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

@@ -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>
)
}