feat: Initial project structure for WebTriviasMulti

- Backend: FastAPI + Python-SocketIO + SQLAlchemy
  - Models for categories, questions, game sessions, events
  - AI services for answer validation and question generation (Claude)
  - Room management with Redis
  - Game logic with stealing mechanics
  - Admin API for question management

- Frontend: React + Vite + TypeScript + Tailwind
  - 5 visual themes (DRRR, Retro, Minimal, RGB, Anime 90s)
  - Real-time game with Socket.IO
  - Achievement system
  - Replay functionality
  - Sound effects per theme

- Docker Compose for deployment
- Design documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 07:50:48 +00:00
commit 43021b9c3c
57 changed files with 5446 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
import { useEffect, useRef, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'
import { useGameStore } from '../stores/gameStore'
import type { GameRoom, ChatMessage, AnswerResult } from '../types'
const SOCKET_URL = import.meta.env.VITE_WS_URL || 'http://localhost:8000'
export function useSocket() {
const socketRef = useRef<Socket | null>(null)
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd } =
useGameStore()
useEffect(() => {
// Create socket connection
socketRef.current = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
autoConnect: true,
})
const socket = socketRef.current
// Connection events
socket.on('connect', () => {
console.log('Connected to server')
})
socket.on('disconnect', () => {
console.log('Disconnected from server')
})
socket.on('error', (data: { message: string }) => {
console.error('Socket error:', data.message)
})
// Room events
socket.on('room_created', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('player_joined', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('player_left', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('team_changed', (data: { room: GameRoom }) => {
setRoom(data.room)
})
// Game events
socket.on('game_started', (data: { room: GameRoom }) => {
setRoom(data.room)
})
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
setRoom(data.room)
// Fetch full question details
})
socket.on('answer_result', (data: AnswerResult) => {
setRoom(data.room)
if (!data.valid && !data.was_steal && data.room.can_steal) {
setShowStealPrompt(true)
}
})
socket.on('steal_attempted', (data: { room: GameRoom }) => {
setRoom(data.room)
setShowStealPrompt(false)
})
socket.on('steal_passed', (data: { room: GameRoom }) => {
setRoom(data.room)
setShowStealPrompt(false)
})
socket.on('time_up', (data: { room: GameRoom; was_steal: boolean }) => {
setRoom(data.room)
if (!data.was_steal && data.room.can_steal) {
setShowStealPrompt(true)
} else {
setShowStealPrompt(false)
}
})
// Chat events
socket.on('chat_message', (data: ChatMessage) => {
addMessage(data)
})
socket.on('emoji_reaction', (data: { player_name: string; team: string; emoji: string }) => {
// Handle emoji reaction display
console.log(`${data.player_name} reacted with ${data.emoji}`)
})
return () => {
socket.disconnect()
}
}, [setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd])
// Socket methods
const createRoom = useCallback((playerName: string) => {
socketRef.current?.emit('create_room', { player_name: playerName })
}, [])
const joinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
socketRef.current?.emit('join_room', {
room_code: roomCode,
player_name: playerName,
team,
})
}, [])
const changeTeam = useCallback((team: 'A' | 'B') => {
socketRef.current?.emit('change_team', { team })
}, [])
const startGame = useCallback((board: Record<string, unknown>) => {
socketRef.current?.emit('start_game', { board })
}, [])
const selectQuestion = useCallback((questionId: number, categoryId: number) => {
socketRef.current?.emit('select_question', {
question_id: questionId,
category_id: categoryId,
})
}, [])
const submitAnswer = useCallback(
(answer: string, question: Record<string, unknown>, isSteal: boolean = false) => {
socketRef.current?.emit('submit_answer', {
answer,
question,
is_steal: isSteal,
})
},
[]
)
const stealDecision = useCallback((attempt: boolean, questionId: number, answer?: string) => {
socketRef.current?.emit('steal_decision', {
attempt,
question_id: questionId,
answer,
})
}, [])
const sendChatMessage = useCallback((message: string) => {
socketRef.current?.emit('chat_message', { message })
}, [])
const sendEmojiReaction = useCallback((emoji: string) => {
socketRef.current?.emit('emoji_reaction', { emoji })
}, [])
const notifyTimerExpired = useCallback(() => {
socketRef.current?.emit('timer_expired', {})
}, [])
return {
socket: socketRef.current,
createRoom,
joinRoom,
changeTeam,
startGame,
selectQuestion,
submitAnswer,
stealDecision,
sendChatMessage,
sendEmojiReaction,
notifyTimerExpired,
}
}