
diff --git a/src/components/CardsField/CardsField.module.scss b/src/components/CardsField/CardsField.module.scss
new file mode 100644
index 0000000..e6f5d04
--- /dev/null
+++ b/src/components/CardsField/CardsField.module.scss
@@ -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:
+ ". . ."
+ ". . ."
+ ". . ."
+ ". . .";
+}
diff --git a/src/components/CardsField/CardsField.tsx b/src/components/CardsField/CardsField.tsx
new file mode 100644
index 0000000..b7491f0
--- /dev/null
+++ b/src/components/CardsField/CardsField.tsx
@@ -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
= ({cards, isGameStarted, finishGameCallback}) => {
+ // TODO use useReducer
+ const [cardsState, setCardsState] = useState(cards.map(el => createCard(el)));
+ const [currentCard, setCurrentCard] = useState(null)
+ const [currentCardPare, setCurrentCardPare] = useState({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) => (
+ cardClickHandler(el.id)}
+ >
+
+ ));
+ }
+
+ return (
+
+ {renderCards()}
+
+ );
+};
+
+export default CardsField;
diff --git a/src/components/Field/Field.module.scss b/src/components/Field/Field.module.scss
deleted file mode 100644
index d076ec6..0000000
--- a/src/components/Field/Field.module.scss
+++ /dev/null
@@ -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;
-}
diff --git a/src/components/Field/Field.tsx b/src/components/Field/Field.tsx
deleted file mode 100644
index 83592fa..0000000
--- a/src/components/Field/Field.tsx
+++ /dev/null
@@ -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(null)
-
- const [alertState, setAlertState] = useState(null);
-
- const [cards, setCards] = useState(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(false);
-
- const [isGameStarted, setGameStart] = useState(false);
-
- const [mistakesCount, setMistakesCount] = useState(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) => );
- }
-
- function renderAlert(state: boolean | null) {
- if (state === null) return;
-
- if (state) return ;
-
- return
- }
-
- 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 (
- <>
-
- {
- isGameFinished
- ? (
-
-
GAME IS FINISHED !
-
Время: 10:10
-
Количество ошибок: {mistakesCount}
-
Сложность: Легко
-
Размер поля: 4х4
-
- )
- : (
- <>
- {renderCards()}
- {renderAlert(alertState)}
- >
- )
- }
-
- {}
- >
-
- );
-};
-
-function randomizeElementInArray (arr: any[]) {
- return arr.sort(() => {
- return Math.random() > 0.5 ? 1 : -1;
- })
-}
-
-export default Field;
diff --git a/src/components/FinishedField/FinishedField.module.scss b/src/components/FinishedField/FinishedField.module.scss
new file mode 100644
index 0000000..2d1a9c4
--- /dev/null
+++ b/src/components/FinishedField/FinishedField.module.scss
@@ -0,0 +1,6 @@
+.FinishedField {
+ color: #ffffff;
+ max-width: 320px;
+ margin: 0 auto;
+ font-family: 'Pacifico', sans-serif;
+}
diff --git a/src/components/FinishedField/FinishedField.tsx b/src/components/FinishedField/FinishedField.tsx
new file mode 100644
index 0000000..e819489
--- /dev/null
+++ b/src/components/FinishedField/FinishedField.tsx
@@ -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 = ({mistakesCount, gameTime}) => {
+ // const {mistakesCount, gameTime} = useMatchGame();
+
+ return (
+
+
GAME IS FINISHED !
+
Время: {gameTime}
+
Количество ошибок: {mistakesCount}
+
Сложность: difficultyLevel
+
Размер поля: fieldSize
+
+ );
+};
+
+export default FinishedField;
diff --git a/src/components/MatchGame/MatchGame.module.scss b/src/components/MatchGame/MatchGame.module.scss
new file mode 100644
index 0000000..4139e4e
--- /dev/null
+++ b/src/components/MatchGame/MatchGame.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/components/MatchGame/MatchGame.tsx b/src/components/MatchGame/MatchGame.tsx
new file mode 100644
index 0000000..1588ec4
--- /dev/null
+++ b/src/components/MatchGame/MatchGame.tsx
@@ -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(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(false);
+ const [isGameStarted, setGameStart] = useState(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 (
+ <>
+
+ {
+ isGameFinished
+ ?
+ :
+ }
+
+
+ >
+
+ );
+};
+
+export default MatchGame;
diff --git a/src/components/UI/Button/Button.module.scss b/src/components/UI/Button/Button.module.scss
new file mode 100644
index 0000000..9e06d60
--- /dev/null
+++ b/src/components/UI/Button/Button.module.scss
@@ -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;
+ }
+}
diff --git a/src/components/UI/Button/Button.tsx b/src/components/UI/Button/Button.tsx
new file mode 100644
index 0000000..0c24d96
--- /dev/null
+++ b/src/components/UI/Button/Button.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import cls from './Button.module.scss';
+
+type ButtonProps = {
+ className?: string,
+ onClick: (e: React.MouseEvent) => void
+}
+
+const Button: React.FC = ({children, className, onClick}) => {
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/src/components/UI/Timer/Timer.module.scss b/src/components/UI/Timer/Timer.module.scss
new file mode 100644
index 0000000..d0c4042
--- /dev/null
+++ b/src/components/UI/Timer/Timer.module.scss
@@ -0,0 +1,6 @@
+.Timer {
+ font-size: 40px;
+ font-weight: bold;
+ font-family: "Pacifico", sans-serif;
+ color: #ffffff;
+}
diff --git a/src/components/UI/Timer/Timer.tsx b/src/components/UI/Timer/Timer.tsx
new file mode 100644
index 0000000..fcf38b4
--- /dev/null
+++ b/src/components/UI/Timer/Timer.tsx
@@ -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 = ({startTime, isStarted, isFinished}) => {
+ const [time, setTime] = useState(startTime);
+ const [timeInterval, setTimeInterval] = useState(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 (
+
+
{getTimeString(new Date(time))}
+
+ );
+};
+
+export default Timer;
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..1c2df0d
--- /dev/null
+++ b/src/constants.ts
@@ -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',
+}
diff --git a/src/context/MatchGameContext.tsx b/src/context/MatchGameContext.tsx
new file mode 100644
index 0000000..d3e5546
--- /dev/null
+++ b/src/context/MatchGameContext.tsx
@@ -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({});
+
+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 (
+
+ {children}
+
+ )
+}
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..cb175de
--- /dev/null
+++ b/src/utils.ts
@@ -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}`
+}
+
diff --git a/tsconfig.json b/tsconfig.json
index 4abc702..14f9bcf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"