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
|
||||
- Vite
|
||||
- CSS3 с анимациями
|
||||
- Docker + Nginx (для production)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
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(() => {
|
||||
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 (
|
||||
<div className="game-over">
|
||||
<div className="game-over-content">
|
||||
<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>
|
||||
|
|
@ -57,12 +164,51 @@ const Game = () => {
|
|||
|
||||
return (
|
||||
<div className="game">
|
||||
<Question
|
||||
question={currentQuestion}
|
||||
questionNumber={currentQuestionIndex + 1}
|
||||
onAnswerClick={handleAnswerClick}
|
||||
revealedAnswers={revealedAnswers}
|
||||
<PlayersModal
|
||||
isOpen={isPlayersModalOpen}
|
||||
onClose={() => setIsPlayersModalOpen(false)}
|
||||
players={players}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue