Compare commits
11
Commits
fin-listes
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3359972679 | ||
|
|
3a13416399 | ||
|
|
e63b31b5e5 | ||
|
|
a69c190641 | ||
|
|
a621bebeec | ||
|
|
de9421b64f | ||
|
|
7faabfbf42 | ||
|
|
e611ee84f6 | ||
|
|
1d1dc199b3 | ||
|
|
9774de53f9 | ||
|
|
2c2260490a |
Generated
+10528
-9930
File diff suppressed because it is too large
Load Diff
+15
-3
@@ -4,21 +4,33 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"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",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.1.2",
|
||||
"chai-enzyme": "^1.0.0-beta.0",
|
||||
"chai-jest-diff": "^1.0.1",
|
||||
"chai-jest-snapshot": "^2.0.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"enzyme": "^3.2.0",
|
||||
"enzyme-adapter-react-16": "^1.1.0",
|
||||
"enzyme-to-json": "^3.2.2",
|
||||
"eslint-config-prettier": "^2.6.0",
|
||||
"eslint-config-react-app": "^2.0.1",
|
||||
"eslint-plugin-prettier": "^2.3.1",
|
||||
"prettier": "^1.7.4"
|
||||
"prettier": "^1.7.4",
|
||||
"react-test-renderer": "^16.2.0",
|
||||
"sinon": "^4.1.3",
|
||||
"sinon-chai": "^2.14.0"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
|
||||
+72
-9
@@ -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 = '😀🎉💖🎩🐶🐱🦄🐬🌍🌛🌞💫🍎🍌🍓🍐🍟🍿'
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { expect } from 'chai'
|
||||
import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import App, { SYMBOLS } from './App'
|
||||
import GuessCount from './GuessCount'
|
||||
|
||||
describe('<App/>', () => {
|
||||
it('renders without crashing', () => {
|
||||
const wrapper = shallow(<App />)
|
||||
})
|
||||
|
||||
it('contains a zero-guess counter', () => {
|
||||
const wrapper = shallow(<App />)
|
||||
|
||||
expect(wrapper).to.contain(<GuessCount guesses={0} />)
|
||||
})
|
||||
|
||||
it('has 36 cards', () => {
|
||||
const wrapper = shallow(<App />)
|
||||
expect(wrapper.find('Card')).to.have.length(36)
|
||||
})
|
||||
|
||||
it('should match its reference snapshot', () => {
|
||||
const mock = sinon
|
||||
.stub(App.prototype, 'generateCards')
|
||||
.returns([...SYMBOLS.repeat(2)])
|
||||
try {
|
||||
const wrapper = shallow(<App />)
|
||||
|
||||
expect(wrapper).to.matchSnapshot()
|
||||
} finally {
|
||||
mock.restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
+15
-2
@@ -1,15 +1,28 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect } from 'chai'
|
||||
import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import Card from './Card'
|
||||
|
||||
describe('<Card/>', () => {
|
||||
it('should trigger its `onClick` prop when clicked', () => {
|
||||
const onClick = sinon.spy()
|
||||
const wrapper = shallow(
|
||||
<Card card="😁" feedback="hidden" index={0} onClick={onClick} />
|
||||
)
|
||||
|
||||
wrapper.simulate('click')
|
||||
expect(onClick).to.have.been.calledWith(0)
|
||||
})
|
||||
|
||||
it('should match its reference snapshot', () => {
|
||||
const onClick = sinon.spy()
|
||||
const wrapper = shallow(
|
||||
<Card card="😁" feedback="hidden" index={0} onClick={onClick} />
|
||||
)
|
||||
|
||||
expect(wrapper).to.matchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,12 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import './GuessCount.css'
|
||||
|
||||
const GuessCount = ({ guesses }) => <div className="guesses">{guesses}</div>
|
||||
|
||||
GuessCount.propTypes = {
|
||||
guesses: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
export default GuessCount
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import './HallOfFame.css'
|
||||
@@ -16,6 +17,17 @@ const HallOfFame = ({ entries }) => (
|
||||
</table>
|
||||
)
|
||||
|
||||
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 ==============================================
|
||||
@@ -26,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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,263 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<App/> should match its reference snapshot 1`] = `
|
||||
<div
|
||||
className="memory"
|
||||
>
|
||||
<GuessCount
|
||||
guesses={0}
|
||||
/>
|
||||
<Card
|
||||
card="😀"
|
||||
feedback="hidden"
|
||||
index={0}
|
||||
key="0"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🎉"
|
||||
feedback="hidden"
|
||||
index={1}
|
||||
key="1"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="💖"
|
||||
feedback="hidden"
|
||||
index={2}
|
||||
key="2"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🎩"
|
||||
feedback="hidden"
|
||||
index={3}
|
||||
key="3"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐶"
|
||||
feedback="hidden"
|
||||
index={4}
|
||||
key="4"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐱"
|
||||
feedback="hidden"
|
||||
index={5}
|
||||
key="5"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🦄"
|
||||
feedback="hidden"
|
||||
index={6}
|
||||
key="6"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐬"
|
||||
feedback="hidden"
|
||||
index={7}
|
||||
key="7"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌍"
|
||||
feedback="hidden"
|
||||
index={8}
|
||||
key="8"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌛"
|
||||
feedback="hidden"
|
||||
index={9}
|
||||
key="9"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌞"
|
||||
feedback="hidden"
|
||||
index={10}
|
||||
key="10"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="💫"
|
||||
feedback="hidden"
|
||||
index={11}
|
||||
key="11"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍎"
|
||||
feedback="hidden"
|
||||
index={12}
|
||||
key="12"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍌"
|
||||
feedback="hidden"
|
||||
index={13}
|
||||
key="13"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍓"
|
||||
feedback="hidden"
|
||||
index={14}
|
||||
key="14"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍐"
|
||||
feedback="hidden"
|
||||
index={15}
|
||||
key="15"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍟"
|
||||
feedback="hidden"
|
||||
index={16}
|
||||
key="16"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍿"
|
||||
feedback="hidden"
|
||||
index={17}
|
||||
key="17"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="😀"
|
||||
feedback="hidden"
|
||||
index={18}
|
||||
key="18"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🎉"
|
||||
feedback="hidden"
|
||||
index={19}
|
||||
key="19"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="💖"
|
||||
feedback="hidden"
|
||||
index={20}
|
||||
key="20"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🎩"
|
||||
feedback="hidden"
|
||||
index={21}
|
||||
key="21"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐶"
|
||||
feedback="hidden"
|
||||
index={22}
|
||||
key="22"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐱"
|
||||
feedback="hidden"
|
||||
index={23}
|
||||
key="23"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🦄"
|
||||
feedback="hidden"
|
||||
index={24}
|
||||
key="24"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🐬"
|
||||
feedback="hidden"
|
||||
index={25}
|
||||
key="25"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌍"
|
||||
feedback="hidden"
|
||||
index={26}
|
||||
key="26"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌛"
|
||||
feedback="hidden"
|
||||
index={27}
|
||||
key="27"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🌞"
|
||||
feedback="hidden"
|
||||
index={28}
|
||||
key="28"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="💫"
|
||||
feedback="hidden"
|
||||
index={29}
|
||||
key="29"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍎"
|
||||
feedback="hidden"
|
||||
index={30}
|
||||
key="30"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍌"
|
||||
feedback="hidden"
|
||||
index={31}
|
||||
key="31"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍓"
|
||||
feedback="hidden"
|
||||
index={32}
|
||||
key="32"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍐"
|
||||
feedback="hidden"
|
||||
index={33}
|
||||
key="33"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍟"
|
||||
feedback="hidden"
|
||||
index={34}
|
||||
key="34"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<Card
|
||||
card="🍿"
|
||||
feedback="hidden"
|
||||
index={35}
|
||||
key="35"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Card/> should match its reference snapshot 1`] = `
|
||||
<div
|
||||
className="card hidden"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="symbol"
|
||||
>
|
||||
❓
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,20 @@
|
||||
import Adapter from 'enzyme-adapter-react-16'
|
||||
import chai from 'chai'
|
||||
import { configure as configureEnzyme } from 'enzyme'
|
||||
import chaiJestSnapshot from 'chai-jest-snapshot'
|
||||
import createChaiEnzyme from 'chai-enzyme'
|
||||
import createChaiJestDiff from 'chai-jest-diff'
|
||||
import dirtyChai from 'dirty-chai'
|
||||
import enzymeToJSON from 'enzyme-to-json/serializer'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
chai
|
||||
.use(dirtyChai)
|
||||
.use(createChaiJestDiff())
|
||||
.use(chaiJestSnapshot)
|
||||
.use(createChaiEnzyme())
|
||||
.use(sinonChai)
|
||||
|
||||
expect.addSnapshotSerializer(enzymeToJSON)
|
||||
|
||||
configureEnzyme({ adapter: new Adapter() })
|
||||
Reference in New Issue
Block a user