diff --git a/.eslintrc.json b/.eslintrc.json
index e37e1e072..d9932d762 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,6 @@
"rules": {
"camelcase": ["error", { "properties": "never" }],
"prettier/prettier": "error",
- "eqeqeq": ["error", "always"],
- "no-unused-vars": ["error"]
+ "eqeqeq": ["error", "always"]
}
}
diff --git a/.gitignore b/.gitignore
index 4d29575de..76c22382d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
/build
# misc
+.idea
.DS_Store
.env.local
.env.development.local
diff --git a/README.md b/README.md
index 9b90842c4..3b8b6827c 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,18 @@ https://skypro-web-developer.github.io/react-memo/
Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
+
+## Первая домашняя работа (оценка)
+
+Предполагаемое время: 8 часов.
+Фактическое время: 5 часов.
+
+## Вторая домашняя работа (оценка)
+
+Предполагаемое время: 8 часов.
+Фактическое время: 12 часов.
+
+## Курсовая работа работа (оценка)
+
+Предполагаемое время: 9 часов.
+Фактическое время: 10 часов.
diff --git a/src/api/LeadersAPI.js b/src/api/LeadersAPI.js
new file mode 100644
index 000000000..b6c065b3f
--- /dev/null
+++ b/src/api/LeadersAPI.js
@@ -0,0 +1,30 @@
+const topLeadersLink = "https://wedev-api.sky.pro/api/v2/leaderboard";
+
+export const getRequest = () => {
+ return fetch(topLeadersLink, {
+ method: "GET",
+ }).then(response => {
+ if (!response.ok) {
+ throw new Error("Что-то я заплутал...");
+ }
+ if (response.status === 400) {
+ throw new Error("Полученные данные не в формате JSON");
+ }
+ return response.json();
+ });
+};
+
+export const postRequest = record => {
+ return fetch(topLeadersLink, {
+ method: "POST",
+ body: JSON.stringify(record),
+ }).then(response => {
+ if (!response.ok) {
+ throw new Error("Что-то пошло не так");
+ }
+ if (response.status === 400) {
+ throw new Error("Полученные данные не в формате JSON!");
+ }
+ return response.json();
+ });
+};
diff --git a/src/components/AchievementsImages/AchievementsImages.jsx b/src/components/AchievementsImages/AchievementsImages.jsx
new file mode 100644
index 000000000..f17e3eed4
--- /dev/null
+++ b/src/components/AchievementsImages/AchievementsImages.jsx
@@ -0,0 +1,62 @@
+import { useState } from "react";
+import styles from "./AchievementsImages.module.css";
+
+import hardLevelImage from "./images/hardLevel.svg";
+import superPowerImage from "./images/superPower.svg";
+import hardLevelGreyImage from "./images/hardLevelGrey.svg";
+import superPowerGreyImage from "./images/superPowerGrey.svg";
+
+export function HardLevelImg({ disabled }) {
+ const [isOpen, setOpen] = useState(false);
+
+ function mouseHover(value) {
+ if (disabled) return;
+
+ setOpen(value);
+ }
+
+ return (
+
+ {!disabled && isOpen && (
+
+ Игра пройдена
+
в сложном режиме
+
+ )}
+

mouseHover(true)}
+ onMouseOut={() => mouseHover(false)}
+ />
+
+ );
+}
+
+export function SuperPowerImg({ disabled }) {
+ const [isOpen, setOpen] = useState(false);
+
+ function mouseHover(value) {
+ if (disabled) return;
+
+ setOpen(value);
+ }
+
+ return (
+
+ {!disabled && isOpen && (
+
+ Игра пройдена
+
+ без супер-сил
+
+ )}
+

mouseHover(true)}
+ onMouseOut={() => mouseHover(false)}
+ />
+
+ );
+}
diff --git a/src/components/AchievementsImages/AchievementsImages.module.css b/src/components/AchievementsImages/AchievementsImages.module.css
new file mode 100644
index 000000000..74883c6aa
--- /dev/null
+++ b/src/components/AchievementsImages/AchievementsImages.module.css
@@ -0,0 +1,39 @@
+
+.cntHint {
+ position: relative;
+
+ img {
+ display: block;
+ position: relative;
+ z-index: 1;
+ }
+}
+
+.cntInsight {
+ background: #C2F5FF;
+ border-radius: 12px;
+ position: absolute;
+ bottom: 45px;
+ left: 3px;
+ padding: 25px 20px 20px;
+ width: 222px;
+ z-index: 1;
+ box-sizing: border-box;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 24px;
+ text-align: center;
+ color: #004980;
+
+ &:after {
+ content: "";
+ width: 20px;
+ height: 14px;
+ position: absolute;
+ bottom: -9px;
+ left: 14px;
+ background: #C2F5FF;
+ clip-path: polygon(0% 100%, 0% 0%, 100% 0%);
+ }
+}
diff --git a/src/components/AchievementsImages/images/hardLevel.svg b/src/components/AchievementsImages/images/hardLevel.svg
new file mode 100644
index 000000000..d101cef6d
--- /dev/null
+++ b/src/components/AchievementsImages/images/hardLevel.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/components/AchievementsImages/images/hardLevelGrey.svg b/src/components/AchievementsImages/images/hardLevelGrey.svg
new file mode 100644
index 000000000..9538c3d7d
--- /dev/null
+++ b/src/components/AchievementsImages/images/hardLevelGrey.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/components/AchievementsImages/images/superPower.svg b/src/components/AchievementsImages/images/superPower.svg
new file mode 100644
index 000000000..c74a8b83d
--- /dev/null
+++ b/src/components/AchievementsImages/images/superPower.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/components/AchievementsImages/images/superPowerGrey.svg b/src/components/AchievementsImages/images/superPowerGrey.svg
new file mode 100644
index 000000000..0557f5282
--- /dev/null
+++ b/src/components/AchievementsImages/images/superPowerGrey.svg
@@ -0,0 +1,30 @@
+
diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css
index 5d3f1f80e..29e36d8eb 100644
--- a/src/components/Button/Button.module.css
+++ b/src/components/Button/Button.module.css
@@ -10,7 +10,7 @@
font-variant-numeric: lining-nums proportional-nums;
/* Pres → Caption S */
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 400;
diff --git a/src/components/Card/Card.module.css b/src/components/Card/Card.module.css
index 86c3fbb5b..03390dec4 100644
--- a/src/components/Card/Card.module.css
+++ b/src/components/Card/Card.module.css
@@ -40,7 +40,7 @@
color: #000;
font-variant-numeric: lining-nums proportional-nums;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 400;
@@ -75,7 +75,7 @@
/* Анимация переворота */
/* весь контейнер поддерживает перспективу */
.flipContainer {
- perspective: 1000;
+ perspective: 1000px;
}
.flipContainer:hover .flipper {
diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx
index 7526a56c8..b325c8a59 100644
--- a/src/components/Cards/Cards.jsx
+++ b/src/components/Cards/Cards.jsx
@@ -1,10 +1,12 @@
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 { EndGameModal } from "../EndGameModal/EndGameModal";
+import { Button } from "../Button/Button";
+import { Card } from "../Card/Card";
+import { GameSettingsContext } from "../../context/GameSettingsContext";
+import { InsightHint } from "../Hints/Hints";
// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
@@ -13,6 +15,7 @@ const STATUS_WON = "STATUS_WON";
const STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS";
// Начало игры: игрок видит все карты в течении нескольких секунд
const STATUS_PREVIEW = "STATUS_PREVIEW";
+const STATUS_FROZEN = "STATUS_FROZEN";
function getTimerValue(startDate, endDate) {
if (!startDate && !endDate) {
@@ -45,6 +48,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
const [cards, setCards] = useState([]);
// Текущий статус игры
const [status, setStatus] = useState(STATUS_PREVIEW);
+ // Статус активности проверки открытой пары карт
+ const [checkPair, setCheckPair] = useState(false);
// Дата начала игры
const [gameStartDate, setGameStartDate] = useState(null);
@@ -57,6 +62,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
minutes: 0,
});
+ const [hasHint, setHasHint] = useState(true);
+ const { liteVersion, getTriesCount, printTriesText } = useContext(GameSettingsContext);
+ const [triesCount, setTriesCount] = useState(getTriesCount());
+ const [openedCard, setOpenedCard] = useState(null);
+
function finishGame(status = STATUS_LOST) {
setGameEndDate(new Date());
setStatus(status);
@@ -67,12 +77,30 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameStartDate(startDate);
setTimer(getTimerValue(startDate, null));
setStatus(STATUS_IN_PROGRESS);
+
+ setTriesCount(getTriesCount());
}
function resetGame() {
setGameStartDate(null);
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
+
+ setCheckPair(false);
+ setTriesCount(getTriesCount());
+ setOpenedCard(null);
+ setHasHint(true);
+ }
+
+ function handleInsightClick() {
+ setStatus(STATUS_FROZEN);
+
+ setTimeout(() => {
+ setStatus(STATUS_IN_PROGRESS);
+ setHasHint(false);
+
+ gameStartDate.setMilliseconds(gameStartDate.getMilliseconds() + 5000);
+ }, 5000);
}
/**
@@ -84,9 +112,14 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
*/
const openCard = clickedCard => {
// Если карта уже открыта, то ничего не делаем
- if (clickedCard.open) {
+ if (clickedCard.open || checkPair) {
return;
}
+
+ if (openedCard) {
+ setCheckPair(true);
+ }
+
// Игровое поле после открытия кликнутой карты
const nextCards = cards.map(card => {
if (card.id !== clickedCard.id) {
@@ -127,15 +160,39 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
+ if (liteVersion && triesCount > 1) {
+ setTriesCount(triesCount - 1);
+ const prevCards = cards.map(card => ({
+ ...card,
+ open: card.open && card.id !== openedCard.id && card.id !== clickedCard.id,
+ }));
+ setTimeout(() => {
+ setCards(prevCards);
+ setOpenedCard(null);
+ setCheckPair(false);
+ }, 500);
+ return;
+ }
+
finishGame(STATUS_LOST);
return;
}
// ... игра продолжается
+
+ if (openCardsWithoutPair.length === 0) {
+ setOpenedCard(null);
+ setCheckPair(false);
+ } else if (openCardsWithoutPair.length === 1) {
+ setOpenedCard(clickedCard);
+ }
};
const isGameEnded = status === STATUS_LOST || status === STATUS_WON;
+ const achievements =
+ status !== STATUS_WON ? [] : pairsCount >= 9 && hasHint ? [1, 2] : pairsCount >= 9 ? [1] : hasHint ? [2] : [];
+
// Игровой цикл
useEffect(() => {
// В статусах кроме превью доп логики не требуется
@@ -153,6 +210,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
return shuffle(generateDeck(pairsCount, 10));
});
+ setTriesCount(getTriesCount());
+
const timerId = setTimeout(() => {
startGame();
}, previewSeconds * 1000);
@@ -165,12 +224,14 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
// Обновляем значение таймера в интервале
useEffect(() => {
const intervalId = setInterval(() => {
+ if (status === STATUS_LOST || status === STATUS_WON || status === STATUS_FROZEN) return;
+
setTimer(getTimerValue(gameStartDate, gameEndDate));
}, 300);
return () => {
clearInterval(intervalId);
};
- }, [gameStartDate, gameEndDate]);
+ }, [gameStartDate, gameEndDate, status]);
return (
@@ -195,7 +256,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
>
)}
- {status === STATUS_IN_PROGRESS ? : null}
+ {status === STATUS_IN_PROGRESS || status === STATUS_FROZEN ? (
+ <>
+
+
+ >
+ ) : null}
@@ -203,17 +269,20 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
openCard(card)}
- open={status !== STATUS_IN_PROGRESS ? true : card.open}
+ open={status !== STATUS_IN_PROGRESS || status === STATUS_FROZEN ? true : card.open}
suit={card.suit}
rank={card.rank}
/>
))}
+ {liteVersion && {printTriesText(triesCount)}
}
+
{isGameEnded ? (
{
+ setLeaders(data.leaders);
+ setDataIsSent(true);
+ setError("");
+ });
+ }
+
+ function handleSubmit() {
+ sendStatics();
+
+ onClick();
+ }
+
+ function handleLinkToLeaderboard(e) {
+ e.preventDefault();
+ // sendStatics();
+ navigate("/leaderboard");
+ }
+
return (
{title}
+ {isWon && (
+
+
+
+ {dataIsSent || (
+
+ )}
+
+ {error &&
{error}
}
+
+ )}
Затраченное время:
{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..25be85b97 100644
--- a/src/components/EndGameModal/EndGameModal.module.css
+++ b/src/components/EndGameModal/EndGameModal.module.css
@@ -1,6 +1,7 @@
.modal {
width: 480px;
- height: 459px;
+ /*height: 459px;*/
+ height: 634px;
border-radius: 12px;
background: #c2f5ff;
display: flex;
@@ -18,19 +19,19 @@
.title {
color: #004980;
font-variant-numeric: lining-nums proportional-nums;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 40px;
font-style: normal;
font-weight: 400;
line-height: 48px;
-
+ text-align: center;
margin-bottom: 28px;
}
.description {
color: #000;
font-variant-numeric: lining-nums proportional-nums;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 400;
@@ -41,7 +42,7 @@
.time {
color: #000;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 64px;
font-style: normal;
font-weight: 400;
@@ -49,3 +50,85 @@
margin-bottom: 40px;
}
+
+.link {
+ color: #004980;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ margin-top: 18px;
+}
+
+.input {
+ width: 276px;
+ height: 45px;
+ top: 334px;
+ left: 374px;
+ border-radius: 10px;
+ background: #ffffff;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+ border: none;
+ outline: none;
+
+ &:read-only {
+ background: lightcyan;
+ }
+}
+
+.placeholder {
+ text-align: center;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ font-family: "StratosSkyeng", sans-serif;
+}
+
+.inputData {
+ margin-bottom: 20px;
+
+ p {
+ text-align: center;
+ }
+}
+
+.addNameUser {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.btnAddUser {
+ height: 48px;
+ border-radius: 12px;
+ background: #7ac100;
+ color: #fff;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ border: none;
+ cursor: pointer;
+
+ &:disabled {
+ background: cadetblue;
+ }
+}
+
+.error {
+ color: lightcoral;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 18px;
+ font-weight: 700;
+}
diff --git a/src/components/Hints/Hints.jsx b/src/components/Hints/Hints.jsx
new file mode 100644
index 000000000..b7d5c1724
--- /dev/null
+++ b/src/components/Hints/Hints.jsx
@@ -0,0 +1,47 @@
+import { useState } from "react";
+import styles from "./Hints.module.css";
+
+import cn from "classnames";
+
+import eyeImage from "./images/eye.svg";
+
+export function InsightHint({ disabled, handleClick }) {
+ const [isOpen, setOpen] = useState(false);
+
+ function useClick() {
+ if (disabled) return;
+
+ setOpen(false);
+ handleClick();
+ }
+
+ function mouseHover(value) {
+ if (disabled) return;
+
+ setOpen(value);
+ }
+
+ return (
+
+ {isOpen && (
+ <>
+
+
+
Прозрение
+
+ На 5 секунд показываются все карты. Таймер длительности игры на это время останавливается.
+
+
+ >
+ )}
+

mouseHover(true)}
+ onMouseOut={() => mouseHover(false)}
+ />
+
+ );
+}
diff --git a/src/components/Hints/Hints.module.css b/src/components/Hints/Hints.module.css
new file mode 100644
index 000000000..c129f3733
--- /dev/null
+++ b/src/components/Hints/Hints.module.css
@@ -0,0 +1,55 @@
+
+.cntHint {
+ position: relative;
+
+ img {
+ position: relative;
+ z-index: 1;
+ }
+}
+
+.disabled {
+ opacity: 0.4;
+}
+
+.bckColor {
+ position: absolute;
+ width: 200vw;
+ height: 200vh;
+ background: #004980;
+ opacity: 0.6;
+ z-index: 1;
+ transform: translate(-50%, -50%);
+}
+
+.cntInsight {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: #C2F5FF;
+ border-radius: 12px;
+ position: absolute;
+ top: 84px;
+ left: -77px;
+ padding: 25px 20px 20px;
+ width: 222px;
+ z-index: 1;
+ box-sizing: border-box;
+ font-family: "StratosSkyeng", sans-serif;
+ color: #004980;
+}
+
+.insightTitle {
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 24px;
+ text-align: center;
+ margin-bottom: 10px;
+}
+
+.insightDescription {
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 24px;
+ text-align: center;
+}
diff --git a/src/components/Hints/images/eye.svg b/src/components/Hints/images/eye.svg
new file mode 100644
index 000000000..40a1796f4
--- /dev/null
+++ b/src/components/Hints/images/eye.svg
@@ -0,0 +1,34 @@
+
diff --git a/src/components/Hints/images/randomCards.svg b/src/components/Hints/images/randomCards.svg
new file mode 100644
index 000000000..40c7ebfc2
--- /dev/null
+++ b/src/components/Hints/images/randomCards.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/context/GameSettingsContext.js b/src/context/GameSettingsContext.js
new file mode 100644
index 000000000..92b5ce20b
--- /dev/null
+++ b/src/context/GameSettingsContext.js
@@ -0,0 +1,32 @@
+import { createContext, useState } from "react";
+
+export const GameSettingsContext = createContext(null);
+
+export const GameSettingsProvider = ({ children }) => {
+ const [levelMode, setLevelMode] = useState(1);
+ const [liteVersion, setLiteVersion] = useState(false);
+
+ function getTriesCount() {
+ return liteVersion ? 3 : 0;
+ }
+
+ function printTriesText(count) {
+ switch (count) {
+ case 3:
+ return "У вас есть 3 попытки";
+ case 2:
+ return "У вас осталось 2 попытки";
+ case 1:
+ default:
+ return "У вас осталась последняя попытка";
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/context/LeaderBoardContext.js b/src/context/LeaderBoardContext.js
new file mode 100644
index 000000000..0d102173a
--- /dev/null
+++ b/src/context/LeaderBoardContext.js
@@ -0,0 +1,25 @@
+import { createContext, useEffect, useState } from "react";
+import { getRequest } from "../api/LeadersAPI";
+
+export const LeadersContext = createContext(null);
+export const leadersTimeSorting = leaders => leaders.sort((a, b) => a.time - b.time);
+
+async function updateData() {
+ getRequest().then(leaders => {
+ const sortingLeaders = leadersTimeSorting(leaders.leaders);
+ //setLeaders(sortingLeaders.splice(0, 10));
+ });
+}
+
+export const LeadersProvider = ({ children }) => {
+ const [leaders, setLeaders] = useState([]);
+
+ useEffect(() => {
+ getRequest().then(leaders => {
+ const sortingLeaders = leadersTimeSorting(leaders.leaders);
+ setLeaders(sortingLeaders.splice(0, 10));
+ });
+ });
+
+ return {children};
+};
diff --git a/src/index.js b/src/index.js
index f689c5f0b..41e0be5da 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,16 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
+import { GameSettingsProvider } from "./context/GameSettingsContext";
+import { LeadersProvider } from "./context/LeaderBoardContext";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
+
+
+
+
,
);
diff --git a/src/pages/GamePage/GamePage.jsx b/src/pages/GamePage/GamePage.jsx
index a4be871db..4e64c13f9 100644
--- a/src/pages/GamePage/GamePage.jsx
+++ b/src/pages/GamePage/GamePage.jsx
@@ -2,8 +2,8 @@ import { useParams } from "react-router-dom";
import { Cards } from "../../components/Cards/Cards";
-export function GamePage() {
- const { pairsCount } = useParams();
+export function GamePage({ otherPairsCount }) {
+ const { pairsCount = otherPairsCount } = useParams();
return (
<>
diff --git a/src/pages/LeaderBoardPage/LeaderBoardPage.jsx b/src/pages/LeaderBoardPage/LeaderBoardPage.jsx
new file mode 100644
index 000000000..e5c03359d
--- /dev/null
+++ b/src/pages/LeaderBoardPage/LeaderBoardPage.jsx
@@ -0,0 +1,49 @@
+import { Link } from "react-router-dom";
+import { Button } from "../../components/Button/Button";
+import styles from "./LeaderBoardPage.module.css";
+import { useContext } from "react";
+import { LeadersContext } from "../../context/LeaderBoardContext";
+import cn from "classnames";
+
+import { HardLevelImg, SuperPowerImg } from "../../components/AchievementsImages/AchievementsImages";
+
+function formatTime(seconds) {
+ const minutes = Math.floor(seconds / 60);
+ seconds = seconds % 60;
+
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+export const LeaderBoardPage = () => {
+ const { leaders } = useContext(LeadersContext);
+
+ return (
+
+
+ Лидерборд
+
+
+
+
+
+
+
Позиция
+
Пользователь
+
Достижения
+
Время
+
+ {leaders.map((leader, index) => (
+
+
{index + 1}
+
{leader.name}
+
+
+
+
+
{formatTime(leader.time)}
+
+ ))}
+
+
+ );
+};
diff --git a/src/pages/LeaderBoardPage/LeaderBoardPage.module.css b/src/pages/LeaderBoardPage/LeaderBoardPage.module.css
new file mode 100644
index 000000000..a420b84b7
--- /dev/null
+++ b/src/pages/LeaderBoardPage/LeaderBoardPage.module.css
@@ -0,0 +1,69 @@
+.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: "StratosSkyeng", sans-serif;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: left;
+ color: #ffffff;
+}
+
+.mainBox {
+ width: 944px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ gap: 66px;
+ box-sizing: border-box;
+ padding-inline: 20px;
+ margin-bottom: 16px;
+ font-size: 24px;
+ font-family: "StratosSkyeng", sans-serif;
+ background-color: #fff;
+ border-radius: 12px;
+}
+
+.usersPosition {
+ width: 178px;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+}
+
+.nameUser {
+ width: 227px;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+}
+
+.achievUser {
+ width: 194px;
+ display: flex;
+ gap: 6px;
+}
+
+.timeRecord {
+ width: 92px;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ box-sizing: border-box;
+}
+
+.headerLine {
+ color: #999999;
+}
+
+
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx
index 758942e51..07bc06995 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.jsx
+++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx
@@ -1,28 +1,70 @@
-import { Link } from "react-router-dom";
+import { useContext } from "react";
+import { Link, useNavigate } from "react-router-dom";
import styles from "./SelectLevelPage.module.css";
+import { Button } from "../../components/Button/Button";
+import { GameSettingsContext } from "../../context/GameSettingsContext";
+
+import cn from "classnames";
export function SelectLevelPage() {
+ const navigate = useNavigate();
+ const { levelMode, setLevelMode, liteVersion, setLiteVersion } = useContext(GameSettingsContext);
+
+ function handleLiteChange() {
+ setLiteVersion(prev => !prev);
+ }
+
+ function startGame() {
+ navigate(`/game/${levelMode * 3}`);
+ }
+
return (
Выбери сложность
+
+
+
+ Перейти к лидерборду
+
);
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css
index 390ac0def..db3a776e7 100644
--- a/src/pages/SelectLevelPage/SelectLevelPage.module.css
+++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css
@@ -21,7 +21,7 @@
color: #004980;
text-align: center;
font-variant-numeric: lining-nums proportional-nums;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 40px;
font-style: normal;
font-weight: 400;
@@ -51,14 +51,46 @@
.levelLink {
color: #0080c1;
text-align: center;
- font-family: StratosSkyeng;
+ font-family: "StratosSkyeng", sans-serif;
font-size: 64px;
font-style: normal;
font-weight: 400;
line-height: 72px;
text-decoration: none;
+
+ &:hover {
+ font-weight: 600;
+ transform: scale(1.05);
+ }
+
+ &.selected {
+ color: #c10080 !important;
+ }
}
.levelLink:visited {
color: #0080c1;
}
+
+.liteMode {
+ color: #004980;
+ font-family: "StratosSkyeng", sans-serif;
+ margin-bottom: 48px;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 32px;
+ text-align: center;
+
+}
+
+.link {
+ color: #004980;
+ text-align: center;
+ font-variant-numeric: lining-nums proportional-nums;
+ font-family: "StratosSkyeng", sans-serif;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 32px;
+ margin-top: 18px;
+}
diff --git a/src/router.js b/src/router.js
index da6e94b51..acb1ff86b 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(
[
@@ -8,10 +9,18 @@ export const router = createBrowserRouter(
path: "/",
element: ,
},
+ {
+ path: "/game",
+ element: ,
+ },
{
path: "/game/:pairsCount",
element: ,
},
+ {
+ path: "/leaderboard",
+ element: ,
+ },
],
/**
* basename нужен для корректной работы в gh pages