create GameField and BottomPanel
This commit is contained in:
parent
7caf1ae57a
commit
1421bc549e
10
package.json
10
package.json
@ -4,10 +4,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^16.11.45",
|
"@types/node": "^16.11.45",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^17.0.47",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"sass": "^1.53.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env BROWSER=none PORT=8080 react-scripts start",
|
"start": "cross-env BROWSER=none PORT=8080 react-scripts start",
|
||||||
@ -35,8 +38,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"sass": "^1.53.0",
|
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/App.module.scss
Normal file
7
src/App.module.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.App {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import cls from './App.module.scss';
|
||||||
|
import Field from './components/Field/Field';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className={cls.App}>
|
||||||
<h1>React start</h1>
|
<Field/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/components/Alert/Alert.tsx
Normal file
22
src/components/Alert/Alert.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import cls from './Alerts.module.scss';
|
||||||
|
|
||||||
|
export type AlertProps = {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Alert: React.FC<AlertProps> = (props) => {
|
||||||
|
const classes = useMemo(() => {
|
||||||
|
return props.success ? [cls.AlertMessage, cls.AlertMessageSuccess] : [cls.AlertMessage, cls.AlertMessageWrong];
|
||||||
|
}, [props.success])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cls.AlertWrapper}>
|
||||||
|
<div className={classes.join(' ')}>
|
||||||
|
{props.success ? <span>✔</span> : <span>✘</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Alert;
|
||||||
42
src/components/Alert/Alerts.module.scss
Normal file
42
src/components/Alert/Alerts.module.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.AlertWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
//background: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
//transition: 500ms;
|
||||||
|
|
||||||
|
.AlertMessage {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
height: 100%;
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-name: slidein;
|
||||||
|
opacity: 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.AlertMessageSuccess {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.AlertMessageWrong {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slidein {
|
||||||
|
from {
|
||||||
|
font-size: 40px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
font-size: 300px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/components/BottomPanel/BottomPanel.module.scss
Normal file
46
src/components/BottomPanel/BottomPanel.module.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.BottomPanel {
|
||||||
|
width: 500px;
|
||||||
|
height: 130px;
|
||||||
|
background: #283593;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
64
src/components/BottomPanel/BottomPanel.tsx
Normal file
64
src/components/BottomPanel/BottomPanel.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import cls from './BottomPanel.module.scss';
|
||||||
|
|
||||||
|
type BottomPanelProps = {
|
||||||
|
start: () => void;
|
||||||
|
retry?: () => void;
|
||||||
|
isFinishedGame?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomPanel: React.FC<BottomPanelProps> = (props) => {
|
||||||
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
|
const [timeInterval, setTimeInterval] = useState<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
|
||||||
|
function onStartBtnClickHandler(event: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
setStartTime(0);
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setStartTime(prev => prev !== null ? prev + 1000 : 0)
|
||||||
|
}, 1000);
|
||||||
|
setTimeInterval(interval);
|
||||||
|
props.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearInterval(timeInterval)
|
||||||
|
}, [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?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRetryButton() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className={cls.StartButton} onClick={onRetryBtnClickHandler}>Retry</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cls.BottomPanel}>
|
||||||
|
{ props.isFinishedGame ? renderRetryButton() : renderActiveStatePanel() }
|
||||||
|
</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;
|
||||||
59
src/components/Card/Card.module.scss
Normal file
59
src/components/Card/Card.module.scss
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
||||||
|
|
||||||
|
.CardWrapper{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: opacity 1300ms ease-in;
|
||||||
|
|
||||||
|
.Card {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-family: 'Pacifico', sans-serif;
|
||||||
|
font-size: 48px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
perspective: 1000px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.frontside, .backside {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: 1s;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
.frontside {
|
||||||
|
background: #7986cb;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backside {
|
||||||
|
//background: #aee571;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
font-size: 40px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse .frontside {transform: rotateY(180deg);}
|
||||||
|
&.reverse .backside {transform: rotateY(360deg);}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
39
src/components/Card/Card.tsx
Normal file
39
src/components/Card/Card.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import cls from './Card.module.scss'
|
||||||
|
|
||||||
|
export type CardProps = {
|
||||||
|
title: string;
|
||||||
|
code: string;
|
||||||
|
onReverse?: (a: any) => void;
|
||||||
|
imageURL: string;
|
||||||
|
id: string;
|
||||||
|
toggled: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
isMatched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card: React.FC<CardProps> = (props) => {
|
||||||
|
|
||||||
|
const canClick = useMemo(() => {
|
||||||
|
return !props.disabled && !props.isMatched
|
||||||
|
}, [props.disabled, props.isMatched]);
|
||||||
|
|
||||||
|
const classes = useMemo(() => {
|
||||||
|
return props.toggled ? [cls.Card, cls.reverse] : [cls.Card];
|
||||||
|
}, [props.toggled])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cls.CardWrapper} style={{
|
||||||
|
opacity: props.isMatched ? 0 : 1
|
||||||
|
}}>
|
||||||
|
<div className={classes.join(' ')} onClick={() => canClick && props.onReverse?.(props)}>
|
||||||
|
<div className={cls.frontside}>{props.title}</div>
|
||||||
|
<div className={cls.backside}>
|
||||||
|
<img src={props.imageURL} alt={props.title}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
||||||
51
src/components/Field/Field.module.scss
Normal file
51
src/components/Field/Field.module.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
181
src/components/Field/Field.tsx
Normal file
181
src/components/Field/Field.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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;
|
||||||
@ -2,4 +2,5 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: #7986cb;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,14 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"components/*": ["src/components/*"],
|
||||||
|
"core/*": ["src/core/*"],
|
||||||
|
"pages/*": ["src/pages/*"],
|
||||||
|
"redux/*": ["src/redux/*"],
|
||||||
|
"src/*": ["src/*"],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user