diff --git a/client/src/components/TeamScores.jsx b/client/src/components/TeamScores.jsx
index 36a6dcc..d6afba8 100644
--- a/client/src/components/TeamScores.jsx
+++ b/client/src/components/TeamScores.jsx
@@ -1,19 +1,35 @@
function TeamScores({ teams, myTeamId }) {
return (
-
- {teams.map((team) => (
-
-
{team.name}
-
{team.score} баллов
-
- ))}
+
+ {teams.map((team, index) => {
+ const isMe = team.id === myTeamId
+ const colors = [
+ 'from-blue-600/30 to-blue-800/30 border-blue-400/30',
+ 'from-purple-600/30 to-purple-800/30 border-purple-400/30',
+ 'from-emerald-600/30 to-emerald-800/30 border-emerald-400/30',
+ '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',
+ ]
+ const colorClass = colors[index % colors.length]
+
+ return (
+
+
+ {isMe && '⭐ '}{team.name}
+
+
{team.score}
+
баллов
+
+ )
+ })}
)
}
diff --git a/client/src/index.css b/client/src/index.css
index c4b29df..95f747d 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -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 components;
@tailwind utilities;
-body {
- margin: 0;
- 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;
+* {
+ box-sizing: border-box;
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+body {
+ margin: 0;
+ 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%);
}
diff --git a/client/src/pages/CreateGame.jsx b/client/src/pages/CreateGame.jsx
index 103dc8c..49d48f0 100644
--- a/client/src/pages/CreateGame.jsx
+++ b/client/src/pages/CreateGame.jsx
@@ -8,14 +8,10 @@ function CreateGame() {
const navigate = useNavigate()
useEffect(() => {
- // Слушаем событие создания игры
socket.on('game-created', ({ gameCode }) => {
setGameCode(gameCode)
})
-
- return () => {
- socket.off('game-created')
- }
+ return () => { socket.off('game-created') }
}, [])
const handleCreate = (e) => {
@@ -29,89 +25,69 @@ function CreateGame() {
}
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 } })
}
if (gameCode) {
return (
-
-
- Игра создана!
-
+
+
-
-
- Код для команд:
-
-
-
- {gameCode}
-
+
+
+ Игра создана!
+
+
+
+
Код для команд:
+
+
+
+
+
Отправьте этот код командам для присоединения
-
-
-
-
-
-
- Отправьте этот код командам для присоединения
-
)
}
return (
-
-
- Создать новую игру
-
+
)
}
diff --git a/client/src/pages/Game.jsx b/client/src/pages/Game.jsx
index 2f558f5..549dea4 100644
--- a/client/src/pages/Game.jsx
+++ b/client/src/pages/Game.jsx
@@ -10,22 +10,13 @@ function Game() {
const location = useLocation()
const navigate = useNavigate()
- // Попытка восстановить сессию из localStorage при перезагрузке
const getSessionData = () => {
- if (location.state) {
- return location.state
- }
-
- // Если state пустой, попробуем восстановить из localStorage
+ if (location.state) return location.state
const savedSession = localStorage.getItem('gameSession')
if (savedSession) {
const session = JSON.parse(savedSession)
- // Проверяем что это та же игра
- if (session.gameCode === gameCode) {
- return session
- }
+ if (session.gameCode === gameCode) return session
}
-
return {}
}
@@ -39,7 +30,6 @@ function Game() {
const [answerMedia, setAnswerMedia] = useState(null)
const [shouldFinishAfterMedia, setShouldFinishAfterMedia] = useState(false)
- // Если нет teamName и не хост, редиректим на главную
useEffect(() => {
if (!teamName && !isHost) {
console.warn('No team name or host status, redirecting to home')
@@ -47,77 +37,54 @@ function Game() {
}
}, [teamName, isHost, navigate])
- // Подключаемся к игре (хост или команда)
useEffect(() => {
if (isHost) {
socket.emit('host-join-game', { gameCode })
} else if (teamName) {
- // Команда переподключается к игре
socket.emit('join-game', { gameCode, teamName })
}
}, [isHost, teamName, gameCode])
useEffect(() => {
- // Socket обработчики
socket.on('game-updated', (updatedGame) => {
setGame(updatedGame)
-
if (!isHost && teamName) {
const team = updatedGame.teams.find(t => t.name === teamName)
setMyTeam(team)
}
-
- // Обновляем selectedCategories когда игра обновляется
- if (updatedGame.selectedCategories) {
- setQuestions(updatedGame.selectedCategories)
- }
+ if (updatedGame.selectedCategories) setQuestions(updatedGame.selectedCategories)
})
socket.on('game-started', (game) => {
setGame(game)
-
if (!isHost && teamName) {
const team = game.teams.find(t => t.name === teamName)
setMyTeam(team)
}
-
- if (game.selectedCategories) {
- setQuestions(game.selectedCategories)
- }
+ if (game.selectedCategories) setQuestions(game.selectedCategories)
})
socket.on('host-game-loaded', (loadedGame) => {
setGame(loadedGame)
- if (loadedGame.selectedCategories) {
- setQuestions(loadedGame.selectedCategories)
- }
+ if (loadedGame.selectedCategories) setQuestions(loadedGame.selectedCategories)
})
socket.on('question-selected', ({ category, points, questionIndex, timer }) => {
- // Ищем вопрос в selectedCategories игры по индексу
const categoryData = game?.selectedCategories?.find(cat => cat.name === category)
-
- // Если есть questionIndex, используем его, иначе ищем по баллам (для обратной совместимости)
const questionData = questionIndex !== undefined
? categoryData?.questions[questionIndex]
: categoryData?.questions.find(q => q.points === points)
-
console.log(`📥 Question selected: category="${category}", points=${points}, questionIndex=${questionIndex}`, questionData);
-
setCurrentQuestion({ ...questionData, category, points })
setTimer(timer)
})
- socket.on('timer-update', ({ timer }) => {
- setTimer(timer)
- })
+ socket.on('timer-update', ({ timer }) => { setTimer(timer) })
socket.on('time-up', ({ answerMedia, shouldFinish }) => {
if (answerMedia) {
setAnswerMedia(answerMedia)
- if (shouldFinish) {
- setShouldFinishAfterMedia(true)
- }
+ if (shouldFinish) setShouldFinishAfterMedia(true)
}
setCurrentQuestion(null)
setTimer(null)
@@ -130,35 +97,27 @@ function Game() {
socket.on('answer-result', ({ teamId, isCorrect, questionClosed, answerMedia, shouldFinish }) => {
console.log('Answer result:', { teamId, isCorrect, questionClosed, answerMedia, shouldFinish })
if (questionClosed) {
- // Правильный ответ - закрываем вопрос
if (answerMedia) {
setAnswerMedia(answerMedia)
- if (shouldFinish) {
- setShouldFinishAfterMedia(true)
- }
+ if (shouldFinish) setShouldFinishAfterMedia(true)
}
setTimeout(() => {
setCurrentQuestion(null)
setTimer(null)
}, 2000)
} else {
- // Неправильный ответ - продолжаем игру
console.log('Incorrect answer, waiting for other teams')
}
})
socket.on('round-changed', ({ round }) => {
console.log(`Round ${round} started`)
- // Принудительно обновляем состояние игры после смены раунда
- // Сервер отправит game-updated сразу после, но мы сбрасываем локальное состояние
setCurrentQuestion(null)
setTimer(null)
setAnswerMedia(null)
})
- socket.on('game-finished', (finishedGame) => {
- setGame(finishedGame)
- })
+ socket.on('game-finished', (finishedGame) => { setGame(finishedGame) })
return () => {
socket.off('game-updated')
@@ -174,25 +133,14 @@ function Game() {
}
}, [teamName, questions, isHost])
- const handleStartGame = () => {
- socket.emit('start-game', { gameCode })
- }
-
+ const handleStartGame = () => { socket.emit('start-game', { gameCode }) }
const handleSelectQuestion = (category, points) => {
- // Только хост может выбирать вопросы
- if (isHost) {
- socket.emit('select-question', { gameCode, category, points })
- }
+ if (isHost) socket.emit('select-question', { gameCode, category, points })
}
-
const handleBuzzIn = () => {
- if (myTeam) {
- socket.emit('buzz-in', { gameCode, teamId: myTeam.id })
- }
+ if (myTeam) socket.emit('buzz-in', { gameCode, teamId: myTeam.id })
}
-
const handleSubmitAnswer = (isCorrect) => {
- // Теперь только ведущий отправляет ответ
console.log('handleSubmitAnswer called', { isHost, answeringTeam: game.answeringTeam, isCorrect })
if (isHost && game.answeringTeam) {
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)
}
}
-
const handleCloseQuestion = () => {
- // Ведущий может досрочно закрыть вопрос
- if (isHost) {
- socket.emit('close-question', { gameCode })
- }
+ if (isHost) socket.emit('close-question', { gameCode })
}
-
const handleFinishGame = () => {
- // Ведущий завершает игру
- if (isHost) {
- socket.emit('finish-game', { gameCode })
- }
+ if (isHost) socket.emit('finish-game', { gameCode })
}
-
const handleCloseAnswerMedia = () => {
setAnswerMedia(null)
if (shouldFinishAfterMedia && isHost) {
@@ -226,63 +165,60 @@ function Game() {
if (!game) {
return (
-
-
-
Загрузка...
+
)
}
- // Экран результатов
+ // Results screen
if (game.status === 'finished') {
const sortedTeams = [...game.teams].sort((a, b) => b.score - a.score)
const winner = sortedTeams[0]
+ const medals = ['🥇', '🥈', '🥉']
return (
-
+
-
-
ИГРА ЗАВЕРШЕНА!
-
-
🏆 ПОБЕДИТЕЛЬ 🏆
-
{winner.name}
-
{winner.score} баллов
+
+
ИГРА ЗАВЕРШЕНА!
+
+
🏆 ПОБЕДИТЕЛЬ 🏆
+
{winner.name}
+
{winner.score} баллов
-
-
Итоговая таблица
-
+
+
Итоговая таблица
+
{sortedTeams.map((team, index) => (
-
- #{index + 1}
- {team.name}
+ {medals[index] || `#${index + 1}`}
+ {team.name}
-
{team.score}
+
{team.score}
))}
-
@@ -291,75 +227,67 @@ function Game() {
}
return (
-
+
- {/* Заголовок */}
-
-
- СВОЯ ИГРА
-
-
-
- Код игры: {gameCode}
-
+ {/* Header */}
+
+
СВОЯ ИГРА
+
+
+ Код: {gameCode}
+
{isHost && (
-
- Ведущий: {hostName}
-
+
• {hostName}
)}
-
+
Раунд {game.currentRound}
-
+
- {/* Счет команд */}
+ {/* Team scores */}
- {/* Статус игры */}
+ {/* Waiting state */}
{game.status === 'waiting' && isHost && (
-
-
- Ожидание команд... ({game.teams.length} команд(ы) присоединилось)
-
- {game.teams.length > 0 && (
-
- Начать игру
-
- )}
- {game.teams.length === 0 && (
-
- Отправьте код игры командам для присоединения
+
+
+
+ Ожидание команд... ({game.teams.length})
- )}
+ {game.teams.length > 0 && (
+
+ ▶️ Начать игру
+
+ )}
+ {game.teams.length === 0 && (
+
Отправьте код игры командам
+ )}
+
)}
{game.status === 'waiting' && !isHost && (
-
- Ожидание начала игры ведущим...
-
+
+
⏳ Ожидание начала игры...
+
)}
- {/* Игровое поле */}
+ {/* Game board */}
{game.status === 'playing' && (
- <>
-
- >
+
)}
- {/* Модальное окно с вопросом */}
+ {/* Question modal */}
{currentQuestion && (
)}
- {/* Модальное окно с ответом (песня) - показываем всем */}
+ {/* Answer media modal */}
{answerMedia && (
-
-
-
+
+
+
Правильный ответ
-
-
-
-
-
+
Закрыть
@@ -406,26 +327,22 @@ function Game() {
)}
- {/* Кнопка следующего раунда */}
+ {/* Next round button */}
{isHost && game.usedQuestions.length >= 25 && game.status === 'playing' && (
-
-
socket.emit('next-round', { gameCode })}
- 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"
- >
- Следующий раунд
+
+ 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]">
+ ⏭️ Следующий раунд
)}
- {/* Кнопка завершить игру для ведущего */}
+ {/* Finish game button */}
{isHost && game.status === 'playing' && (
-
-
- Завершить игру
+
+
+ 🏁 Завершить игру
)}
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx
index afc32fe..932fecc 100644
--- a/client/src/pages/Home.jsx
+++ b/client/src/pages/Home.jsx
@@ -4,28 +4,40 @@ function Home() {
const navigate = useNavigate()
return (
-
-
-
-
+
+ {/* Ambient blobs */}
+
+
+
+
+
+
+ {/* Logo icon */}
+
+ 🧠
+
+
+
СВОЯ ИГРА
-
Интеллектуальная викторина
+
+ Интеллектуальная викторина
+
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]"
>
- Создать игру
+ 🎮 Создать игру
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]"
>
- Присоединиться
+ 🚀 Присоединиться
diff --git a/client/src/pages/JoinGame.jsx b/client/src/pages/JoinGame.jsx
index 4b3a18a..577e35f 100644
--- a/client/src/pages/JoinGame.jsx
+++ b/client/src/pages/JoinGame.jsx
@@ -9,25 +9,16 @@ function JoinGame() {
const navigate = useNavigate()
useEffect(() => {
- // Слушаем события
const handleGameUpdated = () => {
- // Сохраняем в localStorage перед навигацией
localStorage.setItem('gameSession', JSON.stringify({
- gameCode: gameCode.toUpperCase(),
- teamName,
- isHost: false
+ gameCode: gameCode.toUpperCase(), teamName, isHost: false
}))
-
navigate(`/game/${gameCode.toUpperCase()}`, { state: { teamName } })
}
-
- const handleError = ({ message }) => {
- setError(message)
- }
+ const handleError = ({ message }) => { setError(message) }
socket.on('game-updated', handleGameUpdated)
socket.on('error', handleError)
-
return () => {
socket.off('game-updated', handleGameUpdated)
socket.off('error', handleError)
@@ -41,57 +32,44 @@ function JoinGame() {
}
return (
-
-
- Присоединиться к игре
-
+
+
-
)
}
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
index 47d9e4c..5ea7ec5 100644
--- a/client/tailwind.config.js
+++ b/client/tailwind.config.js
@@ -9,43 +9,54 @@ export default {
colors: {
'game-blue': '#0A1E3D',
'game-gold': '#FFD700',
+ 'game-purple': '#1a0533',
+ 'game-deep': '#0d1b2a',
},
animation: {
'blob': 'blob 7s infinite',
'float': 'float 3s ease-in-out infinite',
'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: {
blob: {
- '0%': {
- transform: 'translate(0px, 0px) scale(1)',
- },
- '33%': {
- transform: 'translate(30px, -50px) scale(1.1)',
- },
- '66%': {
- transform: 'translate(-20px, 20px) scale(0.9)',
- },
- '100%': {
- transform: 'translate(0px, 0px) scale(1)',
- },
+ '0%': { transform: 'translate(0px, 0px) scale(1)' },
+ '33%': { transform: 'translate(30px, -50px) scale(1.1)' },
+ '66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
+ '100%': { transform: 'translate(0px, 0px) scale(1)' },
},
float: {
- '0%, 100%': {
- transform: 'translateY(0px)',
- },
- '50%': {
- transform: 'translateY(-20px)',
- },
+ '0%, 100%': { transform: 'translateY(0px)' },
+ '50%': { transform: 'translateY(-20px)' },
},
glow: {
- 'from': {
- 'box-shadow': '0 0 20px rgba(255, 215, 0, 0.5)',
- },
- 'to': {
- 'box-shadow': '0 0 30px rgba(255, 215, 0, 0.8)',
- },
+ 'from': { 'box-shadow': '0 0 20px rgba(255, 215, 0, 0.5)' },
+ '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))',
},
},
},