diff --git a/.eslintrc.json b/.eslintrc.json index e37e1e072..7814cf6f2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,12 @@ "plugins": ["prettier"], "rules": { "camelcase": ["error", { "properties": "never" }], - "prettier/prettier": "error", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], "eqeqeq": ["error", "always"], "no-unused-vars": ["error"] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..50dba2289 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "закрыта" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 9b90842c4..6fac7b3ce 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,9 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +Предплагаемое время на выполнение ДЗ №1 5 часов +фактическое время 6.5 часов + +Предплагаемое время на выполнение ДЗ №2 5 часов +фактическое время 5 часов diff --git a/src/api.js b/src/api.js new file mode 100644 index 000000000..a9d9892ae --- /dev/null +++ b/src/api.js @@ -0,0 +1,28 @@ +//Получить список лидеров + +export async function getLeaders() { + const response = await fetch(`https://wedev-api.sky.pro/api/v2/leaderboard`, { method: "GET" }); + if (!response.status === 200) { + throw new Error("Не удалось получить список лидеров"); + } + const data = await response.json(); + return data; +} + +export async function addLeaders({ name, time, achievements }) { + const response = await fetch(`https://wedev-api.sky.pro/api/v2/leaderboard`, { + method: "POST", + body: JSON.stringify({ + name, + time, + achievements, + }), + }); + if (!response.status === 201) { + throw new Error("Не удалось добавить в список лидеров"); + } else if (response.status === 400) { + throw new Error("Введите Ваше имя"); + } + const data = await response.json(); + return data; +} diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..dcb983ff1 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 { ToolTipComponent } from "../Tooltip/Tooltip"; +import { superPowerData } from "../../lib"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -40,7 +42,7 @@ function getTimerValue(startDate, endDate) { * pairsCount - сколько пар будет в игре * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ -export function Cards({ pairsCount = 3, previewSeconds = 5 }) { +export function Cards({ pairsCount = 3, previewSeconds = 5, isGameMode }) { // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); // Текущий статус игры @@ -57,9 +59,28 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { minutes: 0, }); + // Стейт для счетчика попыток + const [numberOfAttempts, setNumberOfAttempts] = useState(2); + const takeAwayTheAttempt = () => { + setNumberOfAttempts(numberOfAttempts - 1); + }; + // стейт для массива достижений + const [achievementsArr, setAchievementsArr] = useState([]); + + // стейт чтобы определить были ли использованы суперсилы + const [superPowersUsed, setSuperpowersUsed] = useState(false); + function finishGame(status = STATUS_LOST) { setGameEndDate(new Date()); setStatus(status); + if (pairsCount > 8) { + achievementsArr.push(1); + setAchievementsArr(achievementsArr); + } + if (!superPowersUsed) { + achievementsArr.push(2); + setAchievementsArr(achievementsArr); + } } function startGame() { const startDate = new Date(); @@ -73,11 +94,14 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setGameEndDate(null); setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + setNumberOfAttempts(2); + setSuperpowersUsed(false); + setAchievementsArr([]); } /** * Обработка основного действия в игре - открытие карты. - * После открытия карты игра может пепереходит в следующие состояния + * После открытия карты игра может переходит в следующие состояния * - "Игрок выиграл", если на поле открыты все карты * - "Игрок проиграл", если на поле есть две открытые карты без пары * - "Игра продолжается", если не случилось первых двух условий @@ -126,11 +150,25 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const playerLost = openCardsWithoutPair.length >= 2; // "Игрок проиграл", т.к на поле есть две открытые карты без пары - if (playerLost) { - finishGame(STATUS_LOST); - return; - } + if (isGameMode === "true") { + if (playerLost) { + takeAwayTheAttempt(); + if (numberOfAttempts < 1) { + finishGame(STATUS_LOST); + return; + } else { + setTimeout(() => { + setCards(cards.map(card => (openCardsWithoutPair.includes(card) ? { ...card, open: false } : card))); + }, 1000); + } + } + } else { + if (playerLost) { + finishGame(STATUS_LOST); + return; + } + } // ... игра продолжается }; @@ -172,6 +210,23 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { }; }, [gameStartDate, gameEndDate]); + // Закрытые карты на игровом поле + const reversedCards = cards.filter(card => !card.open); + + // Cуперсила "алохомора" + function alohomora() { + if (!superPowersUsed & (reversedCards.length > 2)) { + const randomCard = shuffle(reversedCards)[0]; + const randomPair = reversedCards.filter( + sameCard => randomCard.suit === sameCard.suit && randomCard.rank === sameCard.rank, + ); + randomPair[0].open = true; + randomPair[1].open = true; + setSuperpowersUsed(true); + } + return; + } + return (
@@ -195,7 +250,79 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )}
- {status === STATUS_IN_PROGRESS ? : null} + + {status === STATUS_IN_PROGRESS ? ( + <> +
+ +
+ + + + + + + + + + + + + +
+
+
+ {isGameMode === "true" ? ( +
+ + + +
{numberOfAttempts + 1}
+
+ ) : null} + + + ) : null}
@@ -217,6 +344,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { gameDurationSeconds={timer.seconds} gameDurationMinutes={timer.minutes} onClick={resetGame} + isGameMode + achievementsArr={achievementsArr} />
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..85675d4ae 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -67,6 +67,20 @@ font-style: normal; font-weight: 400; line-height: 32px; - margin-bottom: -12px; } + + +.attemptСounter { + display: flex; + gap: 10px; + color: red; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 30px; + font-style: normal; + font-weight: 400; + line-height: 30px; +} + + diff --git a/src/components/Cards/images/hearts.svg b/src/components/Cards/images/hearts.svg new file mode 100644 index 000000000..0e3720b4f --- /dev/null +++ b/src/components/Cards/images/hearts.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..36d847b5e 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -4,9 +4,60 @@ import { Button } from "../Button/Button"; import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { useNavigate, useParams } from "react-router-dom"; +import { useState } from "react"; +import { addLeaders } from "../../api"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, achievementsArr }) { + console.log(achievementsArr); + isWon = true; + const { isGameMode } = useParams(); + const { pairsCount } = useParams(); + const navigate = useNavigate(); + const gameSeconds = gameDurationMinutes * 60 + gameDurationSeconds; + + const [userData, setuserData] = useState({ + name: " ", + time: gameSeconds, + achievements: achievementsArr, + }); + + const handleInputChange = e => { + const { name, value } = e.target; // Извлекаем имя поля и его значение + + setuserData({ + ...userData, // Копируем текущие данные из состояния + [name]: value, // Обновляем нужное поле + }); + }; + + async function handleAddUser(e) { + e.preventDefault(); + try { + await addLeaders(userData).then(data => { + navigate(`/leaderboard`); + }); + } catch (error) { + alert(error.message); + } + } + async function handleAddUserButton(e) { + e.preventDefault(); + try { + await addLeaders(userData).then(data => { + onClick(); + }); + } catch (error) { + alert(error.message); + } + } + + let title = ""; + if (pairsCount === "9") { + title = isWon ? "Вы попали на лидерборд!" : "Вы проиграли!"; + } else { + title = isWon ? "Вы победили!" : "Вы проиграли!"; + } const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; @@ -16,12 +67,29 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes,
{imgAlt}

{title}

+ {isGameMode === "false" && pairsCount === "9" && isWon ? ( +
+ +
+ ) : null}

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

{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
- - + + {isGameMode === "false" && pairsCount === "9" && isWon ? ( +
+ Перейти к лидерборду +
+ ) : null}
); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..593431089 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,6 +1,7 @@ .modal { width: 480px; - height: 459px; + padding-top: 30px; + padding-bottom: 30px; border-radius: 12px; background: #c2f5ff; display: flex; @@ -23,7 +24,7 @@ font-style: normal; font-weight: 400; line-height: 48px; - + text-align: center; margin-bottom: 28px; } @@ -35,7 +36,6 @@ font-style: normal; font-weight: 400; line-height: 32px; - margin-bottom: 10px; } @@ -46,6 +46,43 @@ font-style: normal; font-weight: 400; line-height: 72px; - margin-bottom: 40px; } + + +.leaderboardLink { + padding-top: 10px; + padding-bottom: 10px; + color: #004980; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 32px; + text-decoration:underline +} + +.leaderboardLink:hover { + text-decoration:none +} + +.form{ + padding-top: 10px; + padding-bottom: 20px; +} +.nameInput { + width:276px; + height:45px; + border-radius:10px; + border: none; + color: #999999; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} diff --git a/src/components/Tooltip/Tooltip.jsx b/src/components/Tooltip/Tooltip.jsx new file mode 100644 index 000000000..89c0daf10 --- /dev/null +++ b/src/components/Tooltip/Tooltip.jsx @@ -0,0 +1,22 @@ +import { useState } from "react"; +import styles from "./Tooltip.module.css"; +export function ToolTipComponent({ children, text, title }) { + const [showToolTip, setShowToolTip] = useState(false); + const onMouseEnterHandler = () => { + setShowToolTip(true); + }; + const onMouseLeaveHandler = () => { + setShowToolTip(false); + }; + return ( +
+ {children} + {showToolTip && ( +
+

{title}

+

{text}

+
+ )} +
+ ); +} diff --git a/src/components/Tooltip/Tooltip.module.css b/src/components/Tooltip/Tooltip.module.css new file mode 100644 index 000000000..38e9cd91f --- /dev/null +++ b/src/components/Tooltip/Tooltip.module.css @@ -0,0 +1,25 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + +} + +.tooltip { + margin-top: 170px; + font-family: StratosSkyeng; + font-size: 18px; + font-style: normal; + position: absolute; + padding: 20px; + justify-content: center; + color: #004980; + background-color: #C2F5FF; + border-radius: 12px; + text-align: center; + white-space: pre-line; + font-weight: 400; + pointer-events: none; + z-index: 2; + max-height: 223px; +} \ No newline at end of file diff --git a/src/lib.jsx b/src/lib.jsx new file mode 100644 index 000000000..335e37b2b --- /dev/null +++ b/src/lib.jsx @@ -0,0 +1,176 @@ +export const superPowerData = { + alohomora: { + title: "Алохомора", + text: "Открывается случайная пара карт", + }, +}; + +export const achievementsText = { + hardMode: "Игра пройдена в сложном режиме", + withoutSuperPowers: "Игра пройдена без супер-сил", +}; + +export const achievementsIcons = { + hardMode: ( + + + + + + + + + + + + + + + + + + ), + hardModeActive: ( + + + + + + + + + + + + + + + + ), + withoutSuperPowers: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + withoutSuperPowersActive: ( + + + + + + + + + + + + + + + ), +}; diff --git a/src/pages/GamePage/GamePage.jsx b/src/pages/GamePage/GamePage.jsx index a4be871db..4c5404265 100644 --- a/src/pages/GamePage/GamePage.jsx +++ b/src/pages/GamePage/GamePage.jsx @@ -4,10 +4,11 @@ import { Cards } from "../../components/Cards/Cards"; export function GamePage() { const { pairsCount } = useParams(); + const { isGameMode } = useParams(); return ( <> - + ); } diff --git a/src/pages/LeaderboardPage/LeaderboardPage.jsx b/src/pages/LeaderboardPage/LeaderboardPage.jsx new file mode 100644 index 000000000..4d07b46e4 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx @@ -0,0 +1,69 @@ +import styles from "./LeaderboardPage.module.css"; +import { useEffect, useState } from "react"; +import { getLeaders } from "../../api"; +import { Button } from "../../components/Button/Button"; +import { useNavigate } from "react-router-dom"; +import { achievementsIcons, achievementsText } from "../../lib"; +import { ToolTipComponent } from "../../components/Tooltip/Tooltip"; + +export function LeaderboardPage() { + const [leaders, setLeaders] = useState([]); + useEffect(() => { + getLeaders().then(leadersList => { + setLeaders(leadersList.leaders); + }); + }, []); + const navigate = useNavigate(); + const startTheGame = e => { + e.preventDefault(); + navigate(`/`); + }; + + let i = 1; + return ( +
+
+

Лидерборд

+ +
+ +
+ ); +} diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css new file mode 100644 index 000000000..319d71573 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css @@ -0,0 +1,69 @@ +* { + margin: 0; + padding: 0; +} + +.container { + padding: 40px; +} + +.header{ + display: flex; + justify-content:space-between +} + +.leaderboard { + padding-top: 40px; + display: flex; + flex-direction:column; + gap: 12px; +} + +.headerTitle { + color: #FFFFFF; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 32px; + text-align: center; +} + +.leadersItemTitle { + color: #999999; + list-style-type:none; + background-color: #FFFFFF; + padding: 10px 20px; + border-radius: 12px; + display: flex; + justify-content:space-between; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} + +.leadersItem { + list-style-type:none; + background-color: #FFFFFF; + padding: 10px 20px; + border-radius: 12px; + display: flex; + justify-content:space-between; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} + +.achievementsIcons{ + display: flex; + gap: 6px; +} + + diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..3a6bab369 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,39 @@ import { Link } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useState } from "react"; export function SelectLevelPage() { + const [checked, setChecked] = useState(false); return (

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

+
+ setChecked(!checked)} id="gameMode" /> + +
+
+ + Перейти к лидерборду + +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..4fe631cc3 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -62,3 +62,54 @@ .levelLink:visited { color: #0080c1; } + +.gameMode { + color: #004980; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 48px; + border-radius: 12px; +} + +.gameMode input { + display: none; +} + +.gameMode label { + border-radius: 12px; + padding: 10px; +} + +.gameMode input:checked+label { + color: white; + background-color: #004980; +} + +.gameMode:hover { + color: white; + background-color: #004980; +} + +.leaderboardLinkBox { + padding-top: 10px; + padding-bottom: 10px; +} + +.leaderboardLink { + color: #004980; + text-align: center; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 32px; +} + +.leaderboardLink:hover { + text-decoration:none +} \ No newline at end of file diff --git a/src/router.js b/src/router.js index da6e94b51..b3a22d233 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( [ @@ -9,9 +10,13 @@ export const router = createBrowserRouter( element: , }, { - path: "/game/:pairsCount", + path: "/game/:pairsCount/:isGameMode", element: , }, + { + path: "/leaderboard", + element: , + }, ], /** * basename нужен для корректной работы в gh pages