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 && (
+
+ )}
+
+
+
+
+
Прозрение
+
На 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 (
{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) ? (
+

+ ) : (
+

+ )}
+ {achievements.includes(2) ? (
+

+ ) : (
+

+ )}
+
+ );
+
+ 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