Initial commit: Своя Игра - multiplayer quiz game
This commit is contained in:
237
client/src/components/QuestionModal.jsx
Normal file
237
client/src/components/QuestionModal.jsx
Normal file
@@ -0,0 +1,237 @@
|
||||
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}`
|
||||
}
|
||||
|
||||
// В production используем текущий хост (Caddy проксирует на сервер)
|
||||
if (import.meta.env.PROD) {
|
||||
return `${window.location.protocol}//${window.location.host}${path}`
|
||||
}
|
||||
|
||||
// В development используем localhost
|
||||
return `http://localhost:3001${path}`
|
||||
}
|
||||
|
||||
const renderQuestionContent = () => {
|
||||
switch (question.type) {
|
||||
case 'image':
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
||||
<img
|
||||
src={getMediaUrl(question.question)}
|
||||
alt="Question"
|
||||
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-lg mb-4"
|
||||
/>
|
||||
{question.questionText && (
|
||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
||||
{question.questionText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'audio':
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
||||
<audio
|
||||
ref={audioRef}
|
||||
controls
|
||||
autoPlay
|
||||
src={getMediaUrl(question.question)}
|
||||
className="w-full mb-4"
|
||||
onEnded={handleMediaEnded}
|
||||
>
|
||||
Ваш браузер не поддерживает аудио элемент.
|
||||
</audio>
|
||||
{question.questionText && (
|
||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
||||
{question.questionText}
|
||||
</p>
|
||||
)}
|
||||
{!mediaEnded && (
|
||||
<p className="text-yellow-400 text-sm md:text-base text-center mt-2">
|
||||
⏸️ Таймер начнется после прослушивания
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'video':
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
autoPlay
|
||||
src={getMediaUrl(question.question)}
|
||||
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-lg mb-4"
|
||||
onEnded={handleMediaEnded}
|
||||
>
|
||||
Ваш браузер не поддерживает видео элемент.
|
||||
</video>
|
||||
{question.questionText && (
|
||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
||||
{question.questionText}
|
||||
</p>
|
||||
)}
|
||||
{!mediaEnded && (
|
||||
<p className="text-yellow-400 text-sm md:text-base text-center mt-2">
|
||||
⏸️ Таймер начнется после просмотра
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
||||
<p className="text-white text-base md:text-2xl text-center leading-relaxed">
|
||||
{question.question}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-800 p-4 md:p-8 lg:p-12 rounded-lg max-w-4xl w-full max-h-screen overflow-y-auto">
|
||||
<div className="text-center mb-4 md:mb-6">
|
||||
<p className="text-game-gold text-base md:text-xl mb-2">{question.category}</p>
|
||||
<p className="text-game-gold text-xl md:text-3xl font-bold mb-3 md:mb-4">{question.points} баллов</p>
|
||||
{timer !== null && (
|
||||
<p className="text-3xl md:text-5xl font-bold text-white mb-4 md:mb-6">{timer}с</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderQuestionContent()}
|
||||
|
||||
{/* Кнопка "Показать ответ" для ведущего */}
|
||||
{isHost && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
{!showAnswer ? (
|
||||
<button
|
||||
onClick={() => setShowAnswer(true)}
|
||||
className="w-full bg-yellow-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-lg text-base md:text-xl hover:bg-yellow-700 transition"
|
||||
>
|
||||
Показать ответ
|
||||
</button>
|
||||
) : (
|
||||
<div className="bg-green-900 p-4 md:p-6 rounded-lg">
|
||||
<p className="text-white text-lg md:text-2xl font-bold text-center">
|
||||
Правильный ответ:
|
||||
</p>
|
||||
<p className="text-game-gold text-xl md:text-3xl text-center mt-2 md:mt-3">
|
||||
{question.answer}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка "Ответить" для команд (до того как кто-то нажал buzz-in) */}
|
||||
{canBuzzIn && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
<button
|
||||
onClick={onBuzzIn}
|
||||
className="w-full bg-red-600 text-white font-bold py-4 md:py-6 px-8 md:px-12 rounded-lg text-lg md:text-2xl hover:bg-red-700 transition animate-pulse"
|
||||
>
|
||||
ОТВЕТИТЬ
|
||||
</button>
|
||||
{timer && (
|
||||
<p className="text-2xl md:text-3xl text-game-gold mt-3 md:mt-4 font-bold text-center">
|
||||
{timer}с
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Для ведущего: показать кто отвечает + кнопки управления */}
|
||||
{isHost && answeringTeamName && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="bg-blue-900 p-4 md:p-6 rounded-lg mb-3 md:mb-4">
|
||||
<p className="text-white text-lg md:text-2xl font-bold text-center">
|
||||
Отвечает команда:
|
||||
</p>
|
||||
<p className="text-game-gold text-xl md:text-3xl text-center mt-2">
|
||||
{answeringTeamName}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 md:gap-4 justify-center">
|
||||
<button
|
||||
onClick={() => onSubmitAnswer(true)}
|
||||
className="flex-1 md:flex-none bg-green-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-lg text-base md:text-xl hover:bg-green-700 transition"
|
||||
>
|
||||
Верно
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSubmitAnswer(false)}
|
||||
className="flex-1 md:flex-none bg-red-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-lg text-base md:text-xl hover:bg-red-700 transition"
|
||||
>
|
||||
Неверно
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Для ведущего: кнопка завершить досрочно (если никто не отвечает) */}
|
||||
{isHost && !answeringTeamName && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
<button
|
||||
onClick={onCloseQuestion}
|
||||
className="w-full bg-orange-600 text-white font-bold py-2 md:py-3 px-4 md:px-6 rounded-lg text-sm md:text-lg hover:bg-orange-700 transition"
|
||||
>
|
||||
Завершить вопрос досрочно
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Сообщение для команд */}
|
||||
{!isHost && answeringTeamName && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
<p className="text-center text-white text-base md:text-xl">
|
||||
Отвечает команда: <span className="text-game-gold font-bold">{answeringTeamName}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Сообщение ожидания для команд */}
|
||||
{!isHost && !canBuzzIn && !answeringTeamName && (
|
||||
<p className="text-center text-gray-400 text-sm md:text-lg">
|
||||
Ожидание...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuestionModal
|
||||
Reference in New Issue
Block a user