diff options
Diffstat (limited to 'diplomacy/web/src/gui/core')
-rw-r--r-- | diplomacy/web/src/gui/core/action.jsx (renamed from diplomacy/web/src/gui/core/widgets.jsx) | 50 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/button.jsx | 52 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/content.jsx | 51 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/fancybox.jsx | 2 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/forms.jsx | 2 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/page.jsx | 277 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/tab.jsx | 29 | ||||
-rw-r--r-- | diplomacy/web/src/gui/core/tabs.jsx | 29 |
8 files changed, 159 insertions, 333 deletions
diff --git a/diplomacy/web/src/gui/core/widgets.jsx b/diplomacy/web/src/gui/core/action.jsx index 62a5eb4..73fe8cb 100644 --- a/diplomacy/web/src/gui/core/widgets.jsx +++ b/diplomacy/web/src/gui/core/action.jsx @@ -17,56 +17,6 @@ import React from "react"; import PropTypes from 'prop-types'; -export class Button extends React.Component { - /** Bootstrap button. - * Bootstrap classes: - * - btn - * - btn-primary - * - mx-1 (margin-left 1px, margin-right 1px) - * Props: title (str), onClick (function). - * **/ - // title - // onClick - // pickEvent = false - // large = false - // small = false - - constructor(props) { - super(props); - this.onClick = this.onClick.bind(this); - } - - onClick(event) { - if (this.props.onClick) - this.props.onClick(this.props.pickEvent ? event : null); - } - - render() { - return ( - <button - className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')} - disabled={this.props.disabled} - onClick={this.onClick}> - <strong>{this.props.title}</strong> - </button> - ); - } -} - -Button.propTypes = { - title: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - color: PropTypes.string, - large: PropTypes.bool, - small: PropTypes.bool, - pickEvent: PropTypes.bool, - disabled: PropTypes.bool -}; - -Button.defaultPropTypes = { - disabled: false -}; - export class Action extends React.Component { // title diff --git a/diplomacy/web/src/gui/core/button.jsx b/diplomacy/web/src/gui/core/button.jsx new file mode 100644 index 0000000..0d5dadd --- /dev/null +++ b/diplomacy/web/src/gui/core/button.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import PropTypes from "prop-types"; + +export class Button extends React.Component { + /** Bootstrap button. + * Bootstrap classes: + * - btn + * - btn-primary + * - mx-1 (margin-left 1px, margin-right 1px) + * Props: title (str), onClick (function). + * **/ + // title + // onClick + // pickEvent = false + // large = false + // small = false + + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + } + + onClick(event) { + if (this.props.onClick) + this.props.onClick(this.props.pickEvent ? event : null); + } + + render() { + return ( + <button + className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')} + disabled={this.props.disabled} + onClick={this.onClick}> + <strong>{this.props.title}</strong> + </button> + ); + } +} + +Button.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + color: PropTypes.string, + large: PropTypes.bool, + small: PropTypes.bool, + pickEvent: PropTypes.bool, + disabled: PropTypes.bool +}; + +Button.defaultPropTypes = { + disabled: false +}; diff --git a/diplomacy/web/src/gui/core/content.jsx b/diplomacy/web/src/gui/core/content.jsx deleted file mode 100644 index 416ba9e..0000000 --- a/diplomacy/web/src/gui/core/content.jsx +++ /dev/null @@ -1,51 +0,0 @@ -// ============================================================================== -// Copyright (C) 2019 - Philip Paquette, Steven Bocco -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License along -// with this program. If not, see <https://www.gnu.org/licenses/>. -// ============================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; - -export class Content extends React.Component { - // PROPERTIES: - // page: pointer to parent Page object - // data: data for current content - - // Each derived class must implement this static method. - static builder(page, data) { - return { - // page title (string) - title: `${data ? 'with data' : 'without data'}`, - // page navigation links: array of couples - // (navigation title, navigation callback ( onClick=() => callback() )) - navigation: [], - // page content: React component (e.g. <MyComponent/>, or <div class="content">...</div>, etc). - component: null - }; - } - - getPage() { - return this.props.page; - } - - componentDidMount() { - window.scrollTo(0, 0); - } -} - - -Content.propTypes = { - page: PropTypes.object.isRequired, - data: PropTypes.object -}; diff --git a/diplomacy/web/src/gui/core/fancybox.jsx b/diplomacy/web/src/gui/core/fancybox.jsx index 4d1013d..66a1efe 100644 --- a/diplomacy/web/src/gui/core/fancybox.jsx +++ b/diplomacy/web/src/gui/core/fancybox.jsx @@ -15,8 +15,8 @@ // with this program. If not, see <https://www.gnu.org/licenses/>. // ============================================================================== import React from 'react'; -import {Button} from "./widgets"; import PropTypes from 'prop-types'; +import {Button} from "./button"; const TIMES = '\u00D7'; diff --git a/diplomacy/web/src/gui/core/forms.jsx b/diplomacy/web/src/gui/core/forms.jsx index 76d188c..da7250d 100644 --- a/diplomacy/web/src/gui/core/forms.jsx +++ b/diplomacy/web/src/gui/core/forms.jsx @@ -15,8 +15,8 @@ // with this program. If not, see <https://www.gnu.org/licenses/>. // ============================================================================== import React from "react"; -import {Button} from "./widgets"; import {UTILS} from "../../diplomacy/utils/utils"; +import {Button} from "./button"; export class Forms { static createOnChangeCallback(component, callback) { diff --git a/diplomacy/web/src/gui/core/page.jsx b/diplomacy/web/src/gui/core/page.jsx index 5ca09fd..ad830f1 100644 --- a/diplomacy/web/src/gui/core/page.jsx +++ b/diplomacy/web/src/gui/core/page.jsx @@ -18,22 +18,14 @@ import React from "react"; import {ContentConnection} from "../diplomacy/contents/content_connection"; -import {ContentGames} from "../diplomacy/contents/content_games"; -import {ContentGame} from "../diplomacy/contents/content_game"; import {UTILS} from "../../diplomacy/utils/utils"; import {Diplog} from "../../diplomacy/utils/diplog"; -import {STRINGS} from "../../diplomacy/utils/strings"; -import {Game} from "../../diplomacy/engine/game"; -import Octicon, {Person} from '@githubprimer/octicons-react'; -import $ from "jquery"; import {FancyBox} from "./fancybox"; import {DipStorage} from "../diplomacy/utils/dipStorage"; - -const CONTENTS = { - connection: ContentConnection, - games: ContentGames, - game: ContentGame -}; +import {PageContext} from "../diplomacy/widgets/page_context"; +import {ContentGames} from "../diplomacy/contents/content_games"; +import {loadGameFromDisk} from "../diplomacy/utils/load_game_from_disk"; +import {ContentGame} from "../diplomacy/contents/content_game"; export class Page extends React.Component { @@ -50,23 +42,18 @@ export class Page extends React.Component { error: null, info: null, success: null, - title: null, // Page content parameters - contentName: 'connection', - contentData: null, + name: null, + body: null, // Games. games: {}, // Games found. myGames: {} // Games locally stored. }; - this.loadPage = this.loadPage.bind(this); - this.loadConnection = this.loadConnection.bind(this); - this.loadGames = this.loadGames.bind(this); - this.loadGame = this.loadGame.bind(this); - this.loadGameFromDisk = this.loadGameFromDisk.bind(this); - this.logout = this.logout.bind(this); this.error = this.error.bind(this); this.info = this.info.bind(this); this.success = this.success.bind(this); + this.logout = this.logout.bind(this); + this.loadGameFromDisk = this.loadGameFromDisk.bind(this); this.unloadFancyBox = this.unloadFancyBox.bind(this); } @@ -80,26 +67,8 @@ export class Page extends React.Component { return games; } - copyState(updatedFields) { - return Object.assign({}, this.state, updatedFields || {}); - } - - //// Methods to check page type. - - __page_is(contentName, contentData) { - return this.state.contentName === contentName && (!contentData || this.state.contentData === contentData); - } - - pageIsConnection(contentData) { - return this.__page_is('connection', contentData); - } - - pageIsGames(contentData) { - return this.__page_is('games', contentData); - } - - pageIsGame(contentData) { - return this.__page_is('game', contentData); + static defaultPage() { + return <ContentConnection/>; } //// Methods to load a global fancybox. @@ -114,91 +83,39 @@ export class Page extends React.Component { //// Methods to load a page. - loadPage(contentName, contentData, messages) { - messages = messages || {}; - messages.error = Page.wrapMessage(messages.error); - messages.info = Page.wrapMessage(messages.info); - messages.success = Page.wrapMessage(messages.success); - Diplog.printMessages(messages); - this.setState(this.copyState({ - error: messages.error, - info: messages.info, - success: messages.success, - contentName: contentName, - contentData: contentData, - title: null, - fancyTitle: null, - onFancyBox: null - })); - } - - loadConnection(contentData, messages) { - this.loadPage('connection', contentData, messages); + load(name, body, messages) { + const newState = {}; + if (messages) { + for (let key of ['error', 'info', 'success']) + newState[key] = Page.wrapMessage(messages[key]); + } + Diplog.printMessages(newState); + newState.name = name; + newState.body = body; + this.setState(newState); } - loadGames(contentData, messages) { - this.loadPage('games', contentData, messages); + loadGames(messages) { + this.load( + 'games', + <ContentGames myGames={this.getMyGames()} gamesFound={this.getGamesFound()}/>, + messages + ); } - loadGame(gameInfo, messages) { - this.loadPage('game', gameInfo, messages); + loadGameFromDisk() { + loadGameFromDisk( + (game) => this.load( + `game: ${game.game_id}`, + <ContentGame data={game}/>, + {success: `Game loaded from disk: ${game.game_id}`} + ), + this.error + ); } - loadGameFromDisk() { - const input = $(document.createElement('input')); - input.attr("type", "file"); - input.trigger('click'); - input.change(event => { - const file = event.target.files[0]; - if (!file.name.match(/\.json$/i)) { - this.error(`Invalid JSON filename ${file.name}`); - } else { - const reader = new FileReader(); - reader.onload = () => { - const savedData = JSON.parse(reader.result); - const gameObject = {}; - gameObject.game_id = `(local) ${savedData.id}`; - gameObject.map_name = savedData.map; - gameObject.rules = savedData.rules; - const state_history = {}; - const message_history = {}; - const order_history = {}; - const result_history = {}; - for (let savedPhase of savedData.phases) { - const gameState = savedPhase.state; - const phaseOrders = savedPhase.orders || {}; - const phaseResults = savedPhase.results || {}; - const phaseMessages = {}; - if (savedPhase.messages) { - for (let message of savedPhase.messages) { - phaseMessages[message.time_sent] = message; - } - } - if (!gameState.name) - gameState.name = savedPhase.name; - state_history[gameState.name] = gameState; - order_history[gameState.name] = phaseOrders; - message_history[gameState.name] = phaseMessages; - result_history[gameState.name] = phaseResults; - } - gameObject.state_history = state_history; - gameObject.message_history = message_history; - gameObject.order_history = order_history; - gameObject.state_history = state_history; - gameObject.result_history = result_history; - gameObject.messages = []; - gameObject.role = STRINGS.OBSERVER_TYPE; - gameObject.status = STRINGS.COMPLETED; - gameObject.timestamp_created = 0; - gameObject.deadline = 0; - gameObject.n_controls = 0; - gameObject.registration_password = ''; - const game = new Game(gameObject); - this.loadGame(game); - }; - reader.readAsText(file); - } - }); + getName() { + return this.state.name; } //// Methods to sign out channel and go back to connection page. @@ -211,16 +128,16 @@ export class Page extends React.Component { this.availableMaps = null; const message = Page.wrapMessage(`Disconnected from channel and server.`); Diplog.success(message); - this.setState(this.copyState({ + this.setState({ error: null, info: null, success: message, - contentName: 'connection', - contentData: null, + name: null, + body: null, // When disconnected, remove all games previously loaded. games: {}, myGames: {} - })); + }); } logout() { @@ -236,10 +153,6 @@ export class Page extends React.Component { //// Methods to be used to set page title and messages. - setTitle(title) { - this.setState({title: title}); - } - error(message) { message = Page.wrapMessage(message); Diplog.error(message); @@ -306,11 +219,14 @@ export class Page extends React.Component { if (game.client) { game.client.leave() .then(() => { - this.disconnectGame(gameID); - this.loadGames(null, {info: `Game ${gameID} left.`}); + this.disconnectGame(gameID).then(() => { + this.loadGames({info: `Game ${gameID} left.`}); + }); }) .catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`)); } + } else { + this.loadGames({info: `No game to left.`}); } } @@ -319,12 +235,13 @@ export class Page extends React.Component { const game = this.state.myGames[gameID]; if (game.client) game.client.clearAllCallbacks(); - this.channel.getGamesInfo({games: [gameID]}) + return this.channel.getGamesInfo({games: [gameID]}) .then(gamesInfo => { this.updateMyGames(gamesInfo); }) .catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`)); } + return null; } addToMyGames(game) { @@ -335,7 +252,7 @@ export class Page extends React.Component { if (gamesFound.hasOwnProperty(game.game_id)) gamesFound[game.game_id] = game; DipStorage.addUserGame(this.channel.username, game.game_id); - this.setState({myGames: myGames, games: gamesFound}); + this.setState({myGames: myGames, games: gamesFound}, () => this.loadGames()); } removeFromMyGames(gameID) { @@ -343,7 +260,7 @@ export class Page extends React.Component { const games = Object.assign({}, this.state.myGames); delete games[gameID]; DipStorage.removeUserGame(this.channel.username, gameID); - this.setState({myGames: games}); + this.setState({myGames: games}, () => this.loadGames()); } } @@ -354,81 +271,37 @@ export class Page extends React.Component { //// Render method. render() { - const content = CONTENTS[this.state.contentName].builder(this, this.state.contentData); - const hasNavigation = UTILS.javascript.hasArray(content.navigation); - - // NB: I currently don't find a better way to update document title from content details. const successMessage = this.state.success || '-'; const infoMessage = this.state.info || '-'; const errorMessage = this.state.error || '-'; - const title = this.state.title || content.title; - document.title = title + ' | Diplomacy'; - return ( - <div className="page container-fluid" id={this.state.contentName}> - <div className={'top-msg row'}> - <div title={successMessage !== '-' ? successMessage : ''} - className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')} - onClick={() => this.success()}> - {successMessage} - </div> - <div title={infoMessage !== '-' ? infoMessage : ''} - className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')} - onClick={() => this.info()}> - {infoMessage} - </div> - <div title={errorMessage !== '-' ? errorMessage : ''} - className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')} - onClick={() => this.error()}> - {errorMessage} - </div> - </div> - {((hasNavigation || this.channel) && ( - <div className={'title row'}> - <div className={'col align-self-center'}><strong>{title}</strong></div> - <div className={'col-sm-1'}> - {(!hasNavigation && ( - <div className={'float-right'}> - <strong> - <u className={'mr-2'}>{this.channel.username}</u> - <Octicon icon={Person}/> - </strong> - </div> - )) || ( - <div className="dropdown float-right"> - <button className="btn btn-secondary dropdown-toggle" type="button" - id="dropdownMenuButton" data-toggle="dropdown" - aria-haspopup="true" aria-expanded="false"> - {(this.channel && this.channel.username && ( - <span> - <u className={'mr-2'}>{this.channel.username}</u> - <Octicon icon={Person}/> - </span> - )) || 'Menu'} - </button> - <div className="dropdown-menu dropdown-menu-right" - aria-labelledby="dropdownMenuButton"> - {content.navigation.map((nav, index) => { - const navTitle = nav[0]; - const navAction = nav[1]; - return <a key={index} className="dropdown-item" - onClick={navAction}>{navTitle}</a>; - })} - </div> - </div> - )} + <PageContext.Provider value={this}> + <div className="page container-fluid" id={this.state.contentName}> + <div className={'top-msg row'}> + <div title={successMessage !== '-' ? successMessage : ''} + className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')} + onClick={() => this.success()}> + {successMessage} + </div> + <div title={infoMessage !== '-' ? infoMessage : ''} + className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')} + onClick={() => this.info()}> + {infoMessage} + </div> + <div title={errorMessage !== '-' ? errorMessage : ''} + className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')} + onClick={() => this.error()}> + {errorMessage} </div> </div> - )) || ( - <div className={'title'}><strong>{title}</strong></div> - )} - {content.component} - {this.state.onFancyBox && ( - <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}> - {this.state.onFancyBox()} - </FancyBox> - )} - </div> + {this.state.body || Page.defaultPage()} + {this.state.onFancyBox && ( + <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}> + {this.state.onFancyBox()} + </FancyBox> + )} + </div> + </PageContext.Provider> ); } } diff --git a/diplomacy/web/src/gui/core/tab.jsx b/diplomacy/web/src/gui/core/tab.jsx new file mode 100644 index 0000000..f1ad4aa --- /dev/null +++ b/diplomacy/web/src/gui/core/tab.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import PropTypes from "prop-types"; + +export class Tab extends React.Component { + render() { + const style = { + display: this.props.display ? 'block' : 'none' + }; + const id = this.props.id ? {id: this.props.id} : {}; + return ( + <div className={'tab mb-4 ' + this.props.className} style={style} {...id}> + {this.props.children} + </div> + ); + } +} + +Tab.propTypes = { + display: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +Tab.defaultProps = { + display: false, + className: '', + id: '' +}; diff --git a/diplomacy/web/src/gui/core/tabs.jsx b/diplomacy/web/src/gui/core/tabs.jsx index 6123219..a3f6b9b 100644 --- a/diplomacy/web/src/gui/core/tabs.jsx +++ b/diplomacy/web/src/gui/core/tabs.jsx @@ -15,36 +15,9 @@ // with this program. If not, see <https://www.gnu.org/licenses/>. // ============================================================================== import React from "react"; -import {Action} from "./widgets"; +import {Action} from "./action"; import PropTypes from 'prop-types'; -export class Tab extends React.Component { - render() { - const style = { - display: this.props.display ? 'block' : 'none' - }; - const id = this.props.id ? {id: this.props.id} : {}; - return ( - <div className={'tab mb-4 ' + this.props.className} style={style} {...id}> - {this.props.children} - </div> - ); - } -} - -Tab.propTypes = { - display: PropTypes.bool, - className: PropTypes.string, - id: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) -}; - -Tab.defaultProps = { - display: false, - className: '', - id: '' -}; - export class Tabs extends React.Component { /** PROPERTIES * active: index of active menu (must be > menu.length). |