├── .gitignore ├── README.md ├── index.html ├── package.json ├── src ├── assets │ └── app.scss ├── components │ ├── App.js │ ├── Card.js │ └── Game.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-memory-game 2 | 3 | This is a memory game app built with React. It was built with the purpose of helping teach React by example. You can follow the step by step process by walking through the commits starting from the beginning. 4 | 5 | Note that there are many helpful in-line comments that provide background and explanation. 6 | 7 | ## Setup 8 | 9 | - Install dependencies: `npm install` 10 | 11 | ## Usage 12 | 13 | - `npm run serve` will run the app at `localhost:4000`. You will notice that hot loading is enabled so changes you make will magically appear without refreshing. (this works for most things but certain state changes you may have to actually refresh). If you install more dependencies you will need to restart the server. 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Memory Game 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-memory-game", 3 | "version": "1.0.0", 4 | "description": "Learn React by building a memory match game", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/webpack", 8 | "serve": "./node_modules/.bin/webpack-dev-server --no-info --port 4000" 9 | }, 10 | "author": "Jonathan Lehman ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "classnames": "^2.1.3", 14 | "react": "^0.13.3" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "example", 19 | "javascript", 20 | "tutorial" 21 | ], 22 | "devDependencies": { 23 | "autoprefixer-core": "^5.2.1", 24 | "babel-core": "^5.8.22", 25 | "babel-loader": "^5.3.2", 26 | "css-loader": "^0.16.0", 27 | "node-sass": "^3.2.0", 28 | "postcss-loader": "^0.5.1", 29 | "react-hot-loader": "^1.2.8", 30 | "sass-loader": "^2.0.0", 31 | "style-loader": "^0.12.3", 32 | "webpack": "^1.11.0", 33 | "webpack-dev-server": "^1.10.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/app.scss: -------------------------------------------------------------------------------- 1 | .Game { 2 | } 3 | 4 | .Card { 5 | float: left; 6 | width: 4rem; 7 | height: 8rem; 8 | padding: 1rem; 9 | margin: 1rem; 10 | border: 1px solid black; 11 | text-align: center; 12 | background-color: black; 13 | 14 | // .Card--flipped 15 | &--flipped { 16 | background-color: gray; 17 | } 18 | 19 | // .Card--matched 20 | &--matched { 21 | background-color: green; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Game from 'components/Game'; 3 | 4 | export default class App extends Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | export default class Card extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.handleClick = this.handleClick.bind(this); 8 | } 9 | 10 | handleClick(e) { 11 | if (!this.props.flipped) { 12 | this.props.checkMatch(this.props.value, this.props.id); 13 | } 14 | } 15 | 16 | render() { 17 | var classes = classnames( 18 | 'Card', 19 | {'Card--flipped': this.props.flipped}, 20 | {'Card--matched': this.props.matched} 21 | ); 22 | var cardValue = this.props.flipped ? this.props.value : ''; 23 | return ( 24 |
25 | {cardValue} 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Game.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Card from 'components/Card'; 3 | 4 | function initialCards() { 5 | return [ 6 | {value: 2, matched: false, flipped: false}, 7 | {value: 4, matched: false, flipped: false}, 8 | {value: 1, matched: false, flipped: false}, 9 | {value: 1, matched: false, flipped: false}, 10 | {value: 3, matched: false, flipped: false}, 11 | {value: 4, matched: false, flipped: false}, 12 | {value: 2, matched: false, flipped: false}, 13 | {value: 3, matched: false, flipped: false} 14 | ]; 15 | } 16 | 17 | export default class Game extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.renderCards = this.renderCards.bind(this); 21 | this.checkMatch = this.checkMatch.bind(this); 22 | this.reset = this.reset.bind(this); 23 | 24 | this.state = { 25 | cards: initialCards(), 26 | lastCard: null, 27 | locked: false, 28 | matches: 0 29 | }; 30 | } 31 | 32 | checkMatch(value, id) { 33 | if (this.state.locked) { 34 | return; 35 | } 36 | 37 | var cards = this.state.cards; 38 | cards[id].flipped = true; 39 | this.setState({cards, locked: true}); 40 | if (this.state.lastCard) { 41 | if (value === this.state.lastCard.value) { 42 | var matches = this.state.matches; 43 | cards[id].matched = true; 44 | cards[this.state.lastCard.id].matched = true; 45 | this.setState({cards, lastCard: null, locked: false, matches: matches + 1}); 46 | } else { 47 | setTimeout(() => { 48 | cards[id].flipped = false; 49 | cards[this.state.lastCard.id].flipped = false; 50 | this.setState({cards, lastCard: null, locked: false}); 51 | }, 1000); 52 | } 53 | } else { 54 | this.setState({ 55 | lastCard: {id, value}, 56 | locked: false 57 | }); 58 | } 59 | } 60 | 61 | renderCards(cards) { 62 | return cards.map((card, index) => { 63 | return ( 64 | 71 | ); 72 | }); 73 | } 74 | 75 | reset() { 76 | this.setState({ 77 | cards: initialCards(), 78 | lastCard: null, 79 | locked: false, 80 | matches: 0 81 | }); 82 | } 83 | 84 | render() { 85 | var btnText = 'Reset'; 86 | if (this.state.matches === this.state.cards.length / 2) { 87 | btnText = 'You Win! Play Again?'; 88 | } 89 | return ( 90 |
91 |
92 | 93 |
94 | {this.renderCards(this.state.cards)} 95 |
96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from 'components/App'; 3 | 4 | require('assets/app.scss'); 5 | 6 | React.render(, document.body); 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var autoprefixer = require('autoprefixer-core'); 4 | 5 | module.exports = { 6 | cache: true, 7 | debug: true, 8 | devtool: 'source-map', 9 | context: path.join(__dirname, 'src'), 10 | entry: [ 11 | 'webpack-dev-server/client?http://localhost:4000', 12 | 'webpack/hot/only-dev-server', 13 | './index' 14 | ], 15 | output: { 16 | path: __dirname + '/dist', 17 | filename: 'memory.js' 18 | }, 19 | resolve: { 20 | extensions: ['', '.js', '.scss'], 21 | modulesDirectories: ['node_modules', 'src'], 22 | fallback: __dirname 23 | }, 24 | module: { 25 | loaders: [ 26 | { test: /\.js$/, loaders: ['react-hot', 'babel'], exclude: /node_modules/ }, 27 | { 28 | test: /\.scss$/, 29 | loaders: ['style', 'css', 'postcss', 'sass'], 30 | exclude: /node_modules/ 31 | } 32 | ] 33 | }, 34 | plugins: [ 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NoErrorsPlugin() 37 | ], 38 | postcss: [autoprefixer] 39 | }; 40 | --------------------------------------------------------------------------------