aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/web/src/gui/components
diff options
context:
space:
mode:
Diffstat (limited to 'diplomacy/web/src/gui/components')
-rw-r--r--diplomacy/web/src/gui/components/action.jsx52
-rw-r--r--diplomacy/web/src/gui/components/button.jsx52
-rw-r--r--diplomacy/web/src/gui/components/delete_button.jsx43
-rw-r--r--diplomacy/web/src/gui/components/fancybox.jsx59
-rw-r--r--diplomacy/web/src/gui/components/forms.jsx116
-rw-r--r--diplomacy/web/src/gui/components/help.jsx13
-rw-r--r--diplomacy/web/src/gui/components/layouts.jsx55
-rw-r--r--diplomacy/web/src/gui/components/message_view.jsx67
-rw-r--r--diplomacy/web/src/gui/components/navigation.jsx61
-rw-r--r--diplomacy/web/src/gui/components/page_context.jsx3
-rw-r--r--diplomacy/web/src/gui/components/power_orders.jsx77
-rw-r--r--diplomacy/web/src/gui/components/power_orders_actions_bar.js26
-rw-r--r--diplomacy/web/src/gui/components/tab.jsx29
-rw-r--r--diplomacy/web/src/gui/components/table.jsx112
-rw-r--r--diplomacy/web/src/gui/components/tabs.jsx69
15 files changed, 834 insertions, 0 deletions
diff --git a/diplomacy/web/src/gui/components/action.jsx b/diplomacy/web/src/gui/components/action.jsx
new file mode 100644
index 0000000..73fe8cb
--- /dev/null
+++ b/diplomacy/web/src/gui/components/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 <https://www.gnu.org/licenses/>.
+// ==============================================================================
+import React from "react";
+import PropTypes from 'prop-types';
+
+
+export class Action extends React.Component {
+ // title
+ // isActive
+ // onClick
+ // See Button parameters.
+
+ render() {
+ return (
+ <div className="action nav-item" onClick={this.props.onClick}>
+ <div
+ className={'nav-link' + (this.props.isActive ? ' active' : '') + (this.props.highlight !== null ? ' updated' : '')}>
+ {this.props.title}
+ {this.props.highlight !== null
+ && this.props.highlight !== undefined
+ && <span className={'update'}>{this.props.highlight}</span>}
+ </div>
+ </div>
+ );
+ }
+}
+
+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/components/button.jsx b/diplomacy/web/src/gui/components/button.jsx
new file mode 100644
index 0000000..0d5dadd
--- /dev/null
+++ b/diplomacy/web/src/gui/components/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/components/delete_button.jsx b/diplomacy/web/src/gui/components/delete_button.jsx
new file mode 100644
index 0000000..59141fd
--- /dev/null
+++ b/diplomacy/web/src/gui/components/delete_button.jsx
@@ -0,0 +1,43 @@
+import React from "react";
+import {Button} from "./button";
+import PropTypes from "prop-types";
+
+export class DeleteButton extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {step: 0};
+ this.onClick = this.onClick.bind(this);
+ }
+
+ onClick() {
+ this.setState({step: this.state.step + 1}, () => {
+ if (this.state.step === 2)
+ this.props.onClick();
+ });
+ }
+
+ render() {
+ let title = '';
+ let color = '';
+ if (this.state.step === 0) {
+ title = this.props.title;
+ color = 'secondary';
+ } else if (this.state.step === 1) {
+ title = this.props.confirmTitle;
+ color = 'danger';
+ } else if (this.state.step === 2) {
+ title = this.props.waitingTitle;
+ color = 'danger';
+ }
+ return (
+ <Button title={title} color={color} onClick={this.onClick} small={true} large={true}/>
+ );
+ }
+}
+
+DeleteButton.propTypes = {
+ title: PropTypes.string.isRequired,
+ confirmTitle: PropTypes.string.isRequired,
+ waitingTitle: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired
+};
diff --git a/diplomacy/web/src/gui/components/fancybox.jsx b/diplomacy/web/src/gui/components/fancybox.jsx
new file mode 100644
index 0000000..66a1efe
--- /dev/null
+++ b/diplomacy/web/src/gui/components/fancybox.jsx
@@ -0,0 +1,59 @@
+// ==============================================================================
+// 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';
+import {Button} from "./button";
+
+const TIMES = '\u00D7';
+
+export class FancyBox extends React.Component {
+ // open-tag (<FancyBox></FancyBox>)
+ // PROPERTIES
+ // title
+ // onClose
+ render() {
+ return (
+ <div className={'fancy-wrapper'} onClick={this.props.onClose}>
+ <div className={'fancy-box container'} onClick={(event) => {
+ if (!event)
+ event = window.event;
+ if (event.hasOwnProperty('cancelBubble'))
+ event.cancelBubble = true;
+ if (event.stopPropagation)
+ event.stopPropagation();
+ }}>
+ <div className={'row fancy-bar'}>
+ <div className={'col-11 align-self-center fancy-title'}>{this.props.title}</div>
+ <div className={'col-1 fancy-button'}>
+ <Button title={TIMES} color={'danger'} onClick={this.props.onClose}/>
+ </div>
+ </div>
+ <div className={'row'}>
+ <div className={'col fancy-content'}>{this.props.children}</div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+
+FancyBox.propTypes = {
+ title: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
+};
diff --git a/diplomacy/web/src/gui/components/forms.jsx b/diplomacy/web/src/gui/components/forms.jsx
new file mode 100644
index 0000000..da7250d
--- /dev/null
+++ b/diplomacy/web/src/gui/components/forms.jsx
@@ -0,0 +1,116 @@
+// ==============================================================================
+// 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 {UTILS} from "../../diplomacy/utils/utils";
+import {Button} from "./button";
+
+export class Forms {
+ static createOnChangeCallback(component, callback) {
+ return (event) => {
+ const value = UTILS.html.isCheckBox(event.target) ? event.target.checked : event.target.value;
+ const fieldName = UTILS.html.isRadioButton(event.target) ? event.target.name : event.target.id;
+ const update = {[fieldName]: value};
+ const state = Object.assign({}, component.state, update);
+ if (callback)
+ callback(state);
+ component.setState(state);
+ };
+ }
+
+ static createOnSubmitCallback(component, callback, resetState) {
+ return (event) => {
+ if (callback)
+ callback(Object.assign({}, component.state));
+ if (resetState)
+ component.setState(resetState);
+ event.preventDefault();
+ };
+ }
+
+ static createOnResetCallback(component, onChangeCallback, resetState) {
+ return (event) => {
+ if (onChangeCallback)
+ onChangeCallback(resetState);
+ component.setState(resetState);
+ if (event && event.preventDefault)
+ event.preventDefault();
+ };
+ }
+
+ static getValue(fieldValues, fieldName, defaultValue) {
+ return fieldValues.hasOwnProperty(fieldName) ? fieldValues[fieldName] : defaultValue;
+ }
+
+ static createReset(title, large, onReset) {
+ return <Button key={'reset'} title={title || 'reset'} onClick={onReset} pickEvent={true} large={large}/>;
+ }
+
+ static createSubmit(title, large, onSubmit) {
+ return <Button key={'submit'} title={title || 'submit'} onClick={onSubmit} pickEvent={true} large={large}/>;
+ }
+
+ static createButton(title, fn, color, large) {
+ const wrapFn = (event) => {
+ fn();
+ event.preventDefault();
+ };
+ return <Button large={large} key={title} color={color} title={title} onClick={wrapFn} pickEvent={true}/>;
+ }
+
+ static createCheckbox(id, title, value, onChange) {
+ const input = <input className={'form-check-input'} key={id} type={'checkbox'} id={id} checked={value}
+ onChange={onChange}/>;
+ const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title}</label>;
+ return [input, label];
+ }
+
+ static createRadio(name, value, title, currentValue, onChange) {
+ const id = `[${name}][${value}]`;
+ const input = <input className={'form-check-input'} key={id} type={'radio'}
+ name={name} value={value} checked={currentValue === value}
+ id={id} onChange={onChange}/>;
+ const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title || value}</label>;
+ return [input, label];
+ }
+
+ static createRow(label, input) {
+ return (
+ <div className={'form-group row'}>
+ {label}
+ <div className={'col'}>{input}</div>
+ </div>
+ );
+ }
+
+ static createLabel(htmFor, title, className) {
+ return <label className={className} htmlFor={htmFor}>{title}</label>;
+ }
+
+ static createColLabel(htmlFor, title) {
+ return Forms.createLabel(htmlFor, title, 'col');
+ }
+
+ static createSelectOptions(values, none) {
+ const options = values.slice();
+ const components = options.map((option, index) => <option key={index} value={option}>{option}</option>);
+ if (none) {
+ components.splice(0, 0, [<option key={-1} value={''}>{none === true ? '(none)' : `${none}`}</option>]);
+ }
+ return components;
+ }
+}
+
diff --git a/diplomacy/web/src/gui/components/help.jsx b/diplomacy/web/src/gui/components/help.jsx
new file mode 100644
index 0000000..1ec1a54
--- /dev/null
+++ b/diplomacy/web/src/gui/components/help.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+export function Help() {
+ return (
+ <div>
+ <p>When building an order, press <strong>ESC</strong> to reset build.</p>
+ <p>Press letter associated to an order type to start building an order of this type.
+ <br/> Order type letter is indicated in order type name after order type radio button.
+ </p>
+ <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p>
+ </div>
+ );
+}
diff --git a/diplomacy/web/src/gui/components/layouts.jsx b/diplomacy/web/src/gui/components/layouts.jsx
new file mode 100644
index 0000000..78189e4
--- /dev/null
+++ b/diplomacy/web/src/gui/components/layouts.jsx
@@ -0,0 +1,55 @@
+// ==============================================================================
+// 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';
+
+class Div extends React.Component {
+ getClassName() {
+ return '';
+ }
+
+ render() {
+ return (
+ <div className={this.getClassName() + (this.props.className ? ' ' + this.props.className : '')}>
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+Div.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
+};
+
+export class Bar extends Div {
+ getClassName() {
+ return 'bar';
+ }
+}
+
+export class Row extends Div {
+ getClassName() {
+ return 'row';
+ }
+}
+
+export class Col extends Div {
+ getClassName() {
+ return 'col';
+ }
+}
diff --git a/diplomacy/web/src/gui/components/message_view.jsx b/diplomacy/web/src/gui/components/message_view.jsx
new file mode 100644
index 0000000..927ff52
--- /dev/null
+++ b/diplomacy/web/src/gui/components/message_view.jsx
@@ -0,0 +1,67 @@
+// ==============================================================================
+// 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 MessageView extends React.Component {
+ // message
+ render() {
+ const message = this.props.message;
+ const owner = this.props.owner;
+ const id = this.props.id ? {id: this.props.id} : {};
+ const messagesLines = message.message.replace('\r\n', '\n')
+ .replace('\r', '\n')
+ .replace('<br>', '\n')
+ .replace('<br/>', '\n')
+ .split('\n');
+ let onClick = null;
+ const classNames = ['game-message', 'row'];
+ if (owner === message.sender)
+ classNames.push('message-sender');
+ else {
+ classNames.push('message-recipient');
+ if (message.read || this.props.read)
+ classNames.push('message-read');
+ onClick = this.props.onClick ? {onClick: () => this.props.onClick(message)} : {};
+ }
+ return (
+ <div className={'game-message-wrapper' + (
+ this.props.phase && this.props.phase !== message.phase ? ' other-phase' : ' new-phase')}
+ {...id}>
+ <div className={classNames.join(' ')} {...onClick}>
+ <div className="message-header col-md-auto text-md-right text-center">
+ {message.phase}
+ </div>
+ <div className="message-content col-md">
+ {messagesLines.map((line, lineIndex) => <div key={lineIndex}>{
+ line.replace(/(<([^>]+)>)/ig, "")
+ }</div>)}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+MessageView.propTypes = {
+ message: PropTypes.object,
+ phase: PropTypes.string,
+ owner: PropTypes.string,
+ onClick: PropTypes.func,
+ id: PropTypes.string,
+ read: PropTypes.bool
+};
diff --git a/diplomacy/web/src/gui/components/navigation.jsx b/diplomacy/web/src/gui/components/navigation.jsx
new file mode 100644
index 0000000..5d961bc
--- /dev/null
+++ b/diplomacy/web/src/gui/components/navigation.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import Octicon, {Person} from "@githubprimer/octicons-react";
+import PropTypes from "prop-types";
+
+export class Navigation extends React.Component {
+ render() {
+ const hasNavigation = this.props.navigation && this.props.navigation.length;
+ if (hasNavigation) {
+ return (
+ <div className={'title row'}>
+ <div className={'col align-self-center'}>
+ <strong>{this.props.title}</strong>
+ {this.props.afterTitle ? this.props.afterTitle : ''}
+ </div>
+ <div className={'col-sm-1'}>
+ {(!hasNavigation && (
+ <div className={'float-right'}>
+ <strong>
+ <u className={'mr-2'}>{this.props.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.props.username && (
+ <span>
+ <u className={'mr-2'}>{this.props.username}</u>
+ <Octicon icon={Person}/>
+ </span>
+ )) || 'Menu'}
+ </button>
+ <div className="dropdown-menu dropdown-menu-right"
+ aria-labelledby="dropdownMenuButton">
+ {this.props.navigation.map((nav, index) => {
+ const navTitle = nav[0];
+ const navAction = nav[1];
+ return <span key={index} className="dropdown-item"
+ onClick={navAction}>{navTitle}</span>;
+ })}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div className={'title'}><strong>{this.props.title}</strong></div>
+ );
+ }
+}
+
+Navigation.propTypes = {
+ title: PropTypes.string.isRequired,
+ afterTitle: PropTypes.object,
+ navigation: PropTypes.array,
+ username: PropTypes.string,
+};
diff --git a/diplomacy/web/src/gui/components/page_context.jsx b/diplomacy/web/src/gui/components/page_context.jsx
new file mode 100644
index 0000000..cfb8252
--- /dev/null
+++ b/diplomacy/web/src/gui/components/page_context.jsx
@@ -0,0 +1,3 @@
+import React from "react";
+
+export const PageContext = React.createContext(null);
diff --git a/diplomacy/web/src/gui/components/power_orders.jsx b/diplomacy/web/src/gui/components/power_orders.jsx
new file mode 100644
index 0000000..b702a9d
--- /dev/null
+++ b/diplomacy/web/src/gui/components/power_orders.jsx
@@ -0,0 +1,77 @@
+// ==============================================================================
+// 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';
+import {Button} from "./button";
+
+export class PowerOrders extends React.Component {
+ render() {
+ const orderEntries = this.props.orders ? Object.entries(this.props.orders) : null;
+ let display = null;
+ if (orderEntries) {
+ if (orderEntries.length) {
+ orderEntries.sort((a, b) => a[1].order.localeCompare(b[1].order));
+ display = (
+ <div className={'container order-list'}>
+ {orderEntries.map((entry, index) => (
+ <div
+ className={`row order-entry entry-${1 + index % 2} ` + (entry[1].local ? 'local' : 'server')}
+ key={index}>
+ <div className={'col align-self-center order'}>
+ <span className={'order-string'}>{entry[1].order}</span>
+ {entry[1].local ? '' : <span className={'order-mark'}> [S]</span>}
+ </div>
+ <div className={'col remove-button'}>
+ <Button title={'-'} onClick={() => this.props.onRemove(this.props.name, entry[1])}/>
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+ } else if (this.props.serverCount === 0) {
+ display = (<div className={'empty-orders'}>Empty orders set</div>);
+ } else {
+ display = (<div className={'empty-orders'}>Local empty orders set</div>);
+ }
+ } else {
+ if (this.props.serverCount < 0) {
+ display = <div className={'no-orders'}>No orders!</div>;
+ } else {
+ display = <div className={'empty-orders'}>Asking to unset orders</div>;
+ }
+ }
+ return (
+ <div className={'power-orders'}>
+ <div className={'title'}>
+ <span className={'name'}>{this.props.name}</span>
+ <span className={this.props.wait ? 'wait' : 'no-wait'}>
+ {(this.props.wait ? ' ' : ' not') + ' waiting'}
+ </span>
+ </div>
+ {display}
+ </div>
+ );
+ }
+}
+
+PowerOrders.propTypes = {
+ wait: PropTypes.bool,
+ name: PropTypes.string,
+ orders: PropTypes.object,
+ serverCount: PropTypes.number,
+ onRemove: PropTypes.func,
+};
diff --git a/diplomacy/web/src/gui/components/power_orders_actions_bar.js b/diplomacy/web/src/gui/components/power_orders_actions_bar.js
new file mode 100644
index 0000000..2e33a6e
--- /dev/null
+++ b/diplomacy/web/src/gui/components/power_orders_actions_bar.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import {Button} from "./button";
+import {Bar} from "./layouts";
+import PropTypes from 'prop-types';
+
+export class PowerOrdersActionBar extends React.Component {
+ render() {
+ return (
+ <Bar className={'p-2'}>
+ <strong className={'mr-4'}>Orders:</strong>
+ <Button title={'reset'} onClick={this.props.onReset}/>
+ <Button title={'delete all'} onClick={this.props.onDeleteAll}/>
+ <Button color={'primary'} title={'update'} onClick={this.props.onUpdate}/>
+ {(!this.props.onProcess &&
+ <Button color={'danger'} title={'process game'} onClick={this.props.onProcess}/>) || ''}
+ </Bar>
+ );
+ }
+}
+
+PowerOrdersActionBar.propTypes = {
+ onReset: PropTypes.func.isRequired,
+ onDeleteAll: PropTypes.func.isRequired,
+ onUpdate: PropTypes.func.isRequired,
+ onProcess: PropTypes.func
+};
diff --git a/diplomacy/web/src/gui/components/tab.jsx b/diplomacy/web/src/gui/components/tab.jsx
new file mode 100644
index 0000000..f1ad4aa
--- /dev/null
+++ b/diplomacy/web/src/gui/components/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/components/table.jsx b/diplomacy/web/src/gui/components/table.jsx
new file mode 100644
index 0000000..cb729e7
--- /dev/null
+++ b/diplomacy/web/src/gui/components/table.jsx
@@ -0,0 +1,112 @@
+// ==============================================================================
+// 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/>.
+// ==============================================================================
+//// Tables.
+
+import React from "react";
+import PropTypes from 'prop-types';
+
+class DefaultWrapper {
+ constructor(data) {
+ this.data = data;
+ this.get = this.get.bind(this);
+ }
+
+ get(fieldName) {
+ return this.data[fieldName];
+ }
+}
+
+function defaultWrapper(data) {
+ return new DefaultWrapper(data);
+}
+
+export class Table extends React.Component {
+ // className
+ // caption
+ // columns : {name: [title, order]}
+ // data: [objects with expected column names]
+ // wrapper: (optional) function to use to wrap one data entry into an object before accessing fields.
+ // Must return an instance with a method get(name).
+ // If provided: wrapper(data_entry).get(field_name)
+ // else: data_entry[field_name]
+
+ constructor(props) {
+ super(props);
+ if (!this.props.wrapper)
+ this.props.wrapper = defaultWrapper;
+ }
+
+ static getHeader(columns) {
+ const header = [];
+ for (let entry of Object.entries(columns)) {
+ const name = entry[0];
+ const title = entry[1][0];
+ const order = entry[1][1];
+ header.push([order, name, title]);
+ }
+ header.sort((a, b) => {
+ let t = a[0] - b[0];
+ if (t === 0)
+ t = a[1].localeCompare(b[1]);
+ if (t === 0)
+ t = a[2].localeCompare(b[2]);
+ return t;
+ });
+ return header;
+ }
+
+ static getHeaderLine(header) {
+ return (
+ <thead className={'thead-light'}>
+ <tr>{header.map((column, colIndex) => <th key={colIndex}>{column[2]}</th>)}</tr>
+ </thead>
+ );
+ }
+
+ static getBodyRow(header, row, rowIndex, wrapper) {
+ const wrapped = wrapper(row);
+ return (<tr key={rowIndex}>
+ {header.map((headerColumn, colIndex) => <td className={'align-middle'}
+ key={colIndex}>{wrapped.get(headerColumn[1])}</td>)}
+ </tr>);
+ }
+
+ static getBodyLines(header, data, wrapper) {
+ return (<tbody>{data.map((row, rowIndex) => Table.getBodyRow(header, row, rowIndex, wrapper))}</tbody>);
+ }
+
+ render() {
+ const header = Table.getHeader(this.props.columns);
+ return (
+ <div className={'table-responsive'}>
+ <table className={this.props.className}>
+ <caption>{this.props.caption} ({this.props.data.length})</caption>
+ {Table.getHeaderLine(header)}
+ {Table.getBodyLines(header, this.props.data, this.props.wrapper)}
+ </table>
+ </div>
+ );
+ }
+}
+
+Table.propTypes = {
+ wrapper: PropTypes.func,
+ columns: PropTypes.object,
+ className: PropTypes.string,
+ caption: PropTypes.string,
+ data: PropTypes.array
+};
diff --git a/diplomacy/web/src/gui/components/tabs.jsx b/diplomacy/web/src/gui/components/tabs.jsx
new file mode 100644
index 0000000..a3f6b9b
--- /dev/null
+++ b/diplomacy/web/src/gui/components/tabs.jsx
@@ -0,0 +1,69 @@
+// ==============================================================================
+// 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 {Action} from "./action";
+import PropTypes from 'prop-types';
+
+export class Tabs extends React.Component {
+ /** PROPERTIES
+ * active: index of active menu (must be > menu.length).
+ * highlights: dictionary mapping a menu indice to a highlight message
+ * onChange: callback(index): receive index of menu to display.
+ * **/
+
+ generateTabAction(tabTitle, tabId, isActive, onChange, highlight) {
+ return <Action isActive={isActive}
+ title={tabTitle}
+ onClick={() => onChange(tabId)}
+ highlight={highlight}
+ key={tabId}/>;
+ }
+
+ render() {
+ if (!this.props.menu.length)
+ throw new Error(`No tab menu given.`);
+ if (this.props.menu.length !== this.props.titles.length)
+ throw new Error(`Menu length (${this.props.menu.length}) != titles length (${this.props.titles.length})`);
+ if (this.props.active && !this.props.menu.includes(this.props.active))
+ throw new Error(`Invalid active tab name, got ${this.props.active}, expected one of: ${this.props.menu.join(', ')}`);
+ const active = this.props.active || this.props.menu[0];
+ return (
+ <div className={'tabs mb-3'}>
+ <nav className={'tabs-bar nav nav-tabs justify-content-center mb-3'}>
+ {this.props.menu.map((tabName, index) => this.generateTabAction(
+ this.props.titles[index], tabName, active === tabName, this.props.onChange,
+ (this.props.highlights.hasOwnProperty(tabName) && this.props.highlights[tabName]) || null
+ ))}
+ </nav>
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+Tabs.propTypes = {
+ menu: PropTypes.arrayOf(PropTypes.string).isRequired, // tab names
+ titles: PropTypes.arrayOf(PropTypes.string).isRequired, // tab titles
+ onChange: PropTypes.func.isRequired, // callback(tab name)
+ children: PropTypes.array.isRequired,
+ active: PropTypes.string, // current active tab name
+ highlights: PropTypes.object, // {tab name => highligh message (optional)}
+};
+
+Tabs.defaultProps = {
+ highlights: {}
+};