diff --git a/.eslintrc.json b/.eslintrc.json
index e37e1e072..cae26617b 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,7 +3,7 @@
"plugins": ["prettier"],
"rules": {
"camelcase": ["error", { "properties": "never" }],
- "prettier/prettier": "error",
+ "prettier/prettier": "off",
"eqeqeq": ["error", "always"],
"no-unused-vars": ["error"]
}
diff --git a/README.md b/README.md
index 9b90842c4..ae5563c6d 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,7 @@ https://skypro-web-developer.github.io/react-memo/
Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
+
+## Ожидаемое время выполнения работы: 6 часов.
+
+## Фактическое время выполнения: больше 20 часов.
\ No newline at end of file
diff --git a/src/api/Api.js b/src/api/Api.js
new file mode 100644
index 000000000..8daa31f1f
--- /dev/null
+++ b/src/api/Api.js
@@ -0,0 +1,27 @@
+const URL = "https://wedev-api.sky.pro/api/leaderboard";
+
+export const getLeadersPage = async () => {
+ const response = await fetch(URL, {
+ method: "GET",
+ });
+ if (!response.ok) {
+ throw new Error("Не удалось получить данные");
+ }
+ const data = await response.json()
+ return data.leaders;
+};
+
+export const createLeader = async (name, time) => {
+ const response = await fetch(URL, {
+ method: "POST",
+ body: JSON.stringify ({
+ name,
+ time,
+ })
+ })
+ if (response.status === 400) {
+ throw new Error ("Не удалось загрузить данные")
+ }
+ const data = await response.json()
+ return data.leaders;
+}
\ No newline at end of file
diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx
index 7526a56c8..5a747e94b 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -5,6 +5,7 @@ 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 { useEasyContext } from "../../context/useContext";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -41,6 +42,9 @@ function getTimerValue(startDate, endDate) {
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
+ const isHardMode = pairsCount === 3;
+ const { isEasyMode } = useEasyContext();
+ const [tries, setTries] = useState(3);
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const [cards, setCards] = useState([]);
// Текущий статус игры
@@ -69,6 +73,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setStatus(STATUS_IN_PROGRESS);
}
function resetGame() {
+ setTries(3);
setGameStartDate(null);
setGameEndDate(null);
setTimer(getTimerValue(null, null));
@@ -82,6 +87,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
* - "Игрок проиграл", если на поле есть две открытые карты без пары
* - "Игра продолжается", если не случилось первых двух условий
*/
+
const openCard = clickedCard => {
// Если карта уже открыта, то ничего не делаем
if (clickedCard.open) {
@@ -126,8 +132,31 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const playerLost = openCardsWithoutPair.length >= 2;
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
+
if (playerLost) {
- finishGame(STATUS_LOST);
+ if (isEasyMode) {
+ setTries(tries - 1);
+ setTimeout(() => {
+ setCards(
+ cards.reduce((accum, card) => {
+ if (card.id === clickedCard.id) {
+ return [...accum, { ...card, open: false }];
+ }
+ return [...accum, card];
+ }, []),
+ );
+ setCards(
+ cards.reduce((accum, card) => {
+ const firstCard = openCardsWithoutPair.find(el => el.id !== clickedCard.id);
+ if (card.id === firstCard.id) {
+ return [...accum, { ...card, open: false }];
+ }
+ return [...accum, card];
+ }, []),
+ );
+ }, 1000);
+ }
+ if (!isEasyMode || tries === 1) finishGame(STATUS_LOST);
return;
}
@@ -196,6 +225,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
)}
{status === STATUS_IN_PROGRESS ? : null}
+ {isEasyMode ? Количество попыток: {tries} : ""}
@@ -214,6 +244,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
- {title}
+ {!isLeader && {title}
}
+
+ {isLeader && (
+
+
Вы попали на Лидерборд!
+
+
+ )}
+ {error && {error}
}
+
+
Затраченное время:
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
-
-
+
+ {isLeader && (
+
+ Перейти к лидерборду
+
+ )}
);
}
diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css
index 9368cb8b5..bb2c4c7a2 100644
--- a/src/components/EndGameModal/EndGameModal.module.css
+++ b/src/components/EndGameModal/EndGameModal.module.css
@@ -1,12 +1,14 @@
.modal {
width: 480px;
- height: 459px;
+ max-height: 634px;
border-radius: 12px;
background: #c2f5ff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+ padding-bottom: 48px;
+ padding-top: 40px;
}
.image {
@@ -27,6 +29,27 @@
margin-bottom: 28px;
}
+.leaderText {
+ text-align: center
+}
+
+.placeholder {
+ border-radius: 10px;
+ border: none;
+ background: rgb(255, 255, 255);
+ width: 276px;
+ height: 45px;
+ left: 374px;
+ top: 334px;
+ /* margin-bottom: 28px; */
+ color: black;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ letter-spacing: 0%;
+ text-align: center;
+}
+
.description {
color: #000;
font-variant-numeric: lining-nums proportional-nums;
@@ -35,7 +58,7 @@
font-style: normal;
font-weight: 400;
line-height: 32px;
-
+ margin-top: 28px;
margin-bottom: 10px;
}
@@ -49,3 +72,40 @@
margin-bottom: 40px;
}
+
+.toLeaderboard {
+ font-size: 18px;
+ color: #565EEF;
+ font-family: StratosSkyeng;
+ margin-top: 18px;
+ text-decoration-line: underline;
+ line-height: 32px;
+}
+
+.error {
+ margin-bottom: 20px;
+ font-size: 18px;
+ color: red;
+ font-family: StratosSkyeng;
+}
+
+.buttonLeader {
+ border-radius: 8px;
+ margin-top: 10px;
+ border: none;
+ width: 150px;
+ height: 30px;
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ background: rgb(255, 255, 255);
+ color: rgba(80, 75, 75, 0.675);
+}
+
+.buttonLeader:hover {
+ color: #000;
+ cursor: pointer;
+}
+
+.buttonLeader:disabled {
+ color: lightgrey;
+}
\ No newline at end of file
diff --git a/src/context/context.jsx b/src/context/context.jsx
new file mode 100644
index 000000000..8d2b4885e
--- /dev/null
+++ b/src/context/context.jsx
@@ -0,0 +1,9 @@
+import { createContext, useState } from "react";
+
+export const EasyContext = createContext(true);
+
+export const EasyProvider = ({ children }) => {
+ const [isEasyMode, setEasyMode] = useState(false);
+ return
{children};
+};
+
diff --git a/src/context/useContext.js b/src/context/useContext.js
new file mode 100644
index 000000000..155f8e67c
--- /dev/null
+++ b/src/context/useContext.js
@@ -0,0 +1,6 @@
+import { useContext } from "react"
+import { EasyContext } from "./context"
+
+export const useEasyContext = () => {
+ return useContext(EasyContext);
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index f689c5f0b..871d7d979 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,13 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
+import { EasyProvider } from "./context/context";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
+
+
,
);
diff --git a/src/pages/Leaderboard/Leaderboard.jsx b/src/pages/Leaderboard/Leaderboard.jsx
new file mode 100644
index 000000000..aed3d7d55
--- /dev/null
+++ b/src/pages/Leaderboard/Leaderboard.jsx
@@ -0,0 +1,60 @@
+import { useState } from "react";
+import { getLeadersPage } from "../../api/Api";
+import { Button } from "../../components/Button/Button";
+import styles from "./Leaderboard.module.css";
+import { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+
+
+
+
+export function Leaderboard() {
+ const [leaderArr, setLeaderArr] = useState([]);
+ const [error, setError] = useState(null);
+ const navigate = useNavigate();
+
+ function StartGame() {
+navigate("/game/9")
+ }
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const data = await getLeadersPage();
+ const filterData = data
+ .sort((a, b) => a.time - b.time)
+ .slice(0, 10);
+ setLeaderArr(filterData);
+ } catch (error) {
+ setError("Не удалось загрузить данные");
+ }
+ }
+ fetchData();
+ }, []);
+
+ return (
+
+
+
Лидерборд
+
+
+
+
+
Позиция
+
Пользователь
+
+
Время
+
+ {leaderArr.map((leader, index) => (
+
+
# {index + 1}
+
{leader.name}
+
+
{`${Math.floor(leader.time / 60).toString().padStart(2, 0)}:${(leader.time % 60).toString().padStart(2, 0)}`}
+
+ ))}
+ {error}
+
+
+ );
+}
diff --git a/src/pages/Leaderboard/Leaderboard.module.css b/src/pages/Leaderboard/Leaderboard.module.css
new file mode 100644
index 000000000..ec0e95140
--- /dev/null
+++ b/src/pages/Leaderboard/Leaderboard.module.css
@@ -0,0 +1,67 @@
+.container {
+ width: 944px;
+ margin: 0 auto;
+ padding: 26px;
+ padding-top: 52px;
+ box-sizing: border-box;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: end;
+ margin-bottom: 40px;
+ }
+
+.title {
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ color: #fff;
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.contentLeaders,
+.contentTitle {
+ display: flex;
+ justify-content: space-between;
+ gap: 66px;
+ align-items: center;
+ border: 3px solid none;
+ background-color: #fff;
+ border-radius: 12px;
+ height: 64px;
+ font-size: 24px;
+ font-family: StratosSkyeng;
+ padding-left: 20px;
+ padding-right: 20px;
+ color: #999;
+}
+
+.contentLeaders {
+ color: black;
+}
+
+.positionTitle {
+width: 178px;
+/* color: #999; */
+}
+
+.nameTitle {
+ width: 324px;
+ /* color: #999; */
+}
+
+.reserveTitle {
+ width: 102px;
+ /* color: #999; */
+}
+
+.timeTitle {
+ width: 102px;
+ /* color: #999; */
+}
\ No newline at end of file
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..8563bea26 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,28 +1,93 @@
-import { Link } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
+import { useEasyContext } from "../../context/useContext";
+import { Button } from "../../components/Button/Button";
+import { useState } from "react";
export function SelectLevelPage() {
+ const navigate = useNavigate();
+ const [error, setError] = useState(null);
+ const { isEasyMode, setEasyMode } = useEasyContext();
+
+ const [level, setLevel] = useState(null);
+
+ function ChengeLevel(e) {
+ setLevel(e.target.value);
+ }
+
+ function StartGame() {
+ if (!level) {
+ setError("Выбери уровень сложности");
+ return;
+ }
+ navigate(level);
+ }
+
return (
Выбери сложность
+
+
+
+
{error}
+
+
+ Перейти к лидерборду
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..98214521f 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -8,7 +8,7 @@
.modal {
width: 480px;
- height: 459px;
+ height: 529px;
border-radius: 12px;
background: #c2f5ff;
display: flex;
@@ -26,6 +26,7 @@
font-style: normal;
font-weight: 400;
line-height: 48px;
+ width: 205px;
}
.levels {
@@ -43,7 +44,6 @@
flex-direction: column;
justify-content: center;
flex-shrink: 0;
-
border-radius: 12px;
background: #fff;
}
@@ -62,3 +62,74 @@
.levelLink:visited {
color: #0080c1;
}
+
+.modeSelection {
+ display: flex;
+ margin-bottom: 38px;
+}
+
+.nameMode {
+ font-family: StratosSkyeng;
+ font-size: 24px;
+}
+
+.checkbox {
+ position: absolute;
+ z-index: -1;
+ opacity: 0;
+}
+
+.customCheckbox {
+ position: relative;
+ display: inline-block;
+ width: 30px;
+ height: 30px;
+ border-radius: 0.25em;
+ margin-right: 0.5em;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-color: #fff;
+}
+
+.customCheckbox::before {
+ content: '';
+ width: 22px;
+ height: 17px;
+ display: inline-block;
+ background-image: url(./Vector.png);
+ background-repeat: no-repeat;
+ margin-left: 4px;
+ transform: scale(0);
+}
+
+.checkbox:checked+.customCheckbox::before {
+ transform: scale(1);
+}
+
+.toLeaderboard {
+ font-size: 18px;
+ color: #565EEF;
+ font-family: StratosSkyeng;
+ margin-top: 18px;
+ text-decoration-line: underline;
+ line-height: 32px;
+}
+
+.levelButton {
+ display: none;
+
+ &[type="radio"]:checked + label {
+ text-shadow: 5px 5px 5px gray;
+ position: relative;
+ bottom: 3px;
+ right: 3px;
+
+ }
+}
+
+.error {
+ margin-bottom: 20px;
+ font-size: 18px;
+ color: red;
+ font-family: StratosSkyeng;
+}
\ No newline at end of file
diff --git a/src/pages/SelectLevelPage/Vector.png b/src/pages/SelectLevelPage/Vector.png
new file mode 100644
index 000000000..4e81b49ab
Binary files /dev/null and b/src/pages/SelectLevelPage/Vector.png differ
diff --git a/src/router.js b/src/router.js
index da6e94b51..988eeebfd 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 { Leaderboard } from "./pages/Leaderboard/Leaderboard";
export const router = createBrowserRouter(
[
@@ -12,6 +13,10 @@ export const router = createBrowserRouter(
path: "/game/:pairsCount",
element:
,
},
+ {
+ path: "/leaderboard",
+ element:
,
+ }
],
/**
* basename нужен для корректной работы в gh pages