diff --git a/.eslintrc.json b/.eslintrc.json
index e37e1e072..7814cf6f2 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/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..50dba2289
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "cSpell.words": [
+ "закрыта"
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 9b90842c4..6fac7b3ce 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,9 @@ https://skypro-web-developer.github.io/react-memo/
Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
+
+Предплагаемое время на выполнение ДЗ №1 5 часов
+фактическое время 6.5 часов
+
+Предплагаемое время на выполнение ДЗ №2 5 часов
+фактическое время 5 часов
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 000000000..a9d9892ae
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,28 @@
+//Получить список лидеров
+
+export async function getLeaders() {
+ const response = await fetch(`https://wedev-api.sky.pro/api/v2/leaderboard`, { method: "GET" });
+ if (!response.status === 200) {
+ throw new Error("Не удалось получить список лидеров");
+ }
+ const data = await response.json();
+ return data;
+}
+
+export async function addLeaders({ name, time, achievements }) {
+ const response = await fetch(`https://wedev-api.sky.pro/api/v2/leaderboard`, {
+ method: "POST",
+ body: JSON.stringify({
+ name,
+ time,
+ achievements,
+ }),
+ });
+ if (!response.status === 201) {
+ 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..dcb983ff1 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -5,6 +5,8 @@ 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 { ToolTipComponent } from "../Tooltip/Tooltip";
+import { superPowerData } from "../../lib";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -40,7 +42,7 @@ function getTimerValue(startDate, endDate) {
* pairsCount - сколько пар будет в игре
* previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры
*/
-export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
+export function Cards({ pairsCount = 3, previewSeconds = 5, isGameMode }) {
// В cards лежит игровое поле - массив карт и их состояние открыта\закрыта
const [cards, setCards] = useState([]);
// Текущий статус игры
@@ -57,9 +59,28 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
minutes: 0,
});
+ // Стейт для счетчика попыток
+ const [numberOfAttempts, setNumberOfAttempts] = useState(2);
+ const takeAwayTheAttempt = () => {
+ setNumberOfAttempts(numberOfAttempts - 1);
+ };
+ // стейт для массива достижений
+ const [achievementsArr, setAchievementsArr] = useState([]);
+
+ // стейт чтобы определить были ли использованы суперсилы
+ const [superPowersUsed, setSuperpowersUsed] = useState(false);
+
function finishGame(status = STATUS_LOST) {
setGameEndDate(new Date());
setStatus(status);
+ if (pairsCount > 8) {
+ achievementsArr.push(1);
+ setAchievementsArr(achievementsArr);
+ }
+ if (!superPowersUsed) {
+ achievementsArr.push(2);
+ setAchievementsArr(achievementsArr);
+ }
}
function startGame() {
const startDate = new Date();
@@ -73,11 +94,14 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
+ setNumberOfAttempts(2);
+ setSuperpowersUsed(false);
+ setAchievementsArr([]);
}
/**
* Обработка основного действия в игре - открытие карты.
- * После открытия карты игра может пепереходит в следующие состояния
+ * После открытия карты игра может переходит в следующие состояния
* - "Игрок выиграл", если на поле открыты все карты
* - "Игрок проиграл", если на поле есть две открытые карты без пары
* - "Игра продолжается", если не случилось первых двух условий
@@ -126,11 +150,25 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const playerLost = openCardsWithoutPair.length >= 2;
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
- if (playerLost) {
- finishGame(STATUS_LOST);
- return;
- }
+ if (isGameMode === "true") {
+ if (playerLost) {
+ takeAwayTheAttempt();
+ if (numberOfAttempts < 1) {
+ finishGame(STATUS_LOST);
+ return;
+ } else {
+ setTimeout(() => {
+ setCards(cards.map(card => (openCardsWithoutPair.includes(card) ? { ...card, open: false } : card)));
+ }, 1000);
+ }
+ }
+ } else {
+ if (playerLost) {
+ finishGame(STATUS_LOST);
+ return;
+ }
+ }
// ... игра продолжается
};
@@ -172,6 +210,23 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
};
}, [gameStartDate, gameEndDate]);
+ // Закрытые карты на игровом поле
+ const reversedCards = cards.filter(card => !card.open);
+
+ // Cуперсила "алохомора"
+ function alohomora() {
+ if (!superPowersUsed & (reversedCards.length > 2)) {
+ const randomCard = shuffle(reversedCards)[0];
+ const randomPair = reversedCards.filter(
+ sameCard => randomCard.suit === sameCard.suit && randomCard.rank === sameCard.rank,
+ );
+ randomPair[0].open = true;
+ randomPair[1].open = true;
+ setSuperpowersUsed(true);
+ }
+ return;
+ }
+
return (
@@ -195,7 +250,79 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
>
)}
- {status === STATUS_IN_PROGRESS ?
: null}
+
+ {status === STATUS_IN_PROGRESS ? (
+ <>
+
+
+
+
+
+
+
+ {isGameMode === "true" ? (
+
+
+
{numberOfAttempts + 1}
+
+ ) : null}
+
+ >
+ ) : null}
@@ -217,6 +344,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
+ isGameMode
+ achievementsArr={achievementsArr}
/>
) : null}
diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css
index 000c5006c..85675d4ae 100644
--- a/src/components/Cards/Cards.module.css
+++ b/src/components/Cards/Cards.module.css
@@ -67,6 +67,20 @@
font-style: normal;
font-weight: 400;
line-height: 32px;
-
margin-bottom: -12px;
}
+
+
+.attemptСounter {
+ display: flex;
+ gap: 10px;
+ color: red;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 30px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 30px;
+}
+
+
diff --git a/src/components/Cards/images/hearts.svg b/src/components/Cards/images/hearts.svg
new file mode 100644
index 000000000..0e3720b4f
--- /dev/null
+++ b/src/components/Cards/images/hearts.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx
index 722394833..36d847b5e 100644
--- a/src/components/EndGameModal/EndGameModal.jsx
+++ b/src/components/EndGameModal/EndGameModal.jsx
@@ -4,9 +4,60 @@ import { Button } from "../Button/Button";
import deadImageUrl from "./images/dead.png";
import celebrationImageUrl from "./images/celebration.png";
+import { useNavigate, useParams } from "react-router-dom";
+import { useState } from "react";
+import { addLeaders } from "../../api";
-export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) {
- const title = isWon ? "Вы победили!" : "Вы проиграли!";
+export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, achievementsArr }) {
+ console.log(achievementsArr);
+ isWon = true;
+ const { isGameMode } = useParams();
+ const { pairsCount } = useParams();
+ const navigate = useNavigate();
+ const gameSeconds = gameDurationMinutes * 60 + gameDurationSeconds;
+
+ const [userData, setuserData] = useState({
+ name: " ",
+ time: gameSeconds,
+ achievements: achievementsArr,
+ });
+
+ const handleInputChange = e => {
+ const { name, value } = e.target; // Извлекаем имя поля и его значение
+
+ setuserData({
+ ...userData, // Копируем текущие данные из состояния
+ [name]: value, // Обновляем нужное поле
+ });
+ };
+
+ async function handleAddUser(e) {
+ e.preventDefault();
+ try {
+ await addLeaders(userData).then(data => {
+ navigate(`/leaderboard`);
+ });
+ } catch (error) {
+ alert(error.message);
+ }
+ }
+ async function handleAddUserButton(e) {
+ e.preventDefault();
+ try {
+ await addLeaders(userData).then(data => {
+ onClick();
+ });
+ } catch (error) {
+ alert(error.message);
+ }
+ }
+
+ let title = "";
+ if (pairsCount === "9") {
+ title = isWon ? "Вы попали на лидерборд!" : "Вы проиграли!";
+ } else {
+ title = isWon ? "Вы победили!" : "Вы проиграли!";
+ }
const imgSrc = isWon ? celebrationImageUrl : deadImageUrl;
@@ -16,12 +67,29 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes,
{title}
+ {isGameMode === "false" && pairsCount === "9" && isWon ? (
+
+ ) : null}
Затраченное время:
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
-
-
+
+ {isGameMode === "false" && pairsCount === "9" && isWon ? (
+
+ Перейти к лидерборду
+
+ ) : null}
);
}
diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css
index 9368cb8b5..593431089 100644
--- a/src/components/EndGameModal/EndGameModal.module.css
+++ b/src/components/EndGameModal/EndGameModal.module.css
@@ -1,6 +1,7 @@
.modal {
width: 480px;
- height: 459px;
+ padding-top: 30px;
+ padding-bottom: 30px;
border-radius: 12px;
background: #c2f5ff;
display: flex;
@@ -23,7 +24,7 @@
font-style: normal;
font-weight: 400;
line-height: 48px;
-
+ text-align: center;
margin-bottom: 28px;
}
@@ -35,7 +36,6 @@
font-style: normal;
font-weight: 400;
line-height: 32px;
-
margin-bottom: 10px;
}
@@ -46,6 +46,43 @@
font-style: normal;
font-weight: 400;
line-height: 72px;
-
margin-bottom: 40px;
}
+
+
+.leaderboardLink {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ color: #004980;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ text-decoration:underline
+}
+
+.leaderboardLink:hover {
+ text-decoration:none
+}
+
+.form{
+ padding-top: 10px;
+ padding-bottom: 20px;
+}
+.nameInput {
+ width:276px;
+ height:45px;
+ border-radius:10px;
+ border: none;
+ color: #999999;
+ 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;
+}
diff --git a/src/components/Tooltip/Tooltip.jsx b/src/components/Tooltip/Tooltip.jsx
new file mode 100644
index 000000000..89c0daf10
--- /dev/null
+++ b/src/components/Tooltip/Tooltip.jsx
@@ -0,0 +1,22 @@
+import { useState } from "react";
+import styles from "./Tooltip.module.css";
+export function ToolTipComponent({ children, text, title }) {
+ const [showToolTip, setShowToolTip] = useState(false);
+ const onMouseEnterHandler = () => {
+ setShowToolTip(true);
+ };
+ const onMouseLeaveHandler = () => {
+ setShowToolTip(false);
+ };
+ return (
+
+ {children}
+ {showToolTip && (
+
+ )}
+
+ );
+}
diff --git a/src/components/Tooltip/Tooltip.module.css b/src/components/Tooltip/Tooltip.module.css
new file mode 100644
index 000000000..38e9cd91f
--- /dev/null
+++ b/src/components/Tooltip/Tooltip.module.css
@@ -0,0 +1,25 @@
+.container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+}
+
+.tooltip {
+ margin-top: 170px;
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ font-style: normal;
+ position: absolute;
+ padding: 20px;
+ justify-content: center;
+ color: #004980;
+ background-color: #C2F5FF;
+ border-radius: 12px;
+ text-align: center;
+ white-space: pre-line;
+ font-weight: 400;
+ pointer-events: none;
+ z-index: 2;
+ max-height: 223px;
+}
\ No newline at end of file
diff --git a/src/lib.jsx b/src/lib.jsx
new file mode 100644
index 000000000..335e37b2b
--- /dev/null
+++ b/src/lib.jsx
@@ -0,0 +1,176 @@
+export const superPowerData = {
+ alohomora: {
+ title: "Алохомора",
+ text: "Открывается случайная пара карт",
+ },
+};
+
+export const achievementsText = {
+ hardMode: "Игра пройдена в сложном режиме",
+ withoutSuperPowers: "Игра пройдена без супер-сил",
+};
+
+export const achievementsIcons = {
+ hardMode: (
+
+ ),
+ hardModeActive: (
+
+ ),
+ withoutSuperPowers: (
+
+ ),
+ withoutSuperPowersActive: (
+
+ ),
+};
diff --git a/src/pages/GamePage/GamePage.jsx b/src/pages/GamePage/GamePage.jsx
index a4be871db..4c5404265 100644
--- a/src/pages/GamePage/GamePage.jsx
+++ b/src/pages/GamePage/GamePage.jsx
@@ -4,10 +4,11 @@ import { Cards } from "../../components/Cards/Cards";
export function GamePage() {
const { pairsCount } = useParams();
+ const { isGameMode } = useParams();
return (
<>
-
+
>
);
}
diff --git a/src/pages/LeaderboardPage/LeaderboardPage.jsx b/src/pages/LeaderboardPage/LeaderboardPage.jsx
new file mode 100644
index 000000000..4d07b46e4
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx
@@ -0,0 +1,69 @@
+import styles from "./LeaderboardPage.module.css";
+import { useEffect, useState } from "react";
+import { getLeaders } from "../../api";
+import { Button } from "../../components/Button/Button";
+import { useNavigate } from "react-router-dom";
+import { achievementsIcons, achievementsText } from "../../lib";
+import { ToolTipComponent } from "../../components/Tooltip/Tooltip";
+
+export function LeaderboardPage() {
+ const [leaders, setLeaders] = useState([]);
+ useEffect(() => {
+ getLeaders().then(leadersList => {
+ setLeaders(leadersList.leaders);
+ });
+ }, []);
+ const navigate = useNavigate();
+ const startTheGame = e => {
+ e.preventDefault();
+ navigate(`/`);
+ };
+
+ let i = 1;
+ return (
+
+
+
Лидерборд
+
+
+
+
+ );
+}
diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css
new file mode 100644
index 000000000..319d71573
--- /dev/null
+++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css
@@ -0,0 +1,69 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+.container {
+ padding: 40px;
+}
+
+.header{
+ display: flex;
+ justify-content:space-between
+}
+
+.leaderboard {
+ padding-top: 40px;
+ display: flex;
+ flex-direction:column;
+ gap: 12px;
+}
+
+.headerTitle {
+ color: #FFFFFF;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+}
+
+.leadersItemTitle {
+ color: #999999;
+ list-style-type:none;
+ background-color: #FFFFFF;
+ padding: 10px 20px;
+ border-radius: 12px;
+ display: flex;
+ justify-content:space-between;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+}
+
+.leadersItem {
+ list-style-type:none;
+ background-color: #FFFFFF;
+ padding: 10px 20px;
+ border-radius: 12px;
+ display: flex;
+ justify-content:space-between;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+}
+
+.achievementsIcons{
+ display: flex;
+ gap: 6px;
+}
+
+
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..3a6bab369 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,28 +1,39 @@
import { Link } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
+import { useState } from "react";
export function SelectLevelPage() {
+ const [checked, setChecked] = useState(false);
return (
Выбери сложность
+
+ setChecked(!checked)} id="gameMode" />
+
+
+
+
+ Перейти к лидерборду
+
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..4fe631cc3 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -62,3 +62,54 @@
.levelLink:visited {
color: #0080c1;
}
+
+.gameMode {
+ 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: 48px;
+ border-radius: 12px;
+}
+
+.gameMode input {
+ display: none;
+}
+
+.gameMode label {
+ border-radius: 12px;
+ padding: 10px;
+}
+
+.gameMode input:checked+label {
+ color: white;
+ background-color: #004980;
+}
+
+.gameMode:hover {
+ color: white;
+ background-color: #004980;
+}
+
+.leaderboardLinkBox {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.leaderboardLink {
+ color: #004980;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: StratosSkyeng;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+}
+
+.leaderboardLink:hover {
+ text-decoration:none
+}
\ No newline at end of file
diff --git a/src/router.js b/src/router.js
index da6e94b51..b3a22d233 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(
[
@@ -9,9 +10,13 @@ export const router = createBrowserRouter(
element: ,
},
{
- path: "/game/:pairsCount",
+ path: "/game/:pairsCount/:isGameMode",
element: ,
},
+ {
+ path: "/leaderboard",
+ element: ,
+ },
],
/**
* basename нужен для корректной работы в gh pages