- 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
215 lines
10 KiB
JavaScript
215 lines
10 KiB
JavaScript
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
|