diff --git a/README.md b/README.md index 9b90842c4..1ab530ac3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ В этом репозитории реализован MVP карточкой игры "Мемо" по [тех.заданию](./docs/mvp-spec.md) Проект задеплоен на gh pages: -https://skypro-web-developer.github.io/react-memo/ +https://ax1lebafer.github.io/react-memory/ ## Разработка @@ -44,3 +44,17 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +### Затраченное время + +1 спринт: +Продполагаемое время: 8 часов вместе с ознакомлением кода +Фактически затраченное время: 6 часов 40 минут + +2 спринт: +Продполагаемое время: 5 часов +Фактически затраченное время: 6 часов 30 минут + +3 спринт +Продполагаемое время: 8 часов +Фактически затраченное время: 4 час 20 минут 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/card_perk.svg b/public/card_perk.svg new file mode 100644 index 000000000..88ab0b523 --- /dev/null +++ b/public/card_perk.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file 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..a5145f3ec --- /dev/null +++ b/src/api.js @@ -0,0 +1,32 @@ +const host = "https://wedev-api.sky.pro/api/v2/leaderboard"; + +export async function getLeaders() { + const response = await fetch(host, { + method: "GET", + }); + + if (!response.ok) { + throw new Error("Ошибка сервера"); + } + + const data = await response.json(); + return data; +} + +export async function postLeader({ name, time, achievements }) { + const response = await fetch(host, { + method: "POST", + body: JSON.stringify({ + name: name, + time: time, + achievements: achievements, + }), + }); + + if (!response.ok) { + throw new Error("Ошибка сервера"); + } + + const data = await response.json(); + return data; +} diff --git a/src/components/Button/Button.jsx b/src/components/Button/Button.jsx index 3d4618a88..3ed74c087 100644 --- a/src/components/Button/Button.jsx +++ b/src/components/Button/Button.jsx @@ -1,8 +1,8 @@ import styles from "./Button.module.css"; -export function Button({ children, onClick }) { +export function Button({ children, onClick, disabled }) { return ( - ); diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..ef5474b1f 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -5,6 +5,8 @@ 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 { useEasyMode } from "../../hooks/useEasyMode"; +import { DEFAULT_MODE_LIVES, EASY_MODE_LIVES } from "../../const"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -35,12 +37,28 @@ function getTimerValue(startDate, endDate) { }; } +function closeUnmatchedCards(setCards, openCardsWithoutPair) { + setTimeout(() => { + setCards(currentCards => + currentCards.map(card => + openCardsWithoutPair.some(openCard => openCard.id === card.id) ? { ...card, open: false } : card, + ), + ); + }, 1000); +} + /** * Основной компонент игры, внутри него находится вся игровая механика и логика. * pairsCount - сколько пар будет в игре * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + const { isEasyMode } = useEasyMode(); + const [lives, setLives] = useState(isEasyMode ? EASY_MODE_LIVES : DEFAULT_MODE_LIVES); + const [perkUses, setPerkUses] = useState(0); + const [counterPerk, setCounterPerk] = useState(2); + const [achievements, setAchievements] = useState(isEasyMode ? [1] : []); + // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); // Текущий статус игры @@ -73,6 +91,37 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setGameEndDate(null); setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + setLives(isEasyMode ? EASY_MODE_LIVES : DEFAULT_MODE_LIVES); + setPerkUses(0); + setCounterPerk(2); + } + + function usePerk() { + if (perkUses >= 2) return; + setPerkUses(prev => prev + 1); + setCounterPerk(prev => prev - 1); + + if (perkUses === 0) { + setAchievements([...achievements, 2]); + } + + const closedCards = cards.filter(card => !card.open); + if (closedCards.length < 2) return; + + const randomCardIndex = Math.floor(Math.random() * closedCards.length); + const randomCard = closedCards[randomCardIndex]; + + const matchingCard = cards.find( + card => card.suit === randomCard.suit && card.rank === randomCard.rank && card.id !== randomCard.id, + ); + + if (matchingCard) { + setCards(currentCards => + currentCards.map(card => + card.id === randomCard.id || card.id === matchingCard.id ? { ...card, open: true } : card, + ), + ); + } } /** @@ -127,10 +176,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // "Игрок проиграл", т.к на поле есть две открытые карты без пары if (playerLost) { - finishGame(STATUS_LOST); - return; + if (!isEasyMode) { + finishGame(STATUS_LOST); + return; + } else { + // Функция закрытия карточек + setLives(prevState => prevState - 1); + closeUnmatchedCards(setCards, openCardsWithoutPair); + } } - // ... игра продолжается }; @@ -172,6 +226,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { }; }, [gameStartDate, gameEndDate]); + useEffect(() => { + if (lives === 0) { + finishGame(STATUS_LOST); + } + }, [lives]); + return (
@@ -195,7 +255,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )}
- {status === STATUS_IN_PROGRESS ? : null} + {status === STATUS_IN_PROGRESS ? ( + <> +
+ eye_perk +
{counterPerk}
+
+ + + ) : null}
@@ -210,13 +278,17 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { ))}
+ {isEasyMode && Осталось попыток: {lives}} + {isGameEnded ? (
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..673c60bc1 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -13,6 +13,16 @@ flex-wrap: wrap; } +.attempts { + display: block; + margin-top: 20px; + font-size: 20px; + font-family: StratosSkyeng; + font-style: normal; + font-weight: 400; + color: #fff; +} + .modalContainer { position: absolute; top: 0; @@ -28,13 +38,13 @@ .header { display: flex; justify-content: space-between; - align-items: end; + align-items: flex-end; margin-bottom: 35px; } .timer { display: flex; - align-items: end; + align-items: flex-end; color: #fff; font-family: StratosSkyeng; @@ -50,6 +60,7 @@ margin-top: 34px; margin-bottom: 10px; } + .previewDescription { font-size: 18px; line-height: 18px; @@ -70,3 +81,23 @@ margin-bottom: -12px; } + +.perks { + display: flex; + gap: 15px; + position: relative; +} + +.cardPerk { + cursor: pointer; +} + +.counterPerk { + position: absolute; + background-color: orange; + border-radius: 100px; + padding: 5px 10px; + font-family: StratosSkyeng; + top: 45px; + left: 45px; +} \ No newline at end of file diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..4fc6e1870 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -4,24 +4,94 @@ import { Button } from "../Button/Button"; import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { useEffect, useState } from "react"; +import { useLeaders } from "../../hooks/useLeaders"; +import { Link, useNavigate } from "react-router-dom"; +import { postLeader } from "../../api"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, pairsCount, gameDurationSeconds, gameDurationMinutes, onClick, achievements }) { + const { leaders, setLeaders, isLeader, setIsLeader } = useLeaders(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + let title = isWon ? "Вы победили!" : "Вы проиграли!"; const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + const [newLeader, setNewLeader] = useState({ + name: "", + time: gameDurationSeconds, + achievements: achievements, + }); + + useEffect(() => { + const isInLeaderboard = + leaders.length > 0 && newLeader.time < leaders[leaders.length - 1].time && isWon && pairsCount === 9; + + if (isInLeaderboard) { + setIsLeader(true); + } + }, [leaders, newLeader.time, isWon, pairsCount, setIsLeader]); + + if (isLeader) { + title = "Вы попали на лидерборд!"; + } + + function handleInputChange(event) { + const { name, value } = event.target; + setNewLeader({ + ...newLeader, + [name]: value, + }); + } + + function handleSaveLeader() { + setIsLoading(true); + + postLeader({ name: newLeader.name, time: newLeader.time, achievements: newLeader.achievements }) + .then(response => { + setLeaders(response.leaders); + navigate("/leaderboard"); + }) + .catch(error => { + alert(error); + }) + .finally(() => { + setIsLoading(false); + }); + } + return (
{imgAlt}

{title}

+ {isLeader && ( +
+ + +
+ )}

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

{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..e76386dee 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: 40px 0 48px 0; } .image { @@ -23,10 +24,44 @@ font-style: normal; font-weight: 400; line-height: 48px; + text-align: center; margin-bottom: 28px; } +.leaderboardInfo { + display: flex; + flex-direction: column; + align-items: center; +} + +.nameInput { + width: 246px; + box-sizing: border-box; + outline: none; + border: none; + padding: 5px 10px; + border-radius: 10px; + height: 45px; + font-size: 24px; + margin-bottom: 20px; + text-align: center; +} + +.nameInput::placeholder { + color: rgb(153, 153, 153); + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; + /* line-height: 32px; */ + letter-spacing: 0%; + text-align: center; +} + +.saveButton { + font-size: 18px; +} + .description { color: #000; font-variant-numeric: lining-nums proportional-nums; @@ -37,6 +72,7 @@ line-height: 32px; margin-bottom: 10px; + margin-top: 10px; } .time { @@ -49,3 +85,12 @@ margin-bottom: 40px; } + +.leaderboardLink { + color: rgb(0, 73, 128); + font-family: StratosSkyeng; + font-size: 18px; + font-weight: 400; + line-height: 32px; + margin-top: 18px; +} \ No newline at end of file diff --git a/src/components/Leaderboard/Leaderboard.jsx b/src/components/Leaderboard/Leaderboard.jsx new file mode 100644 index 000000000..dd2704eba --- /dev/null +++ b/src/components/Leaderboard/Leaderboard.jsx @@ -0,0 +1,65 @@ +import { Link } from "react-router-dom"; +import { Button } from "../Button/Button"; +import { LeaderboardRow } from "../LeaderboardRow/LeaderboardRow"; +import styles from "./Leaderboard.module.css"; +import cn from "classnames"; +import { useLeaders } from "../../hooks/useLeaders"; +import { useEffect, useState } from "react"; +import { getLeaders } from "../../api"; + +export function Leaderboard() { + const { leaders, setLeaders } = useLeaders(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setIsLoading(true); + + getLeaders() + .then(response => { + const sortedLeaders = response.leaders + .map(leader => ({ + ...leader, + name: leader.name.trim() === "" ? "Пользователь" : leader.name, + })) + .sort((a, b) => a.time - b.time) + .slice(0, 10); + setLeaders(sortedLeaders); + }) + .finally(() => { + setIsLoading(false); + }); + }, [setLeaders]); + + return ( +
+
+

Лидерборд

+ + + +
+
+
+

Позиция

+

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

+

Достижения

+

Время

+
+ {isLoading && Загрузка...} + {!isLoading && ( + <> + {leaders.map((leader, index) => ( + + ))} + + )} +
+
+ ); +} diff --git a/src/components/Leaderboard/Leaderboard.module.css b/src/components/Leaderboard/Leaderboard.module.css new file mode 100644 index 000000000..e4474c6a2 --- /dev/null +++ b/src/components/Leaderboard/Leaderboard.module.css @@ -0,0 +1,66 @@ +.leaderboard { + padding-left: calc(50% - 475px); + padding-right: calc(50% - 475px); + padding-top: 50px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + flex-wrap: wrap; + margin-bottom: 40px; +} + +.headerTitle { + color: rgb(255, 255, 255); + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; +} + +.section { + display: flex; + gap: 15px; + flex-direction: column; +} + +.sectionTop { + background-color: #FFFFFF; + border-radius: 12px; + display: flex; + padding: 16px 20px; + justify-content: space-between; + gap: 66px; +} + +.sectionText { + color: rgb(153, 153, 153); + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; +} + +.textPosition { + width: 178px; +} + +.textUser { + width: 227px; +} + +.textAchievement { + width: 194px; +} + +.textTime { + width: 102px; +} + +.loader { + font-size: 24px; + color: #FFFFFF; + font-family: StratosSkyeng; + text-align: center; +} \ No newline at end of file diff --git a/src/components/LeaderboardRow/LeaderboardRow.jsx b/src/components/LeaderboardRow/LeaderboardRow.jsx new file mode 100644 index 000000000..685f3475f --- /dev/null +++ b/src/components/LeaderboardRow/LeaderboardRow.jsx @@ -0,0 +1,59 @@ +import styles from "./LeaderboardRow.module.css"; +import cn from "classnames"; +import { format } from "date-fns"; +import { ModalPuzzle } from "../ModalPuzzle/ModalPuzzle"; +import { useState } from "react"; +import { ModalBall } from "../ModalBall/ModalBall"; + +function formatSeconds(seconds) { + const date = new Date(0); + date.setSeconds(seconds); + return date; +} + +export function LeaderboardRow({ position, userName, achievements, time }) { + const [isPopupPuzzleVisible, setIsPopupPuzzleVisible] = useState(false); + const [isPopupBallVisible, setIsPopupBallVisible] = useState(false); + + const formattedTime = format(formatSeconds(time), "mm:ss"); + + const handleBallMouseEnter = () => { + setIsPopupBallVisible(true); + }; + + const handleBallMouseLeave = () => { + setIsPopupBallVisible(false); + }; + + const handlePuzzleMouseEnter = () => { + setIsPopupPuzzleVisible(true); + }; + + const handlePuzzleMouseLeave = () => { + setIsPopupPuzzleVisible(false); + }; + + return ( +
+

{position}

+

{userName}

+
+ puzzle + ball + {isPopupBallVisible && } + {isPopupPuzzleVisible && } +
+

{formattedTime}

+
+ ); +} diff --git a/src/components/LeaderboardRow/LeaderboardRow.module.css b/src/components/LeaderboardRow/LeaderboardRow.module.css new file mode 100644 index 000000000..068c09762 --- /dev/null +++ b/src/components/LeaderboardRow/LeaderboardRow.module.css @@ -0,0 +1,35 @@ +.sectionTop { + background-color: #FFFFFF; + border-radius: 12px; + display: flex; + padding: 16px 20px; + justify-content: space-between; + gap: 66px; + position: relative; +} + +.sectionText { + color: #000000; + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; +} + +.sectionIcons { + display: flex; + width: 194px; + gap: 6px; + position: relative; +} + +.textPosition { + width: 178px; +} + +.textUser { + width: 227px; +} + +.textTime { + width: 102px; +} \ No newline at end of file diff --git a/src/components/ModalBall/ModalBall.jsx b/src/components/ModalBall/ModalBall.jsx new file mode 100644 index 000000000..64ac199e4 --- /dev/null +++ b/src/components/ModalBall/ModalBall.jsx @@ -0,0 +1,11 @@ +import styles from "./ModalBall.module.css"; + +export function ModalBall() { + return ( +
+
+

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

+
+
+ ); +} diff --git a/src/components/ModalBall/ModalBall.module.css b/src/components/ModalBall/ModalBall.module.css new file mode 100644 index 000000000..93850aee7 --- /dev/null +++ b/src/components/ModalBall/ModalBall.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/ModalPuzzle/ModalPuzzle.jsx b/src/components/ModalPuzzle/ModalPuzzle.jsx new file mode 100644 index 000000000..64a5fd622 --- /dev/null +++ b/src/components/ModalPuzzle/ModalPuzzle.jsx @@ -0,0 +1,11 @@ +import styles from "./ModalPuzzle.module.css"; + +export function ModalPuzzle() { + return ( +
+
+

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

+
+
+ ); +} diff --git a/src/components/ModalPuzzle/ModalPuzzle.module.css b/src/components/ModalPuzzle/ModalPuzzle.module.css new file mode 100644 index 000000000..9e167ed8c --- /dev/null +++ b/src/components/ModalPuzzle/ModalPuzzle.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/const.js b/src/const.js index 9a101d0b9..b8feddd8d 100644 --- a/src/const.js +++ b/src/const.js @@ -2,3 +2,5 @@ export const SPADES_SUIT = "SPADES"; export const CROSS_SUIT = "CROSS"; export const DIAMONDS_SUIT = "DIAMONDS"; export const HEARTS_SUIT = "HEARTS"; +export const EASY_MODE_LIVES = 3; +export const DEFAULT_MODE_LIVES = 1; diff --git a/src/contexts/EasyModeContext.jsx b/src/contexts/EasyModeContext.jsx new file mode 100644 index 000000000..c58290614 --- /dev/null +++ b/src/contexts/EasyModeContext.jsx @@ -0,0 +1,9 @@ +import { createContext, useState } from "react"; + +export const EasyModeContext = createContext(false); + +export function EasyModeProvider({ children }) { + const [isEasyMode, setIsEasyMode] = useState(false); + + return {children}; +} diff --git a/src/contexts/LeadersContext.jsx b/src/contexts/LeadersContext.jsx new file mode 100644 index 000000000..128d1bd38 --- /dev/null +++ b/src/contexts/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/hooks/useEasyMode.jsx b/src/hooks/useEasyMode.jsx new file mode 100644 index 000000000..05873ce6a --- /dev/null +++ b/src/hooks/useEasyMode.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { EasyModeContext } from "../contexts/EasyModeContext"; + +export function useEasyMode() { + return useContext(EasyModeContext); +} diff --git a/src/hooks/useLeaders.jsx b/src/hooks/useLeaders.jsx new file mode 100644 index 000000000..c6bbc94a5 --- /dev/null +++ b/src/hooks/useLeaders.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { LeadersContext } from "../contexts/LeadersContext"; + +export function useLeaders() { + return useContext(LeadersContext); +} diff --git a/src/index.js b/src/index.js index f689c5f0b..02b8a6de9 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,16 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; +import { EasyModeProvider } from "./contexts/EasyModeContext"; +import { LeadersProvider } from "./contexts/LeadersContext"; 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..2e1a73a1a --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx @@ -0,0 +1,5 @@ +import { Leaderboard } from "../../components/Leaderboard/Leaderboard"; + +export function LeaderboardPage() { + return ; +} diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..e4795425f 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,55 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useState } from "react"; +import { useEasyMode } from "../../hooks/useEasyMode"; +// import { useLeaders } from "../../hooks/useLeaders"; +// import { getLeaders } from "../../api"; export function SelectLevelPage() { + const [selectedLevel, setSelectedLevel] = useState(null); + const navigate = useNavigate(); + const { isEasyMode, setIsEasyMode } = useEasyMode(); + + const handleEasyModeChange = event => { + setIsEasyMode(event.target.checked); + }; + + const handleCheckboxChange = level => { + setSelectedLevel(level); + }; + + const handleStartClick = () => { + if (selectedLevel !== null) { + navigate(`/game/${selectedLevel}`); + } else { + alert("Выберите уровень перед началом игры"); + } + }; + return (

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

    -
  • - - 1 - -
  • -
  • - - 2 - -
  • -
  • - - 3 - -
  • + {[3, 6, 9].map((level, index) => ( +
  • + +
  • + ))}
+ + + + Перейти к лидерборду +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..d54bb61b8 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -33,7 +33,7 @@ flex-direction: row; gap: 26px; margin-top: 48px; - margin-bottom: 28px; + margin-bottom: 30px; } .level { @@ -46,6 +46,19 @@ border-radius: 12px; background: #fff; + + box-sizing: border-box; + cursor: pointer; + align-items: center; +} + +.level:hover { + border: 2px solid blue; +} + +.selected { + border: 2px solid blue; + background: #ffe0a6; } .levelLink { @@ -62,3 +75,86 @@ .levelLink:visited { color: #0080c1; } + +.checkboxButton { + user-select: none; + position: relative; + box-sizing: border-box; + cursor: pointer; +} + +.checkboxButton input[type=checkbox] { + z-index: -1; + opacity: 0; + display: block; + width: 0; + height: 0; + box-sizing: border-box; + +} + +.checkboxButton span { + display: inline-block; + cursor: pointer; + padding: 0px 10px; + /* border: 1px solid #999; */ + border-radius: 4px; + transition: background 0.2s ease; + + color: #0080c1; + text-align: center; + font-family: StratosSkyeng; + font-size: 64px; + font-style: normal; + font-weight: 400; + line-height: 72px; + text-decoration: none; +} + +.checkboxButton input[type=checkbox]:checked+span { + background: #ffe0a6; +} + +.checkboxMode { + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + margin-bottom: 25px; +} + +.checkboxMode input[type=checkbox] { + display: inline-block; + width: 20px; + height: 20px; + margin-right: 7px; +} + +.buttonStart { + width: 246px; + height: 48px; + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; + line-height: 32px; + letter-spacing: 0%; + background: #7ac100; + outline: none; + cursor: pointer; + border: none; + border-radius: 12px; + color: #fff; +} + +.buttonStart:hover { + background: #7ac100cc; +} + +.leaderboardLink { + color: rgb(0, 73, 128); + font-family: StratosSkyeng; + font-size: 18px; + font-weight: 400; + line-height: 32px; + margin-top: 18px; +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index da6e94b51..ecd8d90ae 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