From c3ce3134ac480748b1aababa3686b561c1b030fc Mon Sep 17 00:00:00 2001 From: sviridevg Date: Wed, 14 Aug 2024 13:29:23 +0300 Subject: [PATCH 1/5] HW1 --- docs/mvp-spec.md | 7 +- src/components/Cards/Cards.jsx | 82 ++++++++++++++++--- src/components/Cards/Cards.module.css | 17 ++++ src/components/EndGameModal/EndGameModal.jsx | 57 +++++++++++-- src/contexte/contexte.jsx | 10 +++ src/index.js | 5 +- src/pages/SelectLevelPage/SelectLevelPage.jsx | 13 +++ .../SelectLevelPage.module.css | 27 ++++++ 8 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 src/contexte/contexte.jsx 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/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..e856fcabf 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"; // Начало игры: игрок видит все карты в течении нескольких секунд @@ -41,8 +45,18 @@ function getTimerValue(startDate, endDate) { * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + // Когда игра окончена, переход на главную страницу + const navigate = useNavigate(); + function goTo() { + navigate("/"); + } + + // Обработка количества попыток + const { tries, setTries, isEasyMode } = useContext(EasyContext); + // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); + // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); @@ -57,16 +71,32 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { minutes: 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 +105,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 +171,23 @@ 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; + const isGameEnded = status === STATUS_LOST || status === STATUS_WON || status === STATUS_PAUSED; // Игровой цикл useEffect(() => { @@ -195,7 +248,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 +264,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 +272,11 @@ 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..9eb883f01 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -5,23 +5,62 @@ import { Button } from "../Button/Button"; import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, tries }) { + let imgSrc; + let imgAlt; - const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; + if (isWon === "STATUS_PAUSED") { + isWon = "Вы допустили ошибку"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } + if (isWon === "STATUS_WON") { + isWon = "Вы победили!"; + imgSrc = celebrationImageUrl; + imgAlt = "celebration emodji"; + } + if (isWon === "STATUS_LOST") { + isWon = "Вы проиграли!"; + imgSrc = deadImageUrl; + imgAlt = "dead emodji"; + } - const imgAlt = isWon ? "celebration emodji" : "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 === "Вы проиграли!" &&

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

} + + {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 === "Вы допустили ошибку" && } + {isWon === "Вы победили!" && } + {isWon === "Вы проиграли!" && }
); } diff --git a/src/contexte/contexte.jsx b/src/contexte/contexte.jsx new file mode 100644 index 000000000..0d3986da5 --- /dev/null +++ b/src/contexte/contexte.jsx @@ -0,0 +1,10 @@ +import { createContext, useState } from "react"; + +export const EasyContext = createContext(false); + +export const EasyProvider = ({ children }) => { + const [tries, setTries] = useState(3); + const [isEasyMode, setIsEasyMode] = useState(false); + + 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/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..be7d291bd 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,7 +1,11 @@ import { Link } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext } from "react"; +import { EasyContext } from "../../contexte/contexte"; export function SelectLevelPage() { + const { isEasyMode, setIsEasyMode } = useContext(EasyContext); + return (
@@ -23,6 +27,15 @@ export function SelectLevelPage() { +
+ + setIsEasyMode(e.target.checked)} + /> +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..2f4a4d8df 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -62,3 +62,30 @@ .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 { + color: #0080c1; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 48px; + display: flex; +} + +.checkbox { + margin-left: 16px; + width: 24px; + height: 24px; +} From cc81e481df27bf8dfdaa35d2435b917f8cdcd285 Mon Sep 17 00:00:00 2001 From: sviridevg Date: Wed, 14 Aug 2024 13:46:12 +0300 Subject: [PATCH 2/5] HW1_readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 9b90842c4..a85b834a9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,14 @@ https://skypro-web-developer.github.io/react-memo/ Настроены eslint и prettier. Корректность кода проверяется автоматически перед каждым коммитом с помощью lefthook (аналог husky). Закомитить код, который не проходит проверку eslint не получится. + +### Затраченное время на разработку дополнительных функций + +1.1 Реализация упрощенного режима "3 попытки" +Планируемое время 16 часов + +Факт 14 часов + ### Доступные команды #### `npm start` From 5576d54ba1f0d99d6f4fe60d217036b0011acd97 Mon Sep 17 00:00:00 2001 From: sviridevg Date: Mon, 26 Aug 2024 23:15:32 +0300 Subject: [PATCH 3/5] HW_2 --- README.md | 8 +- public/Vector.svg | 3 + src/api/getLeaderboard.js | 14 +++ src/api/postLeaderboard.js | 23 +++++ src/components/Cards/Cards.jsx | 23 ++++- src/components/EndGameModal/EndGameModal.jsx | 68 +++++++++++++-- .../EndGameModal/EndGameModal.module.css | 50 ++++++++++- src/contexte/contexte.jsx | 25 +++++- src/pages/Leaderboard/Leaderboard.jsx | 35 ++++++++ src/pages/Leaderboard/Leaderboard.module.css | 51 +++++++++++ src/pages/Leaderboard/LeaderboardPage.jsx | 19 ++++ .../SelectLevelPage/SelectLevelButton.jsx | 17 ++++ src/pages/SelectLevelPage/SelectLevelPage.jsx | 62 ++++++++----- .../SelectLevelPage.module.css | 86 ++++++++++++++++--- src/router.js | 5 ++ 15 files changed, 445 insertions(+), 44 deletions(-) create mode 100644 public/Vector.svg create mode 100644 src/api/getLeaderboard.js create mode 100644 src/api/postLeaderboard.js create mode 100644 src/pages/Leaderboard/Leaderboard.jsx create mode 100644 src/pages/Leaderboard/Leaderboard.module.css create mode 100644 src/pages/Leaderboard/LeaderboardPage.jsx create mode 100644 src/pages/SelectLevelPage/SelectLevelButton.jsx diff --git a/README.md b/README.md index a85b834a9..7fc5d31f3 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,18 @@ https://skypro-web-developer.github.io/react-memo/ Настроены eslint и prettier. Корректность кода проверяется автоматически перед каждым коммитом с помощью lefthook (аналог husky). Закомитить код, который не проходит проверку eslint не получится. - -### Затраченное время на разработку дополнительных функций +### Затраченное время на разработку дополнительных функций 1.1 Реализация упрощенного режима "3 попытки" Планируемое время 16 часов Факт 14 часов +2.1 Реализация Лидерборда +Планируемое время 24 часов + +Факт 18 часов + ### Доступные команды #### `npm start` 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/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 e856fcabf..8140c1ce3 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -23,6 +23,7 @@ function getTimerValue(startDate, endDate) { return { minutes: 0, seconds: 0, + diffInSecconds: 0, }; } @@ -31,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, }; } @@ -52,7 +55,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { } // Обработка количества попыток - const { tries, setTries, isEasyMode } = useContext(EasyContext); + const { tries, setTries, isEasyMode, checkedLevel, leadrs, setLeaders } = useContext(EasyContext); // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); @@ -69,6 +72,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const [timer, setTimer] = useState({ seconds: 0, minutes: 0, + diffInSecconds: 0, }); // Если количество попыток равно 0 устанавливается стату проиграл и игра заканчивается @@ -187,6 +191,17 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // ... игра продолжается }; + // Проверка на попадание в топ 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; // Игровой цикл @@ -220,6 +235,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const intervalId = setInterval(() => { setTimer(getTimerValue(gameStartDate, gameEndDate)); }, 300); + return () => { clearInterval(intervalId); }; @@ -277,6 +293,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { gameDurationMinutes={timer.minutes} onClick={whatsNext} tries={tries} + checkedLevel={checkedLevel} + isTopTen={isTopTen()} + leadrs={leadrs} + setLeaders={setLeaders} + diffInSecconds={timer.diffInSecconds} /> ) : null} diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 9eb883f01..edb824971 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,11 +1,37 @@ 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, tries }) { let imgSrc; let imgAlt; @@ -14,11 +40,18 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, imgSrc = deadImageUrl; imgAlt = "dead emodji"; } - if (isWon === "STATUS_WON") { + + // Модалка лидерборда + 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; @@ -39,8 +72,17 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes,

{title}

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

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

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

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

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

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

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

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

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

{tries}

@@ -52,15 +94,27 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes,
)} + {/* Модалка лидербборда */} + {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 === "Вы проиграли!" && } + + {isWon === "Вы попали на лидерборд!" && ( + + )} ); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..f632886c8 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; +} \ No newline at end of file diff --git a/src/contexte/contexte.jsx b/src/contexte/contexte.jsx index 0d3986da5..4a84c7132 100644 --- a/src/contexte/contexte.jsx +++ b/src/contexte/contexte.jsx @@ -1,10 +1,31 @@ -import { createContext, useState } from "react"; +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(); - return {children}; + useEffect(() => { + if (leadrs.length === 0) { + getData(); + } + }, []); + + 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/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 be7d291bd..cf1fbd143 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,40 +1,62 @@ -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 } = useContext(EasyContext); + 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 (
-

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

+

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

    -
  • - - 1 - -
  • -
  • - - 2 - -
  • -
  • - - 3 - -
  • + + +
-
- + + +
+ + +

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

+
diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 2f4a4d8df..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 { @@ -73,19 +90,66 @@ } .isEasyModeTitle { - color: #0080c1; - text-align: center; - font-variant-numeric: lining-nums proportional-nums; - font-family: StratosSkyeng; + font-family: Roboto; font-size: 24px; - font-style: normal; font-weight: 400; - line-height: 48px; - display: flex; + line-height: 32px; + text-align: center; + cursor: pointer; } .checkbox { - margin-left: 16px; - width: 24px; - height: 24px; + 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 From d716f7534fee41b57dea988a565c88fadca2ce65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A1=D0=B2?= =?UTF-8?q?=D0=B8=D1=80=D0=B8=D0=B4=D0=BE=D0=B2?= Date: Tue, 26 Nov 2024 20:20:08 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B9=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=20=D0=BB=D0=B8=D0=B4=D0=B5=D1=80=D0=B1=D0=BE=D1=80=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=B7=20=D0=9A=D1=83=D1=80=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/getLeaderboard.js | 3 +- src/pages/Leaderboard/Leaderboard.jsx | 9 ++- src/pages/Leaderboard/Leaderboard.module.css | 60 ++++++++++++++++++- src/pages/Leaderboard/LeaderboardPage.jsx | 33 +++++++++- .../Leaderboard/images/achiv_active_1.svg | 15 +++++ .../Leaderboard/images/achiv_active_2.svg | 14 +++++ .../Leaderboard/images/achiv_start_1.svg | 17 ++++++ .../Leaderboard/images/achiv_start_2.svg | 30 ++++++++++ 8 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 src/pages/Leaderboard/images/achiv_active_1.svg create mode 100644 src/pages/Leaderboard/images/achiv_active_2.svg create mode 100644 src/pages/Leaderboard/images/achiv_start_1.svg create mode 100644 src/pages/Leaderboard/images/achiv_start_2.svg diff --git a/src/api/getLeaderboard.js b/src/api/getLeaderboard.js index fd64ba158..e6a0ed305 100644 --- a/src/api/getLeaderboard.js +++ b/src/api/getLeaderboard.js @@ -1,4 +1,4 @@ -const apiUrl = "https://wedev-api.sky.pro/api/leaderboard"; +const apiUrl = "https://wedev-api.sky.pro/api/v2/leaderboard"; export const getLeaderboard = async () => { // Запрос к API получения списка победителей @@ -10,5 +10,6 @@ export const getLeaderboard = async () => { throw new Error(`Не удалось получить данные с сервера! status: ${response.status}`); } + // console.log(response.json()); return await response.json(); }; diff --git a/src/pages/Leaderboard/Leaderboard.jsx b/src/pages/Leaderboard/Leaderboard.jsx index 3dac563c2..2a7be52fe 100644 --- a/src/pages/Leaderboard/Leaderboard.jsx +++ b/src/pages/Leaderboard/Leaderboard.jsx @@ -24,11 +24,18 @@ export function Leaderboard() {

Позиция

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

+

Достижения

Время

{leadrs?.length && leadrs.map((leader, index) => ( - + ))}
); diff --git a/src/pages/Leaderboard/Leaderboard.module.css b/src/pages/Leaderboard/Leaderboard.module.css index c43287db6..5dc7d9e00 100644 --- a/src/pages/Leaderboard/Leaderboard.module.css +++ b/src/pages/Leaderboard/Leaderboard.module.css @@ -29,7 +29,7 @@ padding: 16px 20px 16px 20px; box-sizing: border-box; display: grid; - grid-template-columns: [start] 264px [line2] 564px [line3] 74px [end]; + grid-template-columns: [start] 206px [line2] 352px [line3] 259px [line4] 0 [end]; } .title { @@ -49,3 +49,61 @@ text-align: left; color: #000000; } + +.imgContainer { + display: flex; + gap: 6px; + position: relative; + align-items: center; +} + +.imgWrapper { + position: relative; + display: flex; + height: 32px; + margin-right: 12px; +} + +.tooltip { + visibility: hidden; + position: absolute; + bottom: 135%; + left: 330%; + box-shadow: 5px 5px 10px #0000000e; + width: 174px; + height: 40px; + transform: translateX(-50%); + background-color: #c2f5ff; + color: #004980; + font-family: Roboto; + padding: 15px 20px 20px 24px; + border-radius: 12px; + font-size: 18px; + white-space: nowrap; + z-index: 10; + opacity: 0; + transition: + opacity 0.2s, + visibility 0.2s; + white-space: normal; + text-align: center; + line-height: 24 px; +} + +.imgWrapper:hover .tooltip { + visibility: visible; + opacity: 1; +} + +.tooltip::after { + content: ""; + position: absolute; + top: 85%; + left: 15%; + transform: translateX(-50%); + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-left: 10px solid #c2f5ff; +} diff --git a/src/pages/Leaderboard/LeaderboardPage.jsx b/src/pages/Leaderboard/LeaderboardPage.jsx index ec2736f46..b1300ea3f 100644 --- a/src/pages/Leaderboard/LeaderboardPage.jsx +++ b/src/pages/Leaderboard/LeaderboardPage.jsx @@ -1,17 +1,44 @@ import styles from "../Leaderboard/Leaderboard.module.css"; +import achivStart1 from "./images/achiv_start_1.svg"; +import achivStart2 from "./images/achiv_start_2.svg"; +import achivActive1 from "./images/achiv_active_1.svg"; +import achivActive2 from "./images/achiv_active_2.svg"; -export function LeaderboardPage({ id, name, time }) { +export function LeaderboardPage({ id, name, time, achievements }) { const minutes = Math.floor(time / 60); const seconds = time % 60; + // Логика отображения достижений + const renderAchivImage = (src, tooltip) => { + const hasTooltip = tooltip !== null; + + return ( +
+ Достижение + {hasTooltip &&
{tooltip}
} +
+ ); + }; + + const firstImagesElement = achievements.includes(1) + ? renderAchivImage(achivActive1, "Игра пройдена в сложном режиме") + : renderAchivImage(achivStart1, null); + + const secondImagesElement = achievements.includes(2) + ? renderAchivImage(achivActive2, "Игра пройдена без супер-сил") + : renderAchivImage(achivStart2, null); + return (

#{id}

{name.slice(0, 20)}

- +
+ {firstImagesElement} + {secondImagesElement} +

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

diff --git a/src/pages/Leaderboard/images/achiv_active_1.svg b/src/pages/Leaderboard/images/achiv_active_1.svg new file mode 100644 index 000000000..135d3b540 --- /dev/null +++ b/src/pages/Leaderboard/images/achiv_active_1.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pages/Leaderboard/images/achiv_active_2.svg b/src/pages/Leaderboard/images/achiv_active_2.svg new file mode 100644 index 000000000..312c80e93 --- /dev/null +++ b/src/pages/Leaderboard/images/achiv_active_2.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pages/Leaderboard/images/achiv_start_1.svg b/src/pages/Leaderboard/images/achiv_start_1.svg new file mode 100644 index 000000000..cafd69a1f --- /dev/null +++ b/src/pages/Leaderboard/images/achiv_start_1.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/pages/Leaderboard/images/achiv_start_2.svg b/src/pages/Leaderboard/images/achiv_start_2.svg new file mode 100644 index 000000000..7949f73ed --- /dev/null +++ b/src/pages/Leaderboard/images/achiv_start_2.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c687a6c7fadcb6fc81d1fac95a3be5846609bf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A1=D0=B2?= =?UTF-8?q?=D0=B8=D1=80=D0=B8=D0=B4=D0=BE=D0=B2?= Date: Tue, 26 Nov 2024 20:29:45 +0300 Subject: [PATCH 5/5] debug --- src/api/getLeaderboard.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/getLeaderboard.js b/src/api/getLeaderboard.js index e6a0ed305..c02fff928 100644 --- a/src/api/getLeaderboard.js +++ b/src/api/getLeaderboard.js @@ -10,6 +10,5 @@ export const getLeaderboard = async () => { throw new Error(`Не удалось получить данные с сервера! status: ${response.status}`); } - // console.log(response.json()); return await response.json(); };