From db9b46ec09146022b481787113a30daeb3860e8b Mon Sep 17 00:00:00 2001 From: Kai Moseley Date: Tue, 20 Sep 2016 23:07:11 +0100 Subject: [PATCH] Presentation pass --- .flowconfig | 3 +- dist/index.html | 6 +- package.json | 37 ++-------- src/components/Game.js | 34 ---------- src/components/Game/game.scss | 37 ++++++++++ src/components/Game/index.js | 44 ++++++++++++ src/components/GameContainer.js | 67 ++++++++++++++++--- src/components/Options/index.js | 38 +++++++++++ src/components/Options/options.scss | 9 +++ src/components/Shell/index.js | 31 +++++++++ src/components/Shell/shell.scss | 27 ++++++++ src/components/ShellGameApp.js | 26 ------- src/components/ShellGameApp/index.js | 27 ++++++++ src/components/ShellGameApp/shellGameApp.scss | 13 ++++ src/components/ShellGameAppContainer.js | 7 +- src/components/index.js | 4 ++ src/utils/gameUtils.js | 11 +-- 17 files changed, 306 insertions(+), 115 deletions(-) delete mode 100644 src/components/Game.js create mode 100644 src/components/Game/game.scss create mode 100644 src/components/Game/index.js create mode 100644 src/components/Options/index.js create mode 100644 src/components/Options/options.scss create mode 100644 src/components/Shell/index.js create mode 100644 src/components/Shell/shell.scss delete mode 100644 src/components/ShellGameApp.js create mode 100644 src/components/ShellGameApp/index.js create mode 100644 src/components/ShellGameApp/shellGameApp.scss diff --git a/.flowconfig b/.flowconfig index efe3848..52ab208 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,2 +1,3 @@ [ignore] -.*/node_modules/fbjs/* \ No newline at end of file +.*/node_modules/fbjs/* +.*/node_modules/react-motion/* \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index f497f6c..2ee33f4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -3,12 +3,10 @@ Shell game + -
- - -
+
diff --git a/package.json b/package.json index 6aa6efe..5f49cad 100644 --- a/package.json +++ b/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": [ - "/src" - ], - "moduleFileExtensions": [ - "js", - "jsx", - "json" - ], - "moduleDirectories": [ - "node_modules" - ], - "modulePaths": [ - "/src" - ], - "moduleNameMapper": { - "^.+\\.(css|less|scss)$": "/src/__mocks__/styleMock.js", - "^.+\\.(gif|ttf|eot|svg)$": "/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" } } diff --git a/src/components/Game.js b/src/components/Game.js deleted file mode 100644 index f8499ae..0000000 --- a/src/components/Game.js +++ /dev/null @@ -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) => ( -
- { props.shells.map(shell => -
props.shellClickHandler(shell.id) } - > - Hi! I'm a shell! (id { shell.id }) -
- )} - - -

{ props.output }

-

{ props.ballShellId }

-
-); \ No newline at end of file diff --git a/src/components/Game/game.scss b/src/components/Game/game.scss new file mode 100644 index 0000000..8955a83 --- /dev/null +++ b/src/components/Game/game.scss @@ -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; +} \ No newline at end of file diff --git a/src/components/Game/index.js b/src/components/Game/index.js new file mode 100644 index 0000000..184bd19 --- /dev/null +++ b/src/components/Game/index.js @@ -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) => ( +
+
+ { props.shells.map((shell, idx) => + props.shellClickHandler(shell.id) } + position={ 10 + (idx * 110) } + containsBall={ props.ballShellId === shell.id } + displayBall={ !props.shuffling && !props.shuffled } + /> + )} +
+ +

{ props.output }

+
+); diff --git a/src/components/GameContainer.js b/src/components/GameContainer.js index 384b284..2641b0b 100644 --- a/src/components/GameContainer.js +++ b/src/components/GameContainer.js @@ -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; 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 { + //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 } /> ); diff --git a/src/components/Options/index.js b/src/components/Options/index.js new file mode 100644 index 0000000..af2623a --- /dev/null +++ b/src/components/Options/index.js @@ -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) => ( +
+
+ + +
+
+ + +
+
+); diff --git a/src/components/Options/options.scss b/src/components/Options/options.scss new file mode 100644 index 0000000..76c817c --- /dev/null +++ b/src/components/Options/options.scss @@ -0,0 +1,9 @@ +.input { + + margin-bottom: 1rem; + + select { + margin-left: 0.5rem; + font-size: 1.2rem; + } +} \ No newline at end of file diff --git a/src/components/Shell/index.js b/src/components/Shell/index.js new file mode 100644 index 0000000..965489a --- /dev/null +++ b/src/components/Shell/index.js @@ -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) => + + { ({ x }) => ( +
+
+ { props.containsBall && props.displayBall && +
} +
+
+ )} + ; + + diff --git a/src/components/Shell/shell.scss b/src/components/Shell/shell.scss new file mode 100644 index 0000000..e4de1d3 --- /dev/null +++ b/src/components/Shell/shell.scss @@ -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 +} \ No newline at end of file diff --git a/src/components/ShellGameApp.js b/src/components/ShellGameApp.js deleted file mode 100644 index a70dd3d..0000000 --- a/src/components/ShellGameApp.js +++ /dev/null @@ -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) => ( -
- -
- - -
-
-); diff --git a/src/components/ShellGameApp/index.js b/src/components/ShellGameApp/index.js new file mode 100644 index 0000000..3248685 --- /dev/null +++ b/src/components/ShellGameApp/index.js @@ -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) => ( +
+

The game of shells

+ + +

Built using React, and React-motion for the shuffling animations.

+
+); diff --git a/src/components/ShellGameApp/shellGameApp.scss b/src/components/ShellGameApp/shellGameApp.scss new file mode 100644 index 0000000..b74c61a --- /dev/null +++ b/src/components/ShellGameApp/shellGameApp.scss @@ -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; +} diff --git a/src/components/ShellGameAppContainer.js b/src/components/ShellGameAppContainer.js index 34016c2..69c8a9f 100644 --- a/src/components/ShellGameAppContainer.js +++ b/src/components/ShellGameAppContainer.js @@ -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 ( ) diff --git a/src/components/index.js b/src/components/index.js index 15544de..00757e2 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -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, } diff --git a/src/utils/gameUtils.js b/src/utils/gameUtils.js index 35f4991..2ddcf73 100644 --- a/src/utils/gameUtils.js +++ b/src/utils/gameUtils.js @@ -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