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

@@ -2,7 +2,7 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { ThemeName } from '../types'
type SoundEffect =
export type SoundEffect =
| 'correct'
| 'incorrect'
| 'steal'
@@ -15,9 +15,13 @@ type SoundEffect =
interface SoundState {
volume: number
muted: boolean
soundsLoaded: boolean
currentLoadedTheme: ThemeName | null
setVolume: (volume: number) => void
setMuted: (muted: boolean) => void
toggleMute: () => void
setSoundsLoaded: (loaded: boolean) => void
setCurrentLoadedTheme: (theme: ThemeName | null) => void
}
export const useSoundStore = create<SoundState>()(
@@ -25,17 +29,23 @@ export const useSoundStore = create<SoundState>()(
(set) => ({
volume: 0.7,
muted: false,
setVolume: (volume) => set({ volume }),
soundsLoaded: false,
currentLoadedTheme: null,
setVolume: (volume) => set({ volume: Math.max(0, Math.min(1, volume)) }),
setMuted: (muted) => set({ muted }),
toggleMute: () => set((state) => ({ muted: !state.muted })),
setSoundsLoaded: (soundsLoaded) => set({ soundsLoaded }),
setCurrentLoadedTheme: (currentLoadedTheme) => set({ currentLoadedTheme }),
}),
{
name: 'trivia-sound',
partialize: (state) => ({ volume: state.volume, muted: state.muted }),
}
)
)
// Sound file paths per theme
// All themes share the same base sounds but can be customized per theme
export const soundPaths: Record<ThemeName, Record<SoundEffect, string>> = {
drrr: {
correct: '/sounds/drrr/correct.mp3',
@@ -88,3 +98,16 @@ export const soundPaths: Record<ThemeName, Record<SoundEffect, string>> = {
select: '/sounds/anime/select.mp3',
},
}
// Fallback sounds using Web Audio API generated tones
// These are used when actual sound files are not available
export const fallbackSoundConfigs: Record<SoundEffect, { frequency: number; duration: number; type: OscillatorType }> = {
correct: { frequency: 880, duration: 0.15, type: 'sine' },
incorrect: { frequency: 220, duration: 0.3, type: 'square' },
steal: { frequency: 660, duration: 0.2, type: 'sawtooth' },
timer_tick: { frequency: 440, duration: 0.05, type: 'sine' },
timer_urgent: { frequency: 880, duration: 0.1, type: 'square' },
victory: { frequency: 523, duration: 0.5, type: 'sine' },
defeat: { frequency: 196, duration: 0.5, type: 'sine' },
select: { frequency: 600, duration: 0.08, type: 'sine' },
}