This commit is contained in:
Dmitry 2025-12-31 20:57:11 +03:00
parent 6a94a76c19
commit ae52913209
7 changed files with 393 additions and 23 deletions

View file

@ -74,6 +74,11 @@
border-color: #667eea;
}
.control-button-new-game:hover {
background: rgba(255, 107, 107, 0.3);
border-color: #ff6b6b;
}
.app-title {
text-align: center;
font-size: clamp(2rem, 5vw, 4rem);
@ -113,6 +118,13 @@
font-size: clamp(1.5rem, 3.5vw, 3rem);
}
.question-counter-wrapper {
display: flex;
align-items: center;
gap: clamp(8px, 1.5vw, 12px);
flex-shrink: 0;
}
.question-counter {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
@ -124,7 +136,32 @@
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
flex-shrink: 0;
}
.show-all-button-top {
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
border: 2px solid rgba(255, 107, 107, 0.5);
border-radius: 15px;
padding: clamp(5px, 1vh, 8px) clamp(12px, 2vw, 18px);
font-size: clamp(0.8rem, 1.5vw, 1rem);
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
.show-all-button-top:hover {
background: rgba(255, 107, 107, 0.4);
border-color: #ff6b6b;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.show-all-button-top:active {
transform: translateY(0);
}
.app-subtitle {

View file

@ -1,18 +1,37 @@
import { useState, useRef } from 'react'
import { useState, useRef, useEffect } from 'react'
import Game from './components/Game'
import Snowflakes from './components/Snowflakes'
import QuestionsModal from './components/QuestionsModal'
import { questions as initialQuestions } from './data/questions'
import { getCookie, setCookie, deleteCookie } from './utils/cookies'
import './App.css'
function App() {
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false)
const [questions, setQuestions] = useState(initialQuestions)
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
const [questions, setQuestions] = useState(() => {
const savedQuestions = getCookie('gameQuestions')
return savedQuestions || initialQuestions
})
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(() => {
const savedIndex = getCookie('gameQuestionIndex')
return savedIndex !== null ? savedIndex : 0
})
const gameRef = useRef(null)
const currentQuestion = questions[currentQuestionIndex]
// Сохраняем вопросы в cookies при изменении
useEffect(() => {
if (questions.length > 0) {
setCookie('gameQuestions', questions)
}
}, [questions])
// Сохраняем индекс вопроса в cookies при изменении
useEffect(() => {
setCookie('gameQuestionIndex', currentQuestionIndex)
}, [currentQuestionIndex])
const handleUpdateQuestions = (updatedQuestions) => {
setQuestions(updatedQuestions)
// Если текущий вопрос был удален, сбрасываем индекс
@ -27,6 +46,31 @@ function App() {
}
}
const handleNewGame = () => {
if (window.confirm('Начать новую игру? Текущий прогресс будет потерян.')) {
deleteCookie('gameQuestions')
deleteCookie('gameQuestionIndex')
deleteCookie('gamePlayers')
deleteCookie('gamePlayerScores')
deleteCookie('gameCurrentPlayerId')
deleteCookie('gameRevealedAnswers')
deleteCookie('gameOver')
setQuestions(initialQuestions)
setCurrentQuestionIndex(0)
if (gameRef.current) {
gameRef.current.newGame()
}
}
}
const handleShowAll = () => {
if (gameRef.current && gameRef.current.showAllAnswers) {
gameRef.current.showAllAnswers()
}
}
return (
<div className="app">
<Snowflakes />
@ -47,6 +91,13 @@ function App() {
>
</button>
<button
className="control-button control-button-new-game"
onClick={handleNewGame}
title="Новая игра"
>
🎮
</button>
</div>
<h1 className="app-title">
@ -56,9 +107,18 @@ function App() {
</h1>
{questions.length > 0 && currentQuestion && (
<div className="question-counter-wrapper">
<div className="question-counter">
{currentQuestionIndex + 1}/{questions.length}
</div>
<button
className="show-all-button-top"
onClick={handleShowAll}
title="Показать все ответы"
>
Показать все
</button>
</div>
)}
</div>

View file

@ -16,6 +16,16 @@
overflow: hidden;
}
/* Горизонтальный layout для узких кнопок */
@media (max-width: 1000px) {
.answer-button {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: clamp(8px, 1.5vw, 15px);
}
}
.answer-button:hover:not(:disabled) {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.4);
@ -99,6 +109,18 @@
-webkit-box-orient: vertical;
flex-shrink: 1;
min-height: 0;
flex: 1;
}
@media (max-width: 1000px) {
.answer-text {
margin-bottom: 0;
margin-right: clamp(8px, 1.5vw, 15px);
text-align: left;
-webkit-line-clamp: 2;
flex: 1;
min-width: 0;
}
}
.answer-points {
@ -108,4 +130,11 @@
flex-shrink: 0;
}
@media (max-width: 1000px) {
.answer-points {
font-size: clamp(1.5rem, 3vw, 2.5rem);
white-space: nowrap;
}
}

View file

@ -1,8 +1,9 @@
import { useState, useImperativeHandle, forwardRef } from 'react'
import { useState, useImperativeHandle, forwardRef, useEffect } from 'react'
import Question from './Question'
import Players from './Players'
import PlayersModal from './PlayersModal'
import QuestionsModal from './QuestionsModal'
import { getCookie, setCookie, deleteCookie } from '../utils/cookies'
import './Game.css'
const Game = forwardRef(({
@ -11,22 +12,97 @@ const Game = forwardRef(({
onQuestionIndexChange,
onQuestionsChange,
}, ref) => {
const [players, setPlayers] = useState([])
const [currentPlayerId, setCurrentPlayerId] = useState(null)
const [playerScores, setPlayerScores] = useState({})
const [gameOver, setGameOver] = useState(false)
const [revealedAnswers, setRevealedAnswers] = useState([])
const [players, setPlayers] = useState(() => {
const savedPlayers = getCookie('gamePlayers')
return savedPlayers || []
})
const [currentPlayerId, setCurrentPlayerId] = useState(() => {
const savedId = getCookie('gameCurrentPlayerId')
return savedId !== null ? savedId : null
})
const [playerScores, setPlayerScores] = useState(() => {
const savedScores = getCookie('gamePlayerScores')
return savedScores || {}
})
const [gameOver, setGameOver] = useState(() => {
const savedGameOver = getCookie('gameOver')
return savedGameOver === true
})
const [revealedAnswers, setRevealedAnswers] = useState(() => {
const savedAnswers = getCookie('gameRevealedAnswers')
return savedAnswers || {}
})
// Получаем открытые ответы для текущего вопроса
const getCurrentRevealedAnswers = () => {
return revealedAnswers[currentQuestionIndex] || []
}
// Обновляем открытые ответы для текущего вопроса
const updateRevealedAnswers = (newAnswers) => {
setRevealedAnswers({
...revealedAnswers,
[currentQuestionIndex]: newAnswers,
})
}
const [isPlayersModalOpen, setIsPlayersModalOpen] = useState(false)
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false)
// Сохраняем состояние в cookies при изменении
useEffect(() => {
if (players.length > 0) {
setCookie('gamePlayers', players)
} else {
deleteCookie('gamePlayers')
}
}, [players])
useEffect(() => {
if (currentPlayerId !== null) {
setCookie('gameCurrentPlayerId', currentPlayerId)
} else {
deleteCookie('gameCurrentPlayerId')
}
}, [currentPlayerId])
useEffect(() => {
if (Object.keys(playerScores).length > 0) {
setCookie('gamePlayerScores', playerScores)
} else {
deleteCookie('gamePlayerScores')
}
}, [playerScores])
useEffect(() => {
setCookie('gameRevealedAnswers', revealedAnswers)
}, [revealedAnswers])
useEffect(() => {
setCookie('gameOver', gameOver)
}, [gameOver])
const currentQuestion = questions[currentQuestionIndex]
const isLastQuestion = currentQuestionIndex === questions.length - 1
const handleShowAllAnswers = () => {
if (!currentQuestion) return
const allAnswerIndices = currentQuestion.answers.map((_, index) => index)
updateRevealedAnswers(allAnswerIndices)
}
useImperativeHandle(ref, () => ({
openPlayersModal: () => setIsPlayersModalOpen(true),
openQuestionsModal: () => setIsQuestionsModalOpen(true),
newGame: () => {
setPlayers([])
setCurrentPlayerId(null)
setPlayerScores({})
setGameOver(false)
setRevealedAnswers({})
},
showAllAnswers: handleShowAllAnswers,
}))
const currentQuestion = questions[currentQuestionIndex]
const isLastQuestion = currentQuestionIndex === questions.length - 1
const handleAddPlayer = (name) => {
const newPlayer = {
id: Date.now(),
@ -76,13 +152,14 @@ const Game = forwardRef(({
}
const handleAnswerClick = (answerIndex, points) => {
if (revealedAnswers.includes(answerIndex)) return
const currentRevealed = getCurrentRevealedAnswers()
if (currentRevealed.includes(answerIndex)) return
if (!currentPlayerId) return
if (!currentQuestion) return
const isLastAnswer = revealedAnswers.length === currentQuestion.answers.length - 1
const isLastAnswer = currentRevealed.length === currentQuestion.answers.length - 1
setRevealedAnswers([...revealedAnswers, answerIndex])
updateRevealedAnswers([...currentRevealed, answerIndex])
// Добавляем очки текущему участнику
setPlayerScores({
@ -121,7 +198,7 @@ const Game = forwardRef(({
if (onQuestionIndexChange) {
onQuestionIndexChange(currentQuestionIndex + 1)
}
setRevealedAnswers([])
// Не сбрасываем открытые ответы - они сохраняются для каждого вопроса отдельно
}
const restartGame = () => {
@ -129,7 +206,7 @@ const Game = forwardRef(({
onQuestionIndexChange(0)
}
setGameOver(false)
setRevealedAnswers([])
setRevealedAnswers({})
const initialScores = {}
players.forEach(player => {
initialScores[player.id] = 0
@ -140,6 +217,31 @@ const Game = forwardRef(({
}
}
const newGame = () => {
setPlayers([])
setCurrentPlayerId(null)
setPlayerScores({})
setGameOver(false)
setRevealedAnswers({})
if (onQuestionIndexChange) {
onQuestionIndexChange(0)
}
}
const handlePreviousQuestion = () => {
if (currentQuestionIndex > 0 && onQuestionIndexChange) {
onQuestionIndexChange(currentQuestionIndex - 1)
// Открытые ответы сохраняются для каждого вопроса отдельно
}
}
const handleNextQuestion = () => {
if (currentQuestionIndex < questions.length - 1 && onQuestionIndexChange) {
onQuestionIndexChange(currentQuestionIndex + 1)
// Открытые ответы сохраняются для каждого вопроса отдельно
}
}
if (gameOver) {
// Находим победителя(ей)
const scores = Object.values(playerScores)
@ -215,7 +317,11 @@ const Game = forwardRef(({
question={currentQuestion}
questionNumber={currentQuestionIndex + 1}
onAnswerClick={handleAnswerClick}
revealedAnswers={revealedAnswers}
revealedAnswers={getCurrentRevealedAnswers()}
onPreviousQuestion={handlePreviousQuestion}
onNextQuestion={handleNextQuestion}
canGoPrevious={currentQuestionIndex > 0}
canGoNext={currentQuestionIndex < questions.length - 1}
/>
) : (
<div className="no-players-message">

View file

@ -19,6 +19,54 @@
flex-shrink: 0;
}
.question-navigation {
display: flex;
align-items: center;
justify-content: center;
gap: clamp(10px, 2vw, 20px);
margin-bottom: clamp(10px, 2vh, 15px);
}
.question-nav-button {
width: clamp(40px, 6vw, 50px);
height: clamp(40px, 6vw, 50px);
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
color: #fff;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.question-nav-button:hover {
transform: translateY(-2px) scale(1.1);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 215, 0, 0.6);
background: rgba(255, 255, 255, 0.2);
}
.question-nav-button:active {
transform: translateY(0) scale(1);
}
.question-nav-button-prev:hover {
background: rgba(78, 205, 196, 0.3);
border-color: #4ecdc4;
}
.question-nav-button-next:hover {
background: rgba(102, 126, 234, 0.3);
border-color: #667eea;
}
.question-number {
display: inline-block;
background: rgba(255, 215, 0, 0.2);
@ -39,6 +87,33 @@
font-weight: bold;
line-height: 1.3;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
margin: 0;
flex: 1;
}
.show-all-button {
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
border: 2px solid rgba(255, 107, 107, 0.5);
border-radius: 15px;
padding: clamp(8px, 1.5vh, 12px) clamp(20px, 3vw, 30px);
font-size: clamp(0.9rem, 1.8vw, 1.2rem);
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.show-all-button:hover {
background: rgba(255, 107, 107, 0.4);
border-color: #ff6b6b;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.show-all-button:active {
transform: translateY(0);
}
.answers-grid {

View file

@ -1,11 +1,44 @@
import Answer from './Answer'
import './Question.css'
const Question = ({ question, questionNumber, onAnswerClick, revealedAnswers }) => {
const Question = ({
question,
questionNumber,
onAnswerClick,
revealedAnswers,
onShowAll,
onPreviousQuestion,
onNextQuestion,
canGoPrevious,
canGoNext,
}) => {
const allAnswersRevealed = question.answers.every((_, index) => revealedAnswers.includes(index))
const hasUnrevealedAnswers = revealedAnswers.length < question.answers.length
return (
<div className="question-container">
<div className="question-box">
<div className="question-navigation">
{canGoPrevious && onPreviousQuestion && (
<button
className="question-nav-button question-nav-button-prev"
onClick={onPreviousQuestion}
title="Предыдущий вопрос"
>
</button>
)}
<h2 className="question-text">{question.text}</h2>
{canGoNext && onNextQuestion && (
<button
className="question-nav-button question-nav-button-next"
onClick={onNextQuestion}
title="Следующий вопрос"
>
</button>
)}
</div>
</div>
<div className="answers-grid">
{question.answers.map((answer, index) => (

30
src/utils/cookies.js Normal file
View file

@ -0,0 +1,30 @@
// Утилиты для работы с cookies
export const setCookie = (name, value, days = 365) => {
const date = new Date()
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
const expires = `expires=${date.toUTCString()}`
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};${expires};path=/`
}
export const getCookie = (name) => {
const nameEQ = `${name}=`
const ca = document.cookie.split(';')
for (let i = 0; i < ca.length; i++) {
let c = ca[i]
while (c.charAt(0) === ' ') c = c.substring(1, c.length)
if (c.indexOf(nameEQ) === 0) {
try {
return JSON.parse(decodeURIComponent(c.substring(nameEQ.length, c.length)))
} catch (e) {
return null
}
}
}
return null
}
export const deleteCookie = (name) => {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`
}