diff --git a/.eslintrc.json b/.eslintrc.json
index e37e1e072..13018ddc0 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,7 +3,12 @@
"plugins": ["prettier"],
"rules": {
"camelcase": ["error", { "properties": "never" }],
- "prettier/prettier": "error",
+ "prettier/prettier": [
+ "error",
+ {
+ "endOfLine": "auto"
+ }
+],
"eqeqeq": ["error", "always"],
"no-unused-vars": ["error"]
}
diff --git a/docs/mvp-spec.md b/docs/mvp-spec.md
index fab47685e..e083d7788 100644
--- a/docs/mvp-spec.md
+++ b/docs/mvp-spec.md
@@ -14,9 +14,10 @@
Количество карточек для каждого уровня сложности можете назначать и свои или выбрать готовый пресет.
Предлагаем следующее пресеты:
- - Легкий уровень - 6 карточек (3 пары)
- - Средний уровень - 12 карточек (6 пар)
- - Сложный уровень - 18 карточек (9 пар)
+
+- Легкий уровень - 6 карточек (3 пары)
+- Средний уровень - 12 карточек (6 пар)
+- Сложный уровень - 18 карточек (9 пар)
Как только уровень сложности выбран, игроку показывается на игровой поле.
diff --git a/lefthook.yml b/lefthook.yml
index 87752f862..3a6272ee7 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -18,3 +18,4 @@ pre-commit:
eslint:
glob: "*.{js,jsx}"
run: npm run lint
+"prettier/prettier": ["error", { "endOfLine": "auto" }]
diff --git a/package-lock.json b/package-lock.json
index edaf5083f..c20f8c388 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"classnames": "^2.3.2",
+ "clsx": "^2.1.1",
"gh-pages": "^6.0.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
@@ -6111,6 +6112,14 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -22658,6 +22667,11 @@
"wrap-ansi": "^7.0.0"
}
},
+ "clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
+ },
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
diff --git a/package.json b/package.json
index e9b7a089e..f9e347ea6 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"classnames": "^2.3.2",
+ "clsx": "^2.1.1",
"gh-pages": "^6.0.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
diff --git a/src/API/leaders.js b/src/API/leaders.js
new file mode 100644
index 000000000..0846ecf15
--- /dev/null
+++ b/src/API/leaders.js
@@ -0,0 +1,29 @@
+export async function getLeaders() {
+ try {
+ const response = await fetch("https://wedev-api.sky.pro/api/leaderboard/", {
+ method: "GET",
+ });
+ const isResponseOk = response.ok;
+ const result = await response.json();
+ if (isResponseOk) {
+ return result.leaders;
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ throw new Error(error.message);
+ }
+}
+
+//добавление лидера в список
+export const addLeader = async data => {
+ const response = await fetch("https://wedev-api.sky.pro/api/leaderboard/", {
+ method: "POST",
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ throw new Error("Упс, ошибка");
+ }
+ return response.json();
+};
diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx
index 7526a56c8..23b2125a4 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -1,10 +1,11 @@
import { shuffle } from "lodash";
-import { useEffect, useState } from "react";
+import { useContext, useEffect, useState } from "react";
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 { EasyContext } from "../../context/Context";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -41,6 +42,7 @@ function getTimerValue(startDate, endDate) {
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
+ const { isEasyMode, tries, setTries } = useContext(EasyContext);
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const [cards, setCards] = useState([]);
// Текущий статус игры
@@ -73,6 +75,9 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
+ if (isEasyMode) {
+ setTries(3);
+ }
}
/**
@@ -127,6 +132,27 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
+ if (isEasyMode) {
+ setTries(tries - 1);
+ if (tries === 1) {
+ finishGame(STATUS_LOST);
+ } else {
+ const newCards = cards.map(c => {
+ if (openCardsWithoutPair.find(card => card.id === c.id)) {
+ return {
+ ...c,
+ open: false,
+ };
+ }
+ return c;
+ });
+
+ setTimeout(() => {
+ setCards(newCards);
+ }, 500);
+ }
+ return;
+ }
finishGame(STATUS_LOST);
return;
}
@@ -196,6 +222,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
)}
{status === STATUS_IN_PROGRESS ? : null}
+ {isEasyMode && Колличество жизней: {tries}}
diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx
index 722394833..4ce815b1c 100644
--- a/src/components/EndGameModal/EndGameModal.jsx
+++ b/src/components/EndGameModal/EndGameModal.jsx
@@ -4,24 +4,75 @@ import { Button } from "../Button/Button";
import deadImageUrl from "./images/dead.png";
import celebrationImageUrl from "./images/celebration.png";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { useState } from "react";
+import { addLeader } from "../../API/leaders.js";
export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) {
- const title = isWon ? "Вы победили!" : "Вы проиграли!";
+ const { pairsCount } = useParams();
+ const [error, setError] = useState();
+ const nav = useNavigate();
+
+ const thirdLevelPairs = 9;
+ const isLeader = isWon && Number(pairsCount) === thirdLevelPairs;
+ const title = isLeader ? "Вы попали на лидерборд!" : isWon ? "Вы выйграли!" : "Вы проиграли!";
+
+ //const title = isWon ? "Вы выйграли!" : "Вы проиграли!";
const imgSrc = isWon ? celebrationImageUrl : deadImageUrl;
const imgAlt = isWon ? "celebration emodji" : "dead emodji";
+ const [leader, setAddLeader] = useState({
+ name: "",
+ time: gameDurationMinutes.toString().padStart("2", "0") + gameDurationSeconds.toString().padStart("2", "0"),
+ });
+
+ const addLeaderToList = async e => {
+ e.preventDefault();
+ if (leader.name === "") {
+ setError("Введите имя пользователя");
+ return;
+ }
+ try {
+ await addLeader({ name: leader.name, time: leader.time }).then(res => {
+ setAddLeader(res.leaders);
+ nav("/leaderBoard");
+ });
+ } catch (error) {
+ setError(error.message);
+ }
+ };
+
return (
{title}
+ {isLeader ? (
+
+ setAddLeader({ ...leader, name: e.target.value })}
+ id="name-input"
+ type="text"
+ name="name"
+ className={styles.input}
+ placeholder="Пользователь"
+ />
+
+ ) : null}
Затраченное время:
- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
+ {gameDurationMinutes.toString().padStart("2", "0")}: {gameDurationSeconds.toString().padStart("2", "0")}
-
-
+
+ {isLeader ? (
+
+
+
+ ) : null}
+ {error &&
{error}
}
);
}
diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css
index 9368cb8b5..4d18073f3 100644
--- a/src/components/EndGameModal/EndGameModal.module.css
+++ b/src/components/EndGameModal/EndGameModal.module.css
@@ -1,12 +1,15 @@
.modal {
width: 480px;
- height: 459px;
+ min-height: 459px;
border-radius: 12px;
background: #c2f5ff;
display: flex;
+ gap: 10px;
flex-direction: column;
justify-content: center;
align-items: center;
+ padding-top: 18px;
+ padding-bottom: 28px;
}
.image {
@@ -23,8 +26,9 @@
font-style: normal;
font-weight: 400;
line-height: 48px;
-
margin-bottom: 28px;
+ width: 276px;
+ text-align: center;
}
.description {
@@ -49,3 +53,37 @@
margin-bottom: 40px;
}
+
+.btnLeaderBoard {
+ padding-top: 10px;
+ background: none;
+ border: none;
+ text-decoration: underline;
+ cursor: pointer;
+ font-size: 18px;
+ font-family: Roboto;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #565eef;
+}
+.userBlockName {
+ margin-bottom: 10px;
+}
+.input {
+ width: 276px;
+ height: 45px;
+ top: 334px;
+ left: 374px;
+ gap: 0px;
+ border-radius: 10px;
+ opacity: 0px;
+ border: none;
+ margin-bottom: 30px;
+ font-family: Roboto;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+ color: #999999;
+}
diff --git a/src/context/Context.jsx b/src/context/Context.jsx
new file mode 100644
index 000000000..ff14509f5
--- /dev/null
+++ b/src/context/Context.jsx
@@ -0,0 +1,9 @@
+import { createContext, useState } from "react";
+
+export const EasyContext = createContext(false);
+
+export const EasyProvider = ({ children }) => {
+ const [tries, setTries] = useState(3);
+ const [isEasyMode, setEasyMode] = useState(false);
+ return
{children};
+};
diff --git a/src/index.css b/src/index.css
index 78f0d3a2b..38e57fd1c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,3 +1,9 @@
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
html {
margin: 0;
}
@@ -35,3 +41,8 @@ ol {
font-weight: 400;
font-style: normal;
}
+
+ul {
+ list-style-type: none;
+}
+
diff --git a/src/index.js b/src/index.js
index f689c5f0b..c955c852f 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..476cda8f6
--- /dev/null
+++ b/src/pages/LeaderBoard/LeaderBoard.jsx
@@ -0,0 +1,46 @@
+import React, { useEffect, useState } from "react";
+import { Button } from "../../components/Button/Button";
+import styles from "./LeaderBoard.module.css";
+import { getLeaders } from "../../API/leaders";
+import { Link } from "react-router-dom";
+
+const LeaderBoard = () => {
+ const [leaders, setLeaders] = useState([{ position: "Позиция", name: "Пользователь", time: "Время" }]);
+ useEffect(() => {
+ getLeaders().then(data => {
+ setLeaders([...leaders, ...data.sort((a, b) => a.time - b.time)]);
+ });
+ }, []);
+
+ const timeFormat = digit => {
+ let minutes = Math.floor(digit / 60);
+ let seconds = digit % 60;
+ return [minutes < 10 ? "0" + minutes : minutes, ":", seconds < 10 ? "0" + seconds : seconds];
+ };
+
+ return (
+
+
+
Лидерборд
+
+
+
+
+
+
+ {leaders.map((player, index) => (
+ -
+
+ - {index === 0 ? player.position : player.id}
+ - {player.name}
+ - {index === 0 ? player.time : timeFormat(player.time)}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default LeaderBoard;
diff --git a/src/pages/LeaderBoard/LeaderBoard.module.css b/src/pages/LeaderBoard/LeaderBoard.module.css
new file mode 100644
index 000000000..bac7ded00
--- /dev/null
+++ b/src/pages/LeaderBoard/LeaderBoard.module.css
@@ -0,0 +1,67 @@
+.leaderBoardBlock {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-family: "StratosSkyeng";
+}
+
+.leaderBoarHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: white;
+ width: 944px;
+ height: 142px;
+}
+
+.players {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+}
+
+.statisticItem {
+ width: 944px;
+ height: 64px;
+ background-color: #ffffff;
+ border-radius: 12px;
+}
+
+.playerStatistic {
+ display: flex;
+ align-items: center;
+ height: 64px;
+ font-size: 24px;
+ padding: 20px 16px;
+}
+.statisticItem:first-child {
+ color: #999999;
+}
+
+.firstColumn {
+ width: 244px;
+}
+
+.secondColumn {
+ width: 558px;
+}
+
+.thirdColumn {
+ width: 102px;
+}
+
+.leaderBoardTitle {
+ font-weight: 400;
+ font-size: 32px;
+}
+
+/* .btnLeaderBoard {
+ font-family: Roboto;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ background: #565EEF;
+
+} */
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..10f4f451d 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,28 +1,55 @@
-import { Link } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
-
+import { useContext, useState } from "react";
+import { EasyContext } from "../../context/Context";
+import clsx from "clsx";
+import { Button } from "../../components/Button/Button";
+const levels = [
+ {
+ level: 1,
+ pairs: 3,
+ },
+ {
+ level: 2,
+ pairs: 6,
+ },
+ {
+ level: 3,
+ pairs: 9,
+ },
+];
export function SelectLevelPage() {
+ const { isEasyMode, setEasyMode } = useContext(EasyContext);
+ const [level, setLevel] = useState(3);
+ const navigate = useNavigate();
+ function onClick(value) {
+ setLevel(value);
+ }
+ function onStart() {
+ navigate(`/game/${level}`);
+ }
return (
Выбери сложность
- -
-
- 1
-
-
- -
-
- 2
-
-
- -
-
- 3
-
-
+ {levels.map(l => (
+ - onClick(l.pairs)} className={clsx(styles.level, { [styles.active]: l.pairs === level })}>
+
{l.level}
+
+ ))}
+
+
+
+
+
+ Перейти к Лидерборду
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..2475bdfa0 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -62,3 +62,54 @@
.levelLink:visited {
color: #0080c1;
}
+
+.active {
+ outline: 4px dotted teal;
+}
+
+.checkbox {
+ width: 20px;
+ height: 20px;
+ border-radius: 5px;
+ border: 1px dotted teal;
+ background-color: white;
+ position: relative;
+ color: #7ac100;
+}
+.label {
+ display: flex;
+ gap: 5px;
+ flex-direction: row-reverse;
+ padding-bottom: 20px;
+ font-family: Roboto;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+}
+.label input {
+ appearance: none;
+}
+.label input:checked + div::after {
+ content: "\2714";
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+.Link {
+ padding-top: 20px;
+ width: 192px;
+ height: 32px;
+ top: 571px;
+ left: 417px;
+ gap: 0px;
+ opacity: 0px;
+ font-family: Roboto;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #004980;
+}
diff --git a/src/router.js b/src/router.js
index da6e94b51..dee6ea7be 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