├── src
├── styles.css
├── images
│ ├── Pickachu.png
│ ├── Pidgetto.png
│ ├── Squirtle.png
│ ├── pokeball.png
│ ├── Bulbasaur.png
│ ├── ButterFree.png
│ └── Charmander.png
├── index.js
├── card.scss
├── card.js
├── app.scss
└── App.js
├── README.md
├── .codesandbox
└── workspace.json
├── package.json
└── public
└── index.html
/src/styles.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/src/images/Pickachu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Pickachu.png
--------------------------------------------------------------------------------
/src/images/Pidgetto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Pidgetto.png
--------------------------------------------------------------------------------
/src/images/Squirtle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Squirtle.png
--------------------------------------------------------------------------------
/src/images/pokeball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/pokeball.png
--------------------------------------------------------------------------------
/src/images/Bulbasaur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Bulbasaur.png
--------------------------------------------------------------------------------
/src/images/ButterFree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/ButterFree.png
--------------------------------------------------------------------------------
/src/images/Charmander.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Charmander.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # memory-card-game
2 |
3 | Check out the detailed article on [creating a memory card game on Medium](https://medium.com/codex/building-a-card-memory-game-in-react-e6400b226b8f)
4 |
5 | Check the [codesandbox link for the demo](https://codesandbox.io/s/clever-smoke-77b6s?from-embed)
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import App from "./App";
5 |
6 | const rootElement = document.getElementById("root");
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | rootElement
12 | );
13 |
--------------------------------------------------------------------------------
/.codesandbox/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "responsive-preview": {
3 | "Mobile": [
4 | 320,
5 | 675
6 | ],
7 | "Tablet": [
8 | 1024,
9 | 765
10 | ],
11 | "Desktop": [
12 | 1400,
13 | 800
14 | ],
15 | "Desktop HD": [
16 | 1920,
17 | 1080
18 | ]
19 | }
20 | }
--------------------------------------------------------------------------------
/src/card.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 100%;
3 | height: 100%;
4 | border-radius: 4px;
5 | box-shadow: 2px 2px 4px 4px #DEDEDE;
6 | transition: 0.3s;
7 | transform-style: preserve-3d;
8 | position: relative;
9 | cursor: pointer;
10 |
11 | img {
12 | width: 95%;
13 | height: 95%;
14 | }
15 |
16 | .card-face {
17 | backface-visibility: hidden;
18 | position: absolute;
19 | width: 100%;
20 | height: 100%;
21 | &.card-back-face {
22 | transform: rotateY(180deg);
23 | }
24 | }
25 |
26 | &.is-flipped {
27 | transform: rotateY(180deg);
28 | }
29 |
30 | &.is-inactive {
31 | // visibility: hidden;
32 | opacity: 0;
33 | }
34 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "version": "1.0.0",
4 | "description": "React example starter project",
5 | "keywords": [
6 | "react",
7 | "starter"
8 | ],
9 | "main": "src/index.js",
10 | "dependencies": {
11 | "@material-ui/core": "4.11.3",
12 | "classnames": "2.3.0",
13 | "react": "17.0.2",
14 | "react-dom": "17.0.2",
15 | "react-scripts": "4.0.0"
16 | },
17 | "devDependencies": {
18 | "@babel/runtime": "7.13.8",
19 | "typescript": "4.1.3"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test --env=jsdom",
25 | "eject": "react-scripts eject"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ]
33 | }
--------------------------------------------------------------------------------
/src/card.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classnames from "classnames";
3 | import pokeball from "./images/pokeball.png";
4 | import "./card.scss";
5 |
6 | const Card = ({ onClick, card, index, isInactive, isFlipped, isDisabled }) => {
7 | const handleClick = () => {
8 | !isFlipped && !isDisabled && onClick(index);
9 | };
10 |
11 | return (
12 |
19 |
20 |

21 |
22 |
23 |

24 |
25 |
26 | );
27 | };
28 |
29 | export default Card;
30 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | .bold {
2 | font-weight: 600;
3 | text-transform: uppercase;
4 | }
5 | .App {
6 | position: absolute;
7 | width:100%;
8 | height: 100%;
9 |
10 | header {
11 | position: relative;
12 | width: 100%;
13 | text-align: center;
14 | margin-bottom: 8px;
15 |
16 | > div {
17 | font-size: 15px;
18 | width: 324px;
19 | text-align: center;
20 | margin: 0 auto;
21 | }
22 | }
23 |
24 | footer {
25 | width: 360px;
26 | position: relative;
27 | margin: 0 auto;
28 | padding: 10px 4px;
29 | margin-top: 10px;
30 |
31 | .score {
32 | justify-content: center;
33 | display: flex;
34 |
35 | div {
36 | padding: 8px
37 | }
38 | }
39 |
40 | .restart {
41 | display: flex;
42 | justify-content: center
43 | }
44 | }
45 |
46 | .container {
47 | border: 1px solid #DEDEDE;
48 | padding: 12px;
49 | box-shadow: 0 0 4px 4px #DEDEDE;
50 | display: grid;
51 | grid-template-columns: repeat(4, 1fr);
52 | grid-template-rows: repeat(3, 1fr);
53 | justify-items: center;
54 | align-items: stretch;
55 | gap: 1rem;
56 | margin: 0 auto;
57 | width: 360px;
58 | height: 300px;
59 | perspective: 100%;
60 | max-width: 720px;
61 | }
62 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import {
3 | Dialog,
4 | DialogActions,
5 | DialogContent,
6 | DialogContentText,
7 | Button,
8 | DialogTitle
9 | } from "@material-ui/core";
10 | import Card from "./card";
11 | import "./app.scss";
12 |
13 | const uniqueElementsArray = [
14 | {
15 | type: "Pikachu",
16 | image: require(`./images/Pickachu.png`)
17 | },
18 | {
19 | type: "ButterFree",
20 | image: require(`./images/ButterFree.png`)
21 | },
22 | {
23 | type: "Charmander",
24 | image: require(`./images/Charmander.png`)
25 | },
26 | {
27 | type: "Squirtle",
28 | image: require(`./images/Squirtle.png`)
29 | },
30 | {
31 | type: "Pidgetto",
32 | image: require(`./images/Pidgetto.png`)
33 | },
34 | {
35 | type: "Bulbasaur",
36 | image: require(`./images/Bulbasaur.png`)
37 | }
38 | ];
39 |
40 | function shuffleCards(array) {
41 | const length = array.length;
42 | for (let i = length; i > 0; i--) {
43 | const randomIndex = Math.floor(Math.random() * i);
44 | const currentIndex = i - 1;
45 | const temp = array[currentIndex];
46 | array[currentIndex] = array[randomIndex];
47 | array[randomIndex] = temp;
48 | }
49 | return array;
50 | }
51 | export default function App() {
52 | const [cards, setCards] = useState(
53 | shuffleCards.bind(null, uniqueElementsArray.concat(uniqueElementsArray))
54 | );
55 | const [openCards, setOpenCards] = useState([]);
56 | const [clearedCards, setClearedCards] = useState({});
57 | const [shouldDisableAllCards, setShouldDisableAllCards] = useState(false);
58 | const [moves, setMoves] = useState(0);
59 | const [showModal, setShowModal] = useState(false);
60 | const [bestScore, setBestScore] = useState(
61 | JSON.parse(localStorage.getItem("bestScore")) || Number.POSITIVE_INFINITY
62 | );
63 | const timeout = useRef(null);
64 |
65 | const disable = () => {
66 | setShouldDisableAllCards(true);
67 | };
68 | const enable = () => {
69 | setShouldDisableAllCards(false);
70 | };
71 |
72 | const checkCompletion = () => {
73 | if (Object.keys(clearedCards).length === uniqueElementsArray.length) {
74 | setShowModal(true);
75 | const highScore = Math.min(moves, bestScore);
76 | setBestScore(highScore);
77 | localStorage.setItem("bestScore", highScore);
78 | }
79 | };
80 | const evaluate = () => {
81 | const [first, second] = openCards;
82 | enable();
83 | if (cards[first].type === cards[second].type) {
84 | setClearedCards((prev) => ({ ...prev, [cards[first].type]: true }));
85 | setOpenCards([]);
86 | return;
87 | }
88 | // This is to flip the cards back after 500ms duration
89 | timeout.current = setTimeout(() => {
90 | setOpenCards([]);
91 | }, 500);
92 | };
93 | const handleCardClick = (index) => {
94 | if (openCards.length === 1) {
95 | setOpenCards((prev) => [...prev, index]);
96 | setMoves((moves) => moves + 1);
97 | disable();
98 | } else {
99 | clearTimeout(timeout.current);
100 | setOpenCards([index]);
101 | }
102 | };
103 |
104 | useEffect(() => {
105 | let timeout = null;
106 | if (openCards.length === 2) {
107 | timeout = setTimeout(evaluate, 300);
108 | }
109 | return () => {
110 | clearTimeout(timeout);
111 | };
112 | }, [openCards]);
113 |
114 | useEffect(() => {
115 | checkCompletion();
116 | }, [clearedCards]);
117 | const checkIsFlipped = (index) => {
118 | return openCards.includes(index);
119 | };
120 |
121 | const checkIsInactive = (card) => {
122 | return Boolean(clearedCards[card.type]);
123 | };
124 |
125 | const handleRestart = () => {
126 | setClearedCards({});
127 | setOpenCards([]);
128 | setShowModal(false);
129 | setMoves(0);
130 | setShouldDisableAllCards(false);
131 | // set a shuffled deck of cards
132 | setCards(shuffleCards(uniqueElementsArray.concat(uniqueElementsArray)));
133 | };
134 |
135 | return (
136 |
137 |
143 |
144 | {cards.map((card, index) => {
145 | return (
146 |
155 | );
156 | })}
157 |
158 |
175 |
197 |
198 | );
199 | }
200 |
--------------------------------------------------------------------------------