Files
my-game/client/src/components/QuestionModal.jsx
Cosmo ddad7f2126 🎮 Fix question selection, add score editing, media host-only playback
- Fix: select questions by questionIndex instead of points (fixes same-points categories)
- Fix: persist questionIndex in MongoDB schema for game state recovery
- Fix: update scores in UI on wrong answer
- Add: host can manually adjust team scores
- Add: audio/video plays only on host device
- Add: replay button for host after media ends
- Fix: answer media modal shown only to host
2026-03-22 10:21:51 +00:00

215 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect } from 'react'
function QuestionModal({ question, timer, onSubmitAnswer, answeringTeamName, isHost, onBuzzIn, canBuzzIn, onCloseQuestion, gameCode, socket }) {
const [showAnswer, setShowAnswer] = useState(false)
const [mediaEnded, setMediaEnded] = useState(false)
const audioRef = useRef(null)
const videoRef = useRef(null)
if (!question) return null
const handleMediaEnded = () => {
console.log('🎬 Media playback ended');
setMediaEnded(true);
if (socket && gameCode) {
socket.emit('media-ended', { gameCode });
}
}
useEffect(() => {
setMediaEnded(false);
}, [question])
const getMediaUrl = (path) => {
if (import.meta.env.VITE_SERVER_URL) return `${import.meta.env.VITE_SERVER_URL}${path}`
if (import.meta.env.PROD) return `${window.location.protocol}//${window.location.host}${path}`
return `http://localhost:3001${path}`
}
const timerColor = timer !== null
? timer <= 5 ? 'text-red-400' : timer <= 10 ? 'text-amber-400' : 'text-white'
: 'text-white'
const timerBg = timer !== null && timer <= 5 ? 'animate-pulse' : ''
const renderQuestionContent = () => {
const wrapperClass = "bg-gradient-to-br from-indigo-900/40 to-purple-900/40 border border-indigo-400/20 p-4 md:p-8 rounded-xl mb-4 md:mb-6 backdrop-blur-sm"
switch (question.type) {
case 'image':
return (
<div className={wrapperClass}>
<img src={getMediaUrl(question.question)} alt="Question"
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-xl mb-4 shadow-lg" />
{question.questionText && (
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
)}
</div>
)
case 'audio':
return (
<div className={wrapperClass}>
<div className="flex items-center justify-center mb-4">
<div className="text-5xl md:text-7xl animate-pulse-slow">🎵</div>
</div>
{isHost ? (
<>
<audio ref={audioRef} controls autoPlay src={getMediaUrl(question.question)}
className="w-full mb-4" onEnded={handleMediaEnded}>
Ваш браузер не поддерживает аудио элемент.
</audio>
{mediaEnded && (
<button onClick={() => { if (audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play(); setMediaEnded(false); } }}
className="w-full bg-gradient-to-r from-indigo-600/60 to-purple-600/60 hover:from-indigo-500/80 hover:to-purple-500/80 text-white font-bold py-2.5 rounded-xl text-sm md:text-base transition-all border border-indigo-400/30 mb-4">
🔄 Проиграть заново
</button>
)}
</>
) : (
<p className="text-indigo-300 text-base md:text-xl text-center mb-4">
🔊 Ведущий проигрывает аудио...
</p>
)}
{question.questionText && (
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
)}
{isHost && !mediaEnded && (
<p className="text-amber-300/80 text-sm md:text-base text-center mt-2">
Таймер начнется после прослушивания
</p>
)}
</div>
)
case 'video':
return (
<div className={wrapperClass}>
{isHost ? (
<>
<video ref={videoRef} controls autoPlay src={getMediaUrl(question.question)}
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-xl mb-4 shadow-lg" onEnded={handleMediaEnded}>
Ваш браузер не поддерживает видео элемент.
</video>
{mediaEnded && (
<button onClick={() => { if (videoRef.current) { videoRef.current.currentTime = 0; videoRef.current.play(); setMediaEnded(false); } }}
className="w-full bg-gradient-to-r from-indigo-600/60 to-purple-600/60 hover:from-indigo-500/80 hover:to-purple-500/80 text-white font-bold py-2.5 rounded-xl text-sm md:text-base transition-all border border-indigo-400/30 mb-4">
🔄 Проиграть заново
</button>
)}
</>
) : (
<p className="text-indigo-300 text-base md:text-xl text-center mb-4">
🔊 Ведущий проигрывает видео...
</p>
)}
{question.questionText && (
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
)}
{isHost && !mediaEnded && (
<p className="text-amber-300/80 text-sm md:text-base text-center mt-2">
Таймер начнется после просмотра
</p>
)}
</div>
)
case 'text':
default:
return (
<div className={wrapperClass}>
<p className="text-white text-lg md:text-2xl text-center leading-relaxed">{question.question}</p>
</div>
)
}
}
return (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-3 md:p-4 animate-scale-in">
<div className="glass-strong p-4 md:p-8 lg:p-10 rounded-2xl max-w-4xl w-full max-h-[95vh] overflow-y-auto shadow-2xl">
{/* Header */}
<div className="text-center mb-4 md:mb-6">
<p className="text-indigo-300 text-sm md:text-lg mb-1 font-medium">{question.category}</p>
<p className="font-display text-gradient-gold text-2xl md:text-4xl font-bold mb-3 md:mb-4">{question.points} баллов</p>
{timer !== null && (
<div className={`inline-flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-full glass border-2 ${timer <= 5 ? 'border-red-400/50' : timer <= 10 ? 'border-amber-400/30' : 'border-white/20'} ${timerBg}`}>
<p className={`font-display text-3xl md:text-4xl font-bold ${timerColor}`}>{timer}</p>
</div>
)}
</div>
{renderQuestionContent()}
{/* Show answer button for host */}
{isHost && (
<div className="mb-4 md:mb-6">
{!showAnswer ? (
<button onClick={() => setShowAnswer(true)}
className="w-full bg-gradient-to-r from-amber-600/80 to-yellow-600/80 hover:from-amber-500 hover:to-yellow-500 text-white font-bold py-3 md:py-4 rounded-xl text-base md:text-lg transition-all duration-300 border border-amber-400/30 hover:scale-[1.01] active:scale-[0.99]">
👁 Показать ответ
</button>
) : (
<div className="bg-gradient-to-r from-emerald-900/40 to-green-900/40 border border-emerald-400/30 p-4 md:p-6 rounded-xl animate-scale-in">
<p className="text-emerald-300 text-sm md:text-lg font-medium text-center">Правильный ответ:</p>
<p className="text-gradient-gold font-display text-xl md:text-3xl text-center mt-2 font-bold">{question.answer}</p>
</div>
)}
</div>
)}
{/* Buzz-in button for teams */}
{canBuzzIn && (
<div className="mb-4 md:mb-6">
<button onClick={onBuzzIn}
className="w-full bg-gradient-to-r from-red-600 to-rose-600 hover:from-red-500 hover:to-rose-500 text-white font-black py-5 md:py-7 rounded-2xl text-xl md:text-3xl transition-all duration-200 shadow-lg shadow-red-500/30 hover:shadow-red-500/50 animate-pulse-slow hover:animate-none hover:scale-[1.03] active:scale-95 active:animate-buzz border border-red-400/30">
🔔 ОТВЕТИТЬ
</button>
</div>
)}
{/* Host: who is answering + correct/incorrect buttons */}
{isHost && answeringTeamName && (
<div className="mb-4 md:mb-6 animate-scale-in">
<div className="bg-gradient-to-r from-blue-600/20 to-indigo-600/20 border border-blue-400/30 p-4 md:p-5 rounded-xl mb-3 md:mb-4 text-center">
<p className="text-gray-300 text-sm md:text-lg">Отвечает команда:</p>
<p className="text-white font-display text-xl md:text-2xl font-bold mt-1">{answeringTeamName}</p>
</div>
<div className="flex gap-3 md:gap-4 justify-center">
<button onClick={() => onSubmitAnswer(true)}
className="flex-1 md:flex-none bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500 text-white font-bold py-3 md:py-4 px-6 md:px-10 rounded-xl text-base md:text-xl transition-all duration-300 hover:scale-[1.03] active:scale-95 border border-emerald-400/30">
Верно
</button>
<button onClick={() => onSubmitAnswer(false)}
className="flex-1 md:flex-none bg-gradient-to-r from-red-600 to-rose-600 hover:from-red-500 hover:to-rose-500 text-white font-bold py-3 md:py-4 px-6 md:px-10 rounded-xl text-base md:text-xl transition-all duration-300 hover:scale-[1.03] active:scale-95 border border-red-400/30">
Неверно
</button>
</div>
</div>
)}
{/* Host: close question early */}
{isHost && !answeringTeamName && (
<div className="mb-4 md:mb-6">
<button onClick={onCloseQuestion}
className="w-full glass hover:bg-white/10 text-gray-300 hover:text-white font-bold py-2.5 md:py-3 rounded-xl text-sm md:text-base transition-all duration-300">
Завершить вопрос досрочно
</button>
</div>
)}
{/* Team view: who is answering */}
{!isHost && answeringTeamName && (
<div className="mb-4 md:mb-6 text-center">
<p className="text-gray-300 text-base md:text-lg">
Отвечает: <span className="text-blue-300 font-bold">{answeringTeamName}</span>
</p>
</div>
)}
{!isHost && !canBuzzIn && !answeringTeamName && (
<p className="text-center text-gray-500 text-sm md:text-base">Ожидание...</p>
)}
</div>
</div>
)
}
export default QuestionModal