From 5c71a0f73717bffefb5e23a9e100adb62fc54a61 Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Sun, 7 Jul 2019 09:23:59 -0400 Subject: Updated the web interface to have 3 tabs - Results / Messages / Current - Updated dependencies in package-lock.json - Set default homepage to "." so that built pages are relative to "index.html". - Add module "helmet" to handle page title. - Simplified page loading (replace static function builder with direct call to a method with component as argument). - Move function loadGameFromDisk in a separate file. - Use React context to access page object. - Add a new React component "Navigation" and simplify page rendering. - Add ability to choose power for any kind of loaded game. In phase history: - Show messages from all past and currently displayed phase. - Display messages from past phase with gray background. - Show messages per protagonist in tabs. - Show message phase in message header - Display message wide (header left, body right). - Display short names for powers in message tabs header. - Add warn function to page component. - Messages from previous phase are displayed with gray color text. - Game registration password input is displayed only if required - On games page: - sorted by descending timestamp created. - In table, game ID is displayed with human readable created date. - Prevent messages from displaying twice. - Re-add checkbox "show orders" to display arrow orders on past maps. - Handle HTML break-lines
and remove all other HTML tags when displaying messages. - Use latest phase as current game phase when loading a game from disk. --- diplomacy/web/src/gui/core/action.jsx | 52 ++++++ diplomacy/web/src/gui/core/button.jsx | 52 ++++++ diplomacy/web/src/gui/core/content.jsx | 51 ------ diplomacy/web/src/gui/core/fancybox.jsx | 2 +- diplomacy/web/src/gui/core/forms.jsx | 2 +- diplomacy/web/src/gui/core/page.jsx | 277 +++++++++----------------------- diplomacy/web/src/gui/core/tab.jsx | 29 ++++ diplomacy/web/src/gui/core/tabs.jsx | 29 +--- diplomacy/web/src/gui/core/widgets.jsx | 102 ------------ 9 files changed, 211 insertions(+), 385 deletions(-) create mode 100644 diplomacy/web/src/gui/core/action.jsx create mode 100644 diplomacy/web/src/gui/core/button.jsx delete mode 100644 diplomacy/web/src/gui/core/content.jsx create mode 100644 diplomacy/web/src/gui/core/tab.jsx delete mode 100644 diplomacy/web/src/gui/core/widgets.jsx (limited to 'diplomacy/web/src/gui/core') diff --git a/diplomacy/web/src/gui/core/action.jsx b/diplomacy/web/src/gui/core/action.jsx new file mode 100644 index 0000000..73fe8cb --- /dev/null +++ b/diplomacy/web/src/gui/core/action.jsx @@ -0,0 +1,52 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from "react"; +import PropTypes from 'prop-types'; + + +export class Action extends React.Component { + // title + // isActive + // onClick + // See Button parameters. + + render() { + return ( +
+
+ {this.props.title} + {this.props.highlight !== null + && this.props.highlight !== undefined + && {this.props.highlight}} +
+
+ ); + } +} + +Action.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + highlight: PropTypes.any, + isActive: PropTypes.bool +}; + +Action.defaultProps = { + highlight: null, + isActive: false +}; 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.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 . -// ============================================================================== -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. , or
...
, 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 . // ============================================================================== 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 . // ============================================================================== 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 ; } //// 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', + , + messages + ); } - loadGame(gameInfo, messages) { - this.loadPage('game', gameInfo, messages); + loadGameFromDisk() { + loadGameFromDisk( + (game) => this.load( + `game: ${game.game_id}`, + , + {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 ( -
-
-
this.success()}> - {successMessage} -
-
this.info()}> - {infoMessage} -
-
this.error()}> - {errorMessage} -
-
- {((hasNavigation || this.channel) && ( -
-
{title}
-
- {(!hasNavigation && ( -
- - {this.channel.username} - - -
- )) || ( -
- -
- {content.navigation.map((nav, index) => { - const navTitle = nav[0]; - const navAction = nav[1]; - return {navTitle}; - })} -
-
- )} + +
+
+
this.success()}> + {successMessage} +
+
this.info()}> + {infoMessage} +
+
this.error()}> + {errorMessage}
- )) || ( -
{title}
- )} - {content.component} - {this.state.onFancyBox && ( - - {this.state.onFancyBox()} - - )} -
+ {this.state.body || Page.defaultPage()} + {this.state.onFancyBox && ( + + {this.state.onFancyBox()} + + )} +
+ ); } } 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 ( +
+ {this.props.children} +
+ ); + } +} + +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 . // ============================================================================== 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 ( -
- {this.props.children} -
- ); - } -} - -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). diff --git a/diplomacy/web/src/gui/core/widgets.jsx b/diplomacy/web/src/gui/core/widgets.jsx deleted file mode 100644 index 62a5eb4..0000000 --- a/diplomacy/web/src/gui/core/widgets.jsx +++ /dev/null @@ -1,102 +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 . -// ============================================================================== -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.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 - // isActive - // onClick - // See Button parameters. - - render() { - return ( -
-
- {this.props.title} - {this.props.highlight !== null - && this.props.highlight !== undefined - && {this.props.highlight}} -
-
- ); - } -} - -Action.propTypes = { - title: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - highlight: PropTypes.any, - isActive: PropTypes.bool -}; - -Action.defaultProps = { - highlight: null, - isActive: false -}; -- cgit v1.2.3