diff options
Diffstat (limited to 'diplomacy/web/src/gui/components')
-rw-r--r-- | diplomacy/web/src/gui/components/action.jsx | 52 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/button.jsx | 52 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/delete_button.jsx | 43 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/fancybox.jsx | 59 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/forms.jsx | 116 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/help.jsx | 13 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/layouts.jsx | 55 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/message_view.jsx | 67 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/navigation.jsx | 61 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/page_context.jsx | 3 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/power_orders.jsx | 77 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/power_orders_actions_bar.js | 26 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/tab.jsx | 29 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/table.jsx | 112 | ||||
-rw-r--r-- | diplomacy/web/src/gui/components/tabs.jsx | 69 |
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: {} +}; |