diff --git a/.prettierrc.js b/.prettierrc.js index 65e18f5ff..28033a2b6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,4 +7,5 @@ module.exports = { bracketSpacing: true, arrowParens: "avoid", htmlWhitespaceSensitivity: "ignore", + endOfLine: "auto", }; 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/package-lock.json b/package-lock.json index edaf5083f..dd3e2dd10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5961,9 +5961,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "funding": [ { "type": "opencollective", @@ -22559,9 +22559,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==" + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", diff --git a/public/Vector.svg b/public/Vector.svg new file mode 100644 index 000000000..dc94e72f4 --- /dev/null +++ b/public/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/robots.txt b/public/robots.txt index e9e57dc4d..5537f0739 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,3 @@ # https://www.robotstxt.org/robotstxt.html User-agent: * -Disallow: +Disallow: \ No newline at end of file diff --git a/src/api/getLeaderboard.js b/src/api/getLeaderboard.js new file mode 100644 index 000000000..fd64ba158 --- /dev/null +++ b/src/api/getLeaderboard.js @@ -0,0 +1,14 @@ +const apiUrl = "https://wedev-api.sky.pro/api/leaderboard"; + +export const getLeaderboard = async () => { + // Запрос к API получения списка победителей + const response = await fetch(apiUrl, { + method: "GET", + }); + + if (!response.ok) { + throw new Error(`Не удалось получить данные с сервера! status: ${response.status}`); + } + + return await response.json(); +}; diff --git a/src/api/postLeaderboard.js b/src/api/postLeaderboard.js new file mode 100644 index 000000000..a3eb0b40e --- /dev/null +++ b/src/api/postLeaderboard.js @@ -0,0 +1,23 @@ +const apiUrl = "https://wedev-api.sky.pro/api/leaderboard"; + +export const postLeaderboard = async ({ userName, userTime }) => { + // Запрос к API отправки победителя + const response = await fetch(apiUrl, { + method: "POST", + body: JSON.stringify({ name: goodByeHacker(userName), time: userTime }), + }); + + if (!response.ok) { + throw new Error(`Не удалось отправить данные на сервер! status: ${response.status}`); + } + + return await response.json(); +}; + +function goodByeHacker(text) { + return text + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("QUOTE_BEGIN", "
") + .replaceAll("QUOTE_END", "
"); +} diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..bdb9ef3e2 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,14 +1,18 @@ 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 "../../contexte/contexte"; +import { useNavigate } from "react-router-dom"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; const STATUS_WON = "STATUS_WON"; +// Пауза игры при допускании ошибки выбора карточки +const STATUS_PAUSED = "STATUS_PAUSED"; // Идет игра: карты закрыты, игрок может их открыть const STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS"; // Начало игры: игрок видит все карты в течении нескольких секунд @@ -19,6 +23,7 @@ function getTimerValue(startDate, endDate) { return { minutes: 0, seconds: 0, + diffInSecconds: 0, }; } @@ -27,11 +32,13 @@ function getTimerValue(startDate, endDate) { } const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000); + const minutes = Math.floor(diffInSecconds / 60); const seconds = diffInSecconds % 60; return { minutes, seconds, + diffInSecconds, }; } @@ -41,8 +48,18 @@ function getTimerValue(startDate, endDate) { * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + // Когда игра окончена, переход на главную страницу + const navigate = useNavigate(); + function goTo() { + navigate("/"); + } + + // Обработка количества попыток + const { tries, setTries, isEasyMode, checkedLevel, leadrs, setLeaders } = useContext(EasyContext); + // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); + // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); @@ -55,18 +72,35 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const [timer, setTimer] = useState({ seconds: 0, minutes: 0, + diffInSecconds: 0, }); - function finishGame(status = STATUS_LOST) { + // Если количество попыток равно 0 устанавливается стату проиграл и игра заканчивается + useEffect(() => { + if (tries === 0) { + finishGame(STATUS_LOST); + } + }, [tries]); + + function finishGame(status) { setGameEndDate(new Date()); setStatus(status); } + + function pausedGame(status = STATUS_PAUSED) { + setStatus(status); + } + function startGame() { const startDate = new Date(); setGameEndDate(null); setGameStartDate(startDate); setTimer(getTimerValue(startDate, null)); setStatus(STATUS_IN_PROGRESS); + // Добавлена проверка на включенный режим 3-х попыток + if (!isEasyMode) { + setTries(1); + } } function resetGame() { setGameStartDate(null); @@ -75,6 +109,24 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setStatus(STATUS_PREVIEW); } + function сontinueGame(status = STATUS_IN_PROGRESS) { + setStatus(status); + } + + // Функция запускает разные сценарии для кнопки в модальном окне + function whatsNext() { + if (status === STATUS_PAUSED) { + сontinueGame(STATUS_IN_PROGRESS); + } + if (status === STATUS_LOST) { + goTo(); + setTries(3); + } + if (status === STATUS_WON) { + resetGame(); + } + } + /** * Обработка основного действия в игре - открытие карты. * После открытия карты игра может пепереходит в следующие состояния @@ -123,18 +175,34 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { return false; }); - const playerLost = openCardsWithoutPair.length >= 2; + const havMistake = openCardsWithoutPair.length >= 2; - // "Игрок проиграл", т.к на поле есть две открытые карты без пары - if (playerLost) { - finishGame(STATUS_LOST); - return; + // Если на поле есть две открытые карты без пары, то игра паузится и уменьшается количество попыток + function minusTries() { + setTries(prev => prev - 1); + } + + // "Игрок допустил ошибку", т.к на поле есть две открытые карты без пары + if (havMistake) { + minusTries(); + pausedGame(STATUS_PAUSED); } // ... игра продолжается }; - const isGameEnded = status === STATUS_LOST || status === STATUS_WON; + // Проверка на попадание в топ 10 игроков + function isTopTen() { + const isTenPlayers = leadrs.length === 10; + if (status === STATUS_WON && checkedLevel === 3) { + if (leadrs.at(-1).time > timer.diffInSecconds || (isTenPlayers && leadrs[9].time > timer.diffInSecconds)) { + return true; + } + } + return false; + } + + const isGameEnded = status === STATUS_LOST || status === STATUS_WON || status === STATUS_PAUSED; // Игровой цикл useEffect(() => { @@ -160,6 +228,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { return () => { clearTimeout(timerId); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [status, pairsCount, previewSeconds]); // Обновляем значение таймера в интервале @@ -167,6 +236,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const intervalId = setInterval(() => { setTimer(getTimerValue(gameStartDate, gameEndDate)); }, 300); + return () => { clearInterval(intervalId); }; @@ -195,7 +265,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )} - {status === STATUS_IN_PROGRESS ? : null} +
+ {isEasyMode && status === STATUS_IN_PROGRESS && ( + Осталось {tries} попытки! + )} + {status === STATUS_IN_PROGRESS ? : null} +
@@ -206,6 +281,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { open={status !== STATUS_IN_PROGRESS ? true : card.open} suit={card.suit} rank={card.rank} + status={STATUS_IN_PROGRESS} /> ))}
@@ -213,10 +289,16 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { {isGameEnded ? (
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..07b4904ef 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -70,3 +70,20 @@ margin-bottom: -12px; } + +.attempt { + color: #fff; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} + +.buttonContainer { + display: flex; + flex-direction: column; + align-items: flex-end; + row-gap: 12px; +} diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..edb824971 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,27 +1,120 @@ import styles from "./EndGameModal.module.css"; - import { Button } from "../Button/Button"; - import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { postLeaderboard } from "../../api/postLeaderboard"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export function EndGameModal({ + isWon, + gameDurationSeconds, + gameDurationMinutes, + onClick, + tries, + checkedLevel, + isTopTen, + leaders, + setLeaders, + diffInSecconds, +}) { + const [inputValue, setInputValue] = useState(""); + + const navigate = useNavigate(); + function goTo() { + navigate("/Leaderboard"); + } + + function handleClick() { + postLeaderboard({ userName: inputValue, userTime: diffInSecconds }).then(leaderboard => { + setLeaders(leaderboard.leaders.sort((a, b) => a.time - b.time).slice(0, 10)); + }); + goTo(); + } -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; + let imgSrc; + let imgAlt; - const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; + if (isWon === "STATUS_PAUSED") { + isWon = "Вы допустили ошибку"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } - const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + // Модалка лидерборда + if (isTopTen) { + isWon = "Вы попали на лидерборд!"; + imgSrc = celebrationImageUrl; + imgAlt = "celebration emodji"; + } else if (isWon === "STATUS_WON") { + isWon = "Вы победили!"; + imgSrc = celebrationImageUrl; + imgAlt = "celebration emodji"; + } + + if (isWon === "STATUS_LOST") { + isWon = "Вы проиграли!"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } + + if (tries === 0) { + isWon = "Вы проиграли!"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } + + const title = isWon; return (
{imgAlt}

{title}

-

Затраченное время:

-
- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} -
+ {isWon === "Вы допустили ошибку" &&

Оставшеся количество попыток:

} + {isWon === "Вы победили!" &&

Затраченное время:

} + {isWon === "Вы попали на лидерборд!" && ( + setInputValue(e.target.value)} + placeholder="Введите имя" + maxLength="20" + /> + )} + {isWon === "Вы попали на лидерборд!" &&

Затраченное время:

} + {isWon === "Вы проиграли!" &&

Затраченное время:

} + {isWon === "Вы допустили ошибку" && ( +
+

{tries}

+
+ )} + {isWon === "Вы победили!" && ( +
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ )} + + {/* Модалка лидербборда */} + {isWon === "Вы попали на лидерборд!" && ( +
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ )} + {isWon === "Вы проиграли!" && ( +
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ )} + {isWon === "Вы допустили ошибку" && } + {isWon === "Вы победили!" && } + {isWon === "Вы попали на лидерборд!" && } + {isWon === "Вы проиграли!" && } - + {isWon === "Вы попали на лидерборд!" && ( + + )}
); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..b4b55c370 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,12 +1,13 @@ .modal { width: 480px; - height: 459px; + /* height: 459px; */ border-radius: 12px; background: #c2f5ff; display: flex; flex-direction: column; justify-content: center; align-items: center; + padding-block: 36px; } .image { @@ -25,6 +26,7 @@ line-height: 48px; margin-bottom: 28px; + text-align: center; } .description { @@ -49,3 +51,49 @@ margin-bottom: 40px; } + +.nameInput { + width: 276px; + height: 45px; + border-radius: 10px; + border: none; + box-sizing: border-box; + padding: 10px 14px 10px 14px; + margin-bottom: 28px; + color: #999999; + font-size: 24px; +} + +.nameInput:active, +:hover, +:focus { + outline: 0; + outline-offset: 0; +} + +.nameInput::placeholder { + color: #999999; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 32px; + text-align: center; +} + +.gameButtonsLink { + background-color: transparent; + border: none; + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: center; + color: #004980; + margin-top: 18px; +} + +.gameButtonsLink:hover { + text-decoration: underline; + cursor: pointer; +} diff --git a/src/contexte/contexte.jsx b/src/contexte/contexte.jsx new file mode 100644 index 000000000..14360784d --- /dev/null +++ b/src/contexte/contexte.jsx @@ -0,0 +1,34 @@ +import { createContext, useState, useEffect } from "react"; +import { getLeaderboard } from "../api/getLeaderboard"; + +export const EasyContext = createContext(false); + +export const EasyProvider = ({ children }) => { + const [tries, setTries] = useState(3); + const [isEasyMode, setIsEasyMode] = useState(false); + const [leadrs, setLeaders] = useState([]); + const [checkedLevel, setCheckedLevel] = useState(); + + useEffect(() => { + if (leadrs.length === 0) { + getData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function getData() { + const data = await getLeaderboard(); + const leaders = data.leaders.sort((a, b) => a.time - b.time).slice(0, 10); + setLeaders(leaders); + } + + + + return ( + + {children} + + ); +}; diff --git a/src/index.js b/src/index.js index f689c5f0b..d39f1dd9d 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 "./contexte/contexte"; 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..3dac563c2 --- /dev/null +++ b/src/pages/Leaderboard/Leaderboard.jsx @@ -0,0 +1,35 @@ +import { Button } from "../../components/Button/Button"; +import styles from "../Leaderboard/Leaderboard.module.css"; +import { useNavigate } from "react-router-dom"; +import { LeaderboardPage } from "./LeaderboardPage"; +import { useContext } from "react"; +import { EasyContext } from "../../contexte/contexte"; + +export function Leaderboard() { + const { leadrs } = useContext(EasyContext); + + const navigate = useNavigate(); + function goTo() { + navigate("/"); + } + + return ( +
+
+

Лидерборд

+
+ +
+
+
+

Позиция

+

Пользователь

+

Время

+
+ {leadrs?.length && + leadrs.map((leader, index) => ( + + ))} +
+ ); +} diff --git a/src/pages/Leaderboard/Leaderboard.module.css b/src/pages/Leaderboard/Leaderboard.module.css new file mode 100644 index 000000000..c43287db6 --- /dev/null +++ b/src/pages/Leaderboard/Leaderboard.module.css @@ -0,0 +1,51 @@ +.container { + max-width: 944px; + margin: 0 auto; + padding-top: 22px; + box-sizing: border-box; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; +} + +.titleLeaderBoard { + font-family: Roboto; + color: #ffffff; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; +} + +.titleBoard { + width: 944px; + height: 64px; + background-color: #ffffff; + border-radius: 12px; + padding: 16px 20px 16px 20px; + box-sizing: border-box; + display: grid; + grid-template-columns: [start] 264px [line2] 564px [line3] 74px [end]; +} + +.title { + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #999999; +} + +.titlePage { + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #000000; +} diff --git a/src/pages/Leaderboard/LeaderboardPage.jsx b/src/pages/Leaderboard/LeaderboardPage.jsx new file mode 100644 index 000000000..ec2736f46 --- /dev/null +++ b/src/pages/Leaderboard/LeaderboardPage.jsx @@ -0,0 +1,19 @@ +import styles from "../Leaderboard/Leaderboard.module.css"; + +export function LeaderboardPage({ id, name, time }) { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + + return ( +
+
+

#{id}

+

{name.slice(0, 20)}

+ +

+ {minutes.toString().padStart("2", "0")}.{seconds.toString().padStart("2", "0")} +

+
+
+ ); +} diff --git a/src/pages/SelectLevelPage/SelectLevelButton.jsx b/src/pages/SelectLevelPage/SelectLevelButton.jsx new file mode 100644 index 000000000..b2c0f1c10 --- /dev/null +++ b/src/pages/SelectLevelPage/SelectLevelButton.jsx @@ -0,0 +1,17 @@ +import styles from "./SelectLevelPage.module.css"; +export function SelectLevelButton({ checkedLevel, levelNumber }) { + return ( + <> + {checkedLevel !== levelNumber && ( +
+ {levelNumber} +
+ )} + {checkedLevel === levelNumber && ( +
+ {levelNumber} +
+ )} + + ); +} diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..cf1fbd143 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,63 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext } from "react"; +import { EasyContext } from "../../contexte/contexte"; +import { Button } from "../../components/Button/Button"; +import { SelectLevelButton } from "./SelectLevelButton"; export function SelectLevelPage() { + const { isEasyMode, setIsEasyMode, checkedLevel, setCheckedLevel } = useContext(EasyContext); + + const handleClick = id => { + setCheckedLevel(Number(id)); + }; + + const navigate = useNavigate(); + function goTo() { + if (!checkedLevel) { + alert("Необходимо выбрать уровень игры"); + } else { + navigate(`/game/${checkedLevel * 3}`); + } + } + return (
-

Выбери сложность

+

+ Выбери
сложность +

+ + +
+ + +

Перейти к лидерборду

+ +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..81aa13230 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -8,13 +8,14 @@ .modal { width: 480px; - height: 459px; + /* height: 459px; */ border-radius: 12px; background: #c2f5ff; display: flex; flex-direction: column; justify-content: center; align-items: center; + padding-block: 36px; } .title { @@ -46,6 +47,22 @@ border-radius: 12px; background: #fff; + cursor: pointer; +} + +.levelChek { + display: flex; + width: 97px; + height: 98px; + flex-direction: column; + justify-content: center; + flex-shrink: 0; + + border-radius: 12px; + background: #fff; + border: 4px solid #0080c1; + box-sizing: border-box; + cursor: pointer; } .levelLink { @@ -62,3 +79,77 @@ .levelLink:visited { color: #0080c1; } + +.isEasyMode { + display: flex; + align-items: center; + box-sizing: border-box; + padding: 8px 16px 8px 16px; + border-radius: 12px; + background-color: #fff; +} + +.isEasyModeTitle { + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + cursor: pointer; +} + +.checkbox { + display: none; +} + +.customCheckbox { + position: relative; + display: inline-block; + width: 30px; + height: 30px; + background-color: #fff; + margin-right: 8px; + vertical-align: sub; + border-radius: 5px; + cursor: pointer; +} + +.customCheckbox::before { + content: ""; + display: inline-block; + width: 20px; + height: 20px; + background-image: url(/public/Vector.svg); + background-size: contain; + background-repeat: no-repeat; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(0); + margin-top: 2px; +} + +.checkbox:checked + .customCheckbox::before { + transform: translate(-50%, -50%) scale(1); +} + +.gameButtonsContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 18px; + margin-top: 38px; +} + +.gameButtonsLink { + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #004980; +} + +.radio { + display: none; +} diff --git a/src/router.js b/src/router.js index da6e94b51..4826abf63 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