diff --git a/package.json b/package.json index 0a80b6a..b2a720c 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,14 @@ "name": "react", "version": "1.0.0", "description": "React example starter project", - "keywords": ["react", "starter"], + "keywords": [ + "react", + "starter" + ], "main": "src/index.js", "dependencies": { + "lodash.shuffle": "4.2.0", + "prop-types": "15.7.2", "react": "17.0.2", "react-dom": "17.0.2", "react-scripts": "4.0.0" @@ -19,5 +24,10 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"] -} + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 42ae2d2..587c69a 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,19 @@ - - - - - - - - - - React App - + React App + - - -
- - - - \ No newline at end of file + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..d1e0a81 --- /dev/null +++ b/src/App.css @@ -0,0 +1,7 @@ +.memory { + display: flex; + flex-wrap: wrap; + width: 350px; + margin: auto; + user-select: none; +} diff --git a/src/App.js b/src/App.js index dbc5c59..abe13ce 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,99 @@ -import "./styles.css"; - -export default function App() { - return ( -
-

Hello CodeSandbox

-

Start editing to see some magic happen!

-
- ); +import React, { Component } from "react"; +import shuffle from "lodash.shuffle"; + +import "./App.css"; + +import Card from "./Card"; +import GuessCount from "./GuessCount"; +import HallOfFame, { FAKE_HOF } from "./HallOfFame"; + +const SIDE = 6; +const SYMBOLS = "πŸ˜€πŸŽ‰πŸ’–πŸŽ©πŸΆπŸ±πŸ¦„πŸ¬πŸŒπŸŒ›πŸŒžπŸ’«πŸŽπŸŒπŸ“πŸπŸŸπŸΏ"; +const VISUAL_PAUSE_MSECS = 750; + +class App extends Component { + state = { + cards: this.generateCards(), + currentPair: [], + guesses: 0, + matchedCardIndices: [] + }; + + generateCards() { + const result = []; + const size = SIDE * SIDE; + const candidates = shuffle(SYMBOLS); + while (result.length < size) { + const card = candidates.pop(); + result.push(card, card); + } + return shuffle(result); + } + + handleNewPairClosedBy(index) { + const { cards, currentPair, guesses, matchedCardIndices } = this.state; + + const newPair = [currentPair[0], index]; + const newGuesses = guesses + 1; + const matched = cards[newPair[0]] === cards[newPair[1]]; + this.setState({ currentPair: newPair, guesses: newGuesses }); + if (matched) { + this.setState({ + matchedCardIndices: [...matchedCardIndices, ...newPair] + }); + } + setTimeout(() => this.setState({ currentPair: [] }), VISUAL_PAUSE_MSECS); + } + + handleCardClick = (index) => { + const { currentPair } = this.state; + + if (currentPair.length === 2) { + return; + } + + if (currentPair.length === 0) { + this.setState({ currentPair: [index] }); + return; + } + + this.handleNewPairClosedBy(index); + }; + + getFeedbackForCard(index) { + const { currentPair, matchedCardIndices } = this.state; + const indexMatched = matchedCardIndices.includes(index); + + if (currentPair.length < 2) { + return indexMatched || index === currentPair[0] ? "visible" : "hidden"; + } + + if (currentPair.includes(index)) { + return indexMatched ? "justMatched" : "justMismatched"; + } + + return indexMatched ? "visible" : "hidden"; + } + + render() { + const { cards, guesses, matchedCardIndices } = this.state; + const won = matchedCardIndices.length === cards.length; + return ( +
+ + {cards.map((card, index) => ( + + ))} + {won && } +
+ ); + } } + +export default App; diff --git a/src/Card.css b/src/Card.css new file mode 100644 index 0000000..716104a --- /dev/null +++ b/src/Card.css @@ -0,0 +1,28 @@ +.memory > .card { + font-size: 2em; + flex: 1 1 calc(100% / 6 - 0.4em); + outline: 0.08em solid rgb(0, 0, 0); + margin: 0.2em; + display: flex; + cursor: default; +} + +.memory > .card.hidden { + background: rgb(90, 69, 69); +} + +.memory > .card.justMatched, +.memory > .card.justMismatched { + outline: 0.1em solid green; +} +.memory > .card.justMismatched { + outline-color: red; +} + +.memory > .card.visible { + cursor: not-allowed; +} + +.memory > .card > .symbol { + margin: auto; +} diff --git a/src/Card.js b/src/Card.js new file mode 100644 index 0000000..90105fc --- /dev/null +++ b/src/Card.js @@ -0,0 +1,28 @@ +import React from "react"; + +import "./Card.css"; +import PropTypes from "prop-types"; + +const HIDDEN_SYMBOL = "❓"; + +const Card = ({ card, feedback, index, onClick }) => ( +
onClick(index)}> + + {feedback === "hidden" ? HIDDEN_SYMBOL : card} + +
+); + +Card.propTypes = { + card: PropTypes.string.isRequired, + feedback: PropTypes.oneOf([ + "hidden", + "justMatched", + "justMismatched", + "visible" + ]).isRequired, + index: PropTypes.number.isRequired, + onClick: PropTypes.func.isRequired +}; + +export default Card; diff --git a/src/GuessCount.css b/src/GuessCount.css new file mode 100644 index 0000000..085e35f --- /dev/null +++ b/src/GuessCount.css @@ -0,0 +1,7 @@ +.memory > .guesses { + font-size: 1em; + width: calc(100% - 0.4em); + margin: 0.2em; + text-align: right; + font-family: Menlo, Monaco, Consolas, Inconsolata, "Courier New", monospace; +} diff --git a/src/GuessCount.js b/src/GuessCount.js new file mode 100644 index 0000000..28a24e3 --- /dev/null +++ b/src/GuessCount.js @@ -0,0 +1,12 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import "./GuessCount.css"; + +const GuessCount = ({ guesses }) =>
{guesses}
; + +GuessCount.propTypes = { + guesses: PropTypes.number.isRequired +}; + +export default GuessCount; diff --git a/src/HallOfFame.css b/src/HallOfFame.css new file mode 100644 index 0000000..6a177cb --- /dev/null +++ b/src/HallOfFame.css @@ -0,0 +1,14 @@ +.hallOfFame { + width: 100%; + border-collapse: collapse; +} + +.hallOfFame .date, +.hallOfFame .guesses { + width: 20%; + text-align: center; +} + +.hallOfFame tr:nth-child(even) { + background: rgb(0, 0, 0); +} diff --git a/src/HallOfFame.js b/src/HallOfFame.js new file mode 100644 index 0000000..c1a6db5 --- /dev/null +++ b/src/HallOfFame.js @@ -0,0 +1,40 @@ +import React from "react"; + +import "./HallOfFame.css"; +import PropTypes from "prop-types"; + +const HallOfFame = ({ entries }) => ( + + + {entries.map(({ date, guesses, id, player }) => ( + + + + + + ))} + +
{date}{guesses}{player}
+); + +HallOfFame.propTypes = { + entries: PropTypes.arrayOf( + PropTypes.shape({ + date: PropTypes.string.isRequired, + guesses: PropTypes.number.isRequired, + id: PropTypes.number.isRequired, + player: PropTypes.string.isRequired + }) + ).isRequired +}; + +export default HallOfFame; + +// == Internal helpers ============================================== + +export const FAKE_HOF = [ + { id: 3, guesses: 18, date: "10/10/2017", player: "Jane" }, + { id: 2, guesses: 23, date: "11/10/2017", player: "Kevin" }, + { id: 1, guesses: 31, date: "06/10/2017", player: "Louisa" }, + { id: 0, guesses: 48, date: "14/10/2017", player: "Marc" } +]; diff --git a/src/index.js b/src/index.js index d65892e..447296c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import { StrictMode } from "react"; +import React from "react"; import ReactDOM from "react-dom"; - import App from "./App"; const rootElement = document.getElementById("root");