diff --git a/app/scripts/components/dashboard-controls.js b/app/scripts/components/dashboard-controls.js new file mode 100644 index 0000000..802530a --- /dev/null +++ b/app/scripts/components/dashboard-controls.js @@ -0,0 +1,197 @@ +import m from 'mithril'; +import ClipboardJS from 'clipboard'; + +class DashboardControlsComponent { + + oninit({ attrs: { game, session } }) { + this.game = game; + this.session = session; + } + + // Prepare game players by creating new players (if necessary) and deciding + // which player has the starting move + setPlayers(gameType) { + if (this.game.players.length > 0) { + // Reset new games before choosing number of players (no need to reset + // the very first game) + this.game.resetGame(); + } + this.game.setPlayers(gameType); + } + + startGame(newStartingPlayer) { + this.game.startGame({ + startingPlayer: newStartingPlayer + }); + } + + endGame(roomCode) { + if (roomCode) { + // The local player ID and room code will be automatically passed by the + // session.emit() function + this.session.emit('end-game'); + } else { + this.game.endGame(); + } + } + + returnToHome() { + this.session.disconnect(); + // Redirect to homepage and clear all app state + window.location.href = '/'; + } + + closeRoom() { + this.session.status = 'closingRoom'; + this.session.emit('close-room', {}, () => { + this.returnToHome(); + }); + } + + declineNewGame() { + this.session.status = 'decliningNewGame'; + this.session.emit('decline-new-game', {}, () => { + this.returnToHome(); + }); + } + + createNewPlayer() { + this.session.status = 'newPlayer'; + } + + setNewPlayerName(inputEvent) { + this.newPlayerName = inputEvent.target.value; + inputEvent.redraw = false; + } + + submitNewPlayer(submitEvent, roomCode) { + submitEvent.preventDefault(); + if (roomCode) { + this.addNewPlayerToGame(roomCode); + } else { + this.startOnlineGame(); + } + } + + addNewPlayerToGame(roomCode) { + this.session.status = 'connecting'; + const submittedPlayer = { name: this.newPlayerName, color: 'blue' }; + this.session.emit('add-player', { roomCode, player: submittedPlayer }, ({ game, localPlayer }) => { + this.game.restoreFromServer({ game, localPlayer }); + m.redraw(); + }); + } + + startOnlineGame() { + this.session.connect(); + // Construct a placeholder player with the name we entered and the default + // first player color + const submittedPlayer = { name: this.newPlayerName, color: 'red' }; + // Request a new room and retrieve the room code returned from the server + this.session.emit('open-room', { player: submittedPlayer }, ({ roomCode, game, localPlayer }) => { + this.game.restoreFromServer({ game, localPlayer }); + console.log('new room', roomCode); + m.route.set(`/room/${roomCode}`); + }); + } + + requestNewOnlineGame() { + this.session.status = 'connecting'; + this.session.emit('request-new-game', { winner: this.game.winner }, ({ localPlayer }) => { + if (this.session.status === 'requestingNewGame') { + this.game.requestingPlayer = localPlayer; + } + m.redraw(); + }); + } + + configureCopyControl({ dom }) { + this.shareLinkCopier = new ClipboardJS(dom); + } + + view({ attrs: { roomCode } }) { + return m('div#dashboard-controls', [ + + // Prompt a player to enter their name when starting an online game, or + // when joining an existing game for the first time + this.session.status === 'newPlayer' ? m('form', { + onsubmit: (submitEvent) => this.submitNewPlayer(submitEvent, roomCode) + }, [ + m('input[type=text][autocomplete=off]#new-player-name', { + name: 'new-player-name', + autofocus: true, + required: true, + oninput: (inputEvent) => this.setNewPlayerName(inputEvent) + }), + m('button[type=submit]', roomCode ? 'Join Game' : 'Start Game') + ]) : + + this.session.status === 'waitingForPlayers' ? [ + m('div#share-controls', [ + m('input[type=text]#share-link', { + value: window.location.href, + onclick: ({ target }) => target.select() + }), + m('button#copy-share-link', { + 'data-clipboard-text': window.location.href, + oncreate: ({ dom }) => this.configureCopyControl({ dom }) + }, 'Copy') + ]), + // If P1 is still waiting for players, offer P1 the option to close + // room + m('button.warn', { onclick: () => this.closeRoom() }, 'Close Room') + ] : + + // If game is in progress, allow user to end game at any time + this.game.inProgress && this.session.status !== 'watchingGame' && !this.session.disconnected ? [ + m('button.warn', { onclick: () => this.endGame(roomCode) }, 'End Game') + ] : + + // If an online game is not in progress (i.e. it was ended early, or there + // is a winner/tie), allow the user to play again + this.session.socket && this.game.players.length === 2 && this.session.status !== 'connecting' && this.session.status !== 'watchingGame' && !this.session.disconnectedPlayer && !this.session.reconnectedPlayer && !this.session.disconnected ? [ + + // Play Again / Yes + m('button', { + onclick: () => this.requestNewOnlineGame(), + disabled: this.session.status === 'requestingNewGame' + }, this.session.status === 'newGameRequested' ? 'Yes!' : this.session.status === 'requestingNewGame' ? 'Pending' : 'Play Again'), + + // No Thanks + this.session.status !== 'requestingNewGame' ? m('button.warn', { + onclick: () => this.declineNewGame(), + disabled: this.session.status === 'requestingNewGame' + }, this.session.status === 'newGameRequested' ? 'Nah' : this.session.status !== 'requestingNewGame' ? 'No Thanks' : null) : null + + ] : + + !this.session.socket ? [ + + // If number of players has been chosen, ask user to choose starting player + this.game.type !== null ? + this.game.players.map((player) => { + return m('button', { + onclick: () => this.startGame(player) + }, player.name); + }) : + + // Select a number of human players + [ + m('button', { + onclick: () => this.setPlayers({ gameType: '1P' }) + }, '1 Player'), + m('button', { + onclick: () => this.setPlayers({ gameType: '2P' }) + }, '2 Players'), + m('button', { + onclick: () => this.createNewPlayer() + }, 'Online') + ] + + ] : null + ]); + } + +} + +export default DashboardControlsComponent; diff --git a/app/scripts/components/dashboard.js b/app/scripts/components/dashboard.js index e283dfa..accfdd4 100644 --- a/app/scripts/components/dashboard.js +++ b/app/scripts/components/dashboard.js @@ -1,6 +1,6 @@ import m from 'mithril'; -import ClipboardJS from 'clipboard'; import classNames from '../classnames.js'; +import DashboardControlsComponent from './dashboard-controls.js'; // The area of the game UI consisting of game UI controls and status messages class DashboardComponent { @@ -10,108 +10,8 @@ class DashboardComponent { this.session = session; } - // Prepare game players by creating new players (if necessary) and deciding - // which player has the starting move - setPlayers(gameType) { - if (this.game.players.length > 0) { - // Reset new games before choosing number of players (no need to reset - // the very first game) - this.game.resetGame(); - } - this.game.setPlayers(gameType); - } - - startGame(newStartingPlayer) { - this.game.startGame({ - startingPlayer: newStartingPlayer - }); - } - - endGame(roomCode) { - if (roomCode) { - // The local player ID and room code will be automatically passed by the - // session.emit() function - this.session.emit('end-game'); - } else { - this.game.endGame(); - } - } - - returnToHome() { - this.session.disconnect(); - // Redirect to homepage and clear all app state - window.location.href = '/'; - } - - closeRoom() { - this.session.status = 'closingRoom'; - this.session.emit('close-room', {}, () => { - this.returnToHome(); - }); - } - - declineNewGame() { - this.session.status = 'decliningNewGame'; - this.session.emit('decline-new-game', {}, () => { - this.returnToHome(); - }); - } - - createNewPlayer() { - this.session.status = 'newPlayer'; - } - - setNewPlayerName(inputEvent) { - this.newPlayerName = inputEvent.target.value; - inputEvent.redraw = false; - } - - submitNewPlayer(submitEvent, roomCode) { - submitEvent.preventDefault(); - if (roomCode) { - this.addNewPlayerToGame(roomCode); - } else { - this.startOnlineGame(); - } - } - - addNewPlayerToGame(roomCode) { - this.session.status = 'connecting'; - const submittedPlayer = { name: this.newPlayerName, color: 'blue' }; - this.session.emit('add-player', { roomCode, player: submittedPlayer }, ({ game, localPlayer }) => { - this.game.restoreFromServer({ game, localPlayer }); - m.redraw(); - }); - } - - startOnlineGame() { - this.session.connect(); - // Construct a placeholder player with the name we entered and the default - // first player color - const submittedPlayer = { name: this.newPlayerName, color: 'red' }; - // Request a new room and retrieve the room code returned from the server - this.session.emit('open-room', { player: submittedPlayer }, ({ roomCode, game, localPlayer }) => { - this.game.restoreFromServer({ game, localPlayer }); - console.log('new room', roomCode); - m.route.set(`/room/${roomCode}`); - }); - } - - requestNewOnlineGame() { - this.session.status = 'connecting'; - this.session.emit('request-new-game', { winner: this.game.winner }, ({ localPlayer }) => { - if (this.session.status === 'requestingNewGame') { - this.game.requestingPlayer = localPlayer; - } - m.redraw(); - }); - } - - configureCopyControl({ dom }) { - this.shareLinkCopier = new ClipboardJS(dom); - } - view({ attrs: { roomCode } }) { + console.log(this.session.status, this.session); return m('div#game-dashboard', { class: classNames({ 'prompting-for-input': this.session.status === 'newPlayer' }) }, [ @@ -146,21 +46,6 @@ class DashboardComponent { this.session.status === 'newPlayer' ? m('label[for=new-player-name]', 'Enter your player name:') : - this.session.status === 'waitingForPlayers' ? - [ - 'Waiting for other player...', - m('div#share-controls', [ - m('input[type=text]#share-link', { - value: window.location.href, - onclick: ({ target }) => target.select() - }), - m('button#copy-share-link', { - 'data-clipboard-text': window.location.href, - oncreate: ({ dom }) => this.configureCopyControl({ dom }) - }, 'Copy') - ]) - ] : - // If the local player has requested a new game this.session.status === 'requestingNewGame' ? `Asking ${this.game.getOtherPlayer(this.game.requestingPlayer).name} to play again...` : @@ -185,78 +70,18 @@ class DashboardComponent { // If either player ends the game early roomCode && this.game.requestingPlayer ? `${this.game.requestingPlayer.name} has ended the game.` : + this.session.status === 'waitingForPlayers' ? + 'Waiting for other player...' : // Otherwise, if game was ended manually by the user 'Game ended. Play again?' ), - // If P1 is still waiting for players, offer P1 the option to close room - this.session.status === 'waitingForPlayers' ? [ - m('button.warn', { onclick: () => this.closeRoom() }, 'Close Room') - ] : - - // If game is in progress, allow user to end game at any time - this.game.inProgress && this.session.status !== 'watchingGame' && !this.session.disconnected ? [ - m('button.warn', { onclick: () => this.endGame(roomCode) }, 'End Game') - ] : - - // If an online game is not in progress (i.e. it was ended early, or there - // is a winner/tie), allow the user to play again - this.session.socket && this.game.players.length === 2 && this.session.status !== 'connecting' && this.session.status !== 'watchingGame' && !this.session.disconnectedPlayer && !this.session.reconnectedPlayer && !this.session.disconnected ? [ - - // Play Again / Yes - m('button', { - onclick: () => this.requestNewOnlineGame(), - disabled: this.session.status === 'requestingNewGame' - }, this.session.status === 'newGameRequested' ? 'Yes!' : this.session.status === 'requestingNewGame' ? 'Pending' : 'Play Again'), - - // No Thanks - this.session.status !== 'requestingNewGame' ? m('button.warn', { - onclick: () => this.declineNewGame(), - disabled: this.session.status === 'requestingNewGame' - }, this.session.status === 'newGameRequested' ? 'Nah' : this.session.status !== 'requestingNewGame' ? 'No Thanks' : null) : null - - ] : - - // Prompt a player to enter their name when starting an online game, or - // when joining an existing game for the first time - this.session.status === 'newPlayer' ? [ - m('form', { - onsubmit: (submitEvent) => this.submitNewPlayer(submitEvent, roomCode) - }, [ - m('input[type=text][autocomplete=off]#new-player-name', { - name: 'new-player-name', - autofocus: true, - required: true, - oninput: (inputEvent) => this.setNewPlayerName(inputEvent) - }), - m('button[type=submit]', roomCode ? 'Join Game' : 'Start Game') - ]) - ] : - - !this.session.socket ? [ - - // If number of players has been chosen, ask user to choose starting player - this.game.type !== null ? - this.game.players.map((player) => { - return m('button', { - onclick: () => this.startGame(player) - }, player.name); - }) : - - // Select a number of human players - [ - m('button', { - onclick: () => this.setPlayers({ gameType: '1P' }) - }, '1 Player'), - m('button', { - onclick: () => this.setPlayers({ gameType: '2P' }) - }, '2 Players'), - m('button', { - onclick: () => this.createNewPlayer() - }, 'Online') - ] + m(DashboardControlsComponent, { + game: this.game, + session: this.session, + roomCode + }) - ] : null ]); } diff --git a/app/styles/_dashboard-controls.scss b/app/styles/_dashboard-controls.scss new file mode 100644 index 0000000..0bf2853 --- /dev/null +++ b/app/styles/_dashboard-controls.scss @@ -0,0 +1,80 @@ +#dashboard-controls { + button { + border: solid 1px rgba(0, 0, 0, 0.25); + border-radius: 3px; + padding: 4px 6px; + background-color: $button-background-color; + font-family: $sans-serif; + color: #fff; + &:hover:active { + background-color: lighten($button-background-color, 5%); + } + &:focus { + outline-width: 0; + } + &.warn { + background-color: $button-warn-background-color; + &:hover:active { + background-color: desaturate(lighten($button-warn-background-color, 10%), 10%); + } + } + &:disabled { + background-color: $button-disabled-background-color; + } + } +} + +input[type='text'] { + -webkit-appearance: none; + border-radius: 3px; + padding: 4px 8px; + border: solid 1px #aaa; + box-sizing: border-box; + font-size: 16px; + text-align: center; + &:focus { + outline-width: 0; + border-color: $input-focus-border-color; + box-shadow: 0 0 3px 1px $input-focus-box-shadow-color; + } +} + +#new-player-name { + display: block; + width: 150px; + margin: 0 auto; + margin-bottom: 10px; +} + +#share-controls { + display: flex; + justify-content: center; + align-items: stretch; + position: relative; + margin-bottom: 10px; +} +#share-link { + width: 150px; + font-size: 12px; +} +#copy-share-link:last-of-type { + margin-right: 0; + transition: background-color 500ms ease-in-out; + &:hover:active { + background-color: $button-action-background-color; + transition-duration: 0ms; + } +} +#share-link-copy-status { + display: block; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 10px; + font-size: 14px; + &.copy-status-flash { + transition: opacity 1000ms ease-in-out; + opacity: 0; + } +} diff --git a/app/styles/_dashboard.scss b/app/styles/_dashboard.scss index 87ac273..1b6e4b3 100644 --- a/app/styles/_dashboard.scss +++ b/app/styles/_dashboard.scss @@ -8,29 +8,6 @@ margin: 0 4px; font-size: 14px; } - button { - border: solid 1px rgba(0, 0, 0, 0.25); - border-radius: 3px; - padding: 4px 6px; - background-color: $button-background-color; - font-family: $sans-serif; - color: #fff; - &:hover:active { - background-color: lighten($button-background-color, 5%); - } - &:focus { - outline-width: 0; - } - &.warn { - background-color: $button-warn-background-color; - &:hover:active { - background-color: desaturate(lighten($button-warn-background-color, 10%), 10%); - } - } - &:disabled { - background-color: $button-disabled-background-color; - } - } @include if-hybrid-layout() { margin-left: auto; margin-right: auto; @@ -54,59 +31,3 @@ } } } - -input[type='text'] { - -webkit-appearance: none; - border-radius: 3px; - padding: 4px 8px; - border: solid 1px #aaa; - box-sizing: border-box; - font-size: 16px; - text-align: center; - &:focus { - outline-width: 0; - border-color: $input-focus-border-color; - box-shadow: 0 0 3px 1px $input-focus-box-shadow-color; - } -} - -#new-player-name { - display: block; - width: 150px; - margin: 0 auto; - margin-bottom: 10px; -} - -#share-controls { - display: flex; - justify-content: center; - align-items: stretch; - position: relative; - margin-top: 10px; - margin-top: 10px; -} -#share-link { - width: 150px; - font-size: 12px; -} -#copy-share-link:last-of-type { - margin-right: 0; - transition: background-color 500ms ease-in-out; - &:hover:active { - background-color: $button-action-background-color; - transition-duration: 0ms; - } -} -#share-link-copy-status { - display: block; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - margin-top: 10px; - font-size: 14px; - &.copy-status-flash { - transition: opacity 1000ms ease-in-out; - opacity: 0; - } -} diff --git a/app/styles/index.scss b/app/styles/index.scss index 885cc69..a7fe44a 100644 --- a/app/styles/index.scss +++ b/app/styles/index.scss @@ -6,6 +6,7 @@ @import 'containers'; @import 'header'; @import 'dashboard'; +@import 'dashboard-controls'; @import 'grid'; @import 'scoreboard'; @import 'update-notification';