This commit is contained in:
Dmitry 2025-12-31 20:05:48 +03:00
parent 08ac2edb96
commit 3b56ee6658
3 changed files with 302 additions and 11 deletions

View file

@ -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)

View file

@ -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;
}
} }

View file

@ -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>
) )
} }