diff --git a/README.md b/README.md
index 9b90842c4..61874ca51 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,11 @@ https://skypro-web-developer.github.io/react-memo/
Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
+
+Оценочное время выполнения работы составляло 10...12 часов.
+По факту получилось существенно больше - порядка 26...28 часов. Много времени ушло на установку зависимостей.
+После этой процедуры никак не запускался проект, выводились ошибки.
+
+Для выполнения ДЗ №2 оценочное время определил в 10..12 часов. По факту получилось несколько больше - 15..16 часов
+
+Для выполнения Курсовой работы оценочное время определил в 20..22 часов. По факту получилось 25..26 часов
\ No newline at end of file
diff --git a/public/assets/fonts/Inter2.woff2 b/public/assets/fonts/Inter2.woff2
new file mode 100644
index 000000000..a78fd7e61
Binary files /dev/null and b/public/assets/fonts/Inter2.woff2 differ
diff --git a/public/assets/fonts/Montserrat1.woff2 b/public/assets/fonts/Montserrat1.woff2
new file mode 100644
index 000000000..7b900875b
Binary files /dev/null and b/public/assets/fonts/Montserrat1.woff2 differ
diff --git a/public/assets/fonts/PoppinsNC3.woff2 b/public/assets/fonts/PoppinsNC3.woff2
new file mode 100644
index 000000000..962b734ef
Binary files /dev/null and b/public/assets/fonts/PoppinsNC3.woff2 differ
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 000000000..3c9b8d7ed
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,33 @@
+// const pathApi = "https://wedev-api.sky.pro/api/leaderboard";
+const pathApiV2 = "https://wedev-api.sky.pro/api/v2/leaderboard";
+
+export async function getLeaders() {
+ const response = await fetch(pathApiV2, {
+ method: "GET",
+ });
+ if (!response.ok & (response.status === 500)) {
+ throw new Error("Ошибка соединения");
+ }
+
+ const data = await response.json();
+ return data;
+}
+
+export async function postLeader({ user, timeUser, achievements }) {
+ const response = await fetch(pathApiV2, {
+ method: "POST",
+ body: JSON.stringify({
+ name: user,
+ time: timeUser,
+ achievements: achievements,
+ }),
+ });
+
+ if (!response.ok & (response.status === 500)) {
+ throw new Error("Ошибка сервера");
+ } else if (response.status === 400) {
+ throw new Error("Плохой запрос");
+ }
+ const data = await response.json();
+ return data;
+}
diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx
index 7526a56c8..6f9b5edf4 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -3,8 +3,12 @@ import { useEffect, useState } from "react";
import { generateDeck } from "../../utils/cards";
import styles from "./Cards.module.css";
import { EndGameModal } from "../../components/EndGameModal/EndGameModal";
+import { AlohomoraModal } from "../../components/forcesModal/AlohomoraModal";
import { Button } from "../../components/Button/Button";
import { Card } from "../../components/Card/Card";
+import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode";
+import { useUser } from "../../contexts/userContext/UseUser";
+import cardsIcon from "../../icons/force2.svg";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -41,11 +45,15 @@ function getTimerValue(startDate, endDate) {
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
+ const [isLeader, setIsLeader] = useState(false);
+ const { isEasyMode, forceCards, setForceCards, setForceEye, isAlohomora, setIsAlohomora } = useEasyMode();
+ const { setUser } = useUser();
+ // Если игорок выбирает легкий уровень с 3 попытками, в attempts организован счетчик этих попыток
+ const [attempts, setAttempts] = useState(isEasyMode ? 3 : 1);
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const [cards, setCards] = useState([]);
// Текущий статус игры
const [status, setStatus] = useState(STATUS_PREVIEW);
-
// Дата начала игры
const [gameStartDate, setGameStartDate] = useState(null);
// Дата конца игры
@@ -73,8 +81,50 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
+ if (isEasyMode) {
+ setAttempts(3);
+ }
+ setForceEye(1);
+ setForceCards(2);
+ }
+ function onAlohomoraMouseEnter() {
+ if (!isGameEnded) {
+ setIsAlohomora(true);
+ }
+ }
+ function onAlohomoraMouseLeave() {
+ setIsAlohomora(false);
}
+ const handleForceCards = () => {
+ if (timer.seconds + timer.minutes > 0) {
+ const closedCards = cards.filter(card => !card.open);
+ const lenClosedCards = closedCards.length;
+ if (forceCards > 0 && lenClosedCards > 1) {
+ if (forceCards === 1) {
+ setIsAlohomora(false);
+ }
+ setForceCards(prev => prev - 1);
+ const randomIndex = Math.floor(Math.random() * (lenClosedCards - 1));
+ const candidate = closedCards[randomIndex];
+ const forcedCards = closedCards.filter(
+ closedCard => closedCard.rank === candidate.rank && closedCard.suit === candidate.suit,
+ );
+ for (let i = 0; i <= forcedCards.length - 1; i++) {
+ forcedCards[i].open = true;
+ }
+ if (lenClosedCards === 2) {
+ if (pairsCount === 9) {
+ setIsLeader(true);
+ }
+ finishGame(STATUS_WON);
+ }
+ } else if (forceCards > 0 && lenClosedCards === 1) {
+ alert("Неоткрытых карт должно быть не менее двух!");
+ }
+ }
+ };
+
/**
* Обработка основного действия в игре - открытие карты.
* После открытия карты игра может пепереходит в следующие состояния
@@ -105,7 +155,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// Победа - все карты на поле открыты
if (isPlayerWon) {
- finishGame(STATUS_WON);
+ if (pairsCount === 9) {
+ setIsLeader(true);
+ }
+ setTimeout(finishGame(STATUS_WON), 1000);
+ setUser("Пользователь");
return;
}
@@ -127,10 +181,36 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
- finishGame(STATUS_LOST);
- return;
- }
+ setIsLeader(false);
+ if (!isEasyMode) {
+ finishGame(STATUS_LOST);
+ setUser("Пользователь");
+ return;
+ } else {
+ // Фунция закрытия карт
+ const closeOpenedCards = () => {
+ if (openCardsWithoutPair.length >= 2) {
+ openCardsWithoutPair.forEach(openedCard => {
+ const currentOpenedCard = nextCards.find(card => card.id === openedCard.id);
+ if (currentOpenedCard) {
+ currentOpenedCard.open = false;
+ }
+ });
+ setAttempts(prevState => prevState - 1);
+ setCards([...nextCards]);
+ }
+ };
+ // Сделаем перед закрытием карт задержку в 1с, чтобы игрок видел ошибочно открытую карту
+ setTimeout(closeOpenedCards, 1000);
+ if (attempts === 0) {
+ finishGame(STATUS_LOST);
+ setAttempts(3);
+ setIsLeader(true);
+ return;
+ }
+ }
+ }
// ... игра продолжается
};
@@ -172,6 +252,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
};
}, [gameStartDate, gameEndDate]);
+ useEffect(() => {
+ if (attempts === 0) {
+ finishGame(STATUS_LOST);
+ }
+ }, [attempts]);
+
return (
@@ -195,6 +281,40 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
>
)}
+
+
+ {forceCards > 0 ? (
+

+ ) : (
+

+ )}
+ {isGameEnded ? (
+ <>
+
0 ? styles.iconCardsEnd : styles.iconCardsOff}`}>
+
0 ? styles.countCardsEnd : styles.countCardsOff}`}>{forceCards}
+ >
+ ) : (
+ <>
+
0 ? styles.iconCards : styles.iconCardsEnd}`}>
+
0 ? styles.countCards : styles.countCardsEnd}`}>{forceCards}
+ >
+ )}
+
+
{status === STATUS_IN_PROGRESS ?
: null}
@@ -210,16 +330,31 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
))}
+ {isEasyMode ? (
+
+ Осталось попыток: {attempts}
+
+ ) : (
+
+ )}
+
{isGameEnded ? (
) : null}
+
+ {isAlohomora & (timer.seconds + timer.minutes > 0) ? (
+
+ ) : null}
);
}
diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css
index 000c5006c..8ee6bfa65 100644
--- a/src/components/Cards/Cards.module.css
+++ b/src/components/Cards/Cards.module.css
@@ -11,6 +11,7 @@
flex-direction: row;
gap: 10px;
flex-wrap: wrap;
+ margin-bottom: 30px;
}
.modalContainer {
@@ -28,20 +29,20 @@
.header {
display: flex;
justify-content: space-between;
- align-items: end;
+ align-items: flex-end;
margin-bottom: 35px;
}
.timer {
display: flex;
- align-items: end;
-
+ align-items: flex-end;
color: #fff;
font-family: StratosSkyeng;
font-size: 64px;
font-style: normal;
font-weight: 400;
line-height: 72px;
+ z-index: -1;
}
.previewText {
@@ -70,3 +71,153 @@
margin-bottom: -12px;
}
+
+.attempts {
+ padding-top: 40px;
+ color: #fff;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 32px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 32px;
+}
+
+.footerEasy {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: baseline;
+}
+
+.footerCard {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+}
+
+.achievement {
+ display: none;
+}
+
+.modalContainerAl {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #004980c0;
+ z-index: 0;
+}
+
+.forcesBox {
+ width: 151px;
+ height: 68px;
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+}
+
+.forceEye,
+.forceCards {
+ position: relative;
+ opacity: 1.0;
+ z-index: 2;
+}
+.forceEyeOff,
+.forceCardsOff {
+ opacity: 0.6;
+}
+
+.forceEye {
+ display: none;
+}
+
+.iconEye {
+ background-color: rgba(223, 120, 25, 1.0);
+ height: 25px;
+ width: 25px;
+ position: absolute;
+ top: 40px;
+ left: 45px;
+ border-radius: 50%;
+ z-index: 2;
+}
+
+.iconEye {
+ display: none;
+}
+
+.countEye {
+ color: #fff;
+ position: relative;
+ top: 45px;
+ left: 54px;
+ font-family: StratosSkyeng;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 14px;
+ z-index: 2;
+}
+
+.countEye {
+ display: none;
+}
+
+.iconCards,
+.iconCardsOff,
+.iconCardsEnd {
+ background-color: rgba(223, 120, 25, 1.0);
+ height: 25px;
+ width: 25px;
+ position: relative;
+ top: 40px;
+ top: -32px;
+ left: 45px;
+ border-radius: 50%;
+ z-index: 2;
+ opacity: 1.0;
+}
+
+.iconCardsEnd {
+ top: -34px;
+ opacity: 0.4;
+}
+
+.countCards,
+.countCardsOff,
+.countCardsEnd {
+ color: #fff;
+ position: relative;
+ top: -50px;
+ left: 54px;
+ font-family: StratosSkyeng;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 14px;
+ z-index: 2;
+ opacity: 1.0;
+}
+
+.countCardsEnd {
+ top: -54px;
+ opacity: 0.4;
+}
+
+.iconCardsOff {
+ top: -34px;
+ left: 50px;
+ opacity: 0.4;
+}
+
+.countCardsOff {
+ top: -52px;
+ left: 58px;
+ opacity: 0.4;
+}
diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx
index 722394833..2cd0b768f 100644
--- a/src/components/EndGameModal/EndGameModal.jsx
+++ b/src/components/EndGameModal/EndGameModal.jsx
@@ -1,27 +1,104 @@
import styles from "./EndGameModal.module.css";
+import { Link } from "react-router-dom";
+import { useUser } from "../../contexts/userContext/UseUser";
+import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode";
import { Button } from "../Button/Button";
import deadImageUrl from "./images/dead.png";
import celebrationImageUrl from "./images/celebration.png";
+import { useState } from "react";
+import { postLeader } from "../../api";
-export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) {
- const title = isWon ? "Вы победили!" : "Вы проиграли!";
+export function EndGameModal({ isWon, isLeader, gameDurationSeconds, gameDurationMinutes, onClick }) {
+ const { user, setUser } = useUser();
+ const [setError] = useState(null);
+ const [stateBtn, setStateBtn] = useState(true);
+ const { isEasyMode, forceCards } = useEasyMode();
+
+ const title = isWon ? (isLeader ? "Вы попали на Лидерборд!" : "Вы победили!") : "Вы проиграли!";
const imgSrc = isWon ? celebrationImageUrl : deadImageUrl;
const imgAlt = isWon ? "celebration emodji" : "dead emodji";
+ const onInputName = event => {
+ setUser(event.target.value);
+ };
+
+ const postUserLeaderboard = async () => {
+ if (user.trim() === "") {
+ alert("Введите имя пользователя");
+ } else {
+ setStateBtn(prev => !prev);
+
+ const timeUser = gameDurationMinutes * 60 + gameDurationSeconds;
+ const achievements = [];
+ if (!isEasyMode) {
+ achievements.push(1);
+ }
+ if (forceCards === 2) {
+ achievements.push(2);
+ }
+
+ try {
+ return postLeader({ user, timeUser, achievements });
+ } catch (error) {
+ console.error(error.message);
+ if (error.message === "Failed to fetch") {
+ setError("Ошибка соединения");
+ return;
+ }
+ }
+ }
+ };
+
return (
-
-

-
{title}
-
Затраченное время:
-
- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
-
-
-
-
+ <>
+ {isLeader ? (
+
+

+
{title}
+
+
+
+
+
Затраченное время:
+
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
+
+
+
+ Перейти к лидерборду
+
+
+ ) : (
+
+

+
{title}
+
Затраченное время:
+
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
+
+
+
+ Перейти к лидерборду
+
+
+ )}
+ >
);
}
diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css
index 9368cb8b5..2800dded2 100644
--- a/src/components/EndGameModal/EndGameModal.module.css
+++ b/src/components/EndGameModal/EndGameModal.module.css
@@ -1,4 +1,5 @@
-.modal {
+.modal,
+.modalLeader {
width: 480px;
height: 459px;
border-radius: 12px;
@@ -9,6 +10,10 @@
align-items: center;
}
+.modalLeader {
+ height: 636px;
+}
+
.image {
width: 96px;
height: 96px;
@@ -16,6 +21,7 @@
}
.title {
+ width: 300px;
color: #004980;
font-variant-numeric: lining-nums proportional-nums;
font-family: StratosSkyeng;
@@ -23,6 +29,7 @@
font-style: normal;
font-weight: 400;
line-height: 48px;
+ text-align: center;
margin-bottom: 28px;
}
@@ -35,7 +42,6 @@
font-style: normal;
font-weight: 400;
line-height: 32px;
-
margin-bottom: 10px;
}
@@ -46,6 +52,72 @@
font-style: normal;
font-weight: 400;
line-height: 72px;
-
margin-bottom: 40px;
}
+
+.goLeaderboard {
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ line-height: 32px;
+ font-weight: 400;
+ color: #004980;
+ margin-top: 16px;
+}
+
+.userName {
+ margin-bottom: 16px;
+ width: 276px;
+ height: 45px;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+ background-color: #fff;
+ border: none;
+ color: #000;
+}
+
+.modalUser {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+}
+
+.nameSave {
+ width: 150px;
+ height: 45px;
+ border-radius: 12px;
+ background: #7ac100;
+ color: #fff;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ border: none;
+ cursor: pointer;
+}
+
+.button:hover {
+ background: #7ac100cc;
+}
+
+.newNameSave {
+ width: 150px;
+ height: 45px;
+ border-radius: 12px;
+ background: #27cece;
+ color: #fff;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ border: none;
+ cursor: not-allowed;
+}
\ No newline at end of file
diff --git a/src/components/forcesModal/AlohomoraModal.jsx b/src/components/forcesModal/AlohomoraModal.jsx
new file mode 100644
index 000000000..04ab8599f
--- /dev/null
+++ b/src/components/forcesModal/AlohomoraModal.jsx
@@ -0,0 +1,14 @@
+import styles from "./AlohomoraModal.module.css";
+
+export function AlohomoraModal() {
+ return (
+
+
+ Алохомора
+
+ Открывается случайная
пара карт.
+
+
+
+ );
+}
diff --git a/src/components/forcesModal/AlohomoraModal.module.css b/src/components/forcesModal/AlohomoraModal.module.css
new file mode 100644
index 000000000..e3e49a134
--- /dev/null
+++ b/src/components/forcesModal/AlohomoraModal.module.css
@@ -0,0 +1,18 @@
+.alohomora {
+ position: relative;
+ left: -80px;
+ top: -330px;
+ width: 212px;
+ height: auto;
+ height: 90px;
+ background-color: #c2f5ff;
+ color: #004980;
+ font-size: 18px;
+ font-weight: 400;
+ font-family: Inter;
+ line-height: 24px;
+ text-align: center;
+ border-radius: 12px;
+ padding: 20px 0;
+ z-index: -1;
+}
diff --git a/src/contexts/easyModeContext/EasyModeContext.jsx b/src/contexts/easyModeContext/EasyModeContext.jsx
new file mode 100644
index 000000000..b83726b84
--- /dev/null
+++ b/src/contexts/easyModeContext/EasyModeContext.jsx
@@ -0,0 +1,48 @@
+import { useState, createContext } from "react";
+
+const getEasyModeFromLocalStorage = () => {
+ const easyModeInfo = localStorage.getItem("easyMode");
+ return easyModeInfo ? JSON.parse(easyModeInfo) : null;
+};
+
+export const EasyModeContext = createContext(false);
+
+export const EasyModeProvider = ({ children }) => {
+ const [isEasyMode, setIsEasyMode] = useState(getEasyModeFromLocalStorage());
+
+ const setIsEasy = newEasyMode => {
+ setIsEasyMode(newEasyMode);
+ localStorage.setItem("easyMode", JSON.stringify(newEasyMode));
+ };
+
+ const clearIsEasy = () => {
+ localStorage.removeItem("easyMode");
+ setIsEasyMode(false);
+ };
+
+ const [selectedLevel, setSelectedLevel] = useState(null);
+ const [forceEye, setForceEye] = useState(true);
+ const [forceCards, setForceCards] = useState(2);
+ const [isAlohomora, setIsAlohomora] = useState(false);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/contexts/easyModeContext/UseEasyMode.jsx b/src/contexts/easyModeContext/UseEasyMode.jsx
new file mode 100644
index 000000000..b044ac036
--- /dev/null
+++ b/src/contexts/easyModeContext/UseEasyMode.jsx
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { EasyModeContext } from "./EasyModeContext";
+
+export const useEasyMode = () => {
+ return useContext(EasyModeContext);
+};
diff --git a/src/contexts/leaderContext/LeaderContext.jsx b/src/contexts/leaderContext/LeaderContext.jsx
new file mode 100644
index 000000000..e9f3cd662
--- /dev/null
+++ b/src/contexts/leaderContext/LeaderContext.jsx
@@ -0,0 +1,9 @@
+import { createContext, useState } from "react";
+
+export const LeadersContext = createContext();
+
+export const LeadersProvider = ({ children }) => {
+ const [leaders, setLeaders] = useState([]);
+
+ return {children};
+};
diff --git a/src/contexts/leaderContext/UseLeaders.jsx b/src/contexts/leaderContext/UseLeaders.jsx
new file mode 100644
index 000000000..78c3c9faf
--- /dev/null
+++ b/src/contexts/leaderContext/UseLeaders.jsx
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { LeadersContext } from "./LeaderContext";
+
+export const useLeaders = () => {
+ return useContext(LeadersContext);
+};
diff --git a/src/contexts/userContext/UseUser.jsx b/src/contexts/userContext/UseUser.jsx
new file mode 100644
index 000000000..8ab465564
--- /dev/null
+++ b/src/contexts/userContext/UseUser.jsx
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { UserContext } from "./UserContext";
+
+export const useUser = () => {
+ return useContext(UserContext);
+};
diff --git a/src/contexts/userContext/UserContext.jsx b/src/contexts/userContext/UserContext.jsx
new file mode 100644
index 000000000..2025d565b
--- /dev/null
+++ b/src/contexts/userContext/UserContext.jsx
@@ -0,0 +1,11 @@
+import { createContext, useState } from "react";
+
+export const UserContext = createContext(null);
+
+const UserProvider = ({ children }) => {
+ const [user, setUser] = useState("Пользователь");
+
+ return {children};
+};
+
+export default UserProvider;
diff --git a/src/icons/achievement1.svg b/src/icons/achievement1.svg
new file mode 100644
index 000000000..30eb863be
--- /dev/null
+++ b/src/icons/achievement1.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/icons/achievement1Non.svg b/src/icons/achievement1Non.svg
new file mode 100644
index 000000000..3ee152c69
--- /dev/null
+++ b/src/icons/achievement1Non.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/icons/achievement2.svg b/src/icons/achievement2.svg
new file mode 100644
index 000000000..312c80e93
--- /dev/null
+++ b/src/icons/achievement2.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/icons/achievement2Non.svg b/src/icons/achievement2Non.svg
new file mode 100644
index 000000000..7949f73ed
--- /dev/null
+++ b/src/icons/achievement2Non.svg
@@ -0,0 +1,30 @@
+
diff --git a/src/icons/eye.svg b/src/icons/eye.svg
new file mode 100644
index 000000000..d88a5843e
--- /dev/null
+++ b/src/icons/eye.svg
@@ -0,0 +1,34 @@
+
diff --git a/src/icons/force2.svg b/src/icons/force2.svg
new file mode 100644
index 000000000..858b1880f
--- /dev/null
+++ b/src/icons/force2.svg
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/src/index.css b/src/index.css
index 78f0d3a2b..885fa5523 100644
--- a/src/index.css
+++ b/src/index.css
@@ -9,6 +9,7 @@ body {
}
#root {
+ /* position: absolute; */
width: 100%;
height: 100%;
}
@@ -35,3 +36,27 @@ ol {
font-weight: 400;
font-style: normal;
}
+@font-face {
+ font-family: "Poppins";
+ src:
+ url("../public/assets/fonts/PoppinsNC3.woff2") format("woff2"),
+ local("sans-serif");
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Montserrat";
+ src:
+ url("../public/assets/fonts/Montserrat1.woff2") format("woff2"),
+ local("sans-serif");
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Inter";
+ src:
+ url("../public/assets/fonts/Inter2.woff2") format("woff2"),
+ local("sans-serif");
+ font-weight: 400;
+ font-style: normal;
+}
diff --git a/src/index.js b/src/index.js
index f689c5f0b..d6396aae3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,19 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
+import { EasyModeProvider } from "./contexts/easyModeContext/EasyModeContext";
+import { LeadersProvider } from "./contexts/leaderContext/LeaderContext";
+import UserProvider from "./contexts/userContext/UserContext";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
+
+
+
+
+
+
,
);
diff --git a/src/pages/LeaderboardPage/LeaderboardPage.jsx b/src/pages/LeaderboardPage/LeaderboardPage.jsx
new file mode 100644
index 000000000..faae3eba8
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx
@@ -0,0 +1,125 @@
+import styles from "./LeaderboardPage.module.css";
+import { useEffect } from "react";
+import { useLeaders } from "../../contexts/leaderContext/UseLeaders";
+import { getLeaders } from "../../api";
+import { Link } from "react-router-dom";
+import { Button } from "../../components/Button/Button";
+import achiev1 from "../../icons/achievement1.svg";
+import achiev1Non from "../../icons/achievement1Non.svg";
+import achiev2 from "../../icons/achievement2.svg";
+import achiev2Non from "../../icons/achievement2Non.svg";
+import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode";
+
+export const LeaderboardPage = () => {
+ const { leaders, setLeaders } = useLeaders();
+ const { setIsEasyMode, setForceEye, setForceCards } = useEasyMode();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const response = await getLeaders();
+ // Отсортируем результаты лидерборда в порядке возрастания времени
+ response.leaders.sort((a, b) => a.time - b.time);
+ // Если результатаов в лидерборде более 10, ограничим просмотр этим количеством
+ const lenLeaders = response.leaders.length < 10 ? response.leaders.length : 10;
+ const sortLeaders = response.leaders.slice(0, lenLeaders);
+ setLeaders(sortLeaders);
+ } catch (error) {
+ console.error(error);
+ throw new Error("Ошибка при получении списка лидеров");
+ }
+ };
+ fetchData();
+ }, [setLeaders]);
+
+ function getTimeViewer(innerSeconds) {
+ const minutes = Math.floor(innerSeconds / 60);
+ const seconds = innerSeconds % 60;
+ const formattedMinutes = minutes.toString().padStart(2, "0");
+ const formattedSeconds = seconds.toString().padStart(2, "0");
+ const viewedTime = `${formattedMinutes}:${formattedSeconds}`;
+ return viewedTime;
+ }
+
+ const handleStartGame = () => {
+ setIsEasyMode(false);
+ setForceEye(1);
+ setForceCards(2);
+ };
+
+ return (
+
+
+
+ Лидерборд
+
+
+
+
+
+
+
+
+ Позиция
+
+
+ Пользователь
+
+
+ Достижения
+
+
+ Время
+
+
+
+ {leaders.map((sortLeader, index) => {
+ return (
+
+
+
+ # {index + 1}
+
+
+ {sortLeader.name}
+
+
+
+ {sortLeader.achievements.includes(1) ? (
+
+
Игра пройдена в сложном режиме
+

+
+ ) : (
+

+ )}
+
+
+
+ {sortLeader.achievements.includes(2) ? (
+
+
+ Игра пройдена
без супер-сил
+
+

+
+ ) : (
+

+ )}
+
+
+
+
+ {getTimeViewer(sortLeader.time)}
+
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css
new file mode 100644
index 000000000..5b7c47604
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css
@@ -0,0 +1,193 @@
+.container {
+ width: 944px;
+ min-height: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #004980;
+ box-sizing: border-box;
+ margin-left: 40px;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ width: 944px;
+}
+
+.title {
+ font-family: StratosSkyeng;
+ font-weight: 400;
+ font-size: 24px;
+ line-height: 32px;
+ color: #fff;
+}
+
+.header {
+ margin-top: 52px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.table {
+ margin-top: 50px;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.boxTitle,
+.boxContent {
+ width: 889px;
+ height: 32px;
+ display: flex;
+ flex-direction: row;
+ color: #999999;
+ justify-content: left;
+ align-items: center;
+ padding-top: 16px;
+ margin-left: 20px;
+}
+
+.boxTitle {
+ gap: 66px;
+}
+
+.boxContent {
+ color: #000;
+}
+
+.tableHeader,
+.tableBody {
+ width: 944px;
+ height: 64px;
+ border-radius: 12px;
+ background-color: #fff;
+ opacity: 0px;
+}
+
+.tableBody {
+ color: #000;
+}
+.userPosition,
+.userName,
+.userTime,
+.userAchievements {
+ font-family: Montserrat, Poppins;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ margin-right: 66px;
+}
+
+.userName {
+ margin-right: 3px;
+ width: 290px;
+}
+
+.userTime {
+ margin-right: 0;
+}
+
+.position,
+.name,
+.time,
+.achievements {
+ font-family: Montserrat;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+}
+
+.position,
+.userPosition {
+ width: 178px;
+}
+
+.name {
+ width: 227px;
+}
+
+.time,
+.userTime {
+ width: 92px;
+}
+
+.achievements,
+.userAchievements {
+ width: 194px;
+}
+
+.emptySpace {
+ width: 122px;
+ margin-left: 66px;
+}
+
+.boxAchievement {
+ width: 32px;
+}
+
+.blockAchiev {
+ width: 70px;
+ display: flex;
+ flex-direction: row;
+}
+
+.spaceAchiev {
+ width: 6px;
+}
+
+.tooltip,
+.tooltip2 {
+ position: relative;
+ display: inline-block;
+}
+
+.tooltip .tooltiptext,
+.tooltip2 .tooltiptext2 {
+ visibility: hidden;
+ width: 212px;
+ height: auto;
+ background-color: #c2f5ff;
+ color: #004980;
+ font-size: 18px;
+ font-weight: 400;
+ font-family: Inter;
+ line-height: 24px;
+ text-align: center;
+ border-radius: 12px;
+ padding: 10px 0;
+ position: absolute;
+ bottom: 40px;
+ z-index: 1;
+}
+
+.tooltip2 .tooltiptext2 {
+ width: 187px;
+}
+
+.tooltip:hover .tooltiptext,
+.tooltip2:hover .tooltiptext2 {
+ visibility: visible;
+ /* -webkit-filter: brightness(70%);
+ -webkit-transition: all 2s ease;
+ -moz-transition: all 2s ease;
+ -o-transition: all 2s ease;
+ -ms-transition: all 2s ease;
+ transition: all 2s ease; */
+}
+
+.tooltip .tooltiptext::after,
+.tooltip2 .tooltiptext2::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 10%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: #c2f5ff transparent transparent transparent;
+}
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..809c429f7 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,28 +1,67 @@
-import { Link } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
+import { Button } from "../../components/Button/Button";
+import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode";
export function SelectLevelPage() {
+ const { isEasyMode, setIsEasy, selectedLevel, setSelectedLevel, clearIsEasy } = useEasyMode();
+ const navigate = useNavigate();
+
+ const handleEasyModeChange = event => {
+ setIsEasy(event.target.checked);
+ };
+
+ const arrLevel = [3, 6, 9];
+
+ const handleCheckboxChange = async level => {
+ setSelectedLevel(level);
+ };
+
+ const handleStartClick = () => {
+ if (!isEasyMode) {
+ clearIsEasy();
+ }
+ if (selectedLevel !== null) {
+ navigate(`/game/${selectedLevel}`);
+ } else {
+ alert("Нужно выбрать уровень");
+ }
+ };
+
return (
Выбери сложность
+
+
+
+ Перейти к лидерборду
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..4decd3134 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -48,7 +48,10 @@
background: #fff;
}
-.levelLink {
+.checkboxButton {
+ width: 97px;
+ height: 98px;
+ border-radius: 12px;
color: #0080c1;
text-align: center;
font-family: StratosSkyeng;
@@ -57,8 +60,69 @@
font-weight: 400;
line-height: 72px;
text-decoration: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ /* margin-bottom: 28px; */
}
-.levelLink:visited {
- color: #0080c1;
+.checkboxButton:hover {
+ color: #08608f;
+ background-color: #e7f06c;
+}
+
+.selected {
+ color: #08608f;
+ background-color: #e7f06c;
+ border: 2px solid #2e9aff;
+}
+
+.selLevel {
+ display: none;
+}
+
+.buttonStart {
+ /* width: 276px;
+ height: 45px;
+ background-color: #7ac100;
+ border-radius: 12px;
+ border: "0.7px solid var(--palette-navy-60, #7ac100)";
+ outline: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ line-height: 32px;
+ font-weight: 400;
+ letter-spacing: -0.14px;
+ color: #fff; */
+ margin-top: 32px;
+}
+
+.easyMode {
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ line-height: 32px;
+ font-weight: 400;
+ letter-spacing: -0.14px;
+ color: #000;
+}
+
+.easyModeCheck {
+ width: 30px;
+ height: 29px;
+ color: #5a6b3e;
+ background: #7ac100;
+ margin-bottom: 28px;
+}
+
+.goLeaderboard {
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ line-height: 32px;
+ font-weight: 400;
+ color: #004980;
+ margin-top: 16px;
}
diff --git a/src/router.js b/src/router.js
index da6e94b51..ecd8d90ae 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