players
This commit is contained in:
parent
08ac2edb96
commit
3b56ee6658
3 changed files with 302 additions and 11 deletions
23
README.md
23
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
|
- React 18
|
||||||
- Vite
|
- Vite
|
||||||
- CSS3 с анимациями
|
- CSS3 с анимациями
|
||||||
|
- Docker + Nginx (для production)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,37 @@
|
||||||
margin: 0 auto;
|
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 {
|
.game-over {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -58,6 +89,71 @@
|
||||||
transform: translateY(0);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.game-over-title {
|
.game-over-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|
@ -71,5 +167,31 @@
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
padding: 12px 30px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,107 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Question from './Question'
|
import Question from './Question'
|
||||||
|
import Players from './Players'
|
||||||
|
import PlayersModal from './PlayersModal'
|
||||||
|
import Score from './Score'
|
||||||
import { questions } from '../data/questions'
|
import { questions } from '../data/questions'
|
||||||
import './Game.css'
|
import './Game.css'
|
||||||
|
|
||||||
const Game = () => {
|
const Game = () => {
|
||||||
|
const [players, setPlayers] = useState([])
|
||||||
|
const [currentPlayerId, setCurrentPlayerId] = useState(null)
|
||||||
|
const [playerScores, setPlayerScores] = useState({})
|
||||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
|
||||||
const [score, setScore] = useState(0)
|
|
||||||
const [gameOver, setGameOver] = useState(false)
|
const [gameOver, setGameOver] = useState(false)
|
||||||
const [revealedAnswers, setRevealedAnswers] = useState([])
|
const [revealedAnswers, setRevealedAnswers] = useState([])
|
||||||
|
const [isPlayersModalOpen, setIsPlayersModalOpen] = useState(false)
|
||||||
|
|
||||||
const currentQuestion = questions[currentQuestionIndex]
|
const currentQuestion = questions[currentQuestionIndex]
|
||||||
const isLastQuestion = currentQuestionIndex === questions.length - 1
|
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) => {
|
const handleAnswerClick = (answerIndex, points) => {
|
||||||
if (revealedAnswers.includes(answerIndex)) return
|
if (revealedAnswers.includes(answerIndex)) return
|
||||||
|
if (!currentPlayerId) return
|
||||||
|
|
||||||
|
const isLastAnswer = revealedAnswers.length === currentQuestion.answers.length - 1
|
||||||
|
|
||||||
setRevealedAnswers([...revealedAnswers, answerIndex])
|
setRevealedAnswers([...revealedAnswers, answerIndex])
|
||||||
setScore(score + points)
|
|
||||||
|
|
||||||
if (revealedAnswers.length === currentQuestion.answers.length - 1) {
|
// Добавляем очки текущему участнику
|
||||||
|
setPlayerScores({
|
||||||
|
...playerScores,
|
||||||
|
[currentPlayerId]: (playerScores[currentPlayerId] || 0) + points,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Переходим к следующему участнику только если это не последний ответ
|
||||||
|
if (!isLastAnswer) {
|
||||||
|
const nextPlayerId = getNextPlayerId()
|
||||||
|
if (nextPlayerId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentPlayerId(nextPlayerId)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если это последний ответ, переходим к следующему участнику перед следующим вопросом
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const nextPlayerId = getNextPlayerId()
|
||||||
|
if (nextPlayerId) {
|
||||||
|
setCurrentPlayerId(nextPlayerId)
|
||||||
|
}
|
||||||
|
|
||||||
if (isLastQuestion) {
|
if (isLastQuestion) {
|
||||||
setGameOver(true)
|
setGameOver(true)
|
||||||
} else {
|
} else {
|
||||||
nextQuestion()
|
setTimeout(() => {
|
||||||
|
nextQuestion()
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
@ -36,17 +114,46 @@ const Game = () => {
|
||||||
|
|
||||||
const restartGame = () => {
|
const restartGame = () => {
|
||||||
setCurrentQuestionIndex(0)
|
setCurrentQuestionIndex(0)
|
||||||
setScore(0)
|
|
||||||
setGameOver(false)
|
setGameOver(false)
|
||||||
setRevealedAnswers([])
|
setRevealedAnswers([])
|
||||||
|
const initialScores = {}
|
||||||
|
players.forEach(player => {
|
||||||
|
initialScores[player.id] = 0
|
||||||
|
})
|
||||||
|
setPlayerScores(initialScores)
|
||||||
|
if (players.length > 0) {
|
||||||
|
setCurrentPlayerId(players[0].id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameOver) {
|
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 (
|
return (
|
||||||
<div className="game-over">
|
<div className="game-over">
|
||||||
<div className="game-over-content">
|
<div className="game-over-content">
|
||||||
<h2 className="game-over-title">🎉 Игра окончена! 🎉</h2>
|
<h2 className="game-over-title">🎉 Игра окончена! 🎉</h2>
|
||||||
<p className="game-over-score">Ваш итоговый счёт: {score}</p>
|
<div className="final-scores">
|
||||||
|
<h3 className="final-scores-title">Итоговые результаты:</h3>
|
||||||
|
{players
|
||||||
|
.sort((a, b) => (playerScores[b.id] || 0) - (playerScores[a.id] || 0))
|
||||||
|
.map((player) => (
|
||||||
|
<div
|
||||||
|
key={player.id}
|
||||||
|
className={`final-score-item ${
|
||||||
|
winners.includes(player) ? 'final-score-winner' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="final-score-name">{player.name}</span>
|
||||||
|
<span className="final-score-value">
|
||||||
|
{playerScores[player.id] || 0} очков
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<button className="restart-button" onClick={restartGame}>
|
<button className="restart-button" onClick={restartGame}>
|
||||||
Играть снова
|
Играть снова
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -57,12 +164,51 @@ const Game = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game">
|
<div className="game">
|
||||||
<Question
|
<PlayersModal
|
||||||
question={currentQuestion}
|
isOpen={isPlayersModalOpen}
|
||||||
questionNumber={currentQuestionIndex + 1}
|
onClose={() => setIsPlayersModalOpen(false)}
|
||||||
onAnswerClick={handleAnswerClick}
|
players={players}
|
||||||
revealedAnswers={revealedAnswers}
|
onAddPlayer={handleAddPlayer}
|
||||||
|
onRemovePlayer={handleRemovePlayer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="game-header">
|
||||||
|
<button
|
||||||
|
className="manage-players-button"
|
||||||
|
onClick={() => setIsPlayersModalOpen(true)}
|
||||||
|
>
|
||||||
|
{players.length === 0 ? 'Добавить участников' : 'Управление участниками'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{players.length > 0 && (
|
||||||
|
<Players
|
||||||
|
players={players}
|
||||||
|
currentPlayerId={currentPlayerId}
|
||||||
|
playerScores={playerScores}
|
||||||
|
onSelectPlayer={handleSelectPlayer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{players.length > 0 && currentPlayerId ? (
|
||||||
|
<>
|
||||||
|
<Score
|
||||||
|
score={playerScores[currentPlayerId] || 0}
|
||||||
|
questionNumber={currentQuestionIndex + 1}
|
||||||
|
totalQuestions={questions.length}
|
||||||
|
/>
|
||||||
|
<Question
|
||||||
|
question={currentQuestion}
|
||||||
|
questionNumber={currentQuestionIndex + 1}
|
||||||
|
onAnswerClick={handleAnswerClick}
|
||||||
|
revealedAnswers={revealedAnswers}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="no-players-message">
|
||||||
|
<p>Добавьте участников, чтобы начать игру</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue