🎨 Redesign UI + 🐛 Fix media answer modal auto-close
This commit is contained in:
@@ -1,33 +1,20 @@
|
|||||||
function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, isHost }) {
|
function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, isHost }) {
|
||||||
const isQuestionUsed = (category, points, questionIndex) => {
|
const isQuestionUsed = (category, points, questionIndex) => {
|
||||||
// Ищем этот конкретный вопрос в использованных
|
|
||||||
// Сначала проверяем по questionIndex (новый метод)
|
|
||||||
const foundByIndex = usedQuestions.find(
|
const foundByIndex = usedQuestions.find(
|
||||||
q => q.category === category && q.points === points && q.questionIndex === questionIndex
|
q => q.category === category && q.points === points && q.questionIndex === questionIndex
|
||||||
);
|
);
|
||||||
|
if (foundByIndex) return true;
|
||||||
|
|
||||||
if (foundByIndex) {
|
|
||||||
console.log(`✓ Question used (by index): cat="${category}", pts=${points}, idx=${questionIndex}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Для обратной совместимости: если в usedQuestions нет questionIndex,
|
|
||||||
// проверяем, сколько вопросов с такими баллами уже использовано
|
|
||||||
const usedCount = usedQuestions.filter(
|
const usedCount = usedQuestions.filter(
|
||||||
q => q.category === category && q.points === points
|
q => q.category === category && q.points === points
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
if (usedCount === 0) return false;
|
if (usedCount === 0) return false;
|
||||||
|
|
||||||
// Получаем категорию и находим индекс вопроса среди вопросов с такими же баллами
|
|
||||||
const categoryData = questions.find(cat => cat.name === category);
|
const categoryData = questions.find(cat => cat.name === category);
|
||||||
const questionsWithSamePoints = categoryData?.questions
|
const questionsWithSamePoints = categoryData?.questions
|
||||||
.map((q, idx) => ({ ...q, originalIndex: idx }))
|
.map((q, idx) => ({ ...q, originalIndex: idx }))
|
||||||
.filter(q => q.points === points) || [];
|
.filter(q => q.points === points) || [];
|
||||||
|
|
||||||
const positionAmongSamePoints = questionsWithSamePoints.findIndex(q => q.originalIndex === questionIndex);
|
const positionAmongSamePoints = questionsWithSamePoints.findIndex(q => q.originalIndex === questionIndex);
|
||||||
|
|
||||||
// Если позиция вопроса меньше количества использованных, значит он уже использован
|
|
||||||
return positionAmongSamePoints >= 0 && positionAmongSamePoints < usedCount;
|
return positionAmongSamePoints >= 0 && positionAmongSamePoints < usedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,15 +22,12 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, i
|
|||||||
return round === 2 ? basePoints * 2 : basePoints
|
return round === 2 ? basePoints * 2 : basePoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверить сколько вопросов осталось в категории
|
|
||||||
const hasAvailableQuestions = (category) => {
|
const hasAvailableQuestions = (category) => {
|
||||||
return category.questions.some((q, idx) => !isQuestionUsed(category.name, q.points, idx))
|
return category.questions.some((q, idx) => !isQuestionUsed(category.name, q.points, idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтруем категории - показываем только те, где есть доступные вопросы
|
|
||||||
const availableCategories = questions.filter(hasAvailableQuestions)
|
const availableCategories = questions.filter(hasAvailableQuestions)
|
||||||
|
|
||||||
// Лог для отладки
|
|
||||||
console.log('📋 GameBoard render:', {
|
console.log('📋 GameBoard render:', {
|
||||||
totalCategories: questions.length,
|
totalCategories: questions.length,
|
||||||
availableCategories: availableCategories.length,
|
availableCategories: availableCategories.length,
|
||||||
@@ -51,35 +35,35 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, i
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-800 p-3 md:p-6 rounded-xl shadow-xl border border-slate-700">
|
<div className="glass-strong rounded-2xl p-3 md:p-5 shadow-2xl">
|
||||||
{!isHost && (
|
{!isHost && (
|
||||||
<div className="mb-3 md:mb-4 bg-blue-900 p-3 md:p-4 rounded-lg text-center">
|
<div className="mb-3 md:mb-4 bg-gradient-to-r from-blue-600/20 to-purple-600/20 border border-blue-400/20 p-3 md:p-4 rounded-xl text-center">
|
||||||
<p className="text-white text-sm md:text-lg font-medium">
|
<p className="text-white text-sm md:text-base font-medium">
|
||||||
Ведущий выбирает вопросы. Будьте готовы отвечать!
|
🎯 Ведущий выбирает вопросы. Будьте готовы!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<div className="mb-3 md:mb-4 bg-blue-900 p-3 md:p-4 rounded-lg text-center">
|
<div className="mb-3 md:mb-4 bg-gradient-to-r from-purple-600/20 to-indigo-600/20 border border-purple-400/20 p-3 md:p-4 rounded-xl text-center">
|
||||||
<p className="text-white text-sm md:text-lg font-bold">
|
<p className="text-white text-sm md:text-base font-bold">
|
||||||
Выберите вопрос
|
👆 Выберите вопрос
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{availableCategories.length > 0 ? (
|
{availableCategories.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 md:space-y-3">
|
||||||
{availableCategories.map((category, catIdx) => (
|
{availableCategories.map((category, catIdx) => (
|
||||||
<div key={catIdx} className="flex flex-col md:flex-row gap-2">
|
<div key={catIdx} className="flex flex-col md:flex-row gap-2 animate-slide-up" style={{ animationDelay: `${catIdx * 0.05}s` }}>
|
||||||
{/* Название категории */}
|
{/* Category name */}
|
||||||
<div className="bg-blue-900 p-2 md:p-4 rounded-lg flex items-center justify-center border border-blue-700 md:min-w-[200px]">
|
<div className="bg-gradient-to-r from-indigo-600/30 to-purple-600/30 border border-indigo-400/20 p-2.5 md:p-4 rounded-xl flex items-center justify-center md:min-w-[200px] backdrop-blur-sm">
|
||||||
<h3 className="text-white font-bold text-sm md:text-lg text-center">
|
<h3 className="text-white font-bold text-xs md:text-base text-center leading-tight">
|
||||||
{category.name}
|
{category.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Вопросы по номиналам */}
|
{/* Question buttons */}
|
||||||
<div className="grid grid-cols-5 md:flex gap-1 md:gap-2 flex-1">
|
<div className="grid grid-cols-5 md:flex gap-1.5 md:gap-2 flex-1">
|
||||||
{category.questions.map((question, questionIndex) => {
|
{category.questions.map((question, questionIndex) => {
|
||||||
const displayPoints = getPointsForRound(question.points, currentRound)
|
const displayPoints = getPointsForRound(question.points, currentRound)
|
||||||
const isUsed = isQuestionUsed(category.name, question.points, questionIndex)
|
const isUsed = isQuestionUsed(category.name, question.points, questionIndex)
|
||||||
@@ -89,12 +73,12 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, i
|
|||||||
key={questionIndex}
|
key={questionIndex}
|
||||||
onClick={() => !isUsed && isHost && onSelectQuestion(category.name, question.points)}
|
onClick={() => !isUsed && isHost && onSelectQuestion(category.name, question.points)}
|
||||||
disabled={isUsed || !isHost}
|
disabled={isUsed || !isHost}
|
||||||
className={`flex-1 p-3 md:p-6 rounded-lg font-bold text-base md:text-2xl transition-all duration-200 ${
|
className={`flex-1 p-3 md:p-5 rounded-xl font-bold text-base md:text-xl transition-all duration-300 ${
|
||||||
isUsed
|
isUsed
|
||||||
? 'bg-slate-700 text-gray-500 cursor-not-allowed border border-slate-600'
|
? 'bg-white/3 text-gray-600 cursor-not-allowed border border-white/5'
|
||||||
: isHost
|
: isHost
|
||||||
? 'bg-blue-600 text-white hover:bg-blue-700 cursor-pointer shadow-lg border border-blue-500'
|
? 'bg-gradient-to-br from-blue-600/60 to-indigo-700/60 text-white hover:from-blue-500/80 hover:to-indigo-600/80 cursor-pointer shadow-lg hover:shadow-blue-500/20 border border-blue-400/20 hover:border-blue-400/40 hover:scale-105 active:scale-95 backdrop-blur-sm'
|
||||||
: 'bg-blue-600 text-white cursor-default opacity-60 border border-blue-500'
|
: 'bg-gradient-to-br from-blue-600/40 to-indigo-700/40 text-white/70 cursor-default border border-blue-400/10 backdrop-blur-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isUsed ? '—' : displayPoints}
|
{isUsed ? '—' : displayPoints}
|
||||||
@@ -106,9 +90,9 @@ function GameBoard({ questions, usedQuestions, onSelectQuestion, currentRound, i
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 md:py-12 bg-slate-700 rounded-lg border border-slate-600">
|
<div className="text-center py-8 md:py-12 glass rounded-xl">
|
||||||
<p className="text-white text-lg md:text-2xl font-bold px-4">
|
<p className="text-white text-lg md:text-2xl font-bold px-4">
|
||||||
Все вопросы раунда {currentRound} использованы!
|
✅ Все вопросы раунда {currentRound} использованы!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,226 +8,175 @@ function QuestionModal({ question, timer, onSubmitAnswer, answeringTeamName, isH
|
|||||||
|
|
||||||
if (!question) return null
|
if (!question) return null
|
||||||
|
|
||||||
// Обработчик окончания проигрывания медиа
|
|
||||||
const handleMediaEnded = () => {
|
const handleMediaEnded = () => {
|
||||||
console.log('🎬 Media playback ended');
|
console.log('🎬 Media playback ended');
|
||||||
setMediaEnded(true);
|
setMediaEnded(true);
|
||||||
|
|
||||||
// Отправляем событие на сервер, чтобы начался таймер
|
|
||||||
if (socket && gameCode) {
|
if (socket && gameCode) {
|
||||||
socket.emit('media-ended', { gameCode });
|
socket.emit('media-ended', { gameCode });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем флаг при смене вопроса
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMediaEnded(false);
|
setMediaEnded(false);
|
||||||
}, [question])
|
}, [question])
|
||||||
|
|
||||||
const getMediaUrl = (path) => {
|
const getMediaUrl = (path) => {
|
||||||
// Если определена переменная окружения, используем её
|
if (import.meta.env.VITE_SERVER_URL) return `${import.meta.env.VITE_SERVER_URL}${path}`
|
||||||
if (import.meta.env.VITE_SERVER_URL) {
|
if (import.meta.env.PROD) return `${window.location.protocol}//${window.location.host}${path}`
|
||||||
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}`
|
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 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) {
|
switch (question.type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
<div className={wrapperClass}>
|
||||||
<img
|
<img src={getMediaUrl(question.question)} alt="Question"
|
||||||
src={getMediaUrl(question.question)}
|
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-xl mb-4 shadow-lg" />
|
||||||
alt="Question"
|
|
||||||
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-lg mb-4"
|
|
||||||
/>
|
|
||||||
{question.questionText && (
|
{question.questionText && (
|
||||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
|
||||||
{question.questionText}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
case 'audio':
|
case 'audio':
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
<div className={wrapperClass}>
|
||||||
<audio
|
<div className="flex items-center justify-center mb-4">
|
||||||
ref={audioRef}
|
<div className="text-5xl md:text-7xl animate-pulse-slow">🎵</div>
|
||||||
controls
|
</div>
|
||||||
autoPlay
|
<audio ref={audioRef} controls autoPlay src={getMediaUrl(question.question)}
|
||||||
src={getMediaUrl(question.question)}
|
className="w-full mb-4" onEnded={handleMediaEnded}>
|
||||||
className="w-full mb-4"
|
|
||||||
onEnded={handleMediaEnded}
|
|
||||||
>
|
|
||||||
Ваш браузер не поддерживает аудио элемент.
|
Ваш браузер не поддерживает аудио элемент.
|
||||||
</audio>
|
</audio>
|
||||||
{question.questionText && (
|
{question.questionText && (
|
||||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
|
||||||
{question.questionText}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{!mediaEnded && (
|
{!mediaEnded && (
|
||||||
<p className="text-yellow-400 text-sm md:text-base text-center mt-2">
|
<p className="text-amber-300/80 text-sm md:text-base text-center mt-2">
|
||||||
⏸️ Таймер начнется после прослушивания
|
⏸️ Таймер начнется после прослушивания
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
case 'video':
|
case 'video':
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
<div className={wrapperClass}>
|
||||||
<video
|
<video ref={videoRef} controls autoPlay src={getMediaUrl(question.question)}
|
||||||
ref={videoRef}
|
className="max-w-full max-h-64 md:max-h-96 mx-auto rounded-xl mb-4 shadow-lg" onEnded={handleMediaEnded}>
|
||||||
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>
|
</video>
|
||||||
{question.questionText && (
|
{question.questionText && (
|
||||||
<p className="text-white text-base md:text-2xl text-center mt-4">
|
<p className="text-white text-base md:text-2xl text-center mt-4">{question.questionText}</p>
|
||||||
{question.questionText}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{!mediaEnded && (
|
{!mediaEnded && (
|
||||||
<p className="text-yellow-400 text-sm md:text-base text-center mt-2">
|
<p className="text-amber-300/80 text-sm md:text-base text-center mt-2">
|
||||||
⏸️ Таймер начнется после просмотра
|
⏸️ Таймер начнется после просмотра
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-900 p-4 md:p-8 rounded-lg mb-4 md:mb-6">
|
<div className={wrapperClass}>
|
||||||
<p className="text-white text-base md:text-2xl text-center leading-relaxed">
|
<p className="text-white text-lg md:text-2xl text-center leading-relaxed">{question.question}</p>
|
||||||
{question.question}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4">
|
<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="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="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">
|
<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-indigo-300 text-sm md:text-lg mb-1 font-medium">{question.category}</p>
|
||||||
<p className="text-game-gold text-xl md:text-3xl font-bold mb-3 md:mb-4">{question.points} баллов</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 && (
|
{timer !== null && (
|
||||||
<p className="text-3xl md:text-5xl font-bold text-white mb-4 md:mb-6">{timer}с</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
{renderQuestionContent()}
|
{renderQuestionContent()}
|
||||||
|
|
||||||
{/* Кнопка "Показать ответ" для ведущего */}
|
{/* Show answer button for host */}
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6">
|
||||||
{!showAnswer ? (
|
{!showAnswer ? (
|
||||||
<button
|
<button onClick={() => setShowAnswer(true)}
|
||||||
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]">
|
||||||
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>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-green-900 p-4 md:p-6 rounded-lg">
|
<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-white text-lg md:text-2xl font-bold text-center">
|
<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>
|
||||||
</p>
|
|
||||||
<p className="text-game-gold text-xl md:text-3xl text-center mt-2 md:mt-3">
|
|
||||||
{question.answer}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Кнопка "Ответить" для команд (до того как кто-то нажал buzz-in) */}
|
{/* Buzz-in button for teams */}
|
||||||
{canBuzzIn && (
|
{canBuzzIn && (
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6">
|
||||||
<button
|
<button onClick={onBuzzIn}
|
||||||
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">
|
||||||
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>
|
</button>
|
||||||
{timer && (
|
|
||||||
<p className="text-2xl md:text-3xl text-game-gold mt-3 md:mt-4 font-bold text-center">
|
|
||||||
{timer}с
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Для ведущего: показать кто отвечает + кнопки управления */}
|
{/* Host: who is answering + correct/incorrect buttons */}
|
||||||
{isHost && answeringTeamName && (
|
{isHost && answeringTeamName && (
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6 animate-scale-in">
|
||||||
<div className="bg-blue-900 p-4 md:p-6 rounded-lg mb-3 md:mb-4">
|
<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-white text-lg md:text-2xl font-bold 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>
|
||||||
</p>
|
|
||||||
<p className="text-game-gold text-xl md:text-3xl text-center mt-2">
|
|
||||||
{answeringTeamName}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 md:gap-4 justify-center">
|
<div className="flex gap-3 md:gap-4 justify-center">
|
||||||
<button
|
<button onClick={() => onSubmitAnswer(true)}
|
||||||
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">
|
||||||
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>
|
||||||
<button
|
<button onClick={() => onSubmitAnswer(false)}
|
||||||
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">
|
||||||
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Для ведущего: кнопка завершить досрочно (если никто не отвечает) */}
|
{/* Host: close question early */}
|
||||||
{isHost && !answeringTeamName && (
|
{isHost && !answeringTeamName && (
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6">
|
||||||
<button
|
<button onClick={onCloseQuestion}
|
||||||
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">
|
||||||
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Сообщение для команд */}
|
{/* Team view: who is answering */}
|
||||||
{!isHost && answeringTeamName && (
|
{!isHost && answeringTeamName && (
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6 text-center">
|
||||||
<p className="text-center text-white text-base md:text-xl">
|
<p className="text-gray-300 text-base md:text-lg">
|
||||||
Отвечает команда: <span className="text-game-gold font-bold">{answeringTeamName}</span>
|
Отвечает: <span className="text-blue-300 font-bold">{answeringTeamName}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Сообщение ожидания для команд */}
|
|
||||||
{!isHost && !canBuzzIn && !answeringTeamName && (
|
{!isHost && !canBuzzIn && !answeringTeamName && (
|
||||||
<p className="text-center text-gray-400 text-sm md:text-lg">
|
<p className="text-center text-gray-500 text-sm md:text-base">Ожидание...</p>
|
||||||
Ожидание...
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
function TeamScores({ teams, myTeamId }) {
|
function TeamScores({ teams, myTeamId }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4 mb-6 md:mb-8">
|
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4 mb-4 md:mb-6">
|
||||||
{teams.map((team) => (
|
{teams.map((team, index) => {
|
||||||
<div
|
const isMe = team.id === myTeamId
|
||||||
key={team.id}
|
const colors = [
|
||||||
className={`p-4 md:p-6 rounded-xl border shadow-lg transition-all duration-200 ${
|
'from-blue-600/30 to-blue-800/30 border-blue-400/30',
|
||||||
team.id === myTeamId
|
'from-purple-600/30 to-purple-800/30 border-purple-400/30',
|
||||||
? 'bg-blue-900 border-blue-500'
|
'from-emerald-600/30 to-emerald-800/30 border-emerald-400/30',
|
||||||
: 'bg-slate-800 border-slate-700'
|
'from-amber-600/30 to-amber-800/30 border-amber-400/30',
|
||||||
}`}
|
'from-rose-600/30 to-rose-800/30 border-rose-400/30',
|
||||||
>
|
'from-cyan-600/30 to-cyan-800/30 border-cyan-400/30',
|
||||||
<h3 className="text-lg md:text-2xl font-bold text-white mb-1 md:mb-2">{team.name}</h3>
|
]
|
||||||
<p className="text-2xl md:text-4xl font-bold text-blue-400">{team.score} баллов</p>
|
const colorClass = colors[index % colors.length]
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
|
<div
|
||||||
|
key={team.id}
|
||||||
|
className={`p-3 md:p-5 rounded-xl border backdrop-blur-sm transition-all duration-300 ${
|
||||||
|
isMe
|
||||||
|
? 'bg-gradient-to-br from-blue-500/20 to-indigo-500/20 border-blue-400/50 glow-blue scale-[1.02]'
|
||||||
|
: `bg-gradient-to-br ${colorClass}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm md:text-lg font-bold text-white mb-0.5 md:mb-1 truncate">
|
||||||
|
{isMe && '⭐ '}{team.name}
|
||||||
|
</h3>
|
||||||
|
<p className="font-display text-xl md:text-3xl font-bold text-gradient-gold">{team.score}</p>
|
||||||
|
<p className="text-[10px] md:text-xs text-gray-400">баллов</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,95 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Orbitron:wght@400;500;600;700;800;900&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
* {
|
||||||
margin: 0;
|
box-sizing: border-box;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
background-color: #0A1E3D;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
body {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
margin: 0;
|
||||||
monospace;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #0a0a1a;
|
||||||
|
color: white;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-display {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-strong {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-gold {
|
||||||
|
background: linear-gradient(135deg, #fbbf24, #f59e0b, #fcd34d);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-blue {
|
||||||
|
box-shadow: 0 0 15px rgba(96, 165, 250, 0.3), 0 0 30px rgba(96, 165, 250, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-purple {
|
||||||
|
box-shadow: 0 0 15px rgba(167, 139, 250, 0.3), 0 0 30px rgba(167, 139, 250, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-gold {
|
||||||
|
box-shadow: 0 0 15px rgba(251, 191, 36, 0.4), 0 0 30px rgba(251, 191, 36, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-glow {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-glow::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-glow:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar { width: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(96, 165, 250, 0.3); border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: rgba(96, 165, 250, 0.5); }
|
||||||
|
|
||||||
|
.bg-pattern {
|
||||||
|
background-image: radial-gradient(circle at 25% 25%, rgba(96, 165, 250, 0.05) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 75%, rgba(167, 139, 250, 0.05) 0%, transparent 50%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,10 @@ function CreateGame() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Слушаем событие создания игры
|
|
||||||
socket.on('game-created', ({ gameCode }) => {
|
socket.on('game-created', ({ gameCode }) => {
|
||||||
setGameCode(gameCode)
|
setGameCode(gameCode)
|
||||||
})
|
})
|
||||||
|
return () => { socket.off('game-created') }
|
||||||
return () => {
|
|
||||||
socket.off('game-created')
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleCreate = (e) => {
|
const handleCreate = (e) => {
|
||||||
@@ -29,89 +25,69 @@ function CreateGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startGame = () => {
|
const startGame = () => {
|
||||||
// Сохраняем в localStorage для восстановления после перезагрузки
|
localStorage.setItem('gameSession', JSON.stringify({ gameCode, isHost: true, hostName }))
|
||||||
localStorage.setItem('gameSession', JSON.stringify({
|
|
||||||
gameCode,
|
|
||||||
isHost: true,
|
|
||||||
hostName
|
|
||||||
}))
|
|
||||||
|
|
||||||
navigate(`/game/${gameCode}`, { state: { isHost: true, hostName } })
|
navigate(`/game/${gameCode}`, { state: { isHost: true, hostName } })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameCode) {
|
if (gameCode) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 px-4">
|
<div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] px-4 bg-pattern">
|
||||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-blue-400 mb-6 md:mb-8">
|
<div className="absolute top-1/3 right-1/4 w-64 h-64 bg-green-500/8 rounded-full blur-3xl animate-blob" />
|
||||||
Игра создана!
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-slate-800 p-6 md:p-8 rounded-xl w-full max-w-md text-center border border-slate-700 shadow-xl">
|
<div className="animate-scale-in relative z-10 w-full max-w-md">
|
||||||
<p className="text-gray-300 text-base md:text-lg mb-4">
|
<h1 className="font-display text-2xl sm:text-3xl md:text-4xl font-bold text-gradient text-center mb-6 md:mb-8">
|
||||||
Код для команд:
|
Игра создана!
|
||||||
</p>
|
</h1>
|
||||||
<div className="bg-slate-900 p-4 md:p-6 rounded-lg mb-6 border border-blue-500">
|
|
||||||
<p className="text-blue-400 text-3xl sm:text-4xl md:text-5xl font-bold tracking-wider">
|
<div className="glass-strong p-6 md:p-8 rounded-2xl w-full text-center shadow-xl">
|
||||||
{gameCode}
|
<p className="text-gray-300 text-base md:text-lg mb-4">Код для команд:</p>
|
||||||
</p>
|
<div className="bg-gradient-to-r from-blue-600/20 to-purple-600/20 p-4 md:p-6 rounded-xl mb-6 border border-blue-400/30 glow-blue">
|
||||||
|
<p className="font-display text-blue-300 text-3xl sm:text-4xl md:text-5xl font-bold tracking-[0.3em]">
|
||||||
|
{gameCode}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={copyGameCode}
|
||||||
|
className="w-full glass hover:bg-white/10 text-white font-bold py-3 rounded-xl transition-all duration-300 mb-3 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
📋 Скопировать код
|
||||||
|
</button>
|
||||||
|
<button onClick={startGame}
|
||||||
|
className="w-full btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3 rounded-xl transition-all duration-300 shadow-lg hover:shadow-blue-500/25 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
Перейти к игре →
|
||||||
|
</button>
|
||||||
|
<p className="text-gray-500 text-xs md:text-sm mt-4">Отправьте этот код командам для присоединения</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={copyGameCode}
|
|
||||||
className="w-full bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg transition-all duration-200 mb-3 shadow-lg"
|
|
||||||
>
|
|
||||||
📋 Скопировать код
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={startGame}
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-all duration-200 shadow-lg"
|
|
||||||
>
|
|
||||||
Перейти к игре
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-gray-400 text-xs md:text-sm mt-4">
|
|
||||||
Отправьте этот код командам для присоединения
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 px-4">
|
<div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] px-4 bg-pattern">
|
||||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-blue-400 mb-6 md:mb-8">
|
<div className="absolute bottom-1/4 left-1/3 w-56 h-56 bg-blue-500/8 rounded-full blur-3xl animate-blob" />
|
||||||
Создать новую игру
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form onSubmit={handleCreate} className="bg-slate-800 p-6 md:p-8 rounded-xl w-full max-w-md border border-slate-700 shadow-xl">
|
<div className="animate-slide-up relative z-10 w-full max-w-md">
|
||||||
<div className="mb-6">
|
<h1 className="font-display text-2xl sm:text-3xl md:text-4xl font-bold text-gradient text-center mb-6 md:mb-8">
|
||||||
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Имя ведущего</label>
|
Создать игру
|
||||||
<input
|
</h1>
|
||||||
type="text"
|
|
||||||
value={hostName}
|
|
||||||
onChange={(e) => setHostName(e.target.value)}
|
|
||||||
className="w-full p-3 rounded-lg bg-slate-900 text-white border border-slate-600 focus:outline-none focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="Введите ваше имя"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<form onSubmit={handleCreate} className="glass-strong p-6 md:p-8 rounded-2xl w-full shadow-xl">
|
||||||
type="submit"
|
<div className="mb-6">
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
|
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Имя ведущего</label>
|
||||||
>
|
<input type="text" value={hostName} onChange={(e) => setHostName(e.target.value)}
|
||||||
Создать
|
className="w-full p-3.5 rounded-xl bg-white/5 text-white border border-white/10 focus:outline-none focus:border-blue-400/50 focus:bg-white/8 transition-all duration-300 placeholder-gray-500"
|
||||||
</button>
|
placeholder="Введите ваше имя" required />
|
||||||
|
</div>
|
||||||
<button
|
<button type="submit"
|
||||||
type="button"
|
className="w-full btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3.5 rounded-xl transition-all duration-300 shadow-lg hover:shadow-blue-500/25 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
onClick={() => navigate('/')}
|
Создать
|
||||||
className="w-full mt-3 bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg transition-all duration-200 shadow-lg"
|
</button>
|
||||||
>
|
<button type="button" onClick={() => navigate('/')}
|
||||||
Назад
|
className="w-full mt-3 glass hover:bg-white/10 text-white font-bold py-3.5 rounded-xl transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
</button>
|
← Назад
|
||||||
</form>
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,13 @@ function Game() {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// Попытка восстановить сессию из localStorage при перезагрузке
|
|
||||||
const getSessionData = () => {
|
const getSessionData = () => {
|
||||||
if (location.state) {
|
if (location.state) return location.state
|
||||||
return location.state
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если state пустой, попробуем восстановить из localStorage
|
|
||||||
const savedSession = localStorage.getItem('gameSession')
|
const savedSession = localStorage.getItem('gameSession')
|
||||||
if (savedSession) {
|
if (savedSession) {
|
||||||
const session = JSON.parse(savedSession)
|
const session = JSON.parse(savedSession)
|
||||||
// Проверяем что это та же игра
|
if (session.gameCode === gameCode) return session
|
||||||
if (session.gameCode === gameCode) {
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +30,6 @@ function Game() {
|
|||||||
const [answerMedia, setAnswerMedia] = useState(null)
|
const [answerMedia, setAnswerMedia] = useState(null)
|
||||||
const [shouldFinishAfterMedia, setShouldFinishAfterMedia] = useState(false)
|
const [shouldFinishAfterMedia, setShouldFinishAfterMedia] = useState(false)
|
||||||
|
|
||||||
// Если нет teamName и не хост, редиректим на главную
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!teamName && !isHost) {
|
if (!teamName && !isHost) {
|
||||||
console.warn('No team name or host status, redirecting to home')
|
console.warn('No team name or host status, redirecting to home')
|
||||||
@@ -47,77 +37,54 @@ function Game() {
|
|||||||
}
|
}
|
||||||
}, [teamName, isHost, navigate])
|
}, [teamName, isHost, navigate])
|
||||||
|
|
||||||
// Подключаемся к игре (хост или команда)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHost) {
|
if (isHost) {
|
||||||
socket.emit('host-join-game', { gameCode })
|
socket.emit('host-join-game', { gameCode })
|
||||||
} else if (teamName) {
|
} else if (teamName) {
|
||||||
// Команда переподключается к игре
|
|
||||||
socket.emit('join-game', { gameCode, teamName })
|
socket.emit('join-game', { gameCode, teamName })
|
||||||
}
|
}
|
||||||
}, [isHost, teamName, gameCode])
|
}, [isHost, teamName, gameCode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Socket обработчики
|
|
||||||
socket.on('game-updated', (updatedGame) => {
|
socket.on('game-updated', (updatedGame) => {
|
||||||
setGame(updatedGame)
|
setGame(updatedGame)
|
||||||
|
|
||||||
if (!isHost && teamName) {
|
if (!isHost && teamName) {
|
||||||
const team = updatedGame.teams.find(t => t.name === teamName)
|
const team = updatedGame.teams.find(t => t.name === teamName)
|
||||||
setMyTeam(team)
|
setMyTeam(team)
|
||||||
}
|
}
|
||||||
|
if (updatedGame.selectedCategories) setQuestions(updatedGame.selectedCategories)
|
||||||
// Обновляем selectedCategories когда игра обновляется
|
|
||||||
if (updatedGame.selectedCategories) {
|
|
||||||
setQuestions(updatedGame.selectedCategories)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('game-started', (game) => {
|
socket.on('game-started', (game) => {
|
||||||
setGame(game)
|
setGame(game)
|
||||||
|
|
||||||
if (!isHost && teamName) {
|
if (!isHost && teamName) {
|
||||||
const team = game.teams.find(t => t.name === teamName)
|
const team = game.teams.find(t => t.name === teamName)
|
||||||
setMyTeam(team)
|
setMyTeam(team)
|
||||||
}
|
}
|
||||||
|
if (game.selectedCategories) setQuestions(game.selectedCategories)
|
||||||
if (game.selectedCategories) {
|
|
||||||
setQuestions(game.selectedCategories)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('host-game-loaded', (loadedGame) => {
|
socket.on('host-game-loaded', (loadedGame) => {
|
||||||
setGame(loadedGame)
|
setGame(loadedGame)
|
||||||
if (loadedGame.selectedCategories) {
|
if (loadedGame.selectedCategories) setQuestions(loadedGame.selectedCategories)
|
||||||
setQuestions(loadedGame.selectedCategories)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('question-selected', ({ category, points, questionIndex, timer }) => {
|
socket.on('question-selected', ({ category, points, questionIndex, timer }) => {
|
||||||
// Ищем вопрос в selectedCategories игры по индексу
|
|
||||||
const categoryData = game?.selectedCategories?.find(cat => cat.name === category)
|
const categoryData = game?.selectedCategories?.find(cat => cat.name === category)
|
||||||
|
|
||||||
// Если есть questionIndex, используем его, иначе ищем по баллам (для обратной совместимости)
|
|
||||||
const questionData = questionIndex !== undefined
|
const questionData = questionIndex !== undefined
|
||||||
? categoryData?.questions[questionIndex]
|
? categoryData?.questions[questionIndex]
|
||||||
: categoryData?.questions.find(q => q.points === points)
|
: categoryData?.questions.find(q => q.points === points)
|
||||||
|
|
||||||
console.log(`📥 Question selected: category="${category}", points=${points}, questionIndex=${questionIndex}`, questionData);
|
console.log(`📥 Question selected: category="${category}", points=${points}, questionIndex=${questionIndex}`, questionData);
|
||||||
|
|
||||||
setCurrentQuestion({ ...questionData, category, points })
|
setCurrentQuestion({ ...questionData, category, points })
|
||||||
setTimer(timer)
|
setTimer(timer)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('timer-update', ({ timer }) => {
|
socket.on('timer-update', ({ timer }) => { setTimer(timer) })
|
||||||
setTimer(timer)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('time-up', ({ answerMedia, shouldFinish }) => {
|
socket.on('time-up', ({ answerMedia, shouldFinish }) => {
|
||||||
if (answerMedia) {
|
if (answerMedia) {
|
||||||
setAnswerMedia(answerMedia)
|
setAnswerMedia(answerMedia)
|
||||||
if (shouldFinish) {
|
if (shouldFinish) setShouldFinishAfterMedia(true)
|
||||||
setShouldFinishAfterMedia(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setCurrentQuestion(null)
|
setCurrentQuestion(null)
|
||||||
setTimer(null)
|
setTimer(null)
|
||||||
@@ -130,35 +97,27 @@ function Game() {
|
|||||||
socket.on('answer-result', ({ teamId, isCorrect, questionClosed, answerMedia, shouldFinish }) => {
|
socket.on('answer-result', ({ teamId, isCorrect, questionClosed, answerMedia, shouldFinish }) => {
|
||||||
console.log('Answer result:', { teamId, isCorrect, questionClosed, answerMedia, shouldFinish })
|
console.log('Answer result:', { teamId, isCorrect, questionClosed, answerMedia, shouldFinish })
|
||||||
if (questionClosed) {
|
if (questionClosed) {
|
||||||
// Правильный ответ - закрываем вопрос
|
|
||||||
if (answerMedia) {
|
if (answerMedia) {
|
||||||
setAnswerMedia(answerMedia)
|
setAnswerMedia(answerMedia)
|
||||||
if (shouldFinish) {
|
if (shouldFinish) setShouldFinishAfterMedia(true)
|
||||||
setShouldFinishAfterMedia(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentQuestion(null)
|
setCurrentQuestion(null)
|
||||||
setTimer(null)
|
setTimer(null)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} else {
|
} else {
|
||||||
// Неправильный ответ - продолжаем игру
|
|
||||||
console.log('Incorrect answer, waiting for other teams')
|
console.log('Incorrect answer, waiting for other teams')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('round-changed', ({ round }) => {
|
socket.on('round-changed', ({ round }) => {
|
||||||
console.log(`Round ${round} started`)
|
console.log(`Round ${round} started`)
|
||||||
// Принудительно обновляем состояние игры после смены раунда
|
|
||||||
// Сервер отправит game-updated сразу после, но мы сбрасываем локальное состояние
|
|
||||||
setCurrentQuestion(null)
|
setCurrentQuestion(null)
|
||||||
setTimer(null)
|
setTimer(null)
|
||||||
setAnswerMedia(null)
|
setAnswerMedia(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('game-finished', (finishedGame) => {
|
socket.on('game-finished', (finishedGame) => { setGame(finishedGame) })
|
||||||
setGame(finishedGame)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('game-updated')
|
socket.off('game-updated')
|
||||||
@@ -174,25 +133,14 @@ function Game() {
|
|||||||
}
|
}
|
||||||
}, [teamName, questions, isHost])
|
}, [teamName, questions, isHost])
|
||||||
|
|
||||||
const handleStartGame = () => {
|
const handleStartGame = () => { socket.emit('start-game', { gameCode }) }
|
||||||
socket.emit('start-game', { gameCode })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectQuestion = (category, points) => {
|
const handleSelectQuestion = (category, points) => {
|
||||||
// Только хост может выбирать вопросы
|
if (isHost) socket.emit('select-question', { gameCode, category, points })
|
||||||
if (isHost) {
|
|
||||||
socket.emit('select-question', { gameCode, category, points })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBuzzIn = () => {
|
const handleBuzzIn = () => {
|
||||||
if (myTeam) {
|
if (myTeam) socket.emit('buzz-in', { gameCode, teamId: myTeam.id })
|
||||||
socket.emit('buzz-in', { gameCode, teamId: myTeam.id })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmitAnswer = (isCorrect) => {
|
const handleSubmitAnswer = (isCorrect) => {
|
||||||
// Теперь только ведущий отправляет ответ
|
|
||||||
console.log('handleSubmitAnswer called', { isHost, answeringTeam: game.answeringTeam, isCorrect })
|
console.log('handleSubmitAnswer called', { isHost, answeringTeam: game.answeringTeam, isCorrect })
|
||||||
if (isHost && game.answeringTeam) {
|
if (isHost && game.answeringTeam) {
|
||||||
console.log('Sending submit-answer', { gameCode, teamId: game.answeringTeam, isCorrect })
|
console.log('Sending submit-answer', { gameCode, teamId: game.answeringTeam, isCorrect })
|
||||||
@@ -201,21 +149,12 @@ function Game() {
|
|||||||
console.log('Not sending: isHost=', isHost, 'answeringTeam=', game.answeringTeam)
|
console.log('Not sending: isHost=', isHost, 'answeringTeam=', game.answeringTeam)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseQuestion = () => {
|
const handleCloseQuestion = () => {
|
||||||
// Ведущий может досрочно закрыть вопрос
|
if (isHost) socket.emit('close-question', { gameCode })
|
||||||
if (isHost) {
|
|
||||||
socket.emit('close-question', { gameCode })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFinishGame = () => {
|
const handleFinishGame = () => {
|
||||||
// Ведущий завершает игру
|
if (isHost) socket.emit('finish-game', { gameCode })
|
||||||
if (isHost) {
|
|
||||||
socket.emit('finish-game', { gameCode })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseAnswerMedia = () => {
|
const handleCloseAnswerMedia = () => {
|
||||||
setAnswerMedia(null)
|
setAnswerMedia(null)
|
||||||
if (shouldFinishAfterMedia && isHost) {
|
if (shouldFinishAfterMedia && isHost) {
|
||||||
@@ -226,63 +165,60 @@ function Game() {
|
|||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen px-4">
|
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] px-4">
|
||||||
<div className="text-center">
|
<div className="text-center animate-pulse-slow">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-game-gold mb-4">Загрузка...</h2>
|
<div className="text-6xl mb-4">🧠</div>
|
||||||
|
<h2 className="font-display text-2xl md:text-3xl font-bold text-gradient mb-4">Загрузка...</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экран результатов
|
// Results screen
|
||||||
if (game.status === 'finished') {
|
if (game.status === 'finished') {
|
||||||
const sortedTeams = [...game.teams].sort((a, b) => b.score - a.score)
|
const sortedTeams = [...game.teams].sort((a, b) => b.score - a.score)
|
||||||
const winner = sortedTeams[0]
|
const winner = sortedTeams[0]
|
||||||
|
const medals = ['🥇', '🥈', '🥉']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-4 md:p-8">
|
<div className="min-h-screen p-4 md:p-8 bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] bg-pattern">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12 animate-slide-up">
|
||||||
<h1 className="text-3xl md:text-6xl font-bold text-game-gold mb-4">ИГРА ЗАВЕРШЕНА!</h1>
|
<h1 className="font-display text-3xl md:text-6xl font-black text-gradient-gold mb-6">ИГРА ЗАВЕРШЕНА!</h1>
|
||||||
<div className="bg-gradient-to-r from-yellow-400 to-yellow-600 p-6 md:p-8 rounded-lg mb-6 md:mb-8">
|
<div className="glass-strong p-6 md:p-10 rounded-2xl mb-6 md:mb-8 glow-gold">
|
||||||
<p className="text-2xl md:text-4xl font-bold text-gray-900 mb-2">🏆 ПОБЕДИТЕЛЬ 🏆</p>
|
<p className="text-2xl md:text-4xl font-bold text-white mb-3">🏆 ПОБЕДИТЕЛЬ 🏆</p>
|
||||||
<p className="text-3xl md:text-5xl font-bold text-gray-900">{winner.name}</p>
|
<p className="font-display text-3xl md:text-5xl font-black text-gradient-gold">{winner.name}</p>
|
||||||
<p className="text-xl md:text-3xl font-bold text-gray-900 mt-2">{winner.score} баллов</p>
|
<p className="font-display text-xl md:text-3xl font-bold text-amber-300 mt-3">{winner.score} баллов</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 p-4 md:p-8 rounded-lg mb-6 md:mb-8">
|
<div className="glass-strong p-4 md:p-8 rounded-2xl mb-6 md:mb-8">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4 md:mb-6 text-center">Итоговая таблица</h2>
|
<h2 className="font-display text-xl md:text-2xl font-bold text-white mb-4 md:mb-6 text-center">Итоговая таблица</h2>
|
||||||
<div className="space-y-3 md:space-y-4">
|
<div className="space-y-2 md:space-y-3">
|
||||||
{sortedTeams.map((team, index) => (
|
{sortedTeams.map((team, index) => (
|
||||||
<div
|
<div key={team.id}
|
||||||
key={team.id}
|
className={`flex justify-between items-center p-4 md:p-5 rounded-xl transition-all duration-300 animate-slide-up border ${
|
||||||
className={`flex justify-between items-center p-4 md:p-6 rounded-lg ${
|
index === 0 ? 'bg-gradient-to-r from-amber-600/30 to-yellow-600/30 border-amber-400/30 glow-gold'
|
||||||
index === 0
|
: index === 1 ? 'bg-gradient-to-r from-gray-400/20 to-gray-500/20 border-gray-400/20'
|
||||||
? 'bg-gradient-to-r from-yellow-600 to-yellow-700'
|
: index === 2 ? 'bg-gradient-to-r from-orange-600/20 to-orange-700/20 border-orange-400/20'
|
||||||
: index === 1
|
: 'glass'
|
||||||
? 'bg-gradient-to-r from-gray-400 to-gray-500'
|
|
||||||
: index === 2
|
|
||||||
? 'bg-gradient-to-r from-orange-600 to-orange-700'
|
|
||||||
: 'bg-gray-700'
|
|
||||||
}`}
|
}`}
|
||||||
|
style={{ animationDelay: `${index * 0.1}s` }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 md:gap-4">
|
<div className="flex items-center gap-2 md:gap-4">
|
||||||
<span className="text-2xl md:text-4xl font-bold text-white">#{index + 1}</span>
|
<span className="text-2xl md:text-4xl">{medals[index] || `#${index + 1}`}</span>
|
||||||
<span className="text-base md:text-2xl font-bold text-white">{team.name}</span>
|
<span className="text-base md:text-xl font-bold text-white">{team.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xl md:text-3xl font-bold text-white">{team.score}</span>
|
<span className="font-display text-xl md:text-2xl font-bold text-gradient-gold">{team.score}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<button
|
<button onClick={() => navigate('/')}
|
||||||
onClick={() => navigate('/')}
|
className="btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3 md:py-4 px-8 md:px-12 rounded-2xl text-base md:text-xl transition-all duration-300 shadow-lg hover:shadow-blue-500/25 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
className="bg-game-gold text-game-blue font-bold py-3 md:py-4 px-8 md:px-12 rounded-lg text-base md:text-xl hover:bg-yellow-500 transition"
|
🏠 На главную
|
||||||
>
|
|
||||||
Вернуться на главную
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,75 +227,67 @@ function Game() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-4 md:p-8 bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900">
|
<div className="min-h-screen p-3 md:p-6 bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] bg-pattern">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Заголовок */}
|
{/* Header */}
|
||||||
<div className="text-center mb-6 md:mb-8">
|
<div className="text-center mb-4 md:mb-6">
|
||||||
<h1 className="text-3xl md:text-5xl font-bold text-blue-400 mb-3 md:mb-4">
|
<h1 className="font-display text-2xl md:text-4xl font-black text-gradient mb-2 md:mb-3">СВОЯ ИГРА</h1>
|
||||||
СВОЯ ИГРА
|
<div className="glass inline-flex items-center gap-3 md:gap-5 px-4 md:px-6 py-2 md:py-3 rounded-xl">
|
||||||
</h1>
|
<span className="text-xs md:text-base text-gray-400">
|
||||||
<div className="bg-slate-800 rounded-xl p-3 md:p-4 inline-block border border-slate-700">
|
Код: <span className="font-display text-blue-300 font-bold tracking-wider">{gameCode}</span>
|
||||||
<p className="text-sm md:text-xl text-gray-300">
|
</span>
|
||||||
Код игры: <span className="text-blue-400 font-bold tracking-wider">{gameCode}</span>
|
|
||||||
</p>
|
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<p className="text-xs md:text-md text-gray-400 mt-2">
|
<span className="text-xs md:text-sm text-gray-500">• {hostName}</span>
|
||||||
Ведущий: {hostName}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 md:mt-3 inline-block bg-blue-600 text-white px-4 md:px-6 py-1 md:py-2 rounded-lg font-bold shadow-lg text-sm md:text-base">
|
<span className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-3 md:px-4 py-0.5 md:py-1 rounded-lg font-bold text-xs md:text-sm font-display">
|
||||||
Раунд {game.currentRound}
|
Раунд {game.currentRound}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Счет команд */}
|
{/* Team scores */}
|
||||||
<TeamScores teams={game.teams} myTeamId={myTeam?.id} />
|
<TeamScores teams={game.teams} myTeamId={myTeam?.id} />
|
||||||
|
|
||||||
{/* Статус игры */}
|
{/* Waiting state */}
|
||||||
{game.status === 'waiting' && isHost && (
|
{game.status === 'waiting' && isHost && (
|
||||||
<div className="text-center my-6 md:my-8">
|
<div className="text-center my-6 md:my-8 animate-slide-up">
|
||||||
<p className="text-white text-base md:text-lg mb-4">
|
<div className="glass-strong p-6 md:p-8 rounded-2xl inline-block">
|
||||||
Ожидание команд... ({game.teams.length} команд(ы) присоединилось)
|
<p className="text-white text-base md:text-lg mb-4">
|
||||||
</p>
|
Ожидание команд... <span className="text-blue-300 font-bold">({game.teams.length})</span>
|
||||||
{game.teams.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={handleStartGame}
|
|
||||||
className="bg-green-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-lg text-lg md:text-xl hover:bg-green-700 transition"
|
|
||||||
>
|
|
||||||
Начать игру
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{game.teams.length === 0 && (
|
|
||||||
<p className="text-gray-400 text-sm md:text-base">
|
|
||||||
Отправьте код игры командам для присоединения
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
{game.teams.length > 0 && (
|
||||||
|
<button onClick={handleStartGame}
|
||||||
|
className="btn-glow 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-8 md:px-10 rounded-xl text-lg md:text-xl transition-all duration-300 shadow-lg shadow-emerald-500/20 hover:scale-[1.03] active:scale-95 border border-emerald-400/30">
|
||||||
|
▶️ Начать игру
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{game.teams.length === 0 && (
|
||||||
|
<p className="text-gray-500 text-sm md:text-base mt-2">Отправьте код игры командам</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{game.status === 'waiting' && !isHost && (
|
{game.status === 'waiting' && !isHost && (
|
||||||
<div className="text-center my-6 md:my-8">
|
<div className="text-center my-6 md:my-8">
|
||||||
<p className="text-white text-base md:text-lg">
|
<div className="glass p-6 rounded-2xl inline-block animate-pulse-slow">
|
||||||
Ожидание начала игры ведущим...
|
<p className="text-white text-base md:text-lg">⏳ Ожидание начала игры...</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Игровое поле */}
|
{/* Game board */}
|
||||||
{game.status === 'playing' && (
|
{game.status === 'playing' && (
|
||||||
<>
|
<GameBoard
|
||||||
<GameBoard
|
questions={questions}
|
||||||
questions={questions}
|
usedQuestions={game.usedQuestions}
|
||||||
usedQuestions={game.usedQuestions}
|
onSelectQuestion={handleSelectQuestion}
|
||||||
onSelectQuestion={handleSelectQuestion}
|
currentRound={game.currentRound}
|
||||||
currentRound={game.currentRound}
|
isHost={isHost}
|
||||||
isHost={isHost}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Модальное окно с вопросом */}
|
{/* Question modal */}
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<QuestionModal
|
<QuestionModal
|
||||||
question={currentQuestion}
|
question={currentQuestion}
|
||||||
@@ -375,30 +303,23 @@ function Game() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Модальное окно с ответом (песня) - показываем всем */}
|
{/* Answer media modal */}
|
||||||
{answerMedia && (
|
{answerMedia && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-scale-in">
|
||||||
<div className="bg-gradient-to-br from-blue-900 to-slate-900 p-6 md:p-12 rounded-2xl max-w-4xl w-full border-4 border-blue-400 shadow-2xl">
|
<div className="glass-strong p-6 md:p-10 rounded-2xl max-w-4xl w-full shadow-2xl glow-blue">
|
||||||
<h2 className="text-2xl md:text-4xl font-bold text-blue-400 mb-6 md:mb-8 text-center">
|
<h2 className="font-display text-2xl md:text-4xl font-bold text-gradient text-center mb-6 md:mb-8">
|
||||||
Правильный ответ
|
Правильный ответ
|
||||||
</h2>
|
</h2>
|
||||||
|
<div className="glass p-4 md:p-6 rounded-xl mb-6 md:mb-8">
|
||||||
<div className="bg-slate-800 p-6 md:p-8 rounded-lg mb-6 md:mb-8">
|
<audio controls autoPlay
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
autoPlay
|
|
||||||
src={`${window.location.protocol}//${window.location.host}${answerMedia}`}
|
src={`${window.location.protocol}//${window.location.host}${answerMedia}`}
|
||||||
className="w-full"
|
className="w-full" onEnded={handleCloseAnswerMedia}>
|
||||||
>
|
|
||||||
Ваш браузер не поддерживает аудио элемент.
|
Ваш браузер не поддерживает аудио элемент.
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<button
|
<button onClick={handleCloseAnswerMedia}
|
||||||
onClick={handleCloseAnswerMedia}
|
className="btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3 md:py-4 px-8 md:px-12 rounded-xl text-lg md:text-xl transition-all duration-300 shadow-lg hover:scale-[1.02] active:scale-[0.98]">
|
||||||
className="bg-blue-600 text-white font-bold py-3 md:py-4 px-8 md:px-12 rounded-lg text-lg md:text-xl hover:bg-blue-700 transition shadow-lg"
|
|
||||||
>
|
|
||||||
Закрыть
|
Закрыть
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -406,26 +327,22 @@ function Game() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Кнопка следующего раунда */}
|
{/* Next round button */}
|
||||||
{isHost && game.usedQuestions.length >= 25 && game.status === 'playing' && (
|
{isHost && game.usedQuestions.length >= 25 && game.status === 'playing' && (
|
||||||
<div className="text-center mt-6 md:mt-8">
|
<div className="text-center mt-4 md:mt-6">
|
||||||
<button
|
<button onClick={() => socket.emit('next-round', { gameCode })}
|
||||||
onClick={() => socket.emit('next-round', { gameCode })}
|
className="btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-xl text-base md:text-lg transition-all duration-300 shadow-lg hover:scale-[1.02] active:scale-[0.98]">
|
||||||
className="bg-blue-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-lg text-lg md:text-xl hover:bg-blue-700 transition"
|
⏭️ Следующий раунд
|
||||||
>
|
|
||||||
Следующий раунд
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Кнопка завершить игру для ведущего */}
|
{/* Finish game button */}
|
||||||
{isHost && game.status === 'playing' && (
|
{isHost && game.status === 'playing' && (
|
||||||
<div className="text-center mt-6 md:mt-8">
|
<div className="text-center mt-4 md:mt-6">
|
||||||
<button
|
<button onClick={handleFinishGame}
|
||||||
onClick={handleFinishGame}
|
className="glass hover:bg-red-500/20 text-red-300 hover:text-red-200 font-bold py-2 md:py-3 px-6 md:px-8 rounded-xl text-sm md:text-base transition-all duration-300 border-red-500/20 hover:border-red-400/30">
|
||||||
className="bg-red-600 text-white font-bold py-2 md:py-3 px-6 md:px-8 rounded-lg text-base md:text-lg hover:bg-red-700 transition"
|
🏁 Завершить игру
|
||||||
>
|
|
||||||
Завершить игру
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,28 +4,40 @@ function Home() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 px-4">
|
<div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] px-4 overflow-hidden bg-pattern">
|
||||||
<div className="text-center w-full max-w-md">
|
{/* Ambient blobs */}
|
||||||
<div className="mb-8 md:mb-16">
|
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-blue-500/10 rounded-full blur-3xl animate-blob" />
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-7xl font-bold text-blue-400 mb-2 md:mb-4">
|
<div className="absolute bottom-1/4 right-1/4 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl animate-blob" style={{ animationDelay: '2s' }} />
|
||||||
|
<div className="absolute top-1/2 left-1/2 w-56 h-56 bg-indigo-500/8 rounded-full blur-3xl animate-blob" style={{ animationDelay: '4s' }} />
|
||||||
|
|
||||||
|
<div className="text-center w-full max-w-md animate-slide-up relative z-10">
|
||||||
|
<div className="mb-10 md:mb-16">
|
||||||
|
{/* Logo icon */}
|
||||||
|
<div className="mx-auto mb-6 w-20 h-20 md:w-24 md:h-24 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-lg glow-purple">
|
||||||
|
<span className="text-4xl md:text-5xl">🧠</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="font-display text-4xl sm:text-5xl md:text-7xl font-black text-gradient mb-3 md:mb-4 tracking-tight">
|
||||||
СВОЯ ИГРА
|
СВОЯ ИГРА
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-base md:text-lg text-gray-400">Интеллектуальная викторина</p>
|
<p className="text-base md:text-lg text-gray-400 font-medium tracking-wide">
|
||||||
|
Интеллектуальная викторина
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 md:gap-4 w-full">
|
<div className="flex flex-col gap-3 md:gap-4 w-full">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/create')}
|
onClick={() => navigate('/create')}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-xl text-lg md:text-xl transition-all duration-200 shadow-lg hover:shadow-xl"
|
className="btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-4 md:py-5 px-6 md:px-8 rounded-2xl text-lg md:text-xl transition-all duration-300 shadow-lg hover:shadow-blue-500/25 hover:scale-[1.02] active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
Создать игру
|
🎮 Создать игру
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/join')}
|
onClick={() => navigate('/join')}
|
||||||
className="bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 md:py-4 px-6 md:px-8 rounded-xl text-lg md:text-xl transition-all duration-200 shadow-lg hover:shadow-xl"
|
className="btn-glow glass hover:bg-white/10 text-white font-bold py-4 md:py-5 px-6 md:px-8 rounded-2xl text-lg md:text-xl transition-all duration-300 hover:shadow-lg hover:scale-[1.02] active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
Присоединиться
|
🚀 Присоединиться
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,25 +9,16 @@ function JoinGame() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Слушаем события
|
|
||||||
const handleGameUpdated = () => {
|
const handleGameUpdated = () => {
|
||||||
// Сохраняем в localStorage перед навигацией
|
|
||||||
localStorage.setItem('gameSession', JSON.stringify({
|
localStorage.setItem('gameSession', JSON.stringify({
|
||||||
gameCode: gameCode.toUpperCase(),
|
gameCode: gameCode.toUpperCase(), teamName, isHost: false
|
||||||
teamName,
|
|
||||||
isHost: false
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
navigate(`/game/${gameCode.toUpperCase()}`, { state: { teamName } })
|
navigate(`/game/${gameCode.toUpperCase()}`, { state: { teamName } })
|
||||||
}
|
}
|
||||||
|
const handleError = ({ message }) => { setError(message) }
|
||||||
const handleError = ({ message }) => {
|
|
||||||
setError(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on('game-updated', handleGameUpdated)
|
socket.on('game-updated', handleGameUpdated)
|
||||||
socket.on('error', handleError)
|
socket.on('error', handleError)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('game-updated', handleGameUpdated)
|
socket.off('game-updated', handleGameUpdated)
|
||||||
socket.off('error', handleError)
|
socket.off('error', handleError)
|
||||||
@@ -41,57 +32,44 @@ function JoinGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 px-4">
|
<div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-[#0a0a1a] via-[#1a1040] to-[#0d1b2a] px-4 bg-pattern">
|
||||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-blue-400 mb-6 md:mb-8">
|
<div className="absolute top-1/3 left-1/4 w-56 h-56 bg-purple-500/8 rounded-full blur-3xl animate-blob" />
|
||||||
Присоединиться к игре
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form onSubmit={handleJoin} className="bg-slate-800 p-6 md:p-8 rounded-xl w-full max-w-md border border-slate-700 shadow-xl">
|
<div className="animate-slide-up relative z-10 w-full max-w-md">
|
||||||
<div className="mb-4">
|
<h1 className="font-display text-2xl sm:text-3xl md:text-4xl font-bold text-gradient text-center mb-6 md:mb-8">
|
||||||
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Код игры</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={gameCode}
|
|
||||||
onChange={(e) => setGameCode(e.target.value.toUpperCase())}
|
|
||||||
className="w-full p-3 rounded-lg bg-slate-900 text-white border border-slate-600 focus:outline-none focus:border-blue-500 uppercase text-center text-xl md:text-2xl tracking-wider transition-colors"
|
|
||||||
placeholder="ABC123"
|
|
||||||
maxLength={6}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-6">
|
|
||||||
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Название команды</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={teamName}
|
|
||||||
onChange={(e) => setTeamName(e.target.value)}
|
|
||||||
className="w-full p-3 rounded-lg bg-slate-900 text-white border border-slate-600 focus:outline-none focus:border-blue-500 transition-colors"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mb-4 p-3 bg-red-600/90 text-white rounded-lg text-sm md:text-base">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
||||||
>
|
|
||||||
Присоединиться
|
Присоединиться
|
||||||
</button>
|
</h1>
|
||||||
|
|
||||||
<button
|
<form onSubmit={handleJoin} className="glass-strong p-6 md:p-8 rounded-2xl w-full shadow-xl">
|
||||||
type="button"
|
<div className="mb-4">
|
||||||
onClick={() => navigate('/')}
|
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Код игры</label>
|
||||||
className="w-full mt-3 bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg transition-all duration-200 shadow-lg"
|
<input type="text" value={gameCode} onChange={(e) => setGameCode(e.target.value.toUpperCase())}
|
||||||
>
|
className="w-full p-3.5 rounded-xl bg-white/5 text-white border border-white/10 focus:outline-none focus:border-blue-400/50 focus:bg-white/8 uppercase text-center text-xl md:text-2xl tracking-[0.3em] font-display transition-all duration-300 placeholder-gray-500"
|
||||||
Назад
|
placeholder="ABC123" maxLength={6} required />
|
||||||
</button>
|
</div>
|
||||||
</form>
|
<div className="mb-6">
|
||||||
|
<label className="block text-gray-300 mb-2 font-medium text-sm md:text-base">Название команды</label>
|
||||||
|
<input type="text" value={teamName} onChange={(e) => setTeamName(e.target.value)}
|
||||||
|
className="w-full p-3.5 rounded-xl bg-white/5 text-white border border-white/10 focus:outline-none focus:border-blue-400/50 focus:bg-white/8 transition-all duration-300 placeholder-gray-500"
|
||||||
|
placeholder="Введите название" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/30 text-red-300 rounded-xl text-sm md:text-base animate-scale-in">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
className="w-full btn-glow bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold py-3.5 rounded-xl transition-all duration-300 shadow-lg hover:shadow-blue-500/25 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
Присоединиться
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => navigate('/')}
|
||||||
|
className="w-full mt-3 glass hover:bg-white/10 text-white font-bold py-3.5 rounded-xl transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]">
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,43 +9,54 @@ export default {
|
|||||||
colors: {
|
colors: {
|
||||||
'game-blue': '#0A1E3D',
|
'game-blue': '#0A1E3D',
|
||||||
'game-gold': '#FFD700',
|
'game-gold': '#FFD700',
|
||||||
|
'game-purple': '#1a0533',
|
||||||
|
'game-deep': '#0d1b2a',
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'blob': 'blob 7s infinite',
|
'blob': 'blob 7s infinite',
|
||||||
'float': 'float 3s ease-in-out infinite',
|
'float': 'float 3s ease-in-out infinite',
|
||||||
'glow': 'glow 2s ease-in-out infinite alternate',
|
'glow': 'glow 2s ease-in-out infinite alternate',
|
||||||
|
'shimmer': 'shimmer 2s linear infinite',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'slide-up': 'slideUp 0.5s ease-out',
|
||||||
|
'scale-in': 'scaleIn 0.3s ease-out',
|
||||||
|
'buzz': 'buzz 0.15s ease-in-out 3',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
blob: {
|
blob: {
|
||||||
'0%': {
|
'0%': { transform: 'translate(0px, 0px) scale(1)' },
|
||||||
transform: 'translate(0px, 0px) scale(1)',
|
'33%': { transform: 'translate(30px, -50px) scale(1.1)' },
|
||||||
},
|
'66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
|
||||||
'33%': {
|
'100%': { transform: 'translate(0px, 0px) scale(1)' },
|
||||||
transform: 'translate(30px, -50px) scale(1.1)',
|
|
||||||
},
|
|
||||||
'66%': {
|
|
||||||
transform: 'translate(-20px, 20px) scale(0.9)',
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'translate(0px, 0px) scale(1)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
float: {
|
float: {
|
||||||
'0%, 100%': {
|
'0%, 100%': { transform: 'translateY(0px)' },
|
||||||
transform: 'translateY(0px)',
|
'50%': { transform: 'translateY(-20px)' },
|
||||||
},
|
|
||||||
'50%': {
|
|
||||||
transform: 'translateY(-20px)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
glow: {
|
glow: {
|
||||||
'from': {
|
'from': { 'box-shadow': '0 0 20px rgba(255, 215, 0, 0.5)' },
|
||||||
'box-shadow': '0 0 20px rgba(255, 215, 0, 0.5)',
|
'to': { 'box-shadow': '0 0 30px rgba(255, 215, 0, 0.8)' },
|
||||||
},
|
|
||||||
'to': {
|
|
||||||
'box-shadow': '0 0 30px rgba(255, 215, 0, 0.8)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
shimmer: {
|
||||||
|
'0%': { 'background-position': '-200% 0' },
|
||||||
|
'100%': { 'background-position': '200% 0' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
scaleIn: {
|
||||||
|
'0%': { transform: 'scale(0.9)', opacity: '0' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
|
},
|
||||||
|
buzz: {
|
||||||
|
'0%, 100%': { transform: 'translateX(0)' },
|
||||||
|
'25%': { transform: 'translateX(-4px)' },
|
||||||
|
'75%': { transform: 'translateX(4px)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user