From ae52913209cd19b7d6c0d730cf63bc08768438d7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 31 Dec 2025 20:57:11 +0300 Subject: [PATCH] ui fixes --- src/App.css | 39 ++++++++++- src/App.jsx | 70 +++++++++++++++++-- src/components/Answer.css | 29 ++++++++ src/components/Game.jsx | 136 ++++++++++++++++++++++++++++++++---- src/components/Question.css | 75 ++++++++++++++++++++ src/components/Question.jsx | 37 +++++++++- src/utils/cookies.js | 30 ++++++++ 7 files changed, 393 insertions(+), 23 deletions(-) create mode 100644 src/utils/cookies.js diff --git a/src/App.css b/src/App.css index 9f065df..213dd55 100644 --- a/src/App.css +++ b/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 { diff --git a/src/App.jsx b/src/App.jsx index b471838..13a7013 100644 --- a/src/App.jsx +++ b/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 (
@@ -47,6 +91,13 @@ function App() { > ❓ +

@@ -56,8 +107,17 @@ function App() {

{questions.length > 0 && currentQuestion && ( -
- {currentQuestionIndex + 1}/{questions.length} +
+
+ {currentQuestionIndex + 1}/{questions.length} +
+
)}
diff --git a/src/components/Answer.css b/src/components/Answer.css index 87a8d4e..656d4bf 100644 --- a/src/components/Answer.css +++ b/src/components/Answer.css @@ -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; + } +} + diff --git a/src/components/Game.jsx b/src/components/Game.jsx index a12b081..781d4d4 100644 --- a/src/components/Game.jsx +++ b/src/components/Game.jsx @@ -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} /> ) : (
diff --git a/src/components/Question.css b/src/components/Question.css index 5b58a95..94f5b14 100644 --- a/src/components/Question.css +++ b/src/components/Question.css @@ -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 { diff --git a/src/components/Question.jsx b/src/components/Question.jsx index cad5675..add548c 100644 --- a/src/components/Question.jsx +++ b/src/components/Question.jsx @@ -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 (
-

{question.text}

+
+ {canGoPrevious && onPreviousQuestion && ( + + )} +

{question.text}

+ {canGoNext && onNextQuestion && ( + + )} +
{question.answers.map((answer, index) => ( diff --git a/src/utils/cookies.js b/src/utils/cookies.js new file mode 100644 index 0000000..cbefda1 --- /dev/null +++ b/src/utils/cookies.js @@ -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=/;` +} +