Skip to content
Open

Hw2 #81

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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

/.idea

# dependencies
/node_modules
/.pnp
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ https://skypro-web-developer.github.io/react-memo/

Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.

### Работа

- Планируемое время: 15 часов
- Фактическое время: 18 часов
- Цель: Запускает eslint проверку кода, эта же команда запускается перед каждым коммитом.
Если не получается закоммитить, попробуйте запустить эту команду и исправить все ошибки и предупреждения.
30 changes: 30 additions & 0 deletions src/api/leaders.js
Original file line number Diff line number Diff line change
@@ -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();
});
};
2 changes: 1 addition & 1 deletion src/components/Card/Card.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions src/components/Card/Card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@
}

.flipContainer:hover .flipper {
transition: 0.2s;
transform: rotateY(35deg);
/*transition: 0.2s;*/
/*transform: rotateY(35deg);*/
}

.flipContainer.flip .flipper {
Expand Down
38 changes: 35 additions & 3 deletions src/components/Cards/Cards.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -42,14 +45,19 @@ 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);

// Дата начала игры
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({
Expand All @@ -73,6 +81,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
setGameEndDate(null);
setTimer(getTimerValue(null, null));
setStatus(STATUS_PREVIEW);
setLives(3);
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -209,6 +238,7 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
/>
))}
</div>
{easyMode ? <p className={styles.subtitle}>Осталось попыток: {lives}</p> : ""}

{isGameEnded ? (
<div className={styles.modalContainer}>
Expand All @@ -217,6 +247,8 @@ export function Cards({ pairsCount = 3, previewSeconds = 5 }) {
gameDurationSeconds={timer.seconds}
gameDurationMinutes={timer.minutes}
onClick={resetGame}
pairsCount={pairsCount}
timer={timer}
/>
</div>
) : null}
Expand Down
7 changes: 7 additions & 0 deletions src/components/Cards/Cards.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,10 @@

margin-bottom: -12px;
}

.subtitle {
color: #fff;
font-size: 18px;
line-height: 18px;
margin-top: 20px;
}
76 changes: 72 additions & 4 deletions src/components/EndGameModal/EndGameModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.modal}>
<img className={styles.image} src={imgSrc} alt={imgAlt} />
<h2 className={styles.title}>{title}</h2>
{isLeadResult ? (
isWon ? (
<form
onSubmit={e => {
e.preventDefault();
if (!inputLeaders.trim()) {
setInputLeaders("Введите имя");
return;
}
onLeaders();
setInputLeaders("");
navigate("/leaderboard");
}}
>
<input
className={styles.input}
onChange={e => {
setInputLeaders(e.target.value);
}}
value={inputLeaders.name}
type="text"
placeholder="Пользователь"
/>
</form>
) : (
""
)
) : (
""
)}
<p className={styles.description}>Затраченное время:</p>
<div className={styles.time}>
{gameDurationMinutes.toString().padStart("2", "0")}.{gameDurationSeconds.toString().padStart("2", "0")}
</div>

<Button onClick={onClick}>Начать сначала</Button>
<Link className={styles.link} to="/leaderboard">
Перейти к лидерборду
</Link>
{error}
</div>
);
}
35 changes: 31 additions & 4 deletions src/components/EndGameModal/EndGameModal.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.modal {
width: 480px;
height: 459px;
/*height: 459px;*/
border-radius: 12px;
background: #c2f5ff;
display: flex;
Expand All @@ -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 {
Expand All @@ -23,7 +41,7 @@
font-style: normal;
font-weight: 400;
line-height: 48px;

text-align: center;
margin-bottom: 28px;
}

Expand All @@ -35,7 +53,6 @@
font-style: normal;
font-weight: 400;
line-height: 32px;

margin-bottom: 10px;
}

Expand All @@ -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;
}
14 changes: 14 additions & 0 deletions src/components/Leaderboard/Leaderboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styles from "./Leaderboard.module.css";

export const Leaderboard = ({ position, user, time, color = "black" }) => {
return (
<>
<li style={{ color: color }} className={styles.item}>
<span className={styles.position}>{position}</span>
<span className={styles.user}>{user}</span>
<span className={styles.time}></span>
<span className={styles.time}>{time}</span>
</li>
</>
);
};
1 change: 1 addition & 0 deletions src/components/Leaderboard/Leaderboard.module.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/components/context/cardsContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext, useState } from "react";

export const CardsContext = createContext(null);
export const CardsProvider = ({ children }) => {
const [cards, setCards] = useState([]);
return <CardsContext.Provider value={{ cards, setCards }}>{children}</CardsContext.Provider>;
};
7 changes: 7 additions & 0 deletions src/components/context/easymodeContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext, useState } from "react";

export const EasyModeContext = createContext(null);
export const EasyModeProvider = ({ children }) => {
const [easyMode, setEasyMode] = useState(false);
return <EasyModeContext.Provider value={{ easyMode, setEasyMode }}>{children}</EasyModeContext.Provider>;
};
Loading