diff --git a/.prettierrc.js b/.prettierrc.js index 65e18f5ff..a037d25c9 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,4 +7,5 @@ module.exports = { bracketSpacing: true, arrowParens: "avoid", htmlWhitespaceSensitivity: "ignore", + endOfLine: "auto", // для своместной разработки и с linux и с windows }; diff --git a/README.md b/README.md index 9b90842c4..a9d1bbae4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +## Оценка времени выполнения работы "Курсовая работа 4" + +- **Ожидаемое время:** 12 часов +- **Фактическое время:** 10 часов + # MVP Карточная игра "Мемо" В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md) diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..f73acf059 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,157 +1,180 @@ import { shuffle } from "lodash"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useContext } from "react"; +import { useNavigate } from "react-router-dom"; import { generateDeck } from "../../utils/cards"; import styles from "./Cards.module.css"; import { EndGameModal } from "../../components/EndGameModal/EndGameModal"; import { Button } from "../../components/Button/Button"; import { Card } from "../../components/Card/Card"; +import { GameModeContext } from "../../context/GameModeContext"; +import { ReactComponent as PowerIcon } from "../../components/EndGameModal/images/power.svg"; -// Игра закончилась +// Константы статусов игры const STATUS_LOST = "STATUS_LOST"; const STATUS_WON = "STATUS_WON"; -// Идет игра: карты закрыты, игрок может их открыть const STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS"; -// Начало игры: игрок видит все карты в течении нескольких секунд const STATUS_PREVIEW = "STATUS_PREVIEW"; -function getTimerValue(startDate, endDate) { - if (!startDate && !endDate) { - return { - minutes: 0, - seconds: 0, - }; - } - - if (endDate === null) { - endDate = new Date(); - } - - const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000); - const minutes = Math.floor(diffInSecconds / 60); - const seconds = diffInSecconds % 60; +function getTimerValue(secondsElapsed) { + const minutes = Math.floor(secondsElapsed / 60); + const seconds = secondsElapsed % 60; return { minutes, seconds, }; } -/** - * Основной компонент игры, внутри него находится вся игровая механика и логика. - * pairsCount - сколько пар будет в игре - * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры - */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { - // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта + const { livesMode } = useContext(GameModeContext); + + // Состояние для игровых карт const [cards, setCards] = useState([]); - // Текущий статус игры + const [initialCardsState, setInitialCardsState] = useState([]); const [status, setStatus] = useState(STATUS_PREVIEW); + const [secondsElapsed, setSecondsElapsed] = useState(0); + const [timer, setTimer] = useState({ minutes: 0, seconds: 0 }); + const [intervalId, setIntervalId] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + const [isSuperPowerUsed, setIsSuperPowerUsed] = useState(false); - // Дата начала игры - const [gameStartDate, setGameStartDate] = useState(null); - // Дата конца игры - const [gameEndDate, setGameEndDate] = useState(null); + const initialLives = livesMode ? 3 : 1; + const [lives, setLives] = useState(initialLives); + const [selectedCards, setSelectedCards] = useState([]); - // Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate - const [timer, setTimer] = useState({ - seconds: 0, - minutes: 0, - }); + // Обновляем количество жизней при изменении режима игры + useEffect(() => { + setLives(initialLives); + }, [initialLives]); - function finishGame(status = STATUS_LOST) { - setGameEndDate(new Date()); - setStatus(status); + // Функция для завершения игры + function finishGame(gameStatus = STATUS_LOST) { + setStatus(gameStatus); + clearInterval(intervalId); } + + // Функция для старта игры function startGame() { - const startDate = new Date(); - setGameEndDate(null); - setGameStartDate(startDate); - setTimer(getTimerValue(startDate, null)); setStatus(STATUS_IN_PROGRESS); + setSecondsElapsed(0); + setSelectedCards([]); + setIsProcessing(false); + setIsSuperPowerUsed(false); + setTimer(getTimerValue(0)); + + const newIntervalId = setInterval(() => { + setSecondsElapsed(prev => prev + 1); + }, 1000); + + setIntervalId(newIntervalId); } + + useEffect(() => { + if (status === STATUS_IN_PROGRESS) { + setTimer(getTimerValue(secondsElapsed)); + } + }, [secondsElapsed, status]); + + // Функция для перезапуска игры function resetGame() { - setGameStartDate(null); - setGameEndDate(null); - setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + setSecondsElapsed(0); + setTimer({ minutes: 0, seconds: 0 }); + setSelectedCards([]); + setIsProcessing(false); + setIsSuperPowerUsed(false); + clearInterval(intervalId); + + // Добавляем сброс жизней на начальное количество + setLives(initialLives); } - /** - * Обработка основного действия в игре - открытие карты. - * После открытия карты игра может пепереходит в следующие состояния - * - "Игрок выиграл", если на поле открыты все карты - * - "Игрок проиграл", если на поле есть две открытые карты без пары - * - "Игра продолжается", если не случилось первых двух условий - */ const openCard = clickedCard => { - // Если карта уже открыта, то ничего не делаем - if (clickedCard.open) { + if (isProcessing || clickedCard.open) { return; } - // Игровое поле после открытия кликнутой карты + const nextCards = cards.map(card => { if (card.id !== clickedCard.id) { return card; } - - return { - ...card, - open: true, - }; + return { ...card, open: true }; }); setCards(nextCards); + const nextSelectedCards = [...selectedCards, clickedCard]; + setSelectedCards(nextSelectedCards); - const isPlayerWon = nextCards.every(card => card.open); + if (nextSelectedCards.length === 2) { + setIsProcessing(true); + const [firstCard, secondCard] = nextSelectedCards; + const isMatch = firstCard.rank === secondCard.rank && firstCard.suit === secondCard.suit; - // Победа - все карты на поле открыты - if (isPlayerWon) { - finishGame(STATUS_WON); - return; + if (isMatch) { + setSelectedCards([]); + const isPlayerWon = nextCards.every(card => card.open); + if (isPlayerWon) { + finishGame(STATUS_WON); + } + setIsProcessing(false); + } else { + const nextLives = lives - 1; + setLives(nextLives); + if (nextLives === 0) { + finishGame(STATUS_LOST); + } else { + setTimeout(() => { + setCards(currentCards => + currentCards.map(card => { + if (card.id === firstCard.id || card.id === secondCard.id) { + return { ...card, open: false }; + } + return card; + }), + ); + setSelectedCards([]); + setIsProcessing(false); + }, 1000); + } + } } + }; - // Открытые карты на игровом поле - const openCards = nextCards.filter(card => card.open); - - // Ищем открытые карты, у которых нет пары среди других открытых - const openCardsWithoutPair = openCards.filter(card => { - const sameCards = openCards.filter(openCard => card.suit === openCard.suit && card.rank === openCard.rank); + const handleSuperPower = () => { + if (!isSuperPowerUsed) { + setIsSuperPowerUsed(true); + setInitialCardsState(cards); - if (sameCards.length < 2) { - return true; - } - - return false; - }); + const openedCards = cards.map(card => ({ ...card, open: true })); + setCards(openedCards); - const playerLost = openCardsWithoutPair.length >= 2; + clearInterval(intervalId); - // "Игрок проиграл", т.к на поле есть две открытые карты без пары - if (playerLost) { - finishGame(STATUS_LOST); - return; + setTimeout(() => { + setCards(initialCardsState); + const newIntervalId = setInterval(() => { + setSecondsElapsed(prev => prev + 1); + }, 1000); + setIntervalId(newIntervalId); + }, 5000); } - - // ... игра продолжается }; const isGameEnded = status === STATUS_LOST || status === STATUS_WON; + const navigate = useNavigate(); - // Игровой цикл useEffect(() => { - // В статусах кроме превью доп логики не требуется if (status !== STATUS_PREVIEW) { return; } - // В статусе превью мы if (pairsCount > 36) { alert("Столько пар сделать невозможно"); return; } - setCards(() => { - return shuffle(generateDeck(pairsCount, 10)); - }); + const deck = shuffle(generateDeck(pairsCount)); + setCards(deck); + setInitialCardsState(deck); const timerId = setTimeout(() => { startGame(); @@ -162,15 +185,9 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { }; }, [status, pairsCount, previewSeconds]); - // Обновляем значение таймера в интервале - useEffect(() => { - const intervalId = setInterval(() => { - setTimer(getTimerValue(gameStartDate, gameEndDate)); - }, 300); - return () => { - clearInterval(intervalId); - }; - }, [gameStartDate, gameEndDate]); + const handleStartGame = () => { + navigate("/"); // Выполняем переход на главную страницу + }; return (
@@ -185,17 +202,35 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { <>
min
-
{timer.minutes.toString().padStart("2", "0")}
+
{timer.minutes.toString().padStart(2, "0")}
.
sec
-
{timer.seconds.toString().padStart("2", "0")}
+
{timer.seconds.toString().padStart(2, "0")}
)}
- {status === STATUS_IN_PROGRESS ? : null} + {status === STATUS_IN_PROGRESS ? ( + <> + {livesMode && ( +
+

Жизни: {lives}

+
+ )} + +
+ +
+
Прозрение
+

На 5 секунд показываются все карты. Таймер длительности игры на это время останавливается.

+
+
+ + ) : null}
@@ -203,13 +238,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { openCard(card)} - open={status !== STATUS_IN_PROGRESS ? true : card.open} + open={status !== STATUS_IN_PROGRESS || card.open} suit={card.suit} rank={card.rank} /> ))}
- + {isGameEnded ? (
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..37ef75264 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -1,3 +1,22 @@ +@import url('https://fonts.googleapis.com/css2?family=Handjet:wght@100..900&display=swap'); + +.lives { + font-size: 32px; + margin-left: 20px; + display: flex; + align-items: center; + font-family: "Handjet", sans-serif; + color: #fff; + /* По умолчанию белый цвет */ +} + +.livesCritical { + color: rgb(255, 55, 55); + /* Красный цвет при 1 жизни */ +} + + + .container { width: 672px; margin: 0 auto; @@ -50,6 +69,7 @@ margin-top: 34px; margin-bottom: 10px; } + .previewDescription { font-size: 18px; line-height: 18px; @@ -70,3 +90,72 @@ margin-bottom: -12px; } + +.buttonEsc { + background-color: #7ac100; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + font-family: StratosSkyeng; + margin-top: 64px; +} + +.buttonEsc:hover { + background-color: #45a049; +} + +.superPowerButton { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.superPowerButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.superPowerContainer { + position: relative; + display: inline-block; +} + +.tooltip { + display: none; + position: absolute; + width: 161px; + height: 195px; + background: #C2F5FF; + color: #004980; + border-radius: 12px; + font-family: 'Inter', sans-serif; + font-size: 18px; + font-weight: 400; + line-height: 24px; + text-align: center; + padding: 10px; + z-index: 10; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + top: 100%; + right: -80%; +} + +.superPowerContainer:hover .tooltip { + display: block; +} + +.superPowerContainer:hover .tooltip .title { + height: 24px; + color: #004980; + font-weight: bold; + text-align: center; +} + +.title { + font-size: 18px; + padding-bottom: 18px; +} \ No newline at end of file diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..9da5a48f4 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,27 +1,78 @@ +import { useState, useContext } from "react"; import styles from "./EndGameModal.module.css"; - import { Button } from "../Button/Button"; - import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { Link } from "react-router-dom"; +import { GameModeContext } from "../../context/GameModeContext"; // Импортируем контекст -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, isSuperPowerUsed }) { + const [playerName, setPlayerName] = useState(""); + const { livesMode } = useContext(GameModeContext); // Получаем данные из контекста + const title = isWon ? "Вы попали\nна Лидерборд!" : "Вы проиграли!"; const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; - const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + const handleSubmit = () => { + const name = playerName || "Пользователь"; + const totalTime = gameDurationMinutes * 60 + gameDurationSeconds; + + // Определяем достижения + const achievements = []; + if (!livesMode) achievements.push(1); // Сложный уровень (без трех жизней) + if (!isSuperPowerUsed) achievements.push(2); // Не использовалась суперсила + + console.log("Отправляем данные:", { name, time: totalTime, achievements }); + + // Отправляем данные в формате JSON без заголовка Content-Type + fetch("https://wedev-api.sky.pro/api/v2/leaderboard", { + method: "POST", + body: JSON.stringify({ + name, + time: totalTime, + achievements, // Передаем массив достижений + }), + }) + .then(response => { + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + return response.json(); + }) + .then(data => { + console.log("Лидер добавлен:", data); // Лог для проверки результата + onClick(); // Закрытие модального окна + }) + .catch(error => { + console.error("Ошибка при добавлении лидера:", error); + }); + }; + return (
{imgAlt}

{title}

+ setPlayerName(e.target.value)} + />

Затраченное время:

- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} + {gameDurationMinutes.toString().padStart(2, "0")}:{gameDurationSeconds.toString().padStart(2, "0")}
- - + {isWon && ( + <> + + + )} + {!isWon && } + + Перейти к лидерборду +
); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..90defb71a 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,6 +1,6 @@ .modal { width: 480px; - height: 459px; + height: 570px; border-radius: 12px; background: #c2f5ff; display: flex; @@ -23,7 +23,7 @@ font-style: normal; font-weight: 400; line-height: 48px; - + text-align: center; margin-bottom: 28px; } @@ -49,3 +49,31 @@ margin-bottom: 40px; } +.input { + background-color: #ffffff; /* Светло-голубой фон */ + border: 1px solid #dcdcdc; /* Светлая рамка */ + border-radius: 10px; /* Закругленные углы */ + font-family: 'Arial', sans-serif; + font-size: 18px; + color: #000000; + padding: 7px; + outline: none; /* Убираем стандартную обводку */ + width: 250px; /* Полная ширина */ + text-align: center; /* Выравнивание плейсхолдера по центру */ + margin-bottom: 18px; +} + +.input::placeholder { + color: #9e9e9e; /* Цвет текста плейсхолдера */ + font-weight: normal; /* Обычный вес шрифта для плейсхолдера */ + text-align: center; /* Выравнивание плейсхолдера по центру */ +} +.leaderboardLink{ + padding-top: 18px; + padding-bottom: 8px; + font-family: StratosSkyeng; + font-size: 16px; /* размер шрифта */ + color: #605bc9; /* сиреневый цвет ссылки */ + text-decoration: underline; /* подчеркивание */ + transition: color 0.3s ease; /* плавное изменение цвета при наведении */ +} \ No newline at end of file diff --git a/src/components/EndGameModal/images/hardlvloff.png b/src/components/EndGameModal/images/hardlvloff.png new file mode 100644 index 000000000..ab6ebf3d1 Binary files /dev/null and b/src/components/EndGameModal/images/hardlvloff.png differ diff --git a/src/components/EndGameModal/images/hardlvlon.png b/src/components/EndGameModal/images/hardlvlon.png new file mode 100644 index 000000000..bc6291e82 Binary files /dev/null and b/src/components/EndGameModal/images/hardlvlon.png differ diff --git a/src/components/EndGameModal/images/power.svg b/src/components/EndGameModal/images/power.svg new file mode 100644 index 000000000..d88a5843e --- /dev/null +++ b/src/components/EndGameModal/images/power.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/EndGameModal/images/superpoweroff.png b/src/components/EndGameModal/images/superpoweroff.png new file mode 100644 index 000000000..51d8b96ee Binary files /dev/null and b/src/components/EndGameModal/images/superpoweroff.png differ diff --git a/src/components/EndGameModal/images/superpoweron.png b/src/components/EndGameModal/images/superpoweron.png new file mode 100644 index 000000000..b6a94ab90 Binary files /dev/null and b/src/components/EndGameModal/images/superpoweron.png differ diff --git a/src/context/GameModeContext.js b/src/context/GameModeContext.js new file mode 100644 index 000000000..04c9e13e1 --- /dev/null +++ b/src/context/GameModeContext.js @@ -0,0 +1,28 @@ +// context/GameModeContext.js +import { createContext, useState, useEffect } from "react"; + +export const GameModeContext = createContext({ + livesMode: false, + usedSuperpower: false, + setLivesMode: () => {}, + setUsedSuperpower: () => {}, +}); + +export function GameModeProvider({ children }) { + const [livesMode, setLivesMode] = useState(() => { + const savedMode = localStorage.getItem("livesMode"); + return savedMode === "true"; + }); + + const [usedSuperpower, setUsedSuperpower] = useState(false); + + useEffect(() => { + localStorage.setItem("livesMode", livesMode); + }, [livesMode]); + + return ( + + {children} + + ); +} diff --git a/src/index.js b/src/index.js index f689c5f0b..30c3029f6 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,20 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; +import { GameModeProvider } from "./context/GameModeContext"; // Импортируем обновленный провайдер const root = ReactDOM.createRoot(document.getElementById("root")); + +function App() { + return ( + + + + ); +} + root.render( - + , ); diff --git a/src/pages/GamePage/GamePage.jsx b/src/pages/GamePage/GamePage.jsx index a4be871db..940738952 100644 --- a/src/pages/GamePage/GamePage.jsx +++ b/src/pages/GamePage/GamePage.jsx @@ -1,13 +1,75 @@ import { useParams } from "react-router-dom"; - import { Cards } from "../../components/Cards/Cards"; +import { useEffect, useContext } from "react"; +import { GameModeContext } from "../../context/GameModeContext"; // Импортируем контекст для режимов игры export function GamePage() { const { pairsCount } = useParams(); + const { livesMode, usedSuperpower } = useContext(GameModeContext); // Получаем режимы игры и использование суперсилы + + // Функция для отправки результата в API с ачивками + const sendResultToLeaderboard = (name, time) => { + const achievements = []; + + // Проверяем, играл ли пользователь в режиме без жизней + if (!livesMode) achievements.push(1); // Если сложный режим (без трех жизней) + + // Проверяем, использовалась ли суперсила + if (!usedSuperpower) achievements.push(2); // Если суперсила не использовалась + + const level = Math.floor(pairsCount / 3); // Рассчитываем уровень на основе количества пар + + // Проверим, что все значения корректны перед отправкой + console.log("Отправляем результат:", { + name, + time, + level, + achievements, + }); + + const result = { + name: name || "Пользователь", // Если имя не указано, используем "Пользователь" + time: parseInt(time, 10), // Убедимся, что время передано как число + level, // Уровень игры + achievements, // Добавляем массив ачивок в запрос + }; + + // Отправляем POST-запрос без заголовка Content-Type + fetch("https://wedev-api.sky.pro/api/v2/leaderboard", { + method: "POST", + body: JSON.stringify(result), // Преобразуем объект в строку JSON + }) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + console.error("Ошибка при добавлении результата:", errorData); + throw new Error(`Ошибка HTTP: ${response.status}`); + }); + } + return response.json(); + }) + .then(data => { + console.log("Результат успешно добавлен:", data); + }) + .catch(error => { + console.error("Ошибка при добавлении результата:", error); + }); + }; + + // Пример эффекта для отправки данных при завершении игры + useEffect(() => { + const gameEnded = true; // Предположим, что игра завершена + const playerName = "Игрок"; // Можно получить откуда-то имя игрока + const gameTime = 120; // Время игры в секундах + + if (gameEnded) { + sendResultToLeaderboard(playerName, gameTime); + } + }, [pairsCount]); return ( <> - + ); } diff --git a/src/pages/LeaderboardPage/LeaderboardPage.jsx b/src/pages/LeaderboardPage/LeaderboardPage.jsx new file mode 100644 index 000000000..c27ac3e19 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx @@ -0,0 +1,87 @@ +import { useState, useEffect } from "react"; +import styles from "./LeaderboardPage.module.css"; +import { useNavigate } from "react-router-dom"; +import hardlvlon from "../../components/EndGameModal/images/hardlvlon.png"; +import hardlvloff from "../../components/EndGameModal/images/hardlvloff.png"; +import superpoweron from "../../components/EndGameModal/images/superpoweron.png"; +import superpoweroff from "../../components/EndGameModal/images/superpoweroff.png"; + +export function LeaderboardPage() { + const [leaders, setLeaders] = useState([]); + const navigate = useNavigate(); + + // Убедитесь, что сортировка корректная и вся информация выводится + useEffect(() => { + fetch("https://wedev-api.sky.pro/api/v2/leaderboard") + .then(response => response.json()) + .then(data => { + const sortedLeaders = data.leaders.sort((a, b) => a.time - b.time).slice(0, 10); // Оставляем только топ-10 лидеров + console.log("Топ-10 лидеров:", sortedLeaders); // Вывод топ-10 лидеров + + setLeaders(sortedLeaders); // Устанавливаем только топ-10 лидеров + }) + .catch(error => { + console.error("Ошибка при получении списка лидеров:", error); + }); + }, []); + + const handleStartGame = () => { + navigate("/"); + }; + + const renderAchievements = (achievements = []) => ( +
+ {achievements.includes(1) ? ( + Hard level achievement + ) : ( + Hard level not achieved + )} + {achievements.includes(2) ? ( + Superpower achievement + ) : ( + Superpower not achieved + )} +
+ ); + + return ( +
+
+

Лидерборд

+
+ +
+
+ + + + + + + + + + + {leaders.map((leader, index) => ( + + + + + + + ))} + +
ПозицияПользовательВремяДостижения
{`#${index + 1}`}{leader.name}{formatTime(leader.time)}{renderAchievements(leader.achievements)}
+
+ ); +} + +function formatTime(timeInSeconds) { + const minutes = Math.floor(timeInSeconds / 60) + .toString() + .padStart(2, "0"); + const seconds = (timeInSeconds % 60).toString().padStart(2, "0"); + return `${minutes}:${seconds}`; +} diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css new file mode 100644 index 000000000..92ad199f0 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css @@ -0,0 +1,79 @@ +.leaderboard { + background-color: #043864; + color: white; + padding: 20px; + border-radius: 8px; +} + +.title { + font-size: 24px; + font-weight: 100; + margin-bottom: 20px; + font-family: StratosSkyeng; +} + +.table { + width: 100%; + border-spacing: 0 10px; +} + +.table th, +.table td { + text-align: left; + padding: 10px 15px; + background-color: white; + color: #043864; + font-family: StratosSkyeng; +} + +.table th { + font-size: 16px; + font-weight: bold; + background-color: #d8eaff; + color: #043864; + +} + +.table tbody tr { + background-color: white; + border-radius: 16px; + box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1); + /* Добавляем тень */ +} + +.buttonContainer { + text-align: right; + margin-top: 20px; +} + +.button { + background-color: #4caf50; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + font-family: StratosSkyeng; +} + +.button:hover { + background-color: #45a049; +} + +.header { + display: flex; + justify-content: space-between; + align-items: baseline; +} +/* Добавляем стили для иконок достижений */ +.achievements { + display: flex; + gap: 10px; + } + + .achievementIcon { + width: 24px; + height: 24px; + } + \ No newline at end of file diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..240f3e07b 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,11 +1,23 @@ import { Link } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext } from "react"; +import { GameModeContext } from "../../context/GameModeContext"; export function SelectLevelPage() { + const { livesMode, setLivesMode } = useContext(GameModeContext); + + const handleCheckboxChange = event => { + setLivesMode(event.target.checked); + }; + return (

Выбери сложность

+
  • @@ -23,6 +35,9 @@ export function SelectLevelPage() {
+ + Перейти к лидерборду +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..9348d81ba 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -1,3 +1,42 @@ +@import url('https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@400;700&display=swap'); + +.checkboxContainer { + display: flex; + align-items: center; + font-family: 'Cabin Sketch', cursive; /* Подключённый шрифт */ + font-size: 18px; + color: #333; +} + +.checkboxContainer input[type="checkbox"] { + width: 20px; + height: 20px; + margin-right: 10px; + position: relative; + cursor: pointer; + appearance: none; + background-color: #c2f5ff; + border: 2px solid #333; + border-radius: 5px; + transition: background-color 0.2s ease; + display: flex; + justify-content: center; + align-items: center; +} + +.checkboxContainer input[type="checkbox"]:checked::before { + content: '✓'; + font-size: 32px; /* Сделаем галочку чуть больше */ + color: #004980; + font-weight: bold; /* Галочка станет толще */ + position: absolute; + left: 63%; + top: 18%; + transform: translate(-50%, -50%); /* Выравнивание галочки по центру */ + font-family: 'Comic Sans MS', cursive; /* стили для галочки */ +} + + .container { width: 100%; min-height: 100%; @@ -62,3 +101,12 @@ .levelLink:visited { color: #0080c1; } +.leaderboardLink{ + padding-top: 18px; + padding-bottom: 8px; + font-family: StratosSkyeng; + font-size: 16px; /* размер шрифта */ + color: #004980; /* сиреневый цвет ссылки */ + text-decoration: underline; /* подчеркивание */ + transition: color 0.3s ease; /* плавное изменение цвета при наведении */ +} diff --git a/src/router.js b/src/router.js index da6e94b51..43cfca483 100644 --- a/src/router.js +++ b/src/router.js @@ -1,6 +1,7 @@ import { createBrowserRouter } from "react-router-dom"; import { GamePage } from "./pages/GamePage/GamePage"; import { SelectLevelPage } from "./pages/SelectLevelPage/SelectLevelPage"; +import { LeaderboardPage } from "./pages/LeaderboardPage/LeaderboardPage"; export const router = createBrowserRouter( [ @@ -12,6 +13,10 @@ export const router = createBrowserRouter( path: "/game/:pairsCount", element: , }, + { + path: "/leaderboard", // Добавлен маршрут для лидерборда + element: , + }, ], /** * basename нужен для корректной работы в gh pages