Presentation pass
This commit is contained in:
parent
caa65a7984
commit
db9b46ec09
|
@ -1,2 +1,3 @@
|
|||
[ignore]
|
||||
.*/node_modules/fbjs/*
|
||||
.*/node_modules/fbjs/*
|
||||
.*/node_modules/react-motion/*
|
|
@ -3,12 +3,10 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Shell game</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-shell-game">
|
||||
|
||||
|
||||
</div>
|
||||
<div id="react-shell-game"></div>
|
||||
</body>
|
||||
|
||||
<script src="/shell-game.bundle.js"></script>
|
||||
|
|
37
package.json
37
package.json
|
@ -1,63 +1,36 @@
|
|||
{
|
||||
"name": "react-custom-home",
|
||||
"version": "0.0.0",
|
||||
"description": "A customisable home screen",
|
||||
"name": "shell-game",
|
||||
"version": "0.0.1",
|
||||
"description": "A simple, React based, game of shells ",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --progress --content-base dist/",
|
||||
"test": "jest"
|
||||
},
|
||||
"jest": {
|
||||
"testPathDirs": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules"
|
||||
],
|
||||
"modulePaths": [
|
||||
"/src"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^.+\\.(css|less|scss)$": "<rootDir>/src/__mocks__/styleMock.js",
|
||||
"^.+\\.(gif|ttf|eot|svg)$": "<rootDir>/src/__mocks__/fileMock.js"
|
||||
}
|
||||
},
|
||||
"author": "Kai Moseley",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-transform-async-to-generator": "^6.8.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.14.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"body-parser": "^1.15.2",
|
||||
"css-loader": "^0.24.0",
|
||||
"enzyme": "^2.4.1",
|
||||
"eslint": "^3.2.2",
|
||||
"eslint-loader": "^1.5.0",
|
||||
"eslint-plugin-babel": "^3.3.0",
|
||||
"eslint-plugin-flowtype": "^2.18.1",
|
||||
"eslint-plugin-react": "^6.0.0",
|
||||
"express": "^4.14.0",
|
||||
"flow-bin": "^0.32.0",
|
||||
"jest": "^15.1.1",
|
||||
"lodash": "^4.15.0",
|
||||
"node-sass": "^3.8.0",
|
||||
"react": "^15.3.1",
|
||||
"react-css-modules": "^3.7.9",
|
||||
"react-dom": "^15.3.1",
|
||||
"react-redux": "^4.4.5",
|
||||
"sass-loader": "^4.0.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-server": "^1.16.1"
|
||||
"webpack-dev-server": "^1.16.1",
|
||||
"react-motion": "^0.4.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
|
||||
type Shell = {
|
||||
position: number,
|
||||
}
|
||||
|
||||
type GameProps = {
|
||||
shells: Shell[],
|
||||
shellClickHandler: (id: number) => {},
|
||||
shuffleShellsClickHandler: (event: Object) => {},
|
||||
startGameClickHandler: (event: Object) => {},
|
||||
output: string,
|
||||
ballShellId: number,
|
||||
inProgress: boolean,
|
||||
shuffled: boolean,
|
||||
}
|
||||
|
||||
export const Game = (props: GameProps) => (
|
||||
<div>
|
||||
{ props.shells.map(shell =>
|
||||
<div
|
||||
key={ shell.id }
|
||||
onClick={ () => props.shellClickHandler(shell.id) }
|
||||
>
|
||||
Hi! I'm a shell! (id { shell.id })
|
||||
</div>
|
||||
)}
|
||||
<button onClick={ props.startGameClickHandler } disabled={ props.inProgress }>Play!</button>
|
||||
<button onClick={ props.shuffleShellsClickHandler } disabled={ props.inProgress && props.shuffled || !props.inProgress }>Shuffle!</button>
|
||||
<p>{ props.output }</p>
|
||||
<p>{ props.ballShellId }</p>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,37 @@
|
|||
.board {
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,0.4);
|
||||
background-color: #efefef;
|
||||
margin: 0 auto 1rem;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.play {
|
||||
background-color: #295e80;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 5px 0 rgba(0,0,0,0.7), inset 60px 0 50px -50px rgba(33, 199, 255, 0.48), inset -60px 0 50px -50px rgba(33, 199, 255, 0.48);
|
||||
transition-property: box-shadow;
|
||||
transition-duration: 350ms;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 3px 0 rgba(0,0,0,0.7), inset 75px 0 50px -50px rgba(33, 199, 255, 0.48), inset -75px 0 50px -50px rgba(33, 199, 255, 0.48);
|
||||
transition-property: box-shadow;
|
||||
transition-duration: 350ms;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.output {
|
||||
border-top: 1px solid #295e80;
|
||||
border-bottom: 1px solid #295e80;
|
||||
padding: 1rem;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
import { Shell as ShellComponent } from '../Shell';
|
||||
import styles from './game.scss';
|
||||
|
||||
type Shell = {
|
||||
id: number,
|
||||
}
|
||||
|
||||
type GameProps = {
|
||||
shells: Shell[],
|
||||
shellClickHandler: (id: number) => void,
|
||||
startGameClickHandler: () => void,
|
||||
output: string,
|
||||
ballShellId: number,
|
||||
inProgress: boolean,
|
||||
shuffled: boolean,
|
||||
}
|
||||
|
||||
type onClick = (event: Object) => void
|
||||
|
||||
export const Game = (props: GameProps) => (
|
||||
<div>
|
||||
<div className={ styles.board } style={{ width: 10 + (props.shells.length * 110) }}>
|
||||
{ props.shells.map((shell, idx) =>
|
||||
<ShellComponent
|
||||
key={ shell.id }
|
||||
onClick={ () => props.shellClickHandler(shell.id) }
|
||||
position={ 10 + (idx * 110) }
|
||||
containsBall={ props.ballShellId === shell.id }
|
||||
displayBall={ !props.shuffling && !props.shuffled }
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className={ styles.play }
|
||||
onClick={ props.startGameClickHandler }
|
||||
disabled={ props.inProgress }
|
||||
>
|
||||
Play!
|
||||
</button>
|
||||
<p className={ styles.output }>{ props.output }</p>
|
||||
</div>
|
||||
);
|
|
@ -14,16 +14,20 @@ import { Game } from './Game';
|
|||
type GameContainerState = {
|
||||
ballShellId: number,
|
||||
shells: Shell[],
|
||||
inProgress: boolean,
|
||||
output: string,
|
||||
shuffleCount: number,
|
||||
shuffled: boolean,
|
||||
shuffling: boolean,
|
||||
inProgress: boolean,
|
||||
};
|
||||
|
||||
type GameContainerProps = {
|
||||
numberOfShells: number,
|
||||
shuffles: number,
|
||||
};
|
||||
|
||||
type Shell = {
|
||||
position: number,
|
||||
id: number,
|
||||
};
|
||||
|
||||
//======================
|
||||
|
@ -32,6 +36,12 @@ type Shell = {
|
|||
export class GameContainer extends Component {
|
||||
state: GameContainerState;
|
||||
props: GameContainerProps;
|
||||
selectRandomShell: ?() => number;
|
||||
_initializeGame: (numberOfShells: number) => void;
|
||||
_startGame: () => void;
|
||||
_setWinningShell: () => void;
|
||||
_shellClickHandler: (shellId: number) => void;
|
||||
_shuffleShells: () => Promise<boolean>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -39,7 +49,9 @@ export class GameContainer extends Component {
|
|||
ballShellId: 0,
|
||||
shells: [],
|
||||
inProgress: false,
|
||||
shuffleCount: 0,
|
||||
shuffled: false,
|
||||
shuffling: false,
|
||||
output: `Click on 'Play' to begin!`,
|
||||
};
|
||||
this.selectRandomShell = null;
|
||||
|
@ -55,9 +67,16 @@ export class GameContainer extends Component {
|
|||
}
|
||||
|
||||
_initializeGame(numberOfShells: number): void {
|
||||
//This is designed to initialize the game whenever the component first mounts, or
|
||||
//whenever a new set of options are passed in via props.
|
||||
this.setState({
|
||||
ballShellId: 0,
|
||||
shells: createShells(numberOfShells)
|
||||
shells: createShells(numberOfShells),
|
||||
output: `Click on 'Play' to begin!`,
|
||||
shuffleCount: 0,
|
||||
shuffling: false,
|
||||
shuffled: false,
|
||||
inProgress: false,
|
||||
});
|
||||
this.selectRandomShell = createRandomNumberFn(0, numberOfShells - 1);
|
||||
}
|
||||
|
@ -66,8 +85,15 @@ export class GameContainer extends Component {
|
|||
this._setWinningShell();
|
||||
this.setState({
|
||||
inProgress: true,
|
||||
output: `Click a shell to see if you guessed correctly!`
|
||||
shuffleCount: 0,
|
||||
output: 'Keep an eye on the ball!',
|
||||
shuffling: false,
|
||||
shuffled: false,
|
||||
});
|
||||
setTimeout(() => this._shuffleShells().then(() =>
|
||||
this.setState({
|
||||
output: 'Pick a shell!',
|
||||
})), 1000);
|
||||
}
|
||||
|
||||
_setWinningShell(): void {
|
||||
|
@ -77,10 +103,10 @@ export class GameContainer extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_shellClickHandler(shellId: number) {
|
||||
_shellClickHandler(shellId: number): void {
|
||||
//We only need to check if the shell is a winning shell if it is in progress and has been shuffled
|
||||
if (!this.state.inProgress) return this.setState({ output: `Click on 'Play' to begin!`});
|
||||
if (!this.state.shuffled) return this.setState({ output: `Click on 'Shuffle' to shuffle the shells!`});
|
||||
if (this.state.shuffling) return this.setState({ output: `Have patience, young padawan`});
|
||||
this.setState({
|
||||
output: this.state.ballShellId === shellId ? `You won! Click 'Play' to play again` : `Better luck next time! Click 'Play' to play again`,
|
||||
inProgress: false,
|
||||
|
@ -88,18 +114,37 @@ export class GameContainer extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
_shuffleShells() {
|
||||
_shuffleShells(): Promise<boolean> {
|
||||
//Shuffle the shells in a time based loop (the animation functionality uses springs, so there's no strict timer
|
||||
//on how long the animation plays for).
|
||||
this.setState({
|
||||
shells: randomizeArrayOrder(this.state.shells),
|
||||
shuffled: true,
|
||||
shuffling: true,
|
||||
output: 'Shuffling...',
|
||||
});
|
||||
|
||||
//Return a promise as this is an async operation
|
||||
return new Promise(res => {
|
||||
const shuffle = () => {
|
||||
const shuffleCount = this.state.shuffleCount + 1;
|
||||
this.setState({
|
||||
shells: randomizeArrayOrder(this.state.shells),
|
||||
shuffled: shuffleCount == this.props.shuffles,
|
||||
shuffleCount,
|
||||
shuffling: shuffleCount < this.props.shuffles,
|
||||
});
|
||||
if (this.state.shuffled) return res(true);
|
||||
return setTimeout(shuffle, 1000);
|
||||
};
|
||||
setTimeout(shuffle, 1000);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._initializeGame(this.props.numberOfShells);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps: GameProps) {
|
||||
componentWillReceiveProps(newProps: GameContainerProps) {
|
||||
if(newProps.numberOfShells !== this.props.numberOfShells) {
|
||||
this._initializeGame(newProps.numberOfShells);
|
||||
}
|
||||
|
@ -112,9 +157,9 @@ export class GameContainer extends Component {
|
|||
output={ this.state.output }
|
||||
ballShellId={ this.state.ballShellId }
|
||||
inProgress={ this.state.inProgress }
|
||||
shuffling={ this.state.shuffling }
|
||||
shuffled={ this.state.shuffled }
|
||||
shellClickHandler={ this._shellClickHandler }
|
||||
shuffleShellsClickHandler={ this._shuffleShells }
|
||||
startGameClickHandler={ this._startGame }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
import styles from './options.scss';
|
||||
|
||||
type OptionsProps = {
|
||||
numberOfShells: number,
|
||||
shuffles: number,
|
||||
onInputChange: (event: Object) => void,
|
||||
};
|
||||
|
||||
export const Options = (props: OptionsProps) => (
|
||||
<div>
|
||||
<div className={ styles.input }>
|
||||
<label htmlFor="numberOfShells">Number of shells: </label>
|
||||
<select
|
||||
name="numberOfShells"
|
||||
value={ props.numberOfShells }
|
||||
onChange={ props.onInputChange }
|
||||
>
|
||||
<option value={3}>3</option>
|
||||
<option value={4}>4</option>
|
||||
<option value={5}>5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={ styles.input }>
|
||||
<label htmlFor="shuffles">Number of shuffles: </label>
|
||||
<select
|
||||
name="shuffles"
|
||||
value={ props.shuffles }
|
||||
onChange={ props.onInputChange }
|
||||
>
|
||||
<option value={3}>3</option>
|
||||
<option value={5}>5</option>
|
||||
<option value={7}>7</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
.input {
|
||||
|
||||
margin-bottom: 1rem;
|
||||
|
||||
select {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
import styles from './shell.scss';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
type ShellProps = {
|
||||
onClick: () => void,
|
||||
position: number,
|
||||
containsBall: boolean,
|
||||
displayBall: boolean,
|
||||
}
|
||||
|
||||
export const Shell = (props: ShellProps) =>
|
||||
<Motion style={{x: spring(props.position, { stiffness: 300, damping: 15 })}}>
|
||||
{ ({ x }) => (
|
||||
<div>
|
||||
<div
|
||||
style={ {
|
||||
left: x
|
||||
} }
|
||||
className={ styles.shell }
|
||||
onClick={ props.onClick }
|
||||
>
|
||||
{ props.containsBall && props.displayBall &&
|
||||
<div className={ styles.ball } /> }
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>;
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
.shell {
|
||||
background-color: #295e80;
|
||||
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.4), inset 0 60px 50px -50px rgba(33, 199, 255, 0.48), inset 0 -60px 50px -50px rgba(33, 199, 255, 0.48);
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
transition: 350ms;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.4), inset 0 90px 50px -50px rgba(33, 199, 255, 0.48), inset 0 -90px 50px -50px rgba(33, 199, 255, 0.48);
|
||||
transition: 350ms;
|
||||
}
|
||||
}
|
||||
|
||||
.ball {
|
||||
background-color: #b00000;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 0 10px 3px rgba(0,0,0,0.4), inset 0 0 20px 2px rgba(255,255,255,0.4);
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 25px
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
import { GameContainer } from './GameContainer';
|
||||
|
||||
type ShellGameAppProps = {
|
||||
numberOfShells: number,
|
||||
onInputChange: (event: Object) => {},
|
||||
};
|
||||
|
||||
export const ShellGameApp = (props: ShellGameAppProps) => (
|
||||
<div>
|
||||
<GameContainer numberOfShells={ props.numberOfShells } />
|
||||
<div>
|
||||
<label htmlFor="numberOfShells">Number of shells: </label>
|
||||
<select
|
||||
name="numberOfShells"
|
||||
value={ props.numberOfShells }
|
||||
onChange={ props.onInputChange }
|
||||
>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,27 @@
|
|||
//@flow
|
||||
import React from 'react';
|
||||
import { GameContainer } from '../GameContainer';
|
||||
import { Options } from '../Options';
|
||||
import styles from './shellGameApp.scss';
|
||||
|
||||
type ShellGameAppProps = {
|
||||
numberOfShells: number,
|
||||
shuffles: number,
|
||||
onInputChange: (event: Object) => void,
|
||||
};
|
||||
|
||||
export const ShellGameApp = (props: ShellGameAppProps) => (
|
||||
<div className={ styles.app }>
|
||||
<h1>The game of shells</h1>
|
||||
<GameContainer
|
||||
numberOfShells={ props.numberOfShells }
|
||||
shuffles={ props.shuffles }
|
||||
/>
|
||||
<Options
|
||||
numberOfShells={ props.numberOfShells }
|
||||
shuffles={ props.shuffles }
|
||||
onInputChange={ props.onInputChange }
|
||||
/>
|
||||
<p className={ styles.info }>Built using React, and React-motion for the shuffling animations.</p>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,13 @@
|
|||
.app {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
width: 700px;
|
||||
margin: 5rem auto;
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,0.4);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.9rem;
|
||||
}
|
|
@ -4,21 +4,23 @@ import { ShellGameApp } from './ShellGameApp';
|
|||
|
||||
type ShellGameAppContainerState = {
|
||||
numberOfShells: number,
|
||||
shuffles: number,
|
||||
}
|
||||
|
||||
export class ShellGameAppContainer extends Component {
|
||||
state: ShellGameAppContainerState;
|
||||
_handleInputUpdate: (event: Object) => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
numberOfShells: 3,
|
||||
shuffles: 5,
|
||||
};
|
||||
|
||||
this._handleInputUpdate = this._handleInputUpdate.bind(this);
|
||||
}
|
||||
|
||||
_handleInputUpdate(e) {
|
||||
_handleInputUpdate(e: Object): void {
|
||||
if (!this.state.hasOwnProperty(e.target.name)) throw new Error('Form input name should be on the GameShellAppContainer state');
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
|
@ -29,6 +31,7 @@ export class ShellGameAppContainer extends Component {
|
|||
return (
|
||||
<ShellGameApp
|
||||
numberOfShells={ this.state.numberOfShells }
|
||||
shuffles={ this.state.shuffles }
|
||||
onInputChange={ this._handleInputUpdate }
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -2,10 +2,14 @@ import { Game } from './Game';
|
|||
import { GameContainer } from './GameContainer';
|
||||
import { ShellGameApp } from './ShellGameApp';
|
||||
import { ShellGameAppContainer } from './ShellGameAppContainer';
|
||||
import { Shell } from './Shell';
|
||||
import { Options } from './Options';
|
||||
|
||||
export {
|
||||
Game,
|
||||
GameContainer,
|
||||
ShellGameApp,
|
||||
ShellGameAppContainer,
|
||||
Shell,
|
||||
Options,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//@flow
|
||||
|
||||
type Shell = {
|
||||
id: number,
|
||||
};
|
||||
//======================
|
||||
// Utility functions
|
||||
//======================
|
||||
|
@ -7,7 +9,7 @@ export function createRandomNumberFn(minimum: number, maximum: number): () => nu
|
|||
return () => minimum + Math.floor(Math.random() * ((maximum-minimum) + 1));
|
||||
}
|
||||
|
||||
export function randomizeArrayOrder(array: array): array {
|
||||
export function randomizeArrayOrder(array: Shell[]): Shell[] {
|
||||
let oldArray = [ ...array ];
|
||||
let newArr = [];
|
||||
|
||||
|
@ -20,9 +22,8 @@ export function randomizeArrayOrder(array: array): array {
|
|||
|
||||
export function createShells(numberOfShells: number): Shell[] {
|
||||
/* The shell containing the ball will be tracked independently of shells, but
|
||||
in order to properly animate the shells later, a fixed id is needed. The position
|
||||
will be used to track where the shell should be displayed, with the id
|
||||
remaining fixed. */
|
||||
in order to properly animate the shells later, a fixed id is desirable.
|
||||
*/
|
||||
let newShellArr = [];
|
||||
for (let i=0; i<numberOfShells; i++) {
|
||||
newShellArr = [ ...newShellArr, {
|
||||
|
|
Loading…
Reference in New Issue