diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..addc2dd
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/plugin-proposal-private-property-in-object"]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 357c47e..1d717da 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,11 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
+ "node-sass": "^4.14.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
- "react-scripts": "4.0.1",
+ "react-scripts": "4.0.3",
+ "styled-components": "^5.1.1",
"web-vitals": "^0.2.4"
},
"scripts": {
diff --git a/public/apple.png b/public/apple.png
new file mode 100755
index 0000000..d24fc80
Binary files /dev/null and b/public/apple.png differ
diff --git a/public/avocado.png b/public/avocado.png
new file mode 100755
index 0000000..0991e07
Binary files /dev/null and b/public/avocado.png differ
diff --git a/public/banana.png b/public/banana.png
new file mode 100755
index 0000000..575fe41
Binary files /dev/null and b/public/banana.png differ
diff --git a/public/corn.png b/public/corn.png
new file mode 100755
index 0000000..5fdc6bd
Binary files /dev/null and b/public/corn.png differ
diff --git a/public/lemon.png b/public/lemon.png
new file mode 100755
index 0000000..c18a651
Binary files /dev/null and b/public/lemon.png differ
diff --git a/public/lettuce.png b/public/lettuce.png
new file mode 100755
index 0000000..b600f79
Binary files /dev/null and b/public/lettuce.png differ
diff --git a/public/onion.png b/public/onion.png
new file mode 100755
index 0000000..f5e5e70
Binary files /dev/null and b/public/onion.png differ
diff --git a/public/strawberry.png b/public/strawberry.png
new file mode 100755
index 0000000..e164b71
Binary files /dev/null and b/public/strawberry.png differ
diff --git a/src/App.js b/src/App.js
index 8c97871..80e8852 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,3 +1,225 @@
+// import styled from 'styled-components';
+/* eslint-disable no-eval */
+import React from 'react';
+import {
+ GAME_STATE_NEW,
+ GAME_STATE_RUNNING,
+ GAME_STATE_FINISHED,
+ CARD_STATE_IDLE,
+ CARD_STATE_ACTIVE,
+ CARD_STATE_FOUND,
+ FRUIT_APPLE,
+ FRUIT_AVOCADO,
+ FRUIT_BANANA,
+ FRUIT_CORN,
+ FRUIT_LEMON,
+ FRUIT_LETTUCE,
+ FRUIT_ONION,
+ FRUIT_STRAWBERRY,
+ TIME_CARD_ACTIVE
+} from './constants'
+import './App.scss';
+
export default function Memory() {
- return Votre Memory ici;
+ const [gameState, setGameState] = React.useState(GAME_STATE_NEW);
+
+ const [seconds, setSeconds] = React.useState(0);
+ const [minutes, setMinutes] = React.useState(0);
+
+ const [cardsShowed, setCardsShowed] = React.useState(0);
+ const [hitsNumber, setHitsNumber] = React.useState(0);
+
+ /* eslint-disable no-unused-vars */
+ const [caseA1State, setCaseA1State] = React.useState({ name: 'A1', state: CARD_STATE_IDLE, value: FRUIT_LETTUCE });
+ const [caseA2State, setCaseA2State] = React.useState({ name: 'A2', state: CARD_STATE_IDLE, value: FRUIT_ONION });
+ const [caseA3State, setCaseA3State] = React.useState({ name: 'A3', state: CARD_STATE_IDLE, value: FRUIT_AVOCADO });
+ const [caseA4State, setCaseA4State] = React.useState({ name: 'A4', state: CARD_STATE_IDLE, value: FRUIT_LEMON });
+
+ const [caseB1State, setCaseB1State] = React.useState({ name: 'B1', state: CARD_STATE_IDLE, value: FRUIT_LETTUCE });
+ const [caseB2State, setCaseB2State] = React.useState({ name: 'B2', state: CARD_STATE_IDLE, value: FRUIT_ONION });
+ const [caseB3State, setCaseB3State] = React.useState({ name: 'B3', state: CARD_STATE_IDLE, value: FRUIT_APPLE });
+ const [caseB4State, setCaseB4State] = React.useState({ name: 'B4', state: CARD_STATE_IDLE, value: FRUIT_LEMON });
+
+ const [caseC1State, setCaseC1State] = React.useState({ name: 'C1', state: CARD_STATE_IDLE, value: FRUIT_STRAWBERRY });
+ const [caseC2State, setCaseC2State] = React.useState({ name: 'C2', state: CARD_STATE_IDLE, value: FRUIT_BANANA });
+ const [caseC3State, setCaseC3State] = React.useState({ name: 'C3', state: CARD_STATE_IDLE, value: FRUIT_STRAWBERRY });
+ const [caseC4State, setCaseC4State] = React.useState({ name: 'C4', state: CARD_STATE_IDLE, value: FRUIT_CORN });
+
+ const [caseD1State, setCaseD1State] = React.useState({ name: 'D1', state: CARD_STATE_IDLE, value: FRUIT_CORN });
+ const [caseD2State, setCaseD2State] = React.useState({ name: 'D2', state: CARD_STATE_IDLE, value: FRUIT_BANANA });
+ const [caseD3State, setCaseD3State] = React.useState({ name: 'D3', state: CARD_STATE_IDLE, value: FRUIT_APPLE });
+ const [caseD4State, setCaseD4State] = React.useState({ name: 'D4', state: CARD_STATE_IDLE, value: FRUIT_AVOCADO });
+ /* eslint-enable no-unused-vars */
+
+ const allCases = [
+ caseA1State,
+ caseA2State,
+ caseA3State,
+ caseA4State,
+ caseB1State,
+ caseB2State,
+ caseB3State,
+ caseB4State,
+ caseC1State,
+ caseC2State,
+ caseC3State,
+ caseC4State,
+ caseD1State,
+ caseD2State,
+ caseD3State,
+ caseD4State
+ ];
+
+ // increment second each second
+ React.useEffect(() => {
+ if (gameState === GAME_STATE_RUNNING) {
+ const interval = setInterval(() => {
+ setSeconds(seconds => {
+ if (seconds === 59) {
+ return 0;
+ } else {
+ return seconds + 1;
+ }
+ });
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }
+ }, [gameState]);
+
+ // increment minutes each minute
+ React.useEffect(() => {
+ if (gameState === GAME_STATE_RUNNING) {
+ const minutesInterval = setInterval(() => {
+ setMinutes(minutes => minutes + 1);
+ }, 60000);
+ return () => clearInterval(minutesInterval);
+ }
+ }, [gameState]);
+
+ // set game status finished if all pairs found
+ React.useEffect(() => {
+ let gameFinished = true;
+ allCases.forEach(oneCase => {
+ if (oneCase.state !== CARD_STATE_FOUND) {
+ gameFinished = false;
+ }
+ });
+
+ gameFinished && setGameState(GAME_STATE_FINISHED);
+ }, allCases); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const reset = () => {
+ setSeconds(0);
+ setMinutes(0);
+ setHitsNumber(0);
+ setGameState(GAME_STATE_NEW);
+
+ // map all cases to set card state idle
+ // ex. setCaseA1State({...caseA1State, state: CARD_STATE_IDLE});
+ allCases.forEach(oneCase => {
+ eval(`setCase${oneCase.name}State`)({ ...eval(`case${oneCase.name}State`), state: CARD_STATE_IDLE})
+ });
+ }
+
+
+ const memoryTableStatusClassname = (status) => {
+ switch (status.state) {
+ case CARD_STATE_IDLE:
+ return 'memoryTableItemIdle';
+ case CARD_STATE_ACTIVE:
+ return 'memoryTableItemActive';
+ case CARD_STATE_FOUND:
+ return 'memoryTableItemFound';
+ default:
+ return 'memoryTableItemIdle';
+ }
+ }
+
+ const displayCard = (setCaseState, caseState) => {
+ if (gameState === GAME_STATE_NEW) {
+ setGameState(GAME_STATE_RUNNING);
+ }
+
+ setCaseState({...caseState, state: CARD_STATE_ACTIVE});
+
+ if (cardsShowed + 1 < 2) {
+ setCardsShowed(cardsShowed + 1);
+
+ } else {
+ setCardsShowed(0);
+ setHitsNumber(hitsNumber + 1);
+
+ let pairFound = false
+
+ // map all cases to set card state found if pairs
+ allCases.forEach(oneCase => {
+ const oneCaseState = eval(`case${oneCase.name}State`);
+ const setOneCaseState = eval(`setCase${oneCase.name}State`);
+
+ // if other card active and not the actual one
+ if (oneCaseState.state === CARD_STATE_ACTIVE && oneCaseState.name !== caseState.name) {
+ // if values match, set status to found for the two cards
+ if (oneCaseState.value === caseState.value) {
+ setTimeout(() => { setOneCaseState({...oneCaseState, state: CARD_STATE_FOUND}); }, TIME_CARD_ACTIVE);
+ setTimeout(() => { setCaseState({...caseState, state: CARD_STATE_FOUND}); }, TIME_CARD_ACTIVE);
+ pairFound = true;
+ } else {
+ // else, set card to idle
+ setTimeout(() => { setOneCaseState({...oneCaseState, state: CARD_STATE_IDLE}); }, TIME_CARD_ACTIVE);
+ }
+ }
+ });
+
+ // if no pair found, set actual card to idle
+ !pairFound && setTimeout(() => { setCaseState({...caseState, state: CARD_STATE_IDLE}) }, TIME_CARD_ACTIVE);
+ }
+ }
+
+ return (
+
+
+
+
+
+ Temps
+
+
+ {minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
+
+
+
+
+ Nombre de coups
+
+
+ {hitsNumber}
+
+
+
+
+ Cliquez sur une carte pour commencer
+
+
+
+ {allCases.map(oneCase => {
+ const oneCaseState = eval(`case${oneCase.name}State`);
+ const setOneCaseState = eval(`setCase${oneCase.name}State`);
+
+ // bind all cases
+ return (
+
+ )
+ })}
+
+
+
+ );
}
diff --git a/src/App.scss b/src/App.scss
new file mode 100644
index 0000000..4efdc8d
--- /dev/null
+++ b/src/App.scss
@@ -0,0 +1,94 @@
+$border-radius: 3px;
+
+.header {
+ position: absolute;
+ top: 0;
+}
+
+.header {
+ &Infos {
+ display: flex;
+ width: 98vw;
+ justify-content: space-between;
+
+ &Time {
+ &Title {
+ color: grey;
+ font-size: 1.25rem;
+ }
+
+ &Clock {
+ font-size: 1.75rem;
+ font-weight: bold;
+ }
+ }
+
+ &Hits {
+ &Title {
+ color: grey;
+ font-size: 1.25rem;
+ }
+
+ &Number {
+ font-size: 1.75rem;
+ font-weight: bold;
+ }
+ }
+ }
+
+ &Instructions {
+ text-align: center;
+ }
+}
+
+.memoryTable {
+ margin-top: 15vh;
+ margin-left: auto;
+ margin-right: auto;
+ display: grid;
+ grid-template-columns: 10rem 10rem 10rem 10rem;
+ grid-template-rows: 10rem 10rem 10rem 10rem;
+ gap: 2%;
+ &Item {
+ cursor: pointer;
+ padding: 10%;
+ background-color: white;
+ border-radius: $border-radius;
+ border: none;
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+
+ &Idle {
+ img {
+ display: none;
+ }
+ }
+
+ &Active {
+ img {
+ display: block;
+ }
+ }
+
+ &Found {
+ opacity: 0.6;
+ }
+
+ img {
+ width: 8.5rem;
+ height: 8.5rem;
+ }
+ }
+}
+
+.resetButton {
+ margin-top: 6vh;
+ background-color: rgb(9, 211, 172);
+ border: none;
+ padding: 7.5px;
+ width: 7.5rem;
+ font-size: 1rem;
+ border-radius: $border-radius;
+}
\ No newline at end of file
diff --git a/src/App.test.js b/src/App.test.js
new file mode 100644
index 0000000..42f9d7e
--- /dev/null
+++ b/src/App.test.js
@@ -0,0 +1,126 @@
+import React from 'react';
+import { fireEvent, waitFor } from '@testing-library/dom';
+import App from './App';
+import {render} from '@testing-library/react'
+import '@testing-library/jest-dom'
+
+it('render layout with all cards', async () => {
+ const { baseElement } = render()
+
+ const header = baseElement.querySelector('.header');
+ const memoryTable = baseElement.querySelector('.memoryTable');
+ const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem');
+
+ await expect(header).toBeTruthy();
+ await expect(memoryTable).toBeTruthy();
+ await expect(memoryTableItems).toHaveLength(16);
+
+ await expect(baseElement).toMatchSnapshot();
+});
+
+it('display active cards then display found cards when we find a pair', async () => {
+ const { baseElement } = render()
+
+ const memoryTable = baseElement.querySelector('.memoryTable');
+ const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem');
+
+ await expect(memoryTable).toBeTruthy();
+ await expect(memoryTableItems).toBeTruthy();
+
+ fireEvent.click(memoryTableItems[0]);
+ fireEvent.click(memoryTableItems[4]);
+
+ const memoryTableItemsActive = baseElement.querySelectorAll('.memoryTableItemActive');
+ expect(memoryTableItemsActive).toHaveLength(2);
+
+ await waitFor(() => {
+ const memoryTableItemsFound = baseElement.querySelectorAll('.memoryTableItemFound');
+ expect(memoryTableItemsFound).toHaveLength(2);
+ });
+
+
+ await expect(baseElement).toMatchSnapshot();
+});
+
+it('display active cards then display idle cards when we dont find a pair', async () => {
+ const { baseElement } = render()
+
+ const memoryTable = baseElement.querySelector('.memoryTable');
+ const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem');
+
+ await expect(memoryTable).toBeTruthy();
+ await expect(memoryTableItems).toBeTruthy();
+
+ fireEvent.click(memoryTableItems[0]);
+ fireEvent.click(memoryTableItems[1]);
+
+ const memoryTableItemsActive = baseElement.querySelectorAll('.memoryTableItemActive');
+ expect(memoryTableItemsActive).toHaveLength(2);
+
+ await waitFor(() => {
+ const memoryTableItemsFound = baseElement.querySelectorAll('.memoryTableItemIdle');
+ expect(memoryTableItemsFound).toHaveLength(16);
+ });
+
+
+ await expect(baseElement).toMatchSnapshot();
+});
+
+it('start clock when playing and count hits', async () => {
+ const { baseElement } = render()
+
+ const memoryTable = baseElement.querySelector('.memoryTable');
+ const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem');
+ const headerInfosTimeClock = baseElement.querySelector('.headerInfosTimeClock');
+ const headerInfosHitsNumber = baseElement.querySelector('.headerInfosHitsNumber');
+
+ await expect(memoryTable).toBeTruthy();
+ await expect(memoryTableItems).toBeTruthy();
+
+ expect(headerInfosTimeClock.innerHTML).toEqual('00:00');
+ expect(headerInfosHitsNumber.innerHTML).toEqual('0');
+
+ fireEvent.click(memoryTableItems[0]);
+ fireEvent.click(memoryTableItems[1]);
+
+ await waitFor(() => {
+ expect(headerInfosTimeClock.innerHTML).not.toEqual('00:00');
+ expect(headerInfosHitsNumber.innerHTML).toEqual('1');
+ });
+
+
+ await expect(baseElement).toMatchSnapshot();
+});
+
+it('can reset game', async () => {
+ const { baseElement } = render()
+
+ const memoryTable = baseElement.querySelector('.memoryTable');
+ const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem');
+ const headerInfosTimeClock = baseElement.querySelector('.headerInfosTimeClock');
+ const headerInfosHitsNumber = baseElement.querySelector('.headerInfosHitsNumber');
+ const resetButton = baseElement.querySelector('.resetButton');
+
+ await expect(memoryTable).toBeTruthy();
+ await expect(memoryTableItems).toBeTruthy();
+
+ expect(headerInfosTimeClock.innerHTML).toEqual('00:00');
+ expect(headerInfosHitsNumber.innerHTML).toEqual('0');
+
+ fireEvent.click(memoryTableItems[0]);
+ fireEvent.click(memoryTableItems[4]);
+
+ await waitFor(() => {
+ expect(headerInfosTimeClock.innerHTML).not.toEqual('00:00');
+ expect(headerInfosHitsNumber.innerHTML).toEqual('1');
+ });
+
+ fireEvent.click(resetButton);
+
+ await waitFor(() => {
+ expect(headerInfosTimeClock.innerHTML).toEqual('00:00');
+ expect(headerInfosHitsNumber.innerHTML).toEqual('0');
+ });
+
+ await expect(baseElement).toMatchSnapshot();
+});
\ No newline at end of file
diff --git a/src/__snapshots__/App.test.js.snap b/src/__snapshots__/App.test.js.snap
new file mode 100644
index 0000000..25d8841
--- /dev/null
+++ b/src/__snapshots__/App.test.js.snap
@@ -0,0 +1,948 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`can reset game 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`display active cards then display found cards when we find a pair 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`display active cards then display idle cards when we dont find a pair 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`render layout with all cards 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`start clock when playing and count hits 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..5fd7afb
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,23 @@
+// game states const
+export const GAME_STATE_NEW = "new";
+export const GAME_STATE_RUNNING = "running";
+export const GAME_STATE_FINISHED = "finished";
+
+// card states const
+export const CARD_STATE_IDLE = "idle";
+export const CARD_STATE_ACTIVE = "active";
+export const CARD_STATE_FOUND = "found";
+
+// fruit values
+export const FRUIT_APPLE = 'apple'
+export const FRUIT_AVOCADO = 'avocado'
+export const FRUIT_BANANA = 'banana'
+export const FRUIT_CORN = 'corn'
+export const FRUIT_LEMON = 'lemon'
+export const FRUIT_LETTUCE = 'lettuce'
+export const FRUIT_ONION = 'onion'
+export const FRUIT_STRAWBERRY = 'strawberry'
+
+export const TIME_CARD_ACTIVE = 1000;
+
+