refactor
This commit is contained in:
parent
1421bc549e
commit
aff0ac7330
@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cls from './App.module.scss';
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className={cls.App}>
|
<div className={cls.App}>
|
||||||
<Field/>
|
<MatchGameProvider>
|
||||||
|
<MatchGame/>
|
||||||
|
</MatchGameProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,40 +7,5 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
border-radius: 10px;
|
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 React, { useEffect, useState } from 'react';
|
||||||
|
import Timer from '../UI/Timer/Timer';
|
||||||
import cls from './BottomPanel.module.scss';
|
import cls from './BottomPanel.module.scss';
|
||||||
|
|
||||||
type BottomPanelProps = {
|
type BottomPanelProps = {
|
||||||
start: () => void;
|
startGame: () => void;
|
||||||
retry?: () => void;
|
retryGame: () => void;
|
||||||
isFinishedGame?: boolean;
|
isFinishedGame?: boolean;
|
||||||
|
shuffleCards: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BottomPanel: React.FC<BottomPanelProps> = (props) => {
|
type PanelState = 'start' | 'retry';
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
|
||||||
const [timeInterval, setTimeInterval] = useState<NodeJS.Timeout>();
|
|
||||||
|
|
||||||
|
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>) {
|
function onStartBtnClickHandler(event: React.MouseEvent<HTMLButtonElement>) {
|
||||||
setStartTime(0);
|
setIsTimerStarted(true);
|
||||||
const interval = setInterval(() => {
|
props.startGame();
|
||||||
setStartTime(prev => prev !== null ? prev + 1000 : 0)
|
|
||||||
}, 1000);
|
|
||||||
setTimeInterval(interval);
|
|
||||||
props.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearInterval(timeInterval)
|
setIsTimerStopped(true)
|
||||||
|
setPanelState(props.isFinishedGame ? 'retry' : 'start')
|
||||||
|
|
||||||
}, [props.isFinishedGame])
|
}, [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() {
|
function onRetryBtnClickHandler() {
|
||||||
setStartTime(null);
|
setIsTimerStarted(false);
|
||||||
props.retry?.();
|
setIsTimerStopped(false);
|
||||||
|
setPanelState('start');
|
||||||
|
|
||||||
|
props.retryGame?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRetryButton() {
|
function renderRetryStatePanel() {
|
||||||
return (
|
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 (
|
return (
|
||||||
<div className={cls.BottomPanel}>
|
<div className={cls.BottomPanel}>
|
||||||
{ props.isFinishedGame ? renderRetryButton() : renderActiveStatePanel() }
|
{ renderPanelByState(panelState) }
|
||||||
</div>
|
</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;
|
export default BottomPanel;
|
||||||
|
|||||||
@ -1,32 +1,33 @@
|
|||||||
import React, {useMemo} from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import cls from './Card.module.scss'
|
import cls from './Card.module.scss'
|
||||||
|
|
||||||
export type CardProps = {
|
export type CardComponentProps = {
|
||||||
title: string;
|
title?: string;
|
||||||
code: string;
|
|
||||||
onReverse?: (a: any) => void;
|
|
||||||
imageURL: string;
|
imageURL: string;
|
||||||
id: string;
|
id: string;
|
||||||
toggled: boolean;
|
toggled: boolean;
|
||||||
disabled: boolean;
|
|
||||||
isMatched: 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(() => {
|
function cardClickHandler(id: string) {
|
||||||
return !props.disabled && !props.isMatched
|
if (props.disabled) return;
|
||||||
}, [props.disabled, props.isMatched]);
|
|
||||||
|
|
||||||
const classes = useMemo(() => {
|
props.onClick?.(id);
|
||||||
return props.toggled ? [cls.Card, cls.reverse] : [cls.Card];
|
}
|
||||||
}, [props.toggled])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cls.CardWrapper} style={{
|
<div
|
||||||
opacity: props.isMatched ? 0 : 1
|
className={cls.CardWrapper}
|
||||||
}}>
|
style={{opacity: props.isMatched ? 0 : 1}}
|
||||||
<div className={classes.join(' ')} onClick={() => canClick && props.onReverse?.(props)}>
|
onClick={() => cardClickHandler(props.id)}
|
||||||
|
>
|
||||||
|
<div className={classes.join(' ')}>
|
||||||
<div className={cls.frontside}>{props.title}</div>
|
<div className={cls.frontside}>{props.title}</div>
|
||||||
<div className={cls.backside}>
|
<div className={cls.backside}>
|
||||||
<img src={props.imageURL} alt={props.title}/>
|
<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,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"paths": {
|
// "paths": {
|
||||||
"components/*": ["src/components/*"],
|
// "components/*": ["src/components/*"],
|
||||||
"core/*": ["src/core/*"],
|
// "core/*": ["src/core/*"],
|
||||||
"pages/*": ["src/pages/*"],
|
// "pages/*": ["src/pages/*"],
|
||||||
"redux/*": ["src/redux/*"],
|
// "redux/*": ["src/redux/*"],
|
||||||
"src/*": ["src/*"],
|
// "src/*": ["src/*"],
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user