refactor
This commit is contained in:
parent
1421bc549e
commit
aff0ac7330
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
import cls from './App.module.scss';
|
||||
import Field from './components/Field/Field';
|
||||
import MatchGame from './components/MatchGame/MatchGame';
|
||||
import { MatchGameProvider } from './context/MatchGameContext';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className={cls.App}>
|
||||
<Field/>
|
||||
<MatchGameProvider>
|
||||
<MatchGame/>
|
||||
</MatchGameProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,40 +7,5 @@
|
||||
align-items: center;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 10px;
|
||||
|
||||
.Timer {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Pacifico", sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.StartButton {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
height: 45px;
|
||||
border-radius: 45px;
|
||||
margin: 10px 20px;
|
||||
font-family: "Pacifico", sans-serif;
|
||||
font-size: 22px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
font-weight: 600;
|
||||
color: #524f4e;
|
||||
background: white;
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, .1);
|
||||
//background: #7986cb;
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
background: #7986cb;
|
||||
box-shadow: 0 15px 20px rgba(121, 134, 203, 0.4);
|
||||
color: white;
|
||||
transform: translateY(-7px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,64 +1,81 @@
|
||||
import { useMatchGame } from '../../context/MatchGameContext';
|
||||
import Button from '../UI/Button/Button';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Timer from '../UI/Timer/Timer';
|
||||
import cls from './BottomPanel.module.scss';
|
||||
|
||||
type BottomPanelProps = {
|
||||
start: () => void;
|
||||
retry?: () => void;
|
||||
startGame: () => void;
|
||||
retryGame: () => void;
|
||||
isFinishedGame?: boolean;
|
||||
shuffleCards: () => void;
|
||||
}
|
||||
|
||||
const BottomPanel: React.FC<BottomPanelProps> = (props) => {
|
||||
const [startTime, setStartTime] = useState<number | null>(null);
|
||||
const [timeInterval, setTimeInterval] = useState<NodeJS.Timeout>();
|
||||
type PanelState = 'start' | 'retry';
|
||||
|
||||
const BottomPanel: React.FC<BottomPanelProps> = (props) => {
|
||||
const [isTimerStarted, setIsTimerStarted] = useState(false);
|
||||
const [isTimerStopped, setIsTimerStopped] = useState(false);
|
||||
const [panelState, setPanelState] = useState<PanelState>('start');
|
||||
|
||||
function onStartBtnClickHandler(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
setStartTime(0);
|
||||
const interval = setInterval(() => {
|
||||
setStartTime(prev => prev !== null ? prev + 1000 : 0)
|
||||
}, 1000);
|
||||
setTimeInterval(interval);
|
||||
props.start();
|
||||
setIsTimerStarted(true);
|
||||
props.startGame();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
clearInterval(timeInterval)
|
||||
setIsTimerStopped(true)
|
||||
setPanelState(props.isFinishedGame ? 'retry' : 'start')
|
||||
|
||||
}, [props.isFinishedGame])
|
||||
|
||||
function renderActiveStatePanel() {
|
||||
return startTime !== null
|
||||
? <p className={cls.Timer}>{getTimeString(new Date(startTime))}</p>
|
||||
: <button className={cls.StartButton} onClick={onStartBtnClickHandler}>START</button>
|
||||
}
|
||||
|
||||
function onRetryBtnClickHandler() {
|
||||
setStartTime(null);
|
||||
props.retry?.();
|
||||
setIsTimerStarted(false);
|
||||
setIsTimerStopped(false);
|
||||
setPanelState('start');
|
||||
|
||||
props.retryGame?.();
|
||||
}
|
||||
|
||||
function renderRetryButton() {
|
||||
function renderRetryStatePanel() {
|
||||
return (
|
||||
<>
|
||||
<button className={cls.StartButton} onClick={onRetryBtnClickHandler}>Retry</button>
|
||||
<Button onClick={onRetryBtnClickHandler}>RETRY</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function onShuffleBtnClickHandler() {
|
||||
props.shuffleCards?.();
|
||||
}
|
||||
|
||||
function renderStartStatePanel() {
|
||||
return isTimerStarted
|
||||
? <Timer isStarted={isTimerStarted} startTime={0} isFinished={isTimerStopped}/>
|
||||
: <>
|
||||
<Button onClick={onStartBtnClickHandler}>START</Button>
|
||||
<Button onClick={onShuffleBtnClickHandler}>SHUFFLE</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
function renderPanelByState(state: PanelState) {
|
||||
if (!state) {
|
||||
console.error('Нет состояния у панели')
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 'start': return renderStartStatePanel();
|
||||
case 'retry': return renderRetryStatePanel();
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls.BottomPanel}>
|
||||
{ props.isFinishedGame ? renderRetryButton() : renderActiveStatePanel() }
|
||||
{ renderPanelByState(panelState) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getTimeString(time: Date): string {
|
||||
let minutes = time.getMinutes().toString();
|
||||
if (+minutes < 10) minutes = '0' + minutes;
|
||||
|
||||
let seconds = time.getSeconds().toString();
|
||||
if (+seconds < 10) seconds = '0' + seconds;
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export default BottomPanel;
|
||||
|
||||
@ -1,32 +1,33 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import cls from './Card.module.scss'
|
||||
|
||||
export type CardProps = {
|
||||
title: string;
|
||||
code: string;
|
||||
onReverse?: (a: any) => void;
|
||||
export type CardComponentProps = {
|
||||
title?: string;
|
||||
imageURL: string;
|
||||
id: string;
|
||||
toggled: boolean;
|
||||
disabled: boolean;
|
||||
isMatched: boolean;
|
||||
disabled: boolean;
|
||||
onClick?: (id: string) => void;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = (props) => {
|
||||
const Card: React.FC<CardComponentProps> = (props) => {
|
||||
const classes = [cls.Card, props.toggled ? cls.reverse : ''];
|
||||
|
||||
const canClick = useMemo(() => {
|
||||
return !props.disabled && !props.isMatched
|
||||
}, [props.disabled, props.isMatched]);
|
||||
function cardClickHandler(id: string) {
|
||||
if (props.disabled) return;
|
||||
|
||||
const classes = useMemo(() => {
|
||||
return props.toggled ? [cls.Card, cls.reverse] : [cls.Card];
|
||||
}, [props.toggled])
|
||||
props.onClick?.(id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls.CardWrapper} style={{
|
||||
opacity: props.isMatched ? 0 : 1
|
||||
}}>
|
||||
<div className={classes.join(' ')} onClick={() => canClick && props.onReverse?.(props)}>
|
||||
<div
|
||||
className={cls.CardWrapper}
|
||||
style={{opacity: props.isMatched ? 0 : 1}}
|
||||
onClick={() => cardClickHandler(props.id)}
|
||||
>
|
||||
<div className={classes.join(' ')}>
|
||||
<div className={cls.frontside}>{props.title}</div>
|
||||
<div className={cls.backside}>
|
||||
<img src={props.imageURL} alt={props.title}/>
|
||||
|
||||
17
src/components/CardsField/CardsField.module.scss
Normal file
17
src/components/CardsField/CardsField.module.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.CardsField {
|
||||
background: #283593;
|
||||
position: relative;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 10px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
". . ."
|
||||
". . ."
|
||||
". . ."
|
||||
". . .";
|
||||
}
|
||||
139
src/components/CardsField/CardsField.tsx
Normal file
139
src/components/CardsField/CardsField.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useMatchGame } from '../../context/MatchGameContext';
|
||||
import Card, { CardComponentProps } from '../Card/Card';
|
||||
import cls from './CardsField.module.scss';
|
||||
|
||||
type CardProps = {
|
||||
code: string;
|
||||
imageURL: string;
|
||||
}
|
||||
|
||||
type CardsFieldProps = {
|
||||
cards: CardProps[];
|
||||
isGameStarted: boolean;
|
||||
finishGameCallback: () => void;
|
||||
}
|
||||
|
||||
type CardHandlerType =
|
||||
'disableAll' |
|
||||
'enableAll' |
|
||||
'showAll' |
|
||||
'hideAll';
|
||||
|
||||
type CurrentCardPareType = {
|
||||
first: CardComponentProps | null;
|
||||
second: CardComponentProps | null;
|
||||
}
|
||||
|
||||
const CardsField: React.FC<CardsFieldProps> = ({cards, isGameStarted, finishGameCallback}) => {
|
||||
// TODO use useReducer
|
||||
const [cardsState, setCardsState] = useState(cards.map(el => createCard(el)));
|
||||
const [currentCard, setCurrentCard] = useState<CardComponentProps | null>(null)
|
||||
const [currentCardPare, setCurrentCardPare] = useState<CurrentCardPareType>({first: null, second: null})
|
||||
|
||||
const {addMistakesCount} = useMatchGame();
|
||||
|
||||
function matchHandler(prev: CardComponentProps, card: CardComponentProps) {
|
||||
setCardsState(prevState => prevState.map(el => {
|
||||
if (el.id === card.id || el.id === prev.id) return {...el, disabled: true, isMatched: true}
|
||||
else return {...el, disabled: true}
|
||||
}));
|
||||
setTimeout(() => cardsHandler('enableAll'), 500)
|
||||
}
|
||||
|
||||
function notMatchHandler() {
|
||||
cardsHandler('disableAll');
|
||||
addMistakesCount?.();
|
||||
setTimeout(() => {
|
||||
setCardsState(prevState => prevState.map(el => {
|
||||
return {...el, toggled: false, disabled: false, isMatched: false};
|
||||
}))
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function cardsHandler(type: CardHandlerType) {
|
||||
switch (type) {
|
||||
case 'disableAll':
|
||||
setCardsState(prevState => prevState.map(el => ({...el, disabled: true})));
|
||||
return;
|
||||
case 'enableAll':
|
||||
setCardsState(prevState => prevState.map(el => ({...el, disabled: false})));
|
||||
return;
|
||||
case 'showAll':
|
||||
setCardsState(prevState => prevState.map(el => ({...el, toggled: true})));
|
||||
return;
|
||||
case 'hideAll':
|
||||
setCardsState(prevState => prevState.map(el => ({...el, toggled: false})));
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
const cardClickHandler = useCallback((id: string) => {
|
||||
const targetCard = cardsState.find(el => el.id === id);
|
||||
if (!targetCard) return;
|
||||
setCardsState(prevState => prevState.map(el => el.id === id ? {...el, toggled: !el.toggled, disabled: true} : el))
|
||||
setCurrentCardPare(prev => {
|
||||
if (!prev.first) return {first: targetCard, second: null};
|
||||
if (prev.first && !prev.second) return {...prev, second: targetCard};
|
||||
|
||||
return {first: targetCard, second: null}
|
||||
})
|
||||
}, [cardsState])
|
||||
|
||||
function isAllCardsMatched(cards: CardComponentProps[]) {
|
||||
return cards.reduce((res, cur) => (res && cur.isMatched), true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAllCardsMatched(cardsState)) setTimeout(finishGameCallback, 800);
|
||||
}, [cardsState])
|
||||
|
||||
useEffect(() => {
|
||||
const {first, second} = currentCardPare;
|
||||
if (first && second) {
|
||||
if (first?.code === second?.code) matchHandler(first, second)
|
||||
else notMatchHandler();
|
||||
}
|
||||
}, [currentCardPare])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGameStarted) return;
|
||||
setCardsState(prev => (prev.map(el => ({...el, disabled: false, toggled: false}))));
|
||||
}, [isGameStarted])
|
||||
|
||||
useEffect(() => {
|
||||
setCardsState(cards.map(el => createCard(el)))
|
||||
}, [cards])
|
||||
|
||||
function createCard (card: CardProps): CardComponentProps {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
toggled: !isGameStarted,
|
||||
disabled: !isGameStarted,
|
||||
isMatched: false,
|
||||
...card
|
||||
};
|
||||
}
|
||||
|
||||
function renderCards() {
|
||||
return cardsState.map((el, index) => (
|
||||
<Card
|
||||
{...el}
|
||||
key={el.id}
|
||||
title={index + 1 + ''}
|
||||
onClick={() => cardClickHandler(el.id)}
|
||||
>
|
||||
</Card>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls.CardsField}>
|
||||
{renderCards()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardsField;
|
||||
@ -1,51 +0,0 @@
|
||||
.Field {
|
||||
background: #283593;
|
||||
position: relative;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 10px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
". . ."
|
||||
". . ."
|
||||
". . ."
|
||||
". . .";
|
||||
}
|
||||
|
||||
.FieldFinish {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: #283593;
|
||||
position: relative;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
}
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
|
||||
|
||||
span {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.FinishBlock {
|
||||
color: #ffffff;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Pacifico', sans-serif;
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
import BottomPanel from '../BottomPanel/BottomPanel';
|
||||
import Alert from '../Alert/Alert';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Card, { CardProps } from '../Card/Card';
|
||||
import cls from './Field.module.scss';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const Field: React.FC = () => {
|
||||
|
||||
const img = {
|
||||
react: 'https://w7.pngwing.com/pngs/79/518/png-transparent-js-react-js-logo-react-react-native-logos-icon-thumbnail.png',
|
||||
js: 'https://www.freepnglogos.com/uploads/javascript-png/javascript-logo-transparent-logo-javascript-images-3.png',
|
||||
angular: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1200px-Angular_full_color_logo.svg.png',
|
||||
}
|
||||
|
||||
function matchHandler(prev: CardProps, card: CardProps) {
|
||||
setAlertState(true);
|
||||
setCards(prevState => prevState.map(el => {
|
||||
if (el.id === card.id || el.id === prev.id) return {...el, disabled: true, isMatched: true}
|
||||
else return {...el, disabled: true}
|
||||
}))
|
||||
setTimeout(enableCards, 1000)
|
||||
}
|
||||
|
||||
function notMatchHandler() {
|
||||
setAlertState(false);
|
||||
setMistakesCount(prev => prev + 1);
|
||||
disableCards();
|
||||
setTimeout(() => {
|
||||
setAlertState(null);
|
||||
setCards(prevState => prevState.map(el => {
|
||||
return {...el, toggled: false, disabled: false, isMatched: false};
|
||||
}))
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function reverseCardHandler(card: CardProps) {
|
||||
setCards(prevState => prevState.map(el => el.id === card.id ? {...el, toggled: !el.toggled, disabled: true} : el))
|
||||
|
||||
setCurrentCard(prev => {
|
||||
if (!prev) return card;
|
||||
else {
|
||||
if (prev.code === card.code) matchHandler(prev, card);
|
||||
else notMatchHandler();
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disableCards() {
|
||||
setCards(prevState => prevState.map(el => ({...el, disabled: true})));
|
||||
}
|
||||
|
||||
function enableCards() {
|
||||
setAlertState(null);
|
||||
setCards(prevState => prevState.map(el => ({...el, disabled: false})));
|
||||
}
|
||||
|
||||
function showCards() {
|
||||
setCards(prevState => prevState.map(el => ({...el, toggled: true})));
|
||||
}
|
||||
|
||||
function hideCards() {
|
||||
setCards(prevState => prevState.map(el => ({...el, toggled: false})));
|
||||
}
|
||||
|
||||
function hideCard(id: string) {
|
||||
setCards(prevState => prevState.map((el) => (
|
||||
el.id === id ? {...el, toggled: false} : el
|
||||
)))
|
||||
}
|
||||
|
||||
function createCard (title: string, code: string, imageURL: string): CardProps {
|
||||
return {id: uuidv4(), title, code, imageURL, onReverse: reverseCardHandler, toggled: true, disabled: false, isMatched: false};
|
||||
}
|
||||
|
||||
const [currentCard, setCurrentCard] = useState<CardProps | null>(null)
|
||||
|
||||
const [alertState, setAlertState] = useState<boolean | null>(null);
|
||||
|
||||
const [cards, setCards] = useState<CardProps[]>(randomizeElementInArray([
|
||||
createCard('1', 'react', img.react),
|
||||
createCard('2', 'angular', img.angular),
|
||||
createCard('3', 'angular', img.angular),
|
||||
createCard('4', 'react', img.react),
|
||||
createCard('5', 'vue', img.js),
|
||||
createCard('6', 'angular', img.angular),
|
||||
createCard('7', 'react', img.react),
|
||||
createCard('8', 'vue', img.js),
|
||||
createCard('9', 'angular', img.angular),
|
||||
createCard('10', 'react', img.react),
|
||||
createCard('11', 'vue', img.js),
|
||||
createCard('12', 'vue', img.js),
|
||||
]));
|
||||
|
||||
const [isGameFinished, setGameFinish] = useState<boolean>(false);
|
||||
|
||||
const [isGameStarted, setGameStart] = useState<boolean>(false);
|
||||
|
||||
const [mistakesCount, setMistakesCount] = useState<number>(0);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const isFinished = cards.reduce((res, cur) => res = res && cur.isMatched, true);
|
||||
isFinished && setTimeout(() => setGameFinish(true), 1500);
|
||||
}, [cards]);
|
||||
|
||||
useEffect(() => {
|
||||
showCards();
|
||||
}, [])
|
||||
|
||||
function renderCards() {
|
||||
return cards.map((el, index) => <Card key={el.id} {...el} title={index + 1 + ''} disabled={isGameStarted ? el.disabled : true}></Card>);
|
||||
}
|
||||
|
||||
function renderAlert(state: boolean | null) {
|
||||
if (state === null) return;
|
||||
|
||||
if (state) return <Alert success={true}/>;
|
||||
|
||||
return <Alert success={false}/>
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
setGameStart(true);
|
||||
cards.forEach((card, index) => {
|
||||
setTimeout(() => hideCard(card.id), index*100)
|
||||
})
|
||||
}
|
||||
|
||||
function retryGame() {
|
||||
setGameFinish(false);
|
||||
setGameStart(false);
|
||||
setCards(prev => randomizeElementInArray(prev.map(el => ({...el, disabled: false, isMatched: false, toggled: true}))))
|
||||
}
|
||||
|
||||
function getWrapperClassList() {
|
||||
let classes = [cls.Field];
|
||||
|
||||
if (isGameFinished) classes = [cls.FieldFinish];
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={getWrapperClassList().join(' ')} style={{marginBottom: 20}}>
|
||||
{
|
||||
isGameFinished
|
||||
? (
|
||||
<div className={cls.FinishBlock}>
|
||||
<h1>GAME IS FINISHED !</h1>
|
||||
<p>Время: <span>10:10</span></p>
|
||||
<p>Количество ошибок: <span>{mistakesCount}</span></p>
|
||||
<p>Сложность: <span>Легко</span></p>
|
||||
<p>Размер поля: <span>4х4</span></p>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{renderCards()}
|
||||
{renderAlert(alertState)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{<BottomPanel start={startGame} isFinishedGame={isGameFinished} retry={retryGame}/>}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
function randomizeElementInArray (arr: any[]) {
|
||||
return arr.sort(() => {
|
||||
return Math.random() > 0.5 ? 1 : -1;
|
||||
})
|
||||
}
|
||||
|
||||
export default Field;
|
||||
6
src/components/FinishedField/FinishedField.module.scss
Normal file
6
src/components/FinishedField/FinishedField.module.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.FinishedField {
|
||||
color: #ffffff;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Pacifico', sans-serif;
|
||||
}
|
||||
24
src/components/FinishedField/FinishedField.tsx
Normal file
24
src/components/FinishedField/FinishedField.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useMatchGame } from '../../context/MatchGameContext';
|
||||
import cls from './FinishedField.module.scss';
|
||||
|
||||
type FinishedFieldProps = {
|
||||
mistakesCount?: number;
|
||||
gameTime?: string;
|
||||
}
|
||||
|
||||
const FinishedField: React.FC<FinishedFieldProps> = ({mistakesCount, gameTime}) => {
|
||||
// const {mistakesCount, gameTime} = useMatchGame();
|
||||
|
||||
return (
|
||||
<div className={cls.FinishedField}>
|
||||
<h1>GAME IS FINISHED !</h1>
|
||||
<p>Время: <span>{gameTime}</span></p>
|
||||
<p>Количество ошибок: <span>{mistakesCount}</span></p>
|
||||
<p>Сложность: <span>difficultyLevel</span></p>
|
||||
<p>Размер поля: <span>fieldSize</span></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinishedField;
|
||||
26
src/components/MatchGame/MatchGame.module.scss
Normal file
26
src/components/MatchGame/MatchGame.module.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.FieldFinish {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: #283593;
|
||||
position: relative;
|
||||
border: 2px solid #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
}
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
|
||||
|
||||
span {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/components/MatchGame/MatchGame.tsx
Normal file
70
src/components/MatchGame/MatchGame.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cardImages } from '../../constants';
|
||||
import { useMatchGame } from '../../context/MatchGameContext';
|
||||
import { randomizeElementInArray } from '../../utils';
|
||||
import BottomPanel from '../BottomPanel/BottomPanel';
|
||||
import CardsField from '../CardsField/CardsField';
|
||||
import FinishedField from '../FinishedField/FinishedField';
|
||||
import cls from './MatchGame.module.scss';
|
||||
|
||||
const MatchGame: React.FC = () => {
|
||||
// TODO replace to CardsField
|
||||
const [cards, setCards] = useState<any[]>(randomizeElementInArray([
|
||||
{code: 'react', imageURL: cardImages.react},
|
||||
{code: 'angular', imageURL: cardImages.angular},
|
||||
{code: 'js', imageURL: cardImages.js},
|
||||
{code: 'react', imageURL: cardImages.react},
|
||||
{code: 'angular', imageURL: cardImages.angular},
|
||||
{code: 'js', imageURL: cardImages.js},
|
||||
]));
|
||||
|
||||
const [isGameFinished, setGameFinish] = useState<boolean>(false);
|
||||
const [isGameStarted, setGameStart] = useState<boolean>(false);
|
||||
|
||||
const {gameTime, mistakesCount, resetGameProgress} = useMatchGame();
|
||||
|
||||
function startGame() {
|
||||
setGameStart(true);
|
||||
}
|
||||
|
||||
function retryGame() {
|
||||
setGameFinish(false);
|
||||
setGameStart(false);
|
||||
setCards(prev => randomizeElementInArray(prev.map(el => ({...el, disabled: false, isMatched: false, toggled: true}))))
|
||||
|
||||
resetGameProgress?.();
|
||||
}
|
||||
|
||||
function finishGame() {
|
||||
setGameFinish(true);
|
||||
}
|
||||
|
||||
// TODO replace to CardsField
|
||||
function shuffleCards() {
|
||||
setCards(randomizeElementInArray([...cards]));
|
||||
}
|
||||
|
||||
function getWrapperClassList() {
|
||||
let classes = [cls.Field];
|
||||
|
||||
if (isGameFinished) classes = [cls.FieldFinish];
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={getWrapperClassList().join(' ')} style={{marginBottom: 20}}>
|
||||
{
|
||||
isGameFinished
|
||||
? <FinishedField gameTime={gameTime} mistakesCount={mistakesCount}/>
|
||||
: <CardsField cards={cards} isGameStarted={isGameStarted} finishGameCallback={finishGame}/>
|
||||
}
|
||||
</div>
|
||||
<BottomPanel startGame={startGame} isFinishedGame={isGameFinished} retryGame={retryGame} shuffleCards={shuffleCards}/>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchGame;
|
||||
26
src/components/UI/Button/Button.module.scss
Normal file
26
src/components/UI/Button/Button.module.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.Button {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
height: 45px;
|
||||
border-radius: 45px;
|
||||
margin: 10px 20px;
|
||||
font-family: "Pacifico", sans-serif;
|
||||
font-size: 22px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
font-weight: 600;
|
||||
color: #524f4e;
|
||||
background: white;
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, .1);
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
background: #7986cb;
|
||||
box-shadow: 0 15px 20px rgba(121, 134, 203, 0.4);
|
||||
color: white;
|
||||
transform: translateY(-7px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
17
src/components/UI/Button/Button.tsx
Normal file
17
src/components/UI/Button/Button.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import cls from './Button.module.scss';
|
||||
|
||||
type ButtonProps = {
|
||||
className?: string,
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({children, className, onClick}) => {
|
||||
return (
|
||||
<button className={[cls.Button, className].join('')} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
6
src/components/UI/Timer/Timer.module.scss
Normal file
6
src/components/UI/Timer/Timer.module.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.Timer {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Pacifico", sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
38
src/components/UI/Timer/Timer.tsx
Normal file
38
src/components/UI/Timer/Timer.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useMatchGame } from '../../../context/MatchGameContext';
|
||||
import { getTimeString } from '../../../utils';
|
||||
import cls from './Timer.module.scss'
|
||||
|
||||
type TimerProps = {
|
||||
startTime: number;
|
||||
isStarted: boolean;
|
||||
isFinished: boolean;
|
||||
}
|
||||
|
||||
const Timer: React.FC<TimerProps> = ({startTime, isStarted, isFinished}) => {
|
||||
const [time, setTime] = useState(startTime);
|
||||
const [timeInterval, setTimeInterval] = useState<NodeJS.Timer | null>(null)
|
||||
const {changeGameTime} = useMatchGame();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStarted) return;
|
||||
|
||||
setTimeInterval(setInterval(() => setTime(prev => prev + 1000), 1000));
|
||||
}, [isStarted])
|
||||
|
||||
useEffect(() => {
|
||||
clearInterval(timeInterval as NodeJS.Timeout);
|
||||
}, [isFinished])
|
||||
|
||||
useEffect(() => {
|
||||
changeGameTime?.(time);
|
||||
}, [time])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className={cls.Timer}>{getTimeString(new Date(time))}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
5
src/constants.ts
Normal file
5
src/constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const cardImages = {
|
||||
react: 'https://w7.pngwing.com/pngs/79/518/png-transparent-js-react-js-logo-react-react-native-logos-icon-thumbnail.png',
|
||||
js: 'https://www.freepnglogos.com/uploads/javascript-png/javascript-logo-transparent-logo-javascript-images-3.png',
|
||||
angular: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1200px-Angular_full_color_logo.svg.png',
|
||||
}
|
||||
42
src/context/MatchGameContext.tsx
Normal file
42
src/context/MatchGameContext.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { getTimeString } from '../utils';
|
||||
|
||||
type MatchGameContextType = {
|
||||
mistakesCount?: number;
|
||||
addMistakesCount?: () => void;
|
||||
gameTime?: string;
|
||||
changeGameTime?: (time: number) => void;
|
||||
resetGameProgress?: () => void;
|
||||
}
|
||||
const MatchGameContext = React.createContext<MatchGameContextType>({});
|
||||
|
||||
export const useMatchGame = () => useContext(MatchGameContext);
|
||||
export const MatchGameProvider: React.FC = ({children}) => {
|
||||
const [mistakesCount, setMistakesCount] = useState(0)
|
||||
const [gameTime, setGameTime] = useState('');
|
||||
|
||||
const addMistakesCount = () => {
|
||||
return setMistakesCount(prev => prev + 1)
|
||||
};
|
||||
|
||||
const changeGameTime = (time: number) => {
|
||||
setGameTime(getTimeString(new Date(time)));
|
||||
}
|
||||
|
||||
const resetGameProgress = () => {
|
||||
setMistakesCount(0);
|
||||
setGameTime(getTimeString(new Date(0)));
|
||||
}
|
||||
|
||||
return (
|
||||
<MatchGameContext.Provider value={{
|
||||
mistakesCount,
|
||||
addMistakesCount,
|
||||
gameTime,
|
||||
changeGameTime,
|
||||
resetGameProgress
|
||||
}}>
|
||||
{children}
|
||||
</MatchGameContext.Provider>
|
||||
)
|
||||
}
|
||||
18
src/utils.ts
Normal file
18
src/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function randomizeElementInArray (arr: any[]) {
|
||||
return arr.sort(() => {
|
||||
return Math.random() > 0.5 ? 1 : -1;
|
||||
})
|
||||
}
|
||||
|
||||
export function getTimeString(time?: Date): string {
|
||||
if (!time) return '';
|
||||
|
||||
let minutes = time.getMinutes().toString();
|
||||
if (+minutes < 10) minutes = '0' + minutes;
|
||||
|
||||
let seconds = time.getSeconds().toString();
|
||||
if (+seconds < 10) seconds = '0' + seconds;
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
@ -19,13 +19,13 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"components/*": ["src/components/*"],
|
||||
"core/*": ["src/core/*"],
|
||||
"pages/*": ["src/pages/*"],
|
||||
"redux/*": ["src/redux/*"],
|
||||
"src/*": ["src/*"],
|
||||
}
|
||||
// "paths": {
|
||||
// "components/*": ["src/components/*"],
|
||||
// "core/*": ["src/core/*"],
|
||||
// "pages/*": ["src/pages/*"],
|
||||
// "redux/*": ["src/redux/*"],
|
||||
// "src/*": ["src/*"],
|
||||
// }
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user