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

@@ -1,6 +1,23 @@
import { create } from 'zustand'
import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
export interface Reaction {
id: string
player_name: string
team: 'A' | 'B'
emoji: string
timestamp: string
}
export interface TeamMessage {
player_name: string
team: 'A' | 'B'
message: string
timestamp: string
}
const MAX_TEAM_MESSAGES = 50
interface GameState {
// Room state
room: GameRoom | null
@@ -23,6 +40,11 @@ interface GameState {
addMessage: (message: ChatMessage) => void
clearMessages: () => void
// Team chat messages
teamMessages: TeamMessage[]
addTeamMessage: (message: TeamMessage) => void
clearTeamMessages: () => void
// Achievements
achievements: Achievement[]
setAchievements: (achievements: Achievement[]) => void
@@ -44,6 +66,12 @@ interface GameState {
showStealPrompt: boolean
setShowStealPrompt: (show: boolean) => void
// Reactions
reactions: Reaction[]
addReaction: (reaction: Omit<Reaction, 'id'>) => void
removeReaction: (id: string) => void
clearReactions: () => void
// Game result
gameResult: {
winner: 'A' | 'B' | null
@@ -88,6 +116,13 @@ export const useGameStore = create<GameState>((set) => ({
set((state) => ({ messages: [...state.messages, message].slice(-100) })),
clearMessages: () => set({ messages: [] }),
teamMessages: [],
addTeamMessage: (message) =>
set((state) => ({
teamMessages: [...state.teamMessages, message].slice(-MAX_TEAM_MESSAGES),
})),
clearTeamMessages: () => set({ teamMessages: [] }),
achievements: [],
setAchievements: (achievements) => set({ achievements }),
unlockAchievement: (id) =>
@@ -105,6 +140,20 @@ export const useGameStore = create<GameState>((set) => ({
showStealPrompt: false,
setShowStealPrompt: (showStealPrompt) => set({ showStealPrompt }),
reactions: [],
addReaction: (reaction) =>
set((state) => ({
reactions: [
...state.reactions,
{ ...reaction, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` },
],
})),
removeReaction: (id) =>
set((state) => ({
reactions: state.reactions.filter((r) => r.id !== id),
})),
clearReactions: () => set({ reactions: [] }),
gameResult: null,
setGameResult: (gameResult) => set({ gameResult }),
@@ -114,6 +163,8 @@ export const useGameStore = create<GameState>((set) => ({
currentQuestion: null,
timerEnd: null,
messages: [],
teamMessages: [],
reactions: [],
stats: initialStats,
showStealPrompt: false,
gameResult: null,