diff --git a/.gitignore b/.gitignore index 4d29575de..e5bd9abd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +/.idea + # dependencies /node_modules /.pnp diff --git a/README.md b/README.md index 9b90842c4..dd21bb1e2 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,10 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +### Работа + +- Планируемое время: 15 часов +- Фактическое время: 18 часов +- Цель: Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. +Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. diff --git a/src/api/leaders.js b/src/api/leaders.js new file mode 100644 index 000000000..daba6bbe1 --- /dev/null +++ b/src/api/leaders.js @@ -0,0 +1,30 @@ +const leadersURL = "https://wedev-api.sky.pro/api/leaderboard"; + +export const getLeaders = () => { + return fetch(leadersURL, { method: "GET" }).then(response => { + if (!response.ok) { + throw new Error("Ошибка сервера"); + } + + if (response.status === 400) { + throw new Error("Полученные данные не в формате JSON!"); + } + return response.json(); + }); +}; + +export const postLeaders = ({ resultLeaderboard }) => { + return fetch(leadersURL, { + method: "POST", + body: JSON.stringify(resultLeaderboard), + }).then(response => { + if (!response.ok) { + throw new Error("Ошибка сервера"); + } + + if (response.status === 400) { + throw new Error("Полученные данные не в формате JSON!"); + } + return response.json(); + }); +}; diff --git a/src/components/Card/Card.jsx b/src/components/Card/Card.jsx index 2ba6a13c8..4a6dec133 100644 --- a/src/components/Card/Card.jsx +++ b/src/components/Card/Card.jsx @@ -1,6 +1,6 @@ import { CROSS_SUIT, DIAMONDS_SUIT, HEARTS_SUIT, SPADES_SUIT } from "../../const"; -import styles from "./Card.module.css"; +import styles from "./Card.module.css"; import heartsImageUrl from "./images/hearts.svg"; import crossImageUrl from "./images/cross.svg"; import spadesImageUrl from "./images/spades.svg"; diff --git a/src/components/Card/Card.module.css b/src/components/Card/Card.module.css index 86c3fbb5b..c3235f6c5 100644 --- a/src/components/Card/Card.module.css +++ b/src/components/Card/Card.module.css @@ -79,8 +79,8 @@ } .flipContainer:hover .flipper { - transition: 0.2s; - transform: rotateY(35deg); + /*transition: 0.2s;*/ + /*transform: rotateY(35deg);*/ } .flipContainer.flip .flipper { diff --git a/src/components/Cards/Cards.jsx b/src/components/Cards/Cards.jsx index 7526a56c8..f46447573 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -1,10 +1,13 @@ 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 { LivesContext } from "../context/livesContext"; +import { EasyModeContext } from "../context/easymodeContext"; +import { CardsContext } from "../context/cardsContext"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -42,7 +45,7 @@ function getTimerValue(startDate, endDate) { */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта - const [cards, setCards] = useState([]); + const { cards, setCards } = useContext(CardsContext); // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); @@ -50,6 +53,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const [gameStartDate, setGameStartDate] = useState(null); // Дата конца игры const [gameEndDate, setGameEndDate] = useState(null); + //Режим трех попыток + const { easyMode } = useContext(EasyModeContext); + console.log(easyMode); + //Счетчик жизней + const { lives, setLives } = useContext(LivesContext); // Стейт для таймера, высчитывается в setInteval на основе gameStartDate и gameEndDate const [timer, setTimer] = useState({ @@ -73,6 +81,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setGameEndDate(null); setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + setLives(3); } /** @@ -126,12 +135,30 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { const playerLost = openCardsWithoutPair.length >= 2; // "Игрок проиграл", т.к на поле есть две открытые карты без пары - if (playerLost) { + if (playerLost && !easyMode) { finishGame(STATUS_LOST); return; } // ... игра продолжается + if (playerLost && easyMode) { + setLives(lives - 1); + nextCards.map(card => { + if (openCardsWithoutPair.some(opencard => opencard.id === card.id)) { + if (card.open) { + setTimeout(() => { + setCards(prev => { + return prev.map(el => (el.id === card.id ? { ...el, open: false } : el)); + }); + }, 1000); + } + } + }); + if (lives === 1) { + finishGame(STATUS_LOST); + return; + } + } }; const isGameEnded = status === STATUS_LOST || status === STATUS_WON; @@ -164,6 +191,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // Обновляем значение таймера в интервале useEffect(() => { + if (status === STATUS_LOST || status === STATUS_WON) return; + const intervalId = setInterval(() => { setTimer(getTimerValue(gameStartDate, gameEndDate)); }, 300); @@ -209,6 +238,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { /> ))} + {easyMode ?

Осталось попыток: {lives}

: ""} {isGameEnded ? (
@@ -217,6 +247,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { gameDurationSeconds={timer.seconds} gameDurationMinutes={timer.minutes} onClick={resetGame} + pairsCount={pairsCount} + timer={timer} />
) : null} diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..f6b55c95d 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -70,3 +70,10 @@ margin-bottom: -12px; } + +.subtitle { + color: #fff; + font-size: 18px; + line-height: 18px; + margin-top: 20px; +} diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..d2c8a0096 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,27 +1,95 @@ import styles from "./EndGameModal.module.css"; - import { Button } from "../Button/Button"; - import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { getTimeInSeconds, sortLeadersElements } from "../../utils/helper"; +import { useContext, useState } from "react"; +import { LeadersContext } from "../context/leaderContext"; +import { postLeaders } from "../../api/leaders"; +import { Link, useNavigate } from "react-router-dom"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick, pairsCount, timer }) { + const timeLeaders = getTimeInSeconds({ minutes: gameDurationMinutes, seconds: gameDurationSeconds }); const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + const [inputLeaders, setInputLeaders] = useState(""); + + const { leaders } = useContext(LeadersContext); + + const sortedLeaders = sortLeadersElements(leaders); + + const leadersLength = sortedLeaders.length; + + const isLeadResult = sortedLeaders[leadersLength - 1].time > getTimeInSeconds(timer) && pairsCount === 9; + + const title = isWon ? (isLeadResult ? "Вы попали на Лидерборд!" : "Вы победили!") : "Вы проиграли!"; + + const navigate = useNavigate(); + + const [error, setError] = useState(""); + + const onLeaders = () => { + const resultLeaderboard = { + name: inputLeaders, + time: timeLeaders, + }; + + postLeaders({ resultLeaderboard }) + .then(res => { + console.log(res); + }) + .catch(err => { + setError(err.message); + }); + }; + return (
{imgAlt}

{title}

+ {isLeadResult ? ( + isWon ? ( +
{ + e.preventDefault(); + if (!inputLeaders.trim()) { + setInputLeaders("Введите имя"); + return; + } + onLeaders(); + setInputLeaders(""); + navigate("/leaderboard"); + }} + > + { + setInputLeaders(e.target.value); + }} + value={inputLeaders.name} + type="text" + placeholder="Пользователь" + /> +
+ ) : ( + "" + ) + ) : ( + "" + )}

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

{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
+ + Перейти к лидерборду + + {error}
); } diff --git a/src/components/EndGameModal/EndGameModal.module.css b/src/components/EndGameModal/EndGameModal.module.css index 9368cb8b5..516586a50 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: 459px;*/ border-radius: 12px; background: #c2f5ff; display: flex; @@ -13,6 +13,24 @@ width: 96px; height: 96px; margin-bottom: 8px; + margin-top: 20px; +} + +.input { + width: 276px; + height: 45px; + top: 334px; + left: 374px; + border-radius: 10px; + background: #ffffff; + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + border: none; + outline: none; + margin-bottom: 20px; } .title { @@ -23,7 +41,7 @@ font-style: normal; font-weight: 400; line-height: 48px; - + text-align: center; margin-bottom: 28px; } @@ -35,7 +53,6 @@ font-style: normal; font-weight: 400; line-height: 32px; - margin-bottom: 10px; } @@ -46,6 +63,16 @@ font-style: normal; font-weight: 400; line-height: 72px; - margin-bottom: 40px; } + +.link { + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #565eef; + padding-top: 20px; + padding-bottom: 40px; +} diff --git a/src/components/Leaderboard/Leaderboard.jsx b/src/components/Leaderboard/Leaderboard.jsx new file mode 100644 index 000000000..da0864b82 --- /dev/null +++ b/src/components/Leaderboard/Leaderboard.jsx @@ -0,0 +1,14 @@ +import styles from "./Leaderboard.module.css"; + +export const Leaderboard = ({ position, user, time, color = "black" }) => { + return ( + <> +
  • + {position} + {user} + + {time} +
  • + + ); +}; diff --git a/src/components/Leaderboard/Leaderboard.module.css b/src/components/Leaderboard/Leaderboard.module.css new file mode 100644 index 000000000..c3a2a7b65 --- /dev/null +++ b/src/components/Leaderboard/Leaderboard.module.css @@ -0,0 +1 @@ +.position { width: 178px; font-family: Poppins; font-size: 24px; font-weight: 400; line-height: 32px; margin-left: 20px; } .user { width: 324px; font-family: Poppins; font-size: 24px; font-weight: 400; line-height: 32px; } .time { width: 102px; font-family: Poppins; font-size: 24px; font-weight: 400; line-height: 32px; } .item { width: 944px; height: 64px; display: flex; gap: 66px; box-sizing: border-box; padding-top: 16px; margin-bottom: 16px; font-family: StratosSkyeng; font-size: 24px; background-color: #fff; border-radius: 12px; } \ No newline at end of file diff --git a/src/components/context/cardsContext.jsx b/src/components/context/cardsContext.jsx new file mode 100644 index 000000000..9ac77b5ba --- /dev/null +++ b/src/components/context/cardsContext.jsx @@ -0,0 +1,7 @@ +import { createContext, useState } from "react"; + +export const CardsContext = createContext(null); +export const CardsProvider = ({ children }) => { + const [cards, setCards] = useState([]); + return {children}; +}; diff --git a/src/components/context/easymodeContext.jsx b/src/components/context/easymodeContext.jsx new file mode 100644 index 000000000..710847f71 --- /dev/null +++ b/src/components/context/easymodeContext.jsx @@ -0,0 +1,7 @@ +import { createContext, useState } from "react"; + +export const EasyModeContext = createContext(null); +export const EasyModeProvider = ({ children }) => { + const [easyMode, setEasyMode] = useState(false); + return {children}; +}; diff --git a/src/components/context/leaderContext.jsx b/src/components/context/leaderContext.jsx new file mode 100644 index 000000000..0d1b9c3a2 --- /dev/null +++ b/src/components/context/leaderContext.jsx @@ -0,0 +1,16 @@ +import { createContext, useEffect, useState } from "react"; +import { getLeaders } from "../../api/leaders"; +import { sortLeadersElements } from "../../utils/helper"; + +export const LeadersContext = createContext(null); + +export const LeadersProvider = ({ children }) => { + const [leaders, setLeaders] = useState([]); + useEffect(() => { + getLeaders().then(leaders => { + const sortedLeaders = sortLeadersElements(leaders.leaders); + setLeaders(sortedLeaders.splice(1, 10)); + }); + }, []); + return {children}; +}; diff --git a/src/components/context/livesContext.jsx b/src/components/context/livesContext.jsx new file mode 100644 index 000000000..f5ce08251 --- /dev/null +++ b/src/components/context/livesContext.jsx @@ -0,0 +1,8 @@ +import { createContext, useState } from "react"; + +export const LivesContext = createContext(null); + +export const LivesProvider = ({ children }) => { + const [lives, setLives] = useState(3); + return {children}; +}; diff --git a/src/index.css b/src/index.css index 78f0d3a2b..af079ac5b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,37 +1 @@ -html { - margin: 0; -} - -body { - background-color: #004980; - margin: 0; - height: 100vh; -} - -#root { - width: 100%; - height: 100%; -} - -h1, -h2, -h3, -h4, -h5, -h6, -ul, -p, -li, -ol { - margin: 0; - padding: 0; -} - -@font-face { - font-family: "StratosSkyeng"; - src: - url("../public/assets/fonts/StratosSkyeng.woff2") format("woff2"), - local("Arial"); - font-weight: 400; - font-style: normal; -} +html { margin: 0; } body { background-color: #004980; margin: 0; height: 100vh; } #root { width: 100%; height: 100%; } h1, h2, h3, h4, h5, h6, ul, p, li, ol { margin: 0; padding: 0; } @font-face { font-family: "StratosSkyeng"; src: url("../public/assets/fonts/StratosSkyeng.woff2") format("woff2"), local("Arial"); font-weight: 400; font-style: normal; } \ No newline at end of file diff --git a/src/index.js b/src/index.js index f689c5f0b..d2ce9f40f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,22 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; +import { LivesProvider } from "./components/context/livesContext"; +import { EasyModeProvider } from "./components/context/easymodeContext"; +import { LeadersProvider } from "./components/context/leaderContext"; +import { CardsProvider } from "./components/context/cardsContext"; 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..a59f3c3af --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx @@ -0,0 +1,50 @@ +import { useContext, useEffect } from "react"; +import { LeadersContext } from "../../components/context/leaderContext"; +import { getLeaders } from "../../api/leaders"; +import { sortLeadersElements } from "../../utils/helper"; +import { Link } from "react-router-dom"; +import { Button } from "../../components/Button/Button"; +import { Leaderboard } from "../../components/Leaderboard/Leaderboard"; +import styles from "./LeaderboardPage.module.css"; + +export function LeaderboardPage() { + const { leaders, setLeaders } = useContext(LeadersContext); + + const formatTime = timeInSeconds => { + const seconds = ("0" + String(timeInSeconds % 60)).slice(-2); + const minutes = ("0" + String(Math.floor(timeInSeconds / 60))).slice(-2); + + return `${minutes}:${seconds}`; + }; + + useEffect(() => { + getLeaders().then(leaders => { + const sortedLeaders = sortLeadersElements(leaders.leaders); + setLeaders(sortedLeaders.splice(1, 10)); + }); + }, []); + + return ( +
    +
    +

    Лидерборд

    + + + +
    +
    +
    +

    Позиция

    +

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

    +

    +

    Время

    +
    + +
    +
    + ); +} diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css new file mode 100644 index 000000000..5a479e8a0 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css @@ -0,0 +1,74 @@ +.container { + padding-left: calc(50% - 472px); + padding-right: calc(50% - 472px); +} + +.header { + display: flex; + justify-content: space-between; + padding-top: 50px; + padding-bottom: 50px; +} + +.title { + font-family: Roboto; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #FFFFFF; +} + +.box { + display: flex; + gap: 66px; + justify-content: space-between; + width: 944px; + height: 64px; + box-sizing: border-box; + padding-top: 16px; + margin-bottom: 16px; + font-size: 24px; + font-family: StratosSkyeng; + background: #FFFFFF; + border-radius: 12px; +} + +.subtitle1 { + width: 178px; + font-family: Poppins; + font-size: 24px; + font-weight: 400; + line-height: 32px; + color: #999999; + margin-left: 20px; +} + +.subtitle2 { + width: 324px; + font-family: Poppins; + font-size: 24px; + font-weight: 400; + line-height: 32px; + color: #999999; +} + +.subtitle3 { + width: 102px; + font-family: Poppins; + font-size: 24px; + font-weight: 400; + line-height: 32px; + color: #999999; +} + +.subtitle4 { + width: 102px; + font-family: Poppins; + font-size: 24px; + font-weight: 400; + line-height: 32px; + color: #999999; + margin-right: 20px; +} + diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..1af27f4c4 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,7 +1,18 @@ import { Link } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { useContext, useEffect } from "react"; +import { EasyModeContext } from "../../components/context/easymodeContext"; export function SelectLevelPage() { + const { setEasyMode } = useContext(EasyModeContext); + const checkBox = () => { + setEasyMode(true); + }; + + useEffect(() => { + setEasyMode(false); + }, []); + return (
    @@ -23,6 +34,13 @@ export function SelectLevelPage() { +
    +

    Легкий режим (3 жизни)

    + +
    + + Перейти к лидерборду +
    ); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..8cdc3bda1 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -62,3 +62,29 @@ .levelLink:visited { color: #0080c1; } + +.subtitle { + 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: 24px; +} + +.wrap { + display: flex; + align-items: flex-end; +} + +.link { + font-family: Roboto; + font-size: 18px; + font-weight: 400; + line-height: 32px; + text-align: left; + color: #004980; + margin-top: 18px; +} 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 diff --git a/src/utils/helper.js b/src/utils/helper.js new file mode 100644 index 000000000..a52c84e98 --- /dev/null +++ b/src/utils/helper.js @@ -0,0 +1,2 @@ +export const sortLeadersElements = leaders => [...leaders].sort((a, b) => a.time - b.time); +export const getTimeInSeconds = timer => timer.seconds + timer.minutes * 60;