Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
"extends": ["eslint:recommended", "prettier", "react-app", "react-app/jest"],
"plugins": ["prettier"],
"rules": {
"camelcase": ["error", { "properties": "never" }],
"prettier/prettier": "error",
"camelcase": ["error"],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"eqeqeq": ["error", "always"],
"no-unused-vars": ["error"]
}
Expand Down
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
endOfLine: "auto",
printWidth: 120,
tabWidth: 2,
useTabs: false,
Expand Down
29 changes: 29 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const hostUrl = "https://wedev-api.sky.pro/api/v2/leaderboard";
export async function getLeaders() {
const response = await fetch(hostUrl, {
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(hostUrl, {
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;
}
2 changes: 1 addition & 1 deletion src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
font-variant-numeric: lining-nums proportional-nums;

/* Pres → Caption S */
font-family: StratosSkyeng;
font-family: Roboto;
font-size: 24px;
font-style: normal;
font-weight: 400;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Card/Card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

color: #000;
font-variant-numeric: lining-nums proportional-nums;
font-family: StratosSkyeng;
font-family: Roboto;
font-size: 24px;
font-style: normal;
font-weight: 400;
Expand Down
77 changes: 69 additions & 8 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 CardsIcon from "../../icon/cards.svg";
import ToolsComponent from "../Tools/ToolsComponent";

// Игра закончилась
const STATUS_LOST = "STATUS_LOST";
Expand Down Expand Up @@ -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([]);
// Текущий статус игры
Expand All @@ -57,6 +59,15 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
minutes: 0,
});

// Стейт для счетчика попыток
const [numberOfAttempts, setNumberOfAttempts] = useState(2);

const [achievements, setAchievements] = useState([]);

const minusOneAttempt = () => {
setNumberOfAttempts(numberOfAttempts - 1);
};

function finishGame(status = STATUS_LOST) {
setGameEndDate(new Date());
setStatus(status);
Expand All @@ -68,11 +79,14 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setTimer(getTimerValue(startDate, null));
setStatus(STATUS_IN_PROGRESS);
}
//добавлено кол-во попыток setNumberOfAttempts и ачивки setAchievements
function resetGame() {
setGameStartDate(null);
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
setNumberOfAttempts(2);
setAchievements(prevState => prevState.filter(achieve => achieve !== 2));
}

/**
Expand Down Expand Up @@ -125,17 +139,47 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {

const playerLost = openCardsWithoutPair.length >= 2;

// "Игрок проиграл", т.к на поле есть две открытые карты без пары
if (playerLost) {
finishGame(STATUS_LOST);
return;
}

// ... игра продолжается
if (isGameMode === "true") {
if (playerLost) {
minusOneAttempt();
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;
}
}
};

const isGameEnded = status === STATUS_LOST || status === STATUS_WON;

//Ачивка на открытие пары карт
const alohomora = () => {
if (achievements.includes(2)) {
alert("Подсказкой можно воспользоваться только 1 раз");
return;
}

let closedCards = cards.filter(card => !card.open);
closedCards = shuffle(closedCards);
closedCards[0].open = true;
closedCards.forEach(card => {
if (card.suit === closedCards[0].suit && card.rank === closedCards[0].rank) {
card.open = true;
}
});
setAchievements(prevState => [...prevState, 2]);
};

// Игровой цикл
useEffect(() => {
// В статусах кроме превью доп логики не требуется
Expand Down Expand Up @@ -171,6 +215,12 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
clearInterval(intervalId);
};
}, [gameStartDate, gameEndDate]);
//Отслеживание состояния использования ачивки
useEffect(() => {
if (!isGameMode) {
setAchievements(prevState => [...prevState, 1]);
}
}, [isGameMode]);

return (
<div className={styles.container}>
Expand All @@ -195,7 +245,17 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
</>
)}
</div>
{status === STATUS_IN_PROGRESS ? <Button onClick={resetGame}>Начать заново</Button> : null}
{status === STATUS_IN_PROGRESS ? (
<>
{isGameMode === "true" ? (
<div className={styles.attemptСounter}>осталось попыток: {numberOfAttempts + 1} </div>
) : null}
<ToolsComponent text={"Алохомора! Открывается случайная пара карт."} customClass={styles.toolsCustom}>
<img className={styles.iconBtn} src={CardsIcon} alt="Открыть пару карточек" onClick={alohomora} />
</ToolsComponent>
<Button onClick={resetGame}>Начать заново</Button>
</>
) : null}
</div>

<div className={styles.cards}>
Expand All @@ -217,6 +277,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
isGameMode={isGameMode}
/>
</div>
) : null}
Expand Down
15 changes: 13 additions & 2 deletions src/components/Cards/Cards.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
align-items: end;

color: #fff;
font-family: StratosSkyeng;
font-family: Roboto;
font-size: 64px;
font-style: normal;
font-weight: 400;
Expand All @@ -62,11 +62,22 @@
.timerDescription {
color: #fff;
font-variant-numeric: lining-nums proportional-nums;
font-family: StratosSkyeng;
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 32px;

margin-bottom: -12px;
}

.attemptСounter {
text-align: center;
color: #fff;
font-variant-numeric: lining-nums proportional-nums;
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 32px;
}
9 changes: 9 additions & 0 deletions src/components/Context/Context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContext, useState } from "react";

export const EasyModeContext = createContext();

export const GameProvider = ({ children }) => {
const [selectedLevel, setSelectedLevel] = useState(null);

return <EasyModeContext.Provider value={{ selectedLevel, setSelectedLevel }}>{children}</EasyModeContext.Provider>;
};
88 changes: 83 additions & 5 deletions src/components/EndGameModal/EndGameModal.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
import styles from "./EndGameModal.module.css";

import { Button } from "../Button/Button";

import deadImageUrl from "./images/dead.png";
import celebrationImageUrl from "./images/celebration.png";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { addLeaders } from "../../api";

export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes, onClick }) {
const title = isWon ? "Вы победили!" : "Вы проиграли!";
//const title = isWon ? "Вы победили!" : "Вы проиграли!"; -изменно
const maxLevel = 9; // Переменная для числа
const { pairsCount } = useParams();
const isMaxLevel = pairsCount === maxLevel; // Переменная для условия

const navigate = useNavigate();
const gameSeconds = gameDurationMinutes * 60 + gameDurationSeconds;
const [userData, setuserData] = useState({
name: "",
time: gameSeconds,
achievements: [],
});
//если isGameMode true то в userData.achievements нужно добавить 1

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 (isMaxLevel) {
// Используем переменную вместо условия
title = isWon ? "Вы попали на лидерборд!" : "Вы проиграли!";
} else {
title = isWon ? "Вы победили!" : "Вы проиграли!";
}

const startTheGame = e => {
e.preventDefault();
navigate(`/`);
};
const imgSrc = isWon ? celebrationImageUrl : deadImageUrl;

const imgAlt = isWon ? "celebration emodji" : "dead emodji";
Expand All @@ -16,12 +71,35 @@ export function EndGameModal({ isWon, gameDurationSeconds, gameDurationMinutes,
<div className={styles.modal}>
<img className={styles.image} src={imgSrc} alt={imgAlt} />
<h2 className={styles.title}>{title}</h2>
{isMaxLevel && isWon ? (
<form className={styles.form}>
<input
className={styles.nameInput}
value={userData.name}
onChange={handleInputChange}
type="text"
name="name"
id="formUser"
placeholder="Пользователь"
/>
</form>
) : null}
<p className={styles.description}>Затраченное время:</p>
<div className={styles.time}>
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
</div>

<Button onClick={onClick}>Начать сначала</Button>
{isMaxLevel && isWon ? (
<>
<Button onClick={handleAddUserButton}>Начать сначала</Button>
<Link onClick={handleAddUser} className={styles.leaderBoardLinkBox}>
Перейти к лидерборду
</Link>
</>
) : (
<>
<Button onClick={startTheGame}>Начать сначала</Button>
</>
)}
</div>
);
}
Loading