8 changed files with 10082 additions and 9949 deletions
+9914 -9938
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@
"prop-types": "^15.6.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-scripts": "1.0.14"
"react-scripts": "^1.0.17"
},
"scripts": {
"start": "react-scripts start",
+71 -8
View File
@@ -6,12 +6,25 @@ import './App.css'
import Card from './Card'
import GuessCount from './GuessCount'
import HallOfFame, { FAKE_HOF } from './HallOfFame'
import HighScoreInput from './HighScoreInput'
const SIDE = 6
const SYMBOLS = '😀🎉💖🎩🐶🐱🦄🐬🌍🌛🌞💫🍎🍌🍓🍐🍟🍿'
const VISUAL_PAUSE_MSECS = 750
class App extends Component {
cards = this.generateCards()
state = {
cards: this.generateCards(),
currentPair: [],
guesses: 0,
hallOfFame: null,
matchedCardIndices: [],
}
// Arrow fx for binding
displayHallOfFame = hallOfFame => {
this.setState({ hallOfFame })
}
generateCards() {
const result = []
@@ -24,24 +37,74 @@ class App extends Component {
return shuffle(result)
}
handleCardClick(card) {
console.log(card, 'clicked')
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'
}
// Arrow fx for binding
handleCardClick = index => {
const { currentPair } = this.state
if (currentPair.length === 2) {
return
}
if (currentPair.length === 0) {
this.setState({ currentPair: [index] })
return
}
this.handleNewPairClosedBy(index)
}
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)
}
render() {
const won = new Date().getSeconds() % 2 === 0
const { cards, guesses, hallOfFame, matchedCardIndices } = this.state
const won = matchedCardIndices.length === cards.length
return (
<div className="memory">
<GuessCount guesses={0} />
{this.cards.map((card, index) => (
<GuessCount guesses={guesses} />
{cards.map((card, index) => (
<Card
card={card}
feedback="visible"
feedback={this.getFeedbackForCard(index)}
index={index}
key={index}
onClick={this.handleCardClick}
/>
))}
{won && <HallOfFame entries={FAKE_HOF} />}
{won &&
(hallOfFame ? (
<HallOfFame entries={hallOfFame} />
) : (
<HighScoreInput
guesses={guesses}
onStored={this.displayHallOfFame}
/>
))}
</div>
)
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<App />, div)
})
+3 -2
View File
@@ -5,8 +5,8 @@ import './Card.css'
const HIDDEN_SYMBOL = '❓'
const Card = ({ card, feedback, onClick }) => (
<div className={`card ${feedback}`} onClick={() => onClick(card)}>
const Card = ({ card, feedback, index, onClick }) => (
<div className={`card ${feedback}`} onClick={() => onClick(index)}>
<span className="symbol">
{feedback === 'hidden' ? HIDDEN_SYMBOL : card}
</span>
@@ -21,6 +21,7 @@ Card.propTypes = {
'justMismatched',
'visible',
]).isRequired,
index: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
}
+25
View File
@@ -38,3 +38,28 @@ export const FAKE_HOF = [
{ id: 1, guesses: 31, date: '06/10/2017', player: 'Louisa' },
{ id: 0, guesses: 48, date: '14/10/2017', player: 'Marc' },
]
const HOF_KEY = '::Memory::HallofFame'
const HOF_MAX_SIZE = 10
export function saveHOFEntry(entry, onStored) {
entry.date = new Date().toLocaleDateString()
entry.id = Date.now()
const entries = JSON.parse(localStorage.getItem(HOF_KEY) || '[]')
const insertionPoint = entries.findIndex(
({ guesses }) => guesses >= entry.guesses
)
if (insertionPoint === -1) {
entries.push(entry)
} else {
entries.splice(insertionPoint, 0, entry)
}
if (entries.length > HOF_MAX_SIZE) {
entries.splice(HOF_MAX_SIZE, entries.length)
}
localStorage.setItem(HOF_KEY, JSON.stringify(entries))
onStored(entries)
}
+11
View File
@@ -0,0 +1,11 @@
.highScoreInput {
font-size: 1em;
width: calc(100% - 0.4em);
margin: 0.2em;
}
.highScoreInput input {
display: block;
width: 100%;
margin-bottom: 0.2em;
font-family: Menlo, Monaco, Consolas, Inconsolata, 'Courier New', monospace;
}
+48
View File
@@ -0,0 +1,48 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import './HighScoreInput.css'
import { saveHOFEntry } from './HallOfFame'
class HighScoreInput extends Component {
state = { winner: '' }
// Arrow fx for binding
handleWinnerUpdate = event => {
this.setState({ winner: event.target.value.toUpperCase() })
}
// Arrow fx for binding
persistWinner = event => {
event.preventDefault()
const newEntry = { guesses: this.props.guesses, player: this.state.winner }
saveHOFEntry(newEntry, this.props.onStored)
}
render() {
return (
<form className="highScoreInput" onSubmit={this.persistWinner}>
<p>
<label>
Bravo ! Entre ton prénom :
<input
autoComplete="given-name"
type="text"
onChange={this.handleWinnerUpdate}
value={this.state.winner}
/>
</label>
<button type="submit">J’ai gagné !</button>
</p>
</form>
)
}
}
HighScoreInput.propTypes = {
guesses: PropTypes.number.isRequired,
onStored: PropTypes.func.isRequired,
}
export default HighScoreInput