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/README.md b/README.md index 9b90842c4..f9812dd99 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,43 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +## Счётчик ошибок + +В игре реализован счётчик ошибок, который показывает количество оставшихся попыток. + +### Формат отображения + +- "Осталось попыток: X" + +### Обоснование выбора + +Этот формат позволяет игрокам легко понять, сколько попыток у них осталось, без отвлечения от игры. Использование красного цвета привлекает внимание к этой важной информации. + +## Оценка времени работы + +- **Инициализация игры**: O(n) + - Игра инициализируется на основе количества карт. Чем больше карт, тем больше времени потребуется для их инициализации. +- **Обработка клика по карте**: O(1) + - Время на обработку клика по карте фиксированное, так как это простое действие — переворот карты. +- **Проверка совпадений**: O(1) + - Проверка совпадения двух карт осуществляется мгновенно, так как это сравнение двух значений. +- **Проверка выигрыша**: O(n) + - Для проверки выигрыша необходимо пройти по всем картам, чтобы убедиться, что все пары найдены. +- **Перемешивание карт**: O(n log n) + - Перемешивание карт реализовано с использованием алгоритма, временная сложность которого составляет O(n log n), что характерно для эффективных алгоритмов сортировки. + + Время на выполнение ДЗ №1: + + - пларируемое - 12 часов + - фактическое - 15 часов + +Время на выполнение ДЗ №2: + + - пларируемое - 12 часов + - фактическое - 14 часов + +Время на выполнение ДЗ №3: + + - пларируемое - 12 часов + - фактическое - 13 часов \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index edaf5083f..662339d85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "classnames": "^2.3.2", + "date-fns": "^3.6.0", "gh-pages": "^6.0.0", "lodash": "^4.17.21", "react": "^18.2.0", @@ -6785,6 +6786,15 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -23136,6 +23146,11 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index e9b7a089e..061aa6e4b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "classnames": "^2.3.2", + "date-fns": "^3.6.0", "gh-pages": "^6.0.0", "lodash": "^4.17.21", "react": "^18.2.0", diff --git a/public/1682744403_papik-pro-p-smail-chekboks-png-32.png b/public/1682744403_papik-pro-p-smail-chekboks-png-32.png new file mode 100644 index 000000000..9f9e0f561 Binary files /dev/null and b/public/1682744403_papik-pro-p-smail-chekboks-png-32.png differ diff --git a/public/card_insight.png b/public/card_insight.png new file mode 100644 index 000000000..aeddd2607 Binary files /dev/null and b/public/card_insight.png differ diff --git a/public/index.html b/public/index.html index c0103cf10..f8f3a611a 100644 --- a/public/index.html +++ b/public/index.html @@ -2,6 +2,9 @@ + + + diff --git a/public/magic_ball.svg b/public/magic_ball.svg new file mode 100644 index 000000000..78d4894ef --- /dev/null +++ b/public/magic_ball.svg @@ -0,0 +1,17 @@ + + + Created with Pixso. + + + + + + + + + + + + + + diff --git a/public/magic_ball_empty.svg b/public/magic_ball_empty.svg new file mode 100644 index 000000000..8de6a592f --- /dev/null +++ b/public/magic_ball_empty.svg @@ -0,0 +1,33 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/puzzle.svg b/public/puzzle.svg new file mode 100644 index 000000000..0b1729123 --- /dev/null +++ b/public/puzzle.svg @@ -0,0 +1,18 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + diff --git a/public/puzzle_empty.svg b/public/puzzle_empty.svg new file mode 100644 index 000000000..b49cb5c68 --- /dev/null +++ b/public/puzzle_empty.svg @@ -0,0 +1,20 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + diff --git a/src/api.js b/src/api.js new file mode 100644 index 000000000..d26ba048a --- /dev/null +++ b/src/api.js @@ -0,0 +1,47 @@ +const API_URL = "https://wedev-api.sky.pro/api/v2/leaderboard"; + +export async function getPlayersList() { + const response = await fetch(API_URL, { + method: "GET", + }); + + const data = await response.json(); + return data.leaders; +} + +// Получить ID достижений +// function getAchievementIds(achievements) { +// if (!Array.isArray(achievements)) { +// return []; +// } +// return achievements; +// } + +export async function updateLeaderboard(name, time, achievements) { + // const achievementIds = getAchievementIds(achievements); + // console.log("Achievements IDs in updateLeaderboard:", achievementIds); + + const requestBody = JSON.stringify({ + name: name || "Пользователь", + time, + achievements, + }); + + try { + const response = await fetch(API_URL, { + method: "POST", + body: requestBody, + }); + + const responseBody = await response.text(); + console.log("Ответ от сервера:", responseBody); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Не удалось обновить лидерборд: ${response.status} ${errorText}`); + } + } catch (error) { + console.error("Ошибка сети или другая ошибка:", error); + throw new Error("Не удалось обновить лидерборд"); + } +} diff --git a/src/components/Ball/Ball.jsx b/src/components/Ball/Ball.jsx new file mode 100644 index 000000000..c67978f70 --- /dev/null +++ b/src/components/Ball/Ball.jsx @@ -0,0 +1,11 @@ +import styles from "./Ball.module.css"; + +export function Ball() { + return ( +
+
+

Игра пройдена в сложном режиме

+
+
+ ); +} diff --git a/src/components/Ball/Ball.module.css b/src/components/Ball/Ball.module.css new file mode 100644 index 000000000..93850aee7 --- /dev/null +++ b/src/components/Ball/Ball.module.css @@ -0,0 +1,34 @@ +.popup { + position: absolute; + top: -100px; +} + +.popupContent { + position: relative; + background-color: #C2F5FF; + border-radius: 8px; + padding: 15px 20px; + width: 212px; + text-align: center; + box-sizing: border-box; +} + +.popupContent::after { + content: ""; + position: absolute; + bottom: -20px; + left: 20px; + width: 0; + height: 0; + border-right: 25px solid transparent; + border-top: 20px solid #C2F5FF; + border-bottom: 5px solid transparent; +} + +.popupText { + color: rgb(0, 73, 128); + font-family: StratosSkyeng; + font-size: 18px; + font-weight: 400; + text-align: center; +} \ No newline at end of file diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..a89697f56 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,10 +1,11 @@ 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 "../../context/context"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -21,11 +22,9 @@ function getTimerValue(startDate, endDate) { seconds: 0, }; } - if (endDate === null) { endDate = new Date(); } - const diffInSecconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000); const minutes = Math.floor(diffInSecconds / 60); const seconds = diffInSecconds % 60; @@ -35,28 +34,25 @@ function getTimerValue(startDate, endDate) { }; } -/** - * Основной компонент игры, внутри него находится вся игровая механика и логика. - * pairsCount - сколько пар будет в игре - * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры - */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { - // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта + const isHardMode = pairsCount === 9; + const { tries, setTries, isEasyMode } = useContext(EasyContext); const [cards, setCards] = useState([]); - // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); - - // Дата начала игры const [gameStartDate, setGameStartDate] = useState(null); - // Дата конца игры const [gameEndDate, setGameEndDate] = useState(null); - - // Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate const [timer, setTimer] = useState({ seconds: 0, minutes: 0, }); + const [achievements, setAchievements] = useState([]); + + const [isRevealed, setIsRevealed] = useState(false); // Состояние для отображения всех карт + const [canUsePower, setCanUsePower] = useState(true); // Состояние для использования суперсилы + const [isPaused, setIsPaused] = useState(false); // Состояние паузы таймера + const [usedSuperpower, setUsedSuperpower] = useState(false); + function finishGame(status = STATUS_LOST) { setGameEndDate(new Date()); setStatus(status); @@ -73,15 +69,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setGameEndDate(null); setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + setTries(3); + setAchievements([]); + setCanUsePower(true); // Сброс возможности использования суперсилы + setUsedSuperpower(false); } - /** - * Обработка основного действия в игре - открытие карты. - * После открытия карты игра может пепереходит в следующие состояния - * - "Игрок выиграл", если на поле открыты все карты - * - "Игрок проиграл", если на поле есть две открытые карты без пары - * - "Игра продолжается", если не случилось первых двух условий - */ const openCard = clickedCard => { // Если карта уже открыта, то ничего не делаем if (clickedCard.open) { @@ -127,15 +120,43 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // "Игрок проиграл", т.к на поле есть две открытые карты без пары if (playerLost) { - finishGame(STATUS_LOST); + if (isEasyMode) { + setTries(prevTries => prevTries - 1); + if (tries - 1 <= 0) { + finishGame(STATUS_LOST); + } else { + setTimeout(() => { + setCards( + nextCards.map(card => + openCardsWithoutPair.some(openCard => openCard.id === card.id) ? { ...card, open: false } : card, + ), + ); + }, 1000); + } + } else { + finishGame(STATUS_LOST); + } return; } - - // ... игра продолжается }; const isGameEnded = status === STATUS_LOST || status === STATUS_WON; + // Логика для активации суперсилы "Прозрение" + const handleReveal = () => { + if (canUsePower) { + setIsRevealed(true); + setIsPaused(true); // Останавливаем таймер + setCanUsePower(false); // Суперсила используется только один раз + setUsedSuperpower(true); + + setTimeout(() => { + setIsRevealed(false); + setIsPaused(false); // Возобновляем таймер через 5 секунд + }, 5000); // 5 секундное действие + } + }; + // Игровой цикл useEffect(() => { // В статусах кроме превью доп логики не требуется @@ -164,13 +185,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // Обновляем значение таймера в интервале useEffect(() => { + if (isPaused) return; + const intervalId = setInterval(() => { setTimer(getTimerValue(gameStartDate, gameEndDate)); }, 300); return () => { clearInterval(intervalId); }; - }, [gameStartDate, gameEndDate]); + }, [gameStartDate, gameEndDate, isPaused]); return (
@@ -195,28 +218,52 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )}
- {status === STATUS_IN_PROGRESS ? : null} - + {isEasyMode && ( + + + + + {tries} + + )} + {status === STATUS_IN_PROGRESS ? ( + <> +
+ eye_perk +
{isRevealed}
+
+ + + ) : null} +
{cards.map(card => ( openCard(card)} - open={status !== STATUS_IN_PROGRESS ? true : card.open} + open={isRevealed || status !== STATUS_IN_PROGRESS ? true : card.open} suit={card.suit} rank={card.rank} /> ))}
- {isGameEnded ? (
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..72e51a7b1 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -28,14 +28,11 @@ .header { display: flex; justify-content: space-between; - align-items: end; margin-bottom: 35px; } .timer { display: flex; - align-items: end; - color: #fff; font-family: StratosSkyeng; font-size: 64px; @@ -67,6 +64,47 @@ font-style: normal; font-weight: 400; line-height: 32px; - margin-bottom: -12px; } + +.insight { + display: flex; + gap: 15px; + position: relative; + border: none; +} + +.cardInsight { + background-color: #C2F5FF; + border: none; + cursor: pointer; + width: 68px; + height: 68px; + radius: 50px; + opacity: 40; +} + +.counterInsight { + position: absolute; + background-color: #C2F5FF; + border-radius: 100px; + + padding: 5px 10px; + font-family: Roboto; + top: 45px; + left: 45px; +} + + +.attemptСounter { + display: flex; + gap: 10px; + color: red; + font-variant-numeric: lining-nums proportional-nums; + font-family: Roboto; + font-size: 30px; + font-style: normal; + font-weight: 400; + line-height: 30px; + padding-bottom: 10px; +} \ No newline at end of file diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..88bd89b9b 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,27 +1,90 @@ import styles from "./EndGameModal.module.css"; - import { Button } from "../Button/Button"; - import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { Link, useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { updateLeaderboard } from "../../api"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; - +export function EndGameModal({ + isWon, + level, + isHardMode, + gameDurationSeconds, + gameDurationMinutes, + onClick, + achievements, + hasUsedSuperPower, +}) { + const title = isWon && level <= 2 ? "Вы победили!" : ""; + const isLeader = isWon && isHardMode && level === 3; + const lossTitle = !isWon ? "Вы проиграли!" : ""; const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; - const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + const navigate = useNavigate(); + const [username, setUsername] = useState(""); + + const handleGameEnd = async () => { + const totalTime = gameDurationMinutes * 60 + gameDurationSeconds; + const cardAchievements = [...achievements]; + + if (isWon && isHardMode && !cardAchievements.includes(1)) { + console.log(achievements); + cardAchievements.push(1); // Ачивка за сложный уровень + } + + if (isWon && !hasUsedSuperPower && !cardAchievements.includes(2)) { + cardAchievements.push(2); // Ачивка за победу без суперсил + } + + if (username.trim()) { + try { + await updateLeaderboard(username, totalTime, cardAchievements); + console.log("Результаты игры успешно отправлены"); + navigate("/leaderboard"); + } catch (error) { + console.error("Ошибка при отправке результатов игры:", error); + alert("Не удалось обновить лидерборд, попробуйте снова."); + } + } else { + alert("Введите имя пользователя перед отправкой!"); + } + }; + + const handleInputChange = event => { + setUsername(event.target.value); + }; return (
{imgAlt} -

{title}

+ {title &&

{title}

} + {lossTitle &&

{lossTitle}

} + {isLeader && ( +
+

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

+ + +
+ )}

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

- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} + {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..d6cc4c100 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,6 +1,6 @@ .modal { width: 480px; - height: 459px; + height: 643px; border-radius: 12px; background: #c2f5ff; display: flex; @@ -35,7 +35,7 @@ font-style: normal; font-weight: 400; line-height: 32px; - + margin-top: 20px; margin-bottom: 10px; } @@ -46,6 +46,90 @@ font-style: normal; font-weight: 400; line-height: 72px; - margin-bottom: 40px; } + +.input, .submitButton { + box-sizing: border-box; + height: 45px; + width: 276px; + border-radius: 8px; + border: none; +} + +.input { + background-color: #ffffff; + color: #999999; + padding: 0 16px; + margin-bottom: 10px; + margin-top: 20px; + font-size: 18px; + line-height: 45px; + text-align: center; + + &::placeholder { + line-height: 32px; + font-size: 24px; + font-weight: 400; + text-align: center; + color: #999999; + } + + &:invalid { + border: 1px solid red; + } +} + +.submitButton { + background-color: #004980; + color: #ffffff; + border: none; + border-radius: 8px; + font-size: 18px; + font-weight: 600; + cursor: pointer; + text-align: center; + + &:hover { + background-color: #003366; + } + + &:active { + background-color: #002244; + } + + &:disabled { + background-color: #cccccc; + cursor: not-allowed; + } +} + +.modalContainer { + position: relative; + width: 250px; + margin: 0 auto; +} + +.leaderboardModal { + font-family: 'Roboto', sans-serif; + font-weight: 400; + font-size: 40px; + line-height: 48px; + color: #004980; + width: 276px; + height: 96px; + top: 210px; + left: 0; + text-align: center; + margin: 20; +} + +.LeaderBoardLink { + margin-top: 20px; + color: #565EEF; + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: left; +} \ No newline at end of file diff --git a/src/components/LeaderBoardPlayer/LeaderBoardPlayer.jsx b/src/components/LeaderBoardPlayer/LeaderBoardPlayer.jsx new file mode 100644 index 000000000..6a251f87e --- /dev/null +++ b/src/components/LeaderBoardPlayer/LeaderBoardPlayer.jsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import styles from "./LeaderBoardPlayer.module.css"; +import { Puzzle } from "../Puzzle/Puzzle"; +import { Ball } from "../Ball/Ball"; + +export function LeaderBoardPlayer({ position, name, achievements = [], time }) { + const [isBallVisible, setIsBallVisible] = useState(false); + const [isPuzzleVisible, setIsPuzzleVisible] = useState(false); + + const handlePuzzleMouseEnter = () => { + setIsPuzzleVisible(true); + }; + const handlePuzzleMouseLeave = () => { + setIsPuzzleVisible(false); + }; + const handleBallMouseEnter = () => { + setIsBallVisible(true); + }; + const handleBallMouseLeave = () => { + setIsBallVisible(false); + }; + + const hasPuzzle = achievements.includes(1); + const hasBall = achievements.includes(2); + + console.log("Achievements for player", name, achievements); + + return ( +
+
{position}
+
{name}
+
+ puzzle + ball + {isPuzzleVisible && } + {isBallVisible && } +
+
{time}
+
+ ); +} diff --git a/src/components/LeaderBoardPlayer/LeaderBoardPlayer.module.css b/src/components/LeaderBoardPlayer/LeaderBoardPlayer.module.css new file mode 100644 index 000000000..55698575c --- /dev/null +++ b/src/components/LeaderBoardPlayer/LeaderBoardPlayer.module.css @@ -0,0 +1,54 @@ +.leaderboardPlayerSection { + display: flex; + gap: 15px; + background-color: #FFFFFF; + border-radius: 12px; + padding: 16px 20px; + justify-content: space-between; + gap: 66px; + position: relative; +} + +.leaderboardPlayerText { + color: #000000; + font-family: Roboto; + font-size: 24px; + font-weight: 400; +} + +.leaderboardPlayerIcons { + display: flex; + width: 194px; + gap: 6px; + position: relative; +} + +.leaderboardPlayerPosition { + width: 178px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: ##000000; +} + +.leaderboardPlayerUser { + width: 227px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: ##000000; +} + +.leaderboardPlayerTime { + width: 102px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: ##000000; +} diff --git a/src/components/Puzzle/Puzzle.jsx b/src/components/Puzzle/Puzzle.jsx new file mode 100644 index 000000000..2f0196956 --- /dev/null +++ b/src/components/Puzzle/Puzzle.jsx @@ -0,0 +1,11 @@ +import styles from "./Puzzle.module.css"; + +export function Puzzle() { + return ( +
+
+

Игра пройдена без супер-сил

+
+
+ ); +} diff --git a/src/components/Puzzle/Puzzle.module.css b/src/components/Puzzle/Puzzle.module.css new file mode 100644 index 000000000..9e167ed8c --- /dev/null +++ b/src/components/Puzzle/Puzzle.module.css @@ -0,0 +1,34 @@ +.popup { + position: absolute; + top: -100px; + left: 35px; +} + +.popupContent { + text-align: center; + background-color: #C2F5FF; + border-radius: 8px; + padding: 15px 20px; + position: relative; + width: 174px; +} + +.popupText { + color: rgb(0, 73, 128); + font-family: StratosSkyeng; + font-size: 18px; + font-weight: 400; + text-align: center; +} + +.popupContent::after { + content: ""; + position: absolute; + bottom: -20px; + left: 20px; + width: 0; + height: 0; + border-right: 25px solid transparent; + border-top: 20px solid #C2F5FF; + border-bottom: 5px solid transparent; +} \ No newline at end of file diff --git a/src/context/LeadersContext.jsx b/src/context/LeadersContext.jsx new file mode 100644 index 000000000..128d1bd38 --- /dev/null +++ b/src/context/LeadersContext.jsx @@ -0,0 +1,12 @@ +import { createContext, useState } from "react"; + +export const LeadersContext = createContext(); + +export function LeadersProvider({ children }) { + const [leaders, setLeaders] = useState([]); + const [isLeader, setIsLeader] = useState(false); + + return ( + {children} + ); +} diff --git a/src/context/context.jsx b/src/context/context.jsx new file mode 100644 index 000000000..ff14509f5 --- /dev/null +++ b/src/context/context.jsx @@ -0,0 +1,9 @@ +import { createContext, useState } from "react"; + +export const EasyContext = createContext(false); + +export const EasyProvider = ({ children }) => { + const [tries, setTries] = useState(3); + const [isEasyMode, setEasyMode] = useState(false); + return {children}; +}; diff --git a/src/hooks/useLeaders.jsx b/src/hooks/useLeaders.jsx new file mode 100644 index 000000000..3ea44c599 --- /dev/null +++ b/src/hooks/useLeaders.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { LeadersContext } from "../context/LeadersContext"; + +export function useLeaders() { + return useContext(LeadersContext); +} 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/LeaderBoardPage/LeaderBoardPage.jsx b/src/pages/LeaderBoardPage/LeaderBoardPage.jsx new file mode 100644 index 000000000..dd7f5d77f --- /dev/null +++ b/src/pages/LeaderBoardPage/LeaderBoardPage.jsx @@ -0,0 +1,65 @@ +import { useState, useEffect } from "react"; +import { getPlayersList } from "../../api"; +import { LeaderBoardPlayer } from "../../components/LeaderBoardPlayer/LeaderBoardPlayer"; +import styles from "./LeaderBoardPage.module.css"; +import { useNavigate } from "react-router-dom"; +import cn from "classnames"; + +export function LeaderBoardPage() { + const [leaderArray, setLeaderArray] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + setLoading(true); + const fetchData = async () => { + try { + const data = await getPlayersList(); + const filteredData = data.sort((a, b) => a.time - b.time).slice(0, 10); + setLeaderArray(filteredData); + } catch (err) { + setError("Не удалось загрузить данные"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const startGame = () => { + navigate(`/game/9`); + }; + + if (loading) return
Загрузка...
; + if (error) return
{error}
; + + return ( + <> +
+
+

Лидерборд

+ +
+
+
Позиция
+
Пользователь
+
Достижения
+
Время
+
+ {leaderArray.map((player, index) => ( + + ))} +
+ + ); +} diff --git a/src/pages/LeaderBoardPage/LeaderBoardPage.module.css b/src/pages/LeaderBoardPage/LeaderBoardPage.module.css new file mode 100644 index 000000000..be8782fbc --- /dev/null +++ b/src/pages/LeaderBoardPage/LeaderBoardPage.module.css @@ -0,0 +1,111 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); + +.leaderboardContainer { + display: flex; + flex-direction: column; + gap: 15px; + width: 944px; + margin: 0 auto; + padding: 26px; + padding-top: 22px; + box-sizing: border-box; +} + +.leaderboardHeader { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.leaderboardTitle{ + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + color: #ffffff; +} + +.leaderboardButton { + background-color: #7ac100; + width: 246px; + height: 50px; + border-radius: 12px; + border-width: 0px; + opacity: 1px; + color: #ffffff; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; +} + +.leaderboardButton:hover { + cursor: pointer; + background-color: #97de1e; +} + +.leaderboardSection { + background-color: #FFFFFF; + border-radius: 12px; + display: flex; + padding: 16px 20px; + justify-content: space-between; + gap: 66px; + position: relative; +} + +.leaderbordText { + color: rgb(153, 153, 153); + font-family: Roboto; + font-size: 24px; + font-weight: 400; + +} +.leaderboardPosition { + width: 178px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #999999; +} +.leaderboardUser { + width: 227px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #999999; +} +.leaderboardAchievement { + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #999999; +} + +.leaderboardTime { + width: 194px; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + color: #999999; +} + + + + + + + diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..a7e0d3df6 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,67 @@ -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext, useState } from "react"; +import { EasyContext } from "../../context/context"; +import { Link } from "react-router-dom"; export function SelectLevelPage() { + const { isEasyMode, setEasyMode } = useContext(EasyContext); + const navigate = useNavigate(); + const [selectedLevel, setSelectedLevel] = useState(null); + const [checked, setChecked] = useState(isEasyMode); + const [level, setLevel] = useState({}); + + const startGame = () => { + if (selectedLevel !== null) { + navigate(`/game/${selectedLevel}`); + } else { + alert("Пожалуйста, выберите уровень сложности"); + } + }; + + const handleInputChangeCheckbox = e => { + const { name } = e.target; + setChecked(!checked); + setLevel({ + ...level, + [name]: !checked, + }); + setEasyMode(!checked); + }; + return (

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

+
+ + + Легкий режим (3 жизни) +
+ + + Перейти к лидерборду +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..76e121b9e 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -1,4 +1,6 @@ .container { + font-family: "Roboto", sans-serif; + width: 100%; min-height: 100%; display: flex; @@ -7,21 +9,26 @@ } .modal { - width: 480px; - height: 459px; - border-radius: 12px; background: #c2f5ff; display: flex; flex-direction: column; justify-content: center; align-items: center; + + width: 480px; + height: 529px; + top: 122px; + left: 272px; + gap: 0px; + border-radius: 12px; + opacity: 0px; } .title { color: #004980; text-align: center; font-variant-numeric: lining-nums proportional-nums; - font-family: StratosSkyeng; + font-family: Roboto; font-size: 40px; font-style: normal; font-weight: 400; @@ -34,6 +41,15 @@ gap: 26px; margin-top: 48px; margin-bottom: 28px; + + color: #0080c1; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: Roboto; + font-size: 64px; + font-style: normal; + font-weight: 400; + line-height: 72px; } .level { @@ -46,12 +62,30 @@ border-radius: 12px; background: #fff; + + cursor: pointer; + padding: 10px; + margin: 5px; + border: 2px solid transparent; + transition: + border-color 0.1s, + background-color 0.1s; +} + +.level:hover { + border-color: #aaa; +} + +.selected { + text-color: #004980; + border-color: #b1b1b1; + background-color: #ddd; } .levelLink { color: #0080c1; text-align: center; - font-family: StratosSkyeng; + font-family: Roboto; font-size: 64px; font-style: normal; font-weight: 400; @@ -62,3 +96,74 @@ .levelLink:visited { color: #0080c1; } + +.labelName { + display: flex; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + gap: 10px; +} + +.easyModeCheckbox { + display: none; +} + +.easyModeLabel { + display: inline-block; + width: 30px; + height: 29px; + border: none; + border-radius: 4px; + position: relative; + cursor: pointer; + background-color: white; +} +.easyModeLabel::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 30px; + height: 29px; + background: url('/public/1682744403_papik-pro-p-smail-chekboks-png-32.png') no-repeat center center; + background-size: contain; + transform: translate(-50%, -50%) scale(0); + transition: transform 0.1s ease-in-out; +} + +.easyModeCheckbox:checked + .easyModeLabel::after { + transform: translate(-50%, -50%) scale(1); +} + +.startButton { + margin-top: 28px; + margin-bottom: 28px; + width: 276px; + height: 45px; + top: 508px; + left: 375px; + border-radius: 12px; + border: none; + opacity: 0px; + background-color: #7ac100; + + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + color: #ffffff; +} + +.LeaderBoardLink { + margin-top: 20px; + color: #004980; + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: left; +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index da6e94b51..c80542c76 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( [ @@ -12,6 +13,10 @@ export const router = createBrowserRouter( path: "/game/:pairsCount", element: , }, + { + path: "/leaderboard", + element: , + }, ], /** * basename нужен для корректной работы в gh pages