diff --git a/.eslintrc.json b/.eslintrc.json index e37e1e072..cae26617b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "plugins": ["prettier"], "rules": { "camelcase": ["error", { "properties": "never" }], - "prettier/prettier": "error", + "prettier/prettier": "off", "eqeqeq": ["error", "always"], "no-unused-vars": ["error"] } diff --git a/README.md b/README.md index 9b90842c4..ae5563c6d 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,7 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +## Ожидаемое время выполнения работы: 6 часов. + +## Фактическое время выполнения: больше 20 часов. \ No newline at end of file diff --git a/src/api/Api.js b/src/api/Api.js new file mode 100644 index 000000000..8daa31f1f --- /dev/null +++ b/src/api/Api.js @@ -0,0 +1,27 @@ +const URL = "https://wedev-api.sky.pro/api/leaderboard"; + +export const getLeadersPage = async () => { + const response = await fetch(URL, { + method: "GET", + }); + if (!response.ok) { + throw new Error("Не удалось получить данные"); + } + const data = await response.json() + return data.leaders; +}; + +export const createLeader = async (name, time) => { + const response = await fetch(URL, { + method: "POST", + body: JSON.stringify ({ + name, + time, + }) + }) + if (response.status === 400) { + throw new Error ("Не удалось загрузить данные") + } + const data = await response.json() + return data.leaders; +} \ No newline at end of file diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..5a747e94b 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -5,6 +5,7 @@ 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 { useEasyContext } from "../../context/useContext"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -41,6 +42,9 @@ function getTimerValue(startDate, endDate) { * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + const isHardMode = pairsCount === 3; + const { isEasyMode } = useEasyContext(); + const [tries, setTries] = useState(3); // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); // Текущий статус игры @@ -69,6 +73,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setStatus(STATUS_IN_PROGRESS); } function resetGame() { + setTries(3); setGameStartDate(null); setGameEndDate(null); setTimer(getTimerValue(null, null)); @@ -82,6 +87,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { * - "Игрок проиграл", если на поле есть две открытые карты без пары * - "Игра продолжается", если не случилось первых двух условий */ + const openCard = clickedCard => { // Если карта уже открыта, то ничего не делаем if (clickedCard.open) { @@ -126,8 +132,31 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const playerLost = openCardsWithoutPair.length >= 2; // "Игрок проиграл", т.к на поле есть две открытые карты без пары + if (playerLost) { - finishGame(STATUS_LOST); + if (isEasyMode) { + setTries(tries - 1); + setTimeout(() => { + setCards( + cards.reduce((accum, card) => { + if (card.id === clickedCard.id) { + return [...accum, { ...card, open: false }]; + } + return [...accum, card]; + }, []), + ); + setCards( + cards.reduce((accum, card) => { + const firstCard = openCardsWithoutPair.find(el => el.id !== clickedCard.id); + if (card.id === firstCard.id) { + return [...accum, { ...card, open: false }]; + } + return [...accum, card]; + }, []), + ); + }, 1000); + } + if (!isEasyMode || tries === 1) finishGame(STATUS_LOST); return; } @@ -196,6 +225,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )} {status === STATUS_IN_PROGRESS ? : null} + {isEasyMode ? Количество попыток: {tries} : ""}
@@ -214,6 +244,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
{imgAlt} -

{title}

+ {!isLeader &&

{title}

} + + {isLeader && ( +
+

Вы попали на Лидерборд!

+ +
+ )} + {error &&

{error}

} + +

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

{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
- - + + {isLeader && ( + + Перейти к лидерборду + + )}
); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..bb2c4c7a2 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,12 +1,14 @@ .modal { width: 480px; - height: 459px; + max-height: 634px; border-radius: 12px; background: #c2f5ff; display: flex; flex-direction: column; justify-content: center; align-items: center; + padding-bottom: 48px; + padding-top: 40px; } .image { @@ -27,6 +29,27 @@ margin-bottom: 28px; } +.leaderText { + text-align: center +} + +.placeholder { + border-radius: 10px; + border: none; + background: rgb(255, 255, 255); + width: 276px; + height: 45px; + left: 374px; + top: 334px; + /* margin-bottom: 28px; */ + color: black; + font-size: 24px; + font-weight: 400; + line-height: 32px; + letter-spacing: 0%; + text-align: center; +} + .description { color: #000; font-variant-numeric: lining-nums proportional-nums; @@ -35,7 +58,7 @@ font-style: normal; font-weight: 400; line-height: 32px; - + margin-top: 28px; margin-bottom: 10px; } @@ -49,3 +72,40 @@ margin-bottom: 40px; } + +.toLeaderboard { + font-size: 18px; + color: #565EEF; + font-family: StratosSkyeng; + margin-top: 18px; + text-decoration-line: underline; + line-height: 32px; +} + +.error { + margin-bottom: 20px; + font-size: 18px; + color: red; + font-family: StratosSkyeng; +} + +.buttonLeader { + border-radius: 8px; + margin-top: 10px; + border: none; + width: 150px; + height: 30px; + font-family: StratosSkyeng; + font-size: 18px; + background: rgb(255, 255, 255); + color: rgba(80, 75, 75, 0.675); +} + +.buttonLeader:hover { + color: #000; + cursor: pointer; +} + +.buttonLeader:disabled { + color: lightgrey; +} \ No newline at end of file diff --git a/src/context/context.jsx b/src/context/context.jsx new file mode 100644 index 000000000..8d2b4885e --- /dev/null +++ b/src/context/context.jsx @@ -0,0 +1,9 @@ +import { createContext, useState } from "react"; + +export const EasyContext = createContext(true); + +export const EasyProvider = ({ children }) => { + const [isEasyMode, setEasyMode] = useState(false); + return {children}; +}; + diff --git a/src/context/useContext.js b/src/context/useContext.js new file mode 100644 index 000000000..155f8e67c --- /dev/null +++ b/src/context/useContext.js @@ -0,0 +1,6 @@ +import { useContext } from "react" +import { EasyContext } from "./context" + +export const useEasyContext = () => { + return useContext(EasyContext); +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index f689c5f0b..871d7d979 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..aed3d7d55 --- /dev/null +++ b/src/pages/Leaderboard/Leaderboard.jsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { getLeadersPage } from "../../api/Api"; +import { Button } from "../../components/Button/Button"; +import styles from "./Leaderboard.module.css"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + + + + +export function Leaderboard() { + const [leaderArr, setLeaderArr] = useState([]); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + function StartGame() { +navigate("/game/9") + } + + useEffect(() => { + const fetchData = async () => { + try { + const data = await getLeadersPage(); + const filterData = data + .sort((a, b) => a.time - b.time) + .slice(0, 10); + setLeaderArr(filterData); + } catch (error) { + setError("Не удалось загрузить данные"); + } + } + fetchData(); + }, []); + + return ( +
+
+

Лидерборд

+ +
+
+
+

Позиция

+

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

+

+

Время

+
+ {leaderArr.map((leader, index) => ( +
+

# {index + 1}

+

{leader.name}

+

+

{`${Math.floor(leader.time / 60).toString().padStart(2, 0)}:${(leader.time % 60).toString().padStart(2, 0)}`}

+
+ ))} + {error} +
+
+ ); +} diff --git a/src/pages/Leaderboard/Leaderboard.module.css b/src/pages/Leaderboard/Leaderboard.module.css new file mode 100644 index 000000000..ec0e95140 --- /dev/null +++ b/src/pages/Leaderboard/Leaderboard.module.css @@ -0,0 +1,67 @@ +.container { + width: 944px; + margin: 0 auto; + padding: 26px; + padding-top: 52px; + box-sizing: border-box; +} + +.header { + display: flex; + justify-content: space-between; + align-items: end; + margin-bottom: 40px; + } + +.title { + font-family: StratosSkyeng; + font-size: 24px; + color: #fff; +} + +.table { + display: flex; + flex-direction: column; + gap: 15px; +} + +.contentLeaders, +.contentTitle { + display: flex; + justify-content: space-between; + gap: 66px; + align-items: center; + border: 3px solid none; + background-color: #fff; + border-radius: 12px; + height: 64px; + font-size: 24px; + font-family: StratosSkyeng; + padding-left: 20px; + padding-right: 20px; + color: #999; +} + +.contentLeaders { + color: black; +} + +.positionTitle { +width: 178px; +/* color: #999; */ +} + +.nameTitle { + width: 324px; + /* color: #999; */ +} + +.reserveTitle { + width: 102px; + /* color: #999; */ +} + +.timeTitle { + width: 102px; + /* color: #999; */ +} \ No newline at end of file diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..8563bea26 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,93 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useEasyContext } from "../../context/useContext"; +import { Button } from "../../components/Button/Button"; +import { useState } from "react"; export function SelectLevelPage() { + const navigate = useNavigate(); + const [error, setError] = useState(null); + const { isEasyMode, setEasyMode } = useEasyContext(); + + const [level, setLevel] = useState(null); + + function ChengeLevel(e) { + setLevel(e.target.value); + } + + function StartGame() { + if (!level) { + setError("Выбери уровень сложности"); + return; + } + navigate(level); + } + return (

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

  • - + +
  • - + +
  • - + +
+
+ +
+

{error}

+ + + Перейти к лидерборду +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..98214521f 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -8,7 +8,7 @@ .modal { width: 480px; - height: 459px; + height: 529px; border-radius: 12px; background: #c2f5ff; display: flex; @@ -26,6 +26,7 @@ font-style: normal; font-weight: 400; line-height: 48px; + width: 205px; } .levels { @@ -43,7 +44,6 @@ flex-direction: column; justify-content: center; flex-shrink: 0; - border-radius: 12px; background: #fff; } @@ -62,3 +62,74 @@ .levelLink:visited { color: #0080c1; } + +.modeSelection { + display: flex; + margin-bottom: 38px; +} + +.nameMode { + font-family: StratosSkyeng; + font-size: 24px; +} + +.checkbox { + position: absolute; + z-index: -1; + opacity: 0; +} + +.customCheckbox { + position: relative; + display: inline-block; + width: 30px; + height: 30px; + border-radius: 0.25em; + margin-right: 0.5em; + background-repeat: no-repeat; + background-position: center center; + background-color: #fff; +} + +.customCheckbox::before { + content: ''; + width: 22px; + height: 17px; + display: inline-block; + background-image: url(./Vector.png); + background-repeat: no-repeat; + margin-left: 4px; + transform: scale(0); +} + +.checkbox:checked+.customCheckbox::before { + transform: scale(1); +} + +.toLeaderboard { + font-size: 18px; + color: #565EEF; + font-family: StratosSkyeng; + margin-top: 18px; + text-decoration-line: underline; + line-height: 32px; +} + +.levelButton { + display: none; + + &[type="radio"]:checked + label { + text-shadow: 5px 5px 5px gray; + position: relative; + bottom: 3px; + right: 3px; + + } +} + +.error { + margin-bottom: 20px; + font-size: 18px; + color: red; + font-family: StratosSkyeng; +} \ No newline at end of file diff --git a/src/pages/SelectLevelPage/Vector.png b/src/pages/SelectLevelPage/Vector.png new file mode 100644 index 000000000..4e81b49ab Binary files /dev/null and b/src/pages/SelectLevelPage/Vector.png differ diff --git a/src/router.js b/src/router.js index da6e94b51..988eeebfd 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