diff --git a/README.md b/README.md index 9b90842c4..61874ca51 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,11 @@ https://skypro-web-developer.github.io/react-memo/ Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом. Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения. + +Оценочное время выполнения работы составляло 10...12 часов. +По факту получилось существенно больше - порядка 26...28 часов. Много времени ушло на установку зависимостей. +После этой процедуры никак не запускался проект, выводились ошибки. + +Для выполнения ДЗ №2 оценочное время определил в 10..12 часов. По факту получилось несколько больше - 15..16 часов + +Для выполнения Курсовой работы оценочное время определил в 20..22 часов. По факту получилось 25..26 часов \ No newline at end of file diff --git a/public/assets/fonts/Inter2.woff2 b/public/assets/fonts/Inter2.woff2 new file mode 100644 index 000000000..a78fd7e61 Binary files /dev/null and b/public/assets/fonts/Inter2.woff2 differ diff --git a/public/assets/fonts/Montserrat1.woff2 b/public/assets/fonts/Montserrat1.woff2 new file mode 100644 index 000000000..7b900875b Binary files /dev/null and b/public/assets/fonts/Montserrat1.woff2 differ diff --git a/public/assets/fonts/PoppinsNC3.woff2 b/public/assets/fonts/PoppinsNC3.woff2 new file mode 100644 index 000000000..962b734ef Binary files /dev/null and b/public/assets/fonts/PoppinsNC3.woff2 differ diff --git a/src/api.js b/src/api.js new file mode 100644 index 000000000..3c9b8d7ed --- /dev/null +++ b/src/api.js @@ -0,0 +1,33 @@ +// const pathApi = "https://wedev-api.sky.pro/api/leaderboard"; +const pathApiV2 = "https://wedev-api.sky.pro/api/v2/leaderboard"; + +export async function getLeaders() { + const response = await fetch(pathApiV2, { + method: "GET", + }); + if (!response.ok & (response.status === 500)) { + throw new Error("Ошибка соединения"); + } + + const data = await response.json(); + return data; +} + +export async function postLeader({ user, timeUser, achievements }) { + const response = await fetch(pathApiV2, { + method: "POST", + body: JSON.stringify({ + name: user, + time: timeUser, + achievements: achievements, + }), + }); + + if (!response.ok & (response.status === 500)) { + 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..6f9b5edf4 100644 --- a/src/components/Cards/Cards.jsx +++ b/src/components/Cards/Cards.jsx @@ -3,8 +3,12 @@ import { useEffect, useState } from "react"; import { generateDeck } from "../../utils/cards"; import styles from "./Cards.module.css"; import { EndGameModal } from "../../components/EndGameModal/EndGameModal"; +import { AlohomoraModal } from "../../components/forcesModal/AlohomoraModal"; import { Button } from "../../components/Button/Button"; import { Card } from "../../components/Card/Card"; +import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode"; +import { useUser } from "../../contexts/userContext/UseUser"; +import cardsIcon from "../../icons/force2.svg"; // Игра закончилась const STATUS_LOST = "STATUS_LOST"; @@ -41,11 +45,15 @@ function getTimerValue(startDate, endDate) { * previewSeconds - сколько секунд пользователь будет видеть все карты открытыми до начала игры */ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { + const [isLeader, setIsLeader] = useState(false); + const { isEasyMode, forceCards, setForceCards, setForceEye, isAlohomora, setIsAlohomora } = useEasyMode(); + const { setUser } = useUser(); + // Если игорок выбирает легкий уровень с 3 попытками, в attempts организован счетчик этих попыток + const [attempts, setAttempts] = useState(isEasyMode ? 3 : 1); // В cards лежит игровое поле - массив карт и их состояние открыта\закрыта const [cards, setCards] = useState([]); // Текущий статус игры const [status, setStatus] = useState(STATUS_PREVIEW); - // Дата начала игры const [gameStartDate, setGameStartDate] = useState(null); // Дата конца игры @@ -73,8 +81,50 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { setGameEndDate(null); setTimer(getTimerValue(null, null)); setStatus(STATUS_PREVIEW); + if (isEasyMode) { + setAttempts(3); + } + setForceEye(1); + setForceCards(2); + } + function onAlohomoraMouseEnter() { + if (!isGameEnded) { + setIsAlohomora(true); + } + } + function onAlohomoraMouseLeave() { + setIsAlohomora(false); } + const handleForceCards = () => { + if (timer.seconds + timer.minutes > 0) { + const closedCards = cards.filter(card => !card.open); + const lenClosedCards = closedCards.length; + if (forceCards > 0 && lenClosedCards > 1) { + if (forceCards === 1) { + setIsAlohomora(false); + } + setForceCards(prev => prev - 1); + const randomIndex = Math.floor(Math.random() * (lenClosedCards - 1)); + const candidate = closedCards[randomIndex]; + const forcedCards = closedCards.filter( + closedCard => closedCard.rank === candidate.rank && closedCard.suit === candidate.suit, + ); + for (let i = 0; i <= forcedCards.length - 1; i++) { + forcedCards[i].open = true; + } + if (lenClosedCards === 2) { + if (pairsCount === 9) { + setIsLeader(true); + } + finishGame(STATUS_WON); + } + } else if (forceCards > 0 && lenClosedCards === 1) { + alert("Неоткрытых карт должно быть не менее двух!"); + } + } + }; + /** * Обработка основного действия в игре - открытие карты. * После открытия карты игра может пепереходит в следующие состояния @@ -105,7 +155,11 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // Победа - все карты на поле открыты if (isPlayerWon) { - finishGame(STATUS_WON); + if (pairsCount === 9) { + setIsLeader(true); + } + setTimeout(finishGame(STATUS_WON), 1000); + setUser("Пользователь"); return; } @@ -127,10 +181,36 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { // "Игрок проиграл", т.к на поле есть две открытые карты без пары if (playerLost) { - finishGame(STATUS_LOST); - return; - } + setIsLeader(false); + if (!isEasyMode) { + finishGame(STATUS_LOST); + setUser("Пользователь"); + return; + } else { + // Фунция закрытия карт + const closeOpenedCards = () => { + if (openCardsWithoutPair.length >= 2) { + openCardsWithoutPair.forEach(openedCard => { + const currentOpenedCard = nextCards.find(card => card.id === openedCard.id); + if (currentOpenedCard) { + currentOpenedCard.open = false; + } + }); + setAttempts(prevState => prevState - 1); + setCards([...nextCards]); + } + }; + // Сделаем перед закрытием карт задержку в 1с, чтобы игрок видел ошибочно открытую карту + setTimeout(closeOpenedCards, 1000); + if (attempts === 0) { + finishGame(STATUS_LOST); + setAttempts(3); + setIsLeader(true); + return; + } + } + } // ... игра продолжается }; @@ -172,6 +252,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { }; }, [gameStartDate, gameEndDate]); + useEffect(() => { + if (attempts === 0) { + finishGame(STATUS_LOST); + } + }, [attempts]); + return (
@@ -195,6 +281,40 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { )}
+
+
+ {forceCards > 0 ? ( + cards + ) : ( + cards + )} + {isGameEnded ? ( + <> +
0 ? styles.iconCardsEnd : styles.iconCardsOff}`}>
+
0 ? styles.countCardsEnd : styles.countCardsOff}`}>{forceCards}
+ + ) : ( + <> +
0 ? styles.iconCards : styles.iconCardsEnd}`}>
+
0 ? styles.countCards : styles.countCardsEnd}`}>{forceCards}
+ + )} +
+
{status === STATUS_IN_PROGRESS ? : null}
@@ -210,16 +330,31 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) { ))} + {isEasyMode ? ( +
+ Осталось попыток: {attempts} +
+ ) : ( +
+ )} + {isGameEnded ? (
) : null} + + {isAlohomora & (timer.seconds + timer.minutes > 0) ? ( +
+ +
+ ) : null} ); } diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css index 000c5006c..8ee6bfa65 100644 --- a/src/components/Cards/Cards.module.css +++ b/src/components/Cards/Cards.module.css @@ -11,6 +11,7 @@ flex-direction: row; gap: 10px; flex-wrap: wrap; + margin-bottom: 30px; } .modalContainer { @@ -28,20 +29,20 @@ .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; font-size: 64px; font-style: normal; font-weight: 400; line-height: 72px; + z-index: -1; } .previewText { @@ -70,3 +71,153 @@ margin-bottom: -12px; } + +.attempts { + padding-top: 40px; + color: #fff; + font-variant-numeric: lining-nums proportional-nums; + font-family: StratosSkyeng; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 32px; +} + +.footerEasy { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: baseline; +} + +.footerCard { + display: flex; + flex-wrap: nowrap; + justify-content: flex-end; +} + +.achievement { + display: none; +} + +.modalContainerAl { + display: inline-block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: #004980c0; + z-index: 0; +} + +.forcesBox { + width: 151px; + height: 68px; + display: flex; + flex-direction: row; + gap: 5px; +} + +.forceEye, +.forceCards { + position: relative; + opacity: 1.0; + z-index: 2; +} +.forceEyeOff, +.forceCardsOff { + opacity: 0.6; +} + +.forceEye { + display: none; +} + +.iconEye { + background-color: rgba(223, 120, 25, 1.0); + height: 25px; + width: 25px; + position: absolute; + top: 40px; + left: 45px; + border-radius: 50%; + z-index: 2; +} + +.iconEye { + display: none; +} + +.countEye { + color: #fff; + position: relative; + top: 45px; + left: 54px; + font-family: StratosSkyeng; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 14px; + z-index: 2; +} + +.countEye { + display: none; +} + +.iconCards, +.iconCardsOff, +.iconCardsEnd { + background-color: rgba(223, 120, 25, 1.0); + height: 25px; + width: 25px; + position: relative; + top: 40px; + top: -32px; + left: 45px; + border-radius: 50%; + z-index: 2; + opacity: 1.0; +} + +.iconCardsEnd { + top: -34px; + opacity: 0.4; +} + +.countCards, +.countCardsOff, +.countCardsEnd { + color: #fff; + position: relative; + top: -50px; + left: 54px; + font-family: StratosSkyeng; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 14px; + z-index: 2; + opacity: 1.0; +} + +.countCardsEnd { + top: -54px; + opacity: 0.4; +} + +.iconCardsOff { + top: -34px; + left: 50px; + opacity: 0.4; +} + +.countCardsOff { + top: -52px; + left: 58px; + opacity: 0.4; +} diff --git a/src/components/EndGameModal/EndGameModal.jsx b/src/components/EndGameModal/EndGameModal.jsx index 722394833..2cd0b768f 100644 --- a/src/components/EndGameModal/EndGameModal.jsx +++ b/src/components/EndGameModal/EndGameModal.jsx @@ -1,27 +1,104 @@ import styles from "./EndGameModal.module.css"; +import { Link } from "react-router-dom"; +import { useUser } from "../../contexts/userContext/UseUser"; +import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode"; import { Button } from "../Button/Button"; import deadImageUrl from "./images/dead.png"; import celebrationImageUrl from "./images/celebration.png"; +import { useState } from "react"; +import { postLeader } from "../../api"; -export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) { - const title = isWon ? "Вы победили!" : "Вы проиграли!"; +export function EndGameModal({ isWon, isLeader, gameDurationSeconds, gameDurationMinutes, onClick }) { + const { user, setUser } = useUser(); + const [setError] = useState(null); + const [stateBtn, setStateBtn] = useState(true); + const { isEasyMode, forceCards } = useEasyMode(); + + const title = isWon ? (isLeader ? "Вы попали на Лидерборд!" : "Вы победили!") : "Вы проиграли!"; const imgSrc = isWon ? celebrationImageUrl : deadImageUrl; const imgAlt = isWon ? "celebration emodji" : "dead emodji"; + const onInputName = event => { + setUser(event.target.value); + }; + + const postUserLeaderboard = async () => { + if (user.trim() === "") { + alert("Введите имя пользователя"); + } else { + setStateBtn(prev => !prev); + + const timeUser = gameDurationMinutes * 60 + gameDurationSeconds; + const achievements = []; + if (!isEasyMode) { + achievements.push(1); + } + if (forceCards === 2) { + achievements.push(2); + } + + try { + return postLeader({ user, timeUser, achievements }); + } catch (error) { + console.error(error.message); + if (error.message === "Failed to fetch") { + setError("Ошибка соединения"); + return; + } + } + } + }; + return ( -
- {imgAlt} -

{title}

-

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

-
- {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} -
- - -
+ <> + {isLeader ? ( +
+ {imgAlt} +

{title}

+
+ + +
+

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

+
+ {gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")} +
+ + + Перейти к лидерборду + +
+ ) : ( +
+ {imgAlt} +

{title}

+

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

+
+ {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..2800dded2 100644 --- a/src/components/EndGameModal/EndGameModal.module.css +++ b/src/components/EndGameModal/EndGameModal.module.css @@ -1,4 +1,5 @@ -.modal { +.modal, +.modalLeader { width: 480px; height: 459px; border-radius: 12px; @@ -9,6 +10,10 @@ align-items: center; } +.modalLeader { + height: 636px; +} + .image { width: 96px; height: 96px; @@ -16,6 +21,7 @@ } .title { + width: 300px; color: #004980; font-variant-numeric: lining-nums proportional-nums; font-family: StratosSkyeng; @@ -23,6 +29,7 @@ font-style: normal; font-weight: 400; line-height: 48px; + text-align: center; margin-bottom: 28px; } @@ -35,7 +42,6 @@ font-style: normal; font-weight: 400; line-height: 32px; - margin-bottom: 10px; } @@ -46,6 +52,72 @@ font-style: normal; font-weight: 400; line-height: 72px; - margin-bottom: 40px; } + +.goLeaderboard { + font-family: StratosSkyeng; + font-size: 18px; + line-height: 32px; + font-weight: 400; + color: #004980; + margin-top: 16px; +} + +.userName { + margin-bottom: 16px; + width: 276px; + height: 45px; + font-family: StratosSkyeng; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: center; + background-color: #fff; + border: none; + color: #000; +} + +.modalUser { + display: flex; + flex-direction: row; + gap: 20px; +} + +.nameSave { + width: 150px; + height: 45px; + border-radius: 12px; + background: #7ac100; + color: #fff; + 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; + border: none; + cursor: pointer; +} + +.button:hover { + background: #7ac100cc; +} + +.newNameSave { + width: 150px; + height: 45px; + border-radius: 12px; + background: #27cece; + color: #fff; + 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; + border: none; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/components/forcesModal/AlohomoraModal.jsx b/src/components/forcesModal/AlohomoraModal.jsx new file mode 100644 index 000000000..04ab8599f --- /dev/null +++ b/src/components/forcesModal/AlohomoraModal.jsx @@ -0,0 +1,14 @@ +import styles from "./AlohomoraModal.module.css"; + +export function AlohomoraModal() { + return ( +
+ + Алохомора +

+ Открывается случайная

пара карт. +

+
+
+ ); +} diff --git a/src/components/forcesModal/AlohomoraModal.module.css b/src/components/forcesModal/AlohomoraModal.module.css new file mode 100644 index 000000000..e3e49a134 --- /dev/null +++ b/src/components/forcesModal/AlohomoraModal.module.css @@ -0,0 +1,18 @@ +.alohomora { + position: relative; + left: -80px; + top: -330px; + width: 212px; + height: auto; + height: 90px; + background-color: #c2f5ff; + color: #004980; + font-size: 18px; + font-weight: 400; + font-family: Inter; + line-height: 24px; + text-align: center; + border-radius: 12px; + padding: 20px 0; + z-index: -1; +} diff --git a/src/contexts/easyModeContext/EasyModeContext.jsx b/src/contexts/easyModeContext/EasyModeContext.jsx new file mode 100644 index 000000000..b83726b84 --- /dev/null +++ b/src/contexts/easyModeContext/EasyModeContext.jsx @@ -0,0 +1,48 @@ +import { useState, createContext } from "react"; + +const getEasyModeFromLocalStorage = () => { + const easyModeInfo = localStorage.getItem("easyMode"); + return easyModeInfo ? JSON.parse(easyModeInfo) : null; +}; + +export const EasyModeContext = createContext(false); + +export const EasyModeProvider = ({ children }) => { + const [isEasyMode, setIsEasyMode] = useState(getEasyModeFromLocalStorage()); + + const setIsEasy = newEasyMode => { + setIsEasyMode(newEasyMode); + localStorage.setItem("easyMode", JSON.stringify(newEasyMode)); + }; + + const clearIsEasy = () => { + localStorage.removeItem("easyMode"); + setIsEasyMode(false); + }; + + const [selectedLevel, setSelectedLevel] = useState(null); + const [forceEye, setForceEye] = useState(true); + const [forceCards, setForceCards] = useState(2); + const [isAlohomora, setIsAlohomora] = useState(false); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/easyModeContext/UseEasyMode.jsx b/src/contexts/easyModeContext/UseEasyMode.jsx new file mode 100644 index 000000000..b044ac036 --- /dev/null +++ b/src/contexts/easyModeContext/UseEasyMode.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { EasyModeContext } from "./EasyModeContext"; + +export const useEasyMode = () => { + return useContext(EasyModeContext); +}; diff --git a/src/contexts/leaderContext/LeaderContext.jsx b/src/contexts/leaderContext/LeaderContext.jsx new file mode 100644 index 000000000..e9f3cd662 --- /dev/null +++ b/src/contexts/leaderContext/LeaderContext.jsx @@ -0,0 +1,9 @@ +import { createContext, useState } from "react"; + +export const LeadersContext = createContext(); + +export const LeadersProvider = ({ children }) => { + const [leaders, setLeaders] = useState([]); + + return {children}; +}; diff --git a/src/contexts/leaderContext/UseLeaders.jsx b/src/contexts/leaderContext/UseLeaders.jsx new file mode 100644 index 000000000..78c3c9faf --- /dev/null +++ b/src/contexts/leaderContext/UseLeaders.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { LeadersContext } from "./LeaderContext"; + +export const useLeaders = () => { + return useContext(LeadersContext); +}; diff --git a/src/contexts/userContext/UseUser.jsx b/src/contexts/userContext/UseUser.jsx new file mode 100644 index 000000000..8ab465564 --- /dev/null +++ b/src/contexts/userContext/UseUser.jsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { UserContext } from "./UserContext"; + +export const useUser = () => { + return useContext(UserContext); +}; diff --git a/src/contexts/userContext/UserContext.jsx b/src/contexts/userContext/UserContext.jsx new file mode 100644 index 000000000..2025d565b --- /dev/null +++ b/src/contexts/userContext/UserContext.jsx @@ -0,0 +1,11 @@ +import { createContext, useState } from "react"; + +export const UserContext = createContext(null); + +const UserProvider = ({ children }) => { + const [user, setUser] = useState("Пользователь"); + + return {children}; +}; + +export default UserProvider; diff --git a/src/icons/achievement1.svg b/src/icons/achievement1.svg new file mode 100644 index 000000000..30eb863be --- /dev/null +++ b/src/icons/achievement1.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/icons/achievement1Non.svg b/src/icons/achievement1Non.svg new file mode 100644 index 000000000..3ee152c69 --- /dev/null +++ b/src/icons/achievement1Non.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/icons/achievement2.svg b/src/icons/achievement2.svg new file mode 100644 index 000000000..312c80e93 --- /dev/null +++ b/src/icons/achievement2.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/icons/achievement2Non.svg b/src/icons/achievement2Non.svg new file mode 100644 index 000000000..7949f73ed --- /dev/null +++ b/src/icons/achievement2Non.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icons/eye.svg b/src/icons/eye.svg new file mode 100644 index 000000000..d88a5843e --- /dev/null +++ b/src/icons/eye.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icons/force2.svg b/src/icons/force2.svg new file mode 100644 index 000000000..858b1880f --- /dev/null +++ b/src/icons/force2.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/index.css b/src/index.css index 78f0d3a2b..885fa5523 100644 --- a/src/index.css +++ b/src/index.css @@ -9,6 +9,7 @@ body { } #root { + /* position: absolute; */ width: 100%; height: 100%; } @@ -35,3 +36,27 @@ ol { font-weight: 400; font-style: normal; } +@font-face { + font-family: "Poppins"; + src: + url("../public/assets/fonts/PoppinsNC3.woff2") format("woff2"), + local("sans-serif"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Montserrat"; + src: + url("../public/assets/fonts/Montserrat1.woff2") format("woff2"), + local("sans-serif"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Inter"; + src: + url("../public/assets/fonts/Inter2.woff2") format("woff2"), + local("sans-serif"); + font-weight: 400; + font-style: normal; +} diff --git a/src/index.js b/src/index.js index f689c5f0b..d6396aae3 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,19 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import { RouterProvider } from "react-router-dom"; import { router } from "./router"; +import { EasyModeProvider } from "./contexts/easyModeContext/EasyModeContext"; +import { LeadersProvider } from "./contexts/leaderContext/LeaderContext"; +import UserProvider from "./contexts/userContext/UserContext"; 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..faae3eba8 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.jsx @@ -0,0 +1,125 @@ +import styles from "./LeaderboardPage.module.css"; +import { useEffect } from "react"; +import { useLeaders } from "../../contexts/leaderContext/UseLeaders"; +import { getLeaders } from "../../api"; +import { Link } from "react-router-dom"; +import { Button } from "../../components/Button/Button"; +import achiev1 from "../../icons/achievement1.svg"; +import achiev1Non from "../../icons/achievement1Non.svg"; +import achiev2 from "../../icons/achievement2.svg"; +import achiev2Non from "../../icons/achievement2Non.svg"; +import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode"; + +export const LeaderboardPage = () => { + const { leaders, setLeaders } = useLeaders(); + const { setIsEasyMode, setForceEye, setForceCards } = useEasyMode(); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await getLeaders(); + // Отсортируем результаты лидерборда в порядке возрастания времени + response.leaders.sort((a, b) => a.time - b.time); + // Если результатаов в лидерборде более 10, ограничим просмотр этим количеством + const lenLeaders = response.leaders.length < 10 ? response.leaders.length : 10; + const sortLeaders = response.leaders.slice(0, lenLeaders); + setLeaders(sortLeaders); + } catch (error) { + console.error(error); + throw new Error("Ошибка при получении списка лидеров"); + } + }; + fetchData(); + }, [setLeaders]); + + function getTimeViewer(innerSeconds) { + const minutes = Math.floor(innerSeconds / 60); + const seconds = innerSeconds % 60; + const formattedMinutes = minutes.toString().padStart(2, "0"); + const formattedSeconds = seconds.toString().padStart(2, "0"); + const viewedTime = `${formattedMinutes}:${formattedSeconds}`; + return viewedTime; + } + + const handleStartGame = () => { + setIsEasyMode(false); + setForceEye(1); + setForceCards(2); + }; + + return ( +
+
+
+ Лидерборд + + + +
+
+
+
+
+ Позиция +
+
+ Пользователь +
+
+ Достижения +
+
+ Время +
+
+
+ {leaders.map((sortLeader, index) => { + return ( +
+
+
+ # {index + 1} +
+
+ {sortLeader.name} +
+
+
+ {sortLeader.achievements.includes(1) ? ( +
+ Игра пройдена в сложном режиме + achiev1 +
+ ) : ( + achiev1Non + )} +
+
+
+ {sortLeader.achievements.includes(2) ? ( +
+ + Игра пройдена

без супер-сил +
+ achiev2 +
+ ) : ( + achiev2Non + )} +
+
+ +
+ {getTimeViewer(sortLeader.time)} +
+
+
+ ); + })} +
+
+
+ ); +}; diff --git a/src/pages/LeaderboardPage/LeaderboardPage.module.css b/src/pages/LeaderboardPage/LeaderboardPage.module.css new file mode 100644 index 000000000..5b7c47604 --- /dev/null +++ b/src/pages/LeaderboardPage/LeaderboardPage.module.css @@ -0,0 +1,193 @@ +.container { + width: 944px; + min-height: auto; + display: flex; + align-items: center; + justify-content: center; + background: #004980; + box-sizing: border-box; + margin-left: 40px; +} + +.content { + display: flex; + flex-direction: column; + width: 944px; +} + +.title { + font-family: StratosSkyeng; + font-weight: 400; + font-size: 24px; + line-height: 32px; + color: #fff; +} + +.header { + margin-top: 52px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.table { + margin-top: 50px; + display: flex; + flex-direction: column; + gap: 15px; +} + +.boxTitle, +.boxContent { + width: 889px; + height: 32px; + display: flex; + flex-direction: row; + color: #999999; + justify-content: left; + align-items: center; + padding-top: 16px; + margin-left: 20px; +} + +.boxTitle { + gap: 66px; +} + +.boxContent { + color: #000; +} + +.tableHeader, +.tableBody { + width: 944px; + height: 64px; + border-radius: 12px; + background-color: #fff; + opacity: 0px; +} + +.tableBody { + color: #000; +} +.userPosition, +.userName, +.userTime, +.userAchievements { + font-family: Montserrat, Poppins; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; + margin-right: 66px; +} + +.userName { + margin-right: 3px; + width: 290px; +} + +.userTime { + margin-right: 0; +} + +.position, +.name, +.time, +.achievements { + font-family: Montserrat; + font-size: 24px; + font-weight: 400; + line-height: 32px; + text-align: left; +} + +.position, +.userPosition { + width: 178px; +} + +.name { + width: 227px; +} + +.time, +.userTime { + width: 92px; +} + +.achievements, +.userAchievements { + width: 194px; +} + +.emptySpace { + width: 122px; + margin-left: 66px; +} + +.boxAchievement { + width: 32px; +} + +.blockAchiev { + width: 70px; + display: flex; + flex-direction: row; +} + +.spaceAchiev { + width: 6px; +} + +.tooltip, +.tooltip2 { + position: relative; + display: inline-block; +} + +.tooltip .tooltiptext, +.tooltip2 .tooltiptext2 { + visibility: hidden; + width: 212px; + height: auto; + background-color: #c2f5ff; + color: #004980; + font-size: 18px; + font-weight: 400; + font-family: Inter; + line-height: 24px; + text-align: center; + border-radius: 12px; + padding: 10px 0; + position: absolute; + bottom: 40px; + z-index: 1; +} + +.tooltip2 .tooltiptext2 { + width: 187px; +} + +.tooltip:hover .tooltiptext, +.tooltip2:hover .tooltiptext2 { + visibility: visible; + /* -webkit-filter: brightness(70%); + -webkit-transition: all 2s ease; + -moz-transition: all 2s ease; + -o-transition: all 2s ease; + -ms-transition: all 2s ease; + transition: all 2s ease; */ +} + +.tooltip .tooltiptext::after, +.tooltip2 .tooltiptext2::after { + content: ""; + position: absolute; + top: 100%; + left: 10%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #c2f5ff transparent transparent transparent; +} diff --git a/src/pages/SelectLevelPage/SelectLevelPage.jsx b/src/pages/SelectLevelPage/SelectLevelPage.jsx index 758942e51..809c429f7 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.jsx +++ b/src/pages/SelectLevelPage/SelectLevelPage.jsx @@ -1,28 +1,67 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./SelectLevelPage.module.css"; +import { Button } from "../../components/Button/Button"; +import { useEasyMode } from "../../contexts/easyModeContext/UseEasyMode"; export function SelectLevelPage() { + const { isEasyMode, setIsEasy, selectedLevel, setSelectedLevel, clearIsEasy } = useEasyMode(); + const navigate = useNavigate(); + + const handleEasyModeChange = event => { + setIsEasy(event.target.checked); + }; + + const arrLevel = [3, 6, 9]; + + const handleCheckboxChange = async level => { + setSelectedLevel(level); + }; + + const handleStartClick = () => { + if (!isEasyMode) { + clearIsEasy(); + } + if (selectedLevel !== null) { + navigate(`/game/${selectedLevel}`); + } else { + alert("Нужно выбрать уровень"); + } + }; + return (

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

+ + + + Перейти к лидерборду +
); diff --git a/src/pages/SelectLevelPage/SelectLevelPage.module.css b/src/pages/SelectLevelPage/SelectLevelPage.module.css index 390ac0def..4decd3134 100644 --- a/src/pages/SelectLevelPage/SelectLevelPage.module.css +++ b/src/pages/SelectLevelPage/SelectLevelPage.module.css @@ -48,7 +48,10 @@ background: #fff; } -.levelLink { +.checkboxButton { + width: 97px; + height: 98px; + border-radius: 12px; color: #0080c1; text-align: center; font-family: StratosSkyeng; @@ -57,8 +60,69 @@ font-weight: 400; line-height: 72px; text-decoration: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + /* margin-bottom: 28px; */ } -.levelLink:visited { - color: #0080c1; +.checkboxButton:hover { + color: #08608f; + background-color: #e7f06c; +} + +.selected { + color: #08608f; + background-color: #e7f06c; + border: 2px solid #2e9aff; +} + +.selLevel { + display: none; +} + +.buttonStart { + /* width: 276px; + height: 45px; + background-color: #7ac100; + border-radius: 12px; + border: "0.7px solid var(--palette-navy-60, #7ac100)"; + outline: none; + display: flex; + align-items: center; + justify-content: center; + font-family: StratosSkyeng; + font-size: 24px; + line-height: 32px; + font-weight: 400; + letter-spacing: -0.14px; + color: #fff; */ + margin-top: 32px; +} + +.easyMode { + font-family: StratosSkyeng; + font-size: 24px; + line-height: 32px; + font-weight: 400; + letter-spacing: -0.14px; + color: #000; +} + +.easyModeCheck { + width: 30px; + height: 29px; + color: #5a6b3e; + background: #7ac100; + margin-bottom: 28px; +} + +.goLeaderboard { + font-family: StratosSkyeng; + font-size: 18px; + line-height: 32px; + font-weight: 400; + color: #004980; + margin-top: 16px; } 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