diff --git a/.gitignore b/.gitignore
index 4d29575de..e5bd9abd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+/.idea
+
# dependencies
/node_modules
/.pnp
diff --git a/README.md b/README.md
index 9b90842c4..dd21bb1e2 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,10 @@ https://skypro-web-developer.github.io/react-memo/
Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
+
+### Работа
+
+- Планируемое время: 15 часов
+- Фактическое время: 18 часов
+- Цель: Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
+Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
diff --git a/src/api/leaders.js b/src/api/leaders.js
new file mode 100644
index 000000000..daba6bbe1
--- /dev/null
+++ b/src/api/leaders.js
@@ -0,0 +1,30 @@
+const leadersURL = "https://wedev-api.sky.pro/api/leaderboard";
+
+export const getLeaders = () => {
+ return fetch(leadersURL, { method: "GET" }).then(response => {
+ if (!response.ok) {
+ throw new Error("Ошибка сервера");
+ }
+
+ if (response.status === 400) {
+ throw new Error("Полученные данные не в формате JSON!");
+ }
+ return response.json();
+ });
+};
+
+export const postLeaders = ({ resultLeaderboard }) => {
+ return fetch(leadersURL, {
+ method: "POST",
+ body: JSON.stringify(resultLeaderboard),
+ }).then(response => {
+ if (!response.ok) {
+ throw new Error("Ошибка сервера");
+ }
+
+ if (response.status === 400) {
+ throw new Error("Полученные данные не в формате JSON!");
+ }
+ return response.json();
+ });
+};
diff --git a/src/components/Card/Card.jsx b/src/components/Card/Card.jsx
index 2ba6a13c8..4a6dec133 100644
--- a/src/components/Card/Card.jsx
+++ b/src/components/Card/Card.jsx
@@ -1,6 +1,6 @@
import { CROSS_SUIT, DIAMONDS_SUIT, HEARTS_SUIT, SPADES_SUIT } from "../../const";
-import styles from "./Card.module.css";
+import styles from "./Card.module.css";
import heartsImageUrl from "./images/hearts.svg";
import crossImageUrl from "./images/cross.svg";
import spadesImageUrl from "./images/spades.svg";
diff --git a/src/components/Card/Card.module.css b/src/components/Card/Card.module.css
index 86c3fbb5b..c3235f6c5 100644
--- a/src/components/Card/Card.module.css
+++ b/src/components/Card/Card.module.css
@@ -79,8 +79,8 @@
}
.flipContainer:hover .flipper {
- transition: 0.2s;
- transform: rotateY(35deg);
+ /*transition: 0.2s;*/
+ /*transform: rotateY(35deg);*/
}
.flipContainer.flip .flipper {
diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx
index 7526a56c8..f46447573 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -1,10 +1,13 @@
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 { LivesContext } from "../context/livesContext";
+import { EasyModeContext } from "../context/easymodeContext";
+import { CardsContext } from "../context/cardsContext";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -42,7 +45,7 @@ function getTimerValue(startDate, endDate) {
*/
export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
- const [cards, setCards] = useState([]);
+ const { cards, setCards } = useContext(CardsContext);
// Текущий статус игры
const [status, setStatus] = useState(STATUS_PREVIEW);
@@ -50,6 +53,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const [gameStartDate, setGameStartDate] = useState(null);
// Дата конца игры
const [gameEndDate, setGameEndDate] = useState(null);
+ //Режим трех попыток
+ const { easyMode } = useContext(EasyModeContext);
+ console.log(easyMode);
+ //Счетчик жизней
+ const { lives, setLives } = useContext(LivesContext);
// Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate
const [timer, setTimer] = useState({
@@ -73,6 +81,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
+ setLives(3);
}
/**
@@ -126,12 +135,30 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const playerLost = openCardsWithoutPair.length >= 2;
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
- if (playerLost) {
+ if (playerLost && !easyMode) {
finishGame(STATUS_LOST);
return;
}
// ... игра продолжается
+ if (playerLost && easyMode) {
+ setLives(lives - 1);
+ nextCards.map(card => {
+ if (openCardsWithoutPair.some(opencard => opencard.id === card.id)) {
+ if (card.open) {
+ setTimeout(() => {
+ setCards(prev => {
+ return prev.map(el => (el.id === card.id ? { ...el, open: false } : el));
+ });
+ }, 1000);
+ }
+ }
+ });
+ if (lives === 1) {
+ finishGame(STATUS_LOST);
+ return;
+ }
+ }
};
const isGameEnded = status === STATUS_LOST || status === STATUS_WON;
@@ -164,6 +191,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// Обновляем значение таймера в интервале
useEffect(() => {
+ if (status === STATUS_LOST || status === STATUS_WON) return;
+
const intervalId = setInterval(() => {
setTimer(getTimerValue(gameStartDate, gameEndDate));
}, 300);
@@ -209,6 +238,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
/>
))}
+ {easyMode ?
Осталось попыток: {lives}
: ""}
{isGameEnded ? (
@@ -217,6 +247,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
+ pairsCount={pairsCount}
+ timer={timer}
/>
) : null}
diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css
index 000c5006c..f6b55c95d 100644
--- a/src/components/Cards/Cards.module.css
+++ b/src/components/Cards/Cards.module.css
@@ -70,3 +70,10 @@
margin-bottom: -12px;
}
+
+.subtitle {
+ color: #fff;
+ font-size: 18px;
+ line-height: 18px;
+ margin-top: 20px;
+}
diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx
index 722394833..d2c8a0096 100644
--- a/src/components/EndGameModal/EndGameModal.jsx
+++ b/src/components/EndGameModal/EndGameModal.jsx
@@ -1,27 +1,95 @@
import styles from "./EndGameModal.module.css";
-
import { Button } from "../Button/Button";
-
import deadImageUrl from "./images/dead.png";
import celebrationImageUrl from "./images/celebration.png";
+import { getTimeInSeconds, sortLeadersElements } from "../../utils/helper";
+import { useContext, useState } from "react";
+import { LeadersContext } from "../context/leaderContext";
+import { postLeaders } from "../../api/leaders";
+import { Link, useNavigate } from "react-router-dom";
-export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) {
- const title = isWon ? "Вы победили!" : "Вы проиграли!";
+export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, pairsCount, timer }) {
+ const timeLeaders = getTimeInSeconds({ minutes: gameDurationMinutes, seconds: gameDurationSeconds });
const imgSrc = isWon ? celebrationImageUrl : deadImageUrl;
const imgAlt = isWon ? "celebration emodji" : "dead emodji";
+ const [inputLeaders, setInputLeaders] = useState("");
+
+ const { leaders } = useContext(LeadersContext);
+
+ const sortedLeaders = sortLeadersElements(leaders);
+
+ const leadersLength = sortedLeaders.length;
+
+ const isLeadResult = sortedLeaders[leadersLength - 1].time > getTimeInSeconds(timer) && pairsCount === 9;
+
+ const title = isWon ? (isLeadResult ? "Вы попали на Лидерборд!" : "Вы победили!") : "Вы проиграли!";
+
+ const navigate = useNavigate();
+
+ const [error, setError] = useState("");
+
+ const onLeaders = () => {
+ const resultLeaderboard = {
+ name: inputLeaders,
+ time: timeLeaders,
+ };
+
+ postLeaders({ resultLeaderboard })
+ .then(res => {
+ console.log(res);
+ })
+ .catch(err => {
+ setError(err.message);
+ });
+ };
+
return (
{title}
+ {isLeadResult ? (
+ isWon ? (
+
+ ) : (
+ ""
+ )
+ ) : (
+ ""
+ )}
Затраченное время:
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
+
+ Перейти к лидерборду
+
+ {error}
);
}
diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css
index 9368cb8b5..516586a50 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: 459px;*/
border-radius: 12px;
background: #c2f5ff;
display: flex;
@@ -13,6 +13,24 @@
width: 96px;
height: 96px;
margin-bottom: 8px;
+ margin-top: 20px;
+}
+
+.input {
+ width: 276px;
+ height: 45px;
+ top: 334px;
+ left: 374px;
+ border-radius: 10px;
+ background: #ffffff;
+ font-family: Roboto;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+ border: none;
+ outline: none;
+ margin-bottom: 20px;
}
.title {
@@ -23,7 +41,7 @@
font-style: normal;
font-weight: 400;
line-height: 48px;
-
+ text-align: center;
margin-bottom: 28px;
}
@@ -35,7 +53,6 @@
font-style: normal;
font-weight: 400;
line-height: 32px;
-
margin-bottom: 10px;
}
@@ -46,6 +63,16 @@
font-style: normal;
font-weight: 400;
line-height: 72px;
-
margin-bottom: 40px;
}
+
+.link {
+ font-family: Roboto;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #565eef;
+ padding-top: 20px;
+ padding-bottom: 40px;
+}
diff --git a/src/components/Leaderboard/Leaderboard.jsx b/src/components/Leaderboard/Leaderboard.jsx
new file mode 100644
index 000000000..da0864b82
--- /dev/null
+++ b/src/components/Leaderboard/Leaderboard.jsx
@@ -0,0 +1,14 @@
+import styles from "./Leaderboard.module.css";
+
+export const Leaderboard = ({ position, user, time, color = "black" }) => {
+ return (
+ <>
+
+ {position}
+ {user}
+
+ {time}
+
+ >
+ );
+};
diff --git a/src/components/Leaderboard/Leaderboard.module.css b/src/components/Leaderboard/Leaderboard.module.css
new file mode 100644
index 000000000..c3a2a7b65
--- /dev/null
+++ b/src/components/Leaderboard/Leaderboard.module.css
@@ -0,0 +1 @@
+.position {
width: 178px;
font-family: Poppins;
font-size: 24px;
font-weight: 400;
line-height: 32px;
margin-left: 20px;
}
.user {
width: 324px;
font-family: Poppins;
font-size: 24px;
font-weight: 400;
line-height: 32px;
}
.time {
width: 102px;
font-family: Poppins;
font-size: 24px;
font-weight: 400;
line-height: 32px;
}
.item {
width: 944px;
height: 64px;
display: flex;
gap: 66px;
box-sizing: border-box;
padding-top: 16px;
margin-bottom: 16px;
font-family: StratosSkyeng;
font-size: 24px;
background-color: #fff;
border-radius: 12px;
}
\ No newline at end of file
diff --git a/src/components/context/cardsContext.jsx b/src/components/context/cardsContext.jsx
new file mode 100644
index 000000000..9ac77b5ba
--- /dev/null
+++ b/src/components/context/cardsContext.jsx
@@ -0,0 +1,7 @@
+import { createContext, useState } from "react";
+
+export const CardsContext = createContext(null);
+export const CardsProvider = ({ children }) => {
+ const [cards, setCards] = useState([]);
+ return {children};
+};
diff --git a/src/components/context/easymodeContext.jsx b/src/components/context/easymodeContext.jsx
new file mode 100644
index 000000000..710847f71
--- /dev/null
+++ b/src/components/context/easymodeContext.jsx
@@ -0,0 +1,7 @@
+import { createContext, useState } from "react";
+
+export const EasyModeContext = createContext(null);
+export const EasyModeProvider = ({ children }) => {
+ const [easyMode, setEasyMode] = useState(false);
+ return {children};
+};
diff --git a/src/components/context/leaderContext.jsx b/src/components/context/leaderContext.jsx
new file mode 100644
index 000000000..0d1b9c3a2
--- /dev/null
+++ b/src/components/context/leaderContext.jsx
@@ -0,0 +1,16 @@
+import { createContext, useEffect, useState } from "react";
+import { getLeaders } from "../../api/leaders";
+import { sortLeadersElements } from "../../utils/helper";
+
+export const LeadersContext = createContext(null);
+
+export const LeadersProvider = ({ children }) => {
+ const [leaders, setLeaders] = useState([]);
+ useEffect(() => {
+ getLeaders().then(leaders => {
+ const sortedLeaders = sortLeadersElements(leaders.leaders);
+ setLeaders(sortedLeaders.splice(1, 10));
+ });
+ }, []);
+ return {children};
+};
diff --git a/src/components/context/livesContext.jsx b/src/components/context/livesContext.jsx
new file mode 100644
index 000000000..f5ce08251
--- /dev/null
+++ b/src/components/context/livesContext.jsx
@@ -0,0 +1,8 @@
+import { createContext, useState } from "react";
+
+export const LivesContext = createContext(null);
+
+export const LivesProvider = ({ children }) => {
+ const [lives, setLives] = useState(3);
+ return {children};
+};
diff --git a/src/index.css b/src/index.css
index 78f0d3a2b..af079ac5b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,37 +1 @@
-html {
- margin: 0;
-}
-
-body {
- background-color: #004980;
- margin: 0;
- height: 100vh;
-}
-
-#root {
- width: 100%;
- height: 100%;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-ul,
-p,
-li,
-ol {
- margin: 0;
- padding: 0;
-}
-
-@font-face {
- font-family: "StratosSkyeng";
- src:
- url("../public/assets/fonts/StratosSkyeng.woff2") format("woff2"),
- local("Arial");
- font-weight: 400;
- font-style: normal;
-}
+html {
margin: 0;
}
body {
background-color: #004980;
margin: 0;
height: 100vh;
}
#root {
width: 100%;
height: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6,
ul,
p,
li,
ol {
margin: 0;
padding: 0;
}
@font-face {
font-family: "StratosSkyeng";
src:
url("../public/assets/fonts/StratosSkyeng.woff2") format("woff2"),
local("Arial");
font-weight: 400;
font-style: normal;
}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index f689c5f0b..d2ce9f40f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,22 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
+import { LivesProvider } from "./components/context/livesContext";
+import { EasyModeProvider } from "./components/context/easymodeContext";
+import { LeadersProvider } from "./components/context/leaderContext";
+import { CardsProvider } from "./components/context/cardsContext";
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..a59f3c3af
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx
@@ -0,0 +1,50 @@
+import { useContext, useEffect } from "react";
+import { LeadersContext } from "../../components/context/leaderContext";
+import { getLeaders } from "../../api/leaders";
+import { sortLeadersElements } from "../../utils/helper";
+import { Link } from "react-router-dom";
+import { Button } from "../../components/Button/Button";
+import { Leaderboard } from "../../components/Leaderboard/Leaderboard";
+import styles from "./LeaderboardPage.module.css";
+
+export function LeaderboardPage() {
+ const { leaders, setLeaders } = useContext(LeadersContext);
+
+ const formatTime = timeInSeconds => {
+ const seconds = ("0" + String(timeInSeconds % 60)).slice(-2);
+ const minutes = ("0" + String(Math.floor(timeInSeconds / 60))).slice(-2);
+
+ return `${minutes}:${seconds}`;
+ };
+
+ useEffect(() => {
+ getLeaders().then(leaders => {
+ const sortedLeaders = sortLeadersElements(leaders.leaders);
+ setLeaders(sortedLeaders.splice(1, 10));
+ });
+ }, []);
+
+ return (
+
+
+ Лидерборд
+
+
+
+
+
+
+
Позиция
+
Пользователь
+
+
Время
+
+
+ {leaders.map((el, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css
new file mode 100644
index 000000000..5a479e8a0
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css
@@ -0,0 +1,74 @@
+.container {
+ padding-left: calc(50% - 472px);
+ padding-right: calc(50% - 472px);
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 50px;
+ padding-bottom: 50px;
+}
+
+.title {
+ font-family: Roboto;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #FFFFFF;
+}
+
+.box {
+ display: flex;
+ gap: 66px;
+ justify-content: space-between;
+ width: 944px;
+ height: 64px;
+ box-sizing: border-box;
+ padding-top: 16px;
+ margin-bottom: 16px;
+ font-size: 24px;
+ font-family: StratosSkyeng;
+ background: #FFFFFF;
+ border-radius: 12px;
+}
+
+.subtitle1 {
+ width: 178px;
+ font-family: Poppins;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ color: #999999;
+ margin-left: 20px;
+}
+
+.subtitle2 {
+ width: 324px;
+ font-family: Poppins;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ color: #999999;
+}
+
+.subtitle3 {
+ width: 102px;
+ font-family: Poppins;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ color: #999999;
+}
+
+.subtitle4 {
+ width: 102px;
+ font-family: Poppins;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ color: #999999;
+ margin-right: 20px;
+}
+
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..1af27f4c4 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,7 +1,18 @@
import { Link } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
+import { useContext, useEffect } from "react";
+import { EasyModeContext } from "../../components/context/easymodeContext";
export function SelectLevelPage() {
+ const { setEasyMode } = useContext(EasyModeContext);
+ const checkBox = () => {
+ setEasyMode(true);
+ };
+
+ useEffect(() => {
+ setEasyMode(false);
+ }, []);
+
return (
@@ -23,6 +34,13 @@ export function SelectLevelPage() {
+
+
Легкий режим (3 жизни)
+
+
+
+ Перейти к лидерборду
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..8cdc3bda1 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -62,3 +62,29 @@
.levelLink:visited {
color: #0080c1;
}
+
+.subtitle {
+ color: #004980;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+}
+
+.wrap {
+ display: flex;
+ align-items: flex-end;
+}
+
+.link {
+ font-family: Roboto;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #004980;
+ margin-top: 18px;
+}
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
diff --git a/src/utils/helper.js b/src/utils/helper.js
new file mode 100644
index 000000000..a52c84e98
--- /dev/null
+++ b/src/utils/helper.js
@@ -0,0 +1,2 @@
+export const sortLeadersElements = leaders => [...leaders].sort((a, b) => a.time - b.time);
+export const getTimeInSeconds = timer => timer.seconds + timer.minutes * 60;