ui fixes
This commit is contained in:
parent
6a94a76c19
commit
ae52913209
7 changed files with 393 additions and 23 deletions
39
src/App.css
39
src/App.css
|
|
@ -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 {
|
||||
|
|
|
|||
70
src/App.jsx
70
src/App.jsx
|
|
@ -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,8 +107,17 @@ function App() {
|
|||
</h1>
|
||||
|
||||
{questions.length > 0 && currentQuestion && (
|
||||
<div className="question-counter">
|
||||
{currentQuestionIndex + 1}/{questions.length}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<h2 className="question-text">{question.text}</h2>
|
||||
<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
30
src/utils/cookies.js
Normal 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=/;`
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue