diff --git a/README.md b/README.md index 16e1c69..be3eb96 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,32 @@ npm run dev - Увеличенные элементы интерфейса - Чёткая видимость с расстояния +## Развёртывание через Coolify + +Проект настроен для развёртывания через Coolify: + +1. Подключите репозиторий в Coolify +2. Coolify автоматически определит Dockerfile +3. Проект будет собран и развёрнут автоматически + +### Локальная сборка для проверки + +Для проверки Docker образа локально: + +```bash +# Сборка образа +docker build -t sto-k-odnomu . + +# Запуск контейнера +docker run -p 8080:80 sto-k-odnomu +``` + +Сайт будет доступен по адресу `http://localhost:8080` + ## Технологии - React 18 - Vite - CSS3 с анимациями +- Docker + Nginx (для production) diff --git a/src/components/Game.css b/src/components/Game.css index 8a48533..421804e 100644 --- a/src/components/Game.css +++ b/src/components/Game.css @@ -4,6 +4,37 @@ margin: 0 auto; } +.game-header { + margin-bottom: 30px; + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; +} + +.manage-players-button { + background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); + color: white; + border: none; + padding: 12px 30px; + font-size: 1rem; + font-weight: bold; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(78, 205, 196, 0.3); + white-space: nowrap; +} + +.manage-players-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(78, 205, 196, 0.5); +} + +.manage-players-button:active { + transform: translateY(0); +} + .game-over { display: flex; justify-content: center; @@ -58,6 +89,71 @@ transform: translateY(0); } +.no-players-message { + text-align: center; + padding: 60px 20px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + border: 2px solid rgba(255, 215, 0, 0.3); +} + +.no-players-message p { + color: #fff; + font-size: 1.5rem; + margin: 0; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); +} + +.final-scores { + width: 100%; + margin: 30px 0; +} + +.final-scores-title { + color: #fff; + font-size: 2rem; + margin-bottom: 20px; + text-align: center; +} + +.final-score-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 25px; + margin: 10px 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + border: 2px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.final-score-item.final-score-winner { + background: rgba(255, 215, 0, 0.2); + border-color: #ffd700; + box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); +} + +.final-score-name { + color: #fff; + font-size: 1.5rem; + font-weight: 500; +} + +.final-score-value { + color: #ffd700; + font-size: 1.5rem; + font-weight: bold; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); +} + +.final-score-winner .final-score-name, +.final-score-winner .final-score-value { + color: #ffd700; + text-shadow: 0 0 15px rgba(255, 215, 0, 0.8); +} + @media (max-width: 768px) { .game-over-title { font-size: 2rem; @@ -71,5 +167,31 @@ font-size: 1.1rem; padding: 12px 30px; } + + .no-players-message p { + font-size: 1.2rem; + } + + .final-scores-title { + font-size: 1.5rem; + } + + .final-score-item { + padding: 12px 20px; + } + + .final-score-name, + .final-score-value { + font-size: 1.2rem; + } + + .manage-players-button { + font-size: 0.9rem; + padding: 10px 20px; + } + + .game-header { + margin-bottom: 20px; + } } diff --git a/src/components/Game.jsx b/src/components/Game.jsx index 8448193..4de5bba 100644 --- a/src/components/Game.jsx +++ b/src/components/Game.jsx @@ -1,29 +1,107 @@ import { useState } from 'react' import Question from './Question' +import Players from './Players' +import PlayersModal from './PlayersModal' +import Score from './Score' import { questions } from '../data/questions' import './Game.css' const Game = () => { + const [players, setPlayers] = useState([]) + const [currentPlayerId, setCurrentPlayerId] = useState(null) + const [playerScores, setPlayerScores] = useState({}) const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) - const [score, setScore] = useState(0) const [gameOver, setGameOver] = useState(false) const [revealedAnswers, setRevealedAnswers] = useState([]) + const [isPlayersModalOpen, setIsPlayersModalOpen] = useState(false) const currentQuestion = questions[currentQuestionIndex] const isLastQuestion = currentQuestionIndex === questions.length - 1 + const handleAddPlayer = (name) => { + const newPlayer = { + id: Date.now(), + name: name, + } + const updatedPlayers = [...players, newPlayer] + setPlayers(updatedPlayers) + + // Если это первый участник, делаем его текущим + if (updatedPlayers.length === 1) { + setCurrentPlayerId(newPlayer.id) + setPlayerScores({ [newPlayer.id]: 0 }) + } else { + setPlayerScores({ ...playerScores, [newPlayer.id]: 0 }) + } + } + + const handleSelectPlayer = (playerId) => { + setCurrentPlayerId(playerId) + } + + const handleRemovePlayer = (playerId) => { + const updatedPlayers = players.filter(p => p.id !== playerId) + setPlayers(updatedPlayers) + + const updatedScores = { ...playerScores } + delete updatedScores[playerId] + setPlayerScores(updatedScores) + + // Если удалили текущего участника, выбираем другого + if (currentPlayerId === playerId) { + if (updatedPlayers.length > 0) { + setCurrentPlayerId(updatedPlayers[0].id) + } else { + setCurrentPlayerId(null) + } + } + } + + const getNextPlayerId = () => { + if (players.length === 0) return null + if (players.length === 1) return currentPlayerId + + const currentIndex = players.findIndex(p => p.id === currentPlayerId) + const nextIndex = (currentIndex + 1) % players.length + return players[nextIndex].id + } + const handleAnswerClick = (answerIndex, points) => { if (revealedAnswers.includes(answerIndex)) return + if (!currentPlayerId) return + const isLastAnswer = revealedAnswers.length === currentQuestion.answers.length - 1 + setRevealedAnswers([...revealedAnswers, answerIndex]) - setScore(score + points) + + // Добавляем очки текущему участнику + setPlayerScores({ + ...playerScores, + [currentPlayerId]: (playerScores[currentPlayerId] || 0) + points, + }) - if (revealedAnswers.length === currentQuestion.answers.length - 1) { + // Переходим к следующему участнику только если это не последний ответ + if (!isLastAnswer) { + const nextPlayerId = getNextPlayerId() + if (nextPlayerId) { + setTimeout(() => { + setCurrentPlayerId(nextPlayerId) + }, 500) + } + } else { + // Если это последний ответ, переходим к следующему участнику перед следующим вопросом setTimeout(() => { + const nextPlayerId = getNextPlayerId() + if (nextPlayerId) { + setCurrentPlayerId(nextPlayerId) + } + if (isLastQuestion) { setGameOver(true) } else { - nextQuestion() + setTimeout(() => { + nextQuestion() + }, 500) } }, 2000) } @@ -36,17 +114,46 @@ const Game = () => { const restartGame = () => { setCurrentQuestionIndex(0) - setScore(0) setGameOver(false) setRevealedAnswers([]) + const initialScores = {} + players.forEach(player => { + initialScores[player.id] = 0 + }) + setPlayerScores(initialScores) + if (players.length > 0) { + setCurrentPlayerId(players[0].id) + } } if (gameOver) { + // Находим победителя(ей) + const scores = Object.values(playerScores) + const maxScore = scores.length > 0 ? Math.max(...scores) : 0 + const winners = players.filter(p => playerScores[p.id] === maxScore) + return (
Ваш итоговый счёт: {score}
+Добавьте участников, чтобы начать игру
+