diff options
Diffstat (limited to 'www/app/components/main_panel')
-rw-r--r-- | www/app/components/main_panel/account_list.js | 113 | ||||
-rw-r--r-- | www/app/components/main_panel/email_section.js | 0 | ||||
-rw-r--r-- | www/app/components/main_panel/index.js | 57 | ||||
-rw-r--r-- | www/app/components/main_panel/main_panel.less | 208 | ||||
-rw-r--r-- | www/app/components/main_panel/section_layout.js | 59 | ||||
-rw-r--r-- | www/app/components/main_panel/user_section.js | 71 | ||||
-rw-r--r-- | www/app/components/main_panel/vpn_section.js | 0 |
7 files changed, 508 insertions, 0 deletions
diff --git a/www/app/components/main_panel/account_list.js b/www/app/components/main_panel/account_list.js new file mode 100644 index 00000000..d0ef092f --- /dev/null +++ b/www/app/components/main_panel/account_list.js @@ -0,0 +1,113 @@ +import React from 'react' +import {Button, ButtonGroup, ButtonToolbar, Glyphicon} from 'react-bootstrap' + +import App from 'app' +import Account from 'models/account' + +export default class AccountList extends React.Component { + + static get defaultProps() {return{ + account: null, + accounts: [], + onAdd: null, + onRemove: null, + onSelect: null + }} + + constructor(props) { + super(props) + + this.state = { + mode: 'expanded' + } + + // prebind: + this.select = this.select.bind(this) + this.add = this.add.bind(this) + this.remove = this.remove.bind(this) + this.expand = this.expand.bind(this) + this.collapse = this.collapse.bind(this) + } + + select(e) { + let account = this.props.accounts.find( + account => account.id == e.currentTarget.dataset.id + ) + if (this.props.onSelect) { + this.props.onSelect(account) + } + } + + add() { + App.show('wizard') + } + + remove() { + } + + expand() { + this.setState({mode: 'expanded'}) + } + + collapse() { + this.setState({mode: 'collapsed'}) + } + + render() { + let style = {} + let expandButton = null + let plusminusButtons = null + + if (this.state.mode == 'expanded') { + expandButton = ( + <Button onClick={this.collapse} className="expander btn-inverse btn-flat pull-right"> + <Glyphicon glyph="triangle-left" /> + </Button> + ) + plusminusButtons = ( + <ButtonGroup style={style}> + <Button onClick={this.add} className="btn-inverse"> + <Glyphicon glyph="plus" /> + </Button> + <Button disabled={this.props.account == null} onClick={this.remove} className="btn-inverse"> + <Glyphicon glyph="minus" /> + </Button> + </ButtonGroup> + ) + } else { + style.width = '60px' + expandButton = ( + <Button onClick={this.expand} className="expander btn-inverse btn-flat pull-right"> + <Glyphicon glyph="triangle-right" /> + </Button> + ) + } + + let items = this.props.accounts.map((account, i) => { + let className = account == this.props.account ? 'active' : 'inactive' + return ( + <li key={i} className={className} onClick={this.select} data-id={account.id}> + <span className="username">{account.userpart}</span> + <span className="domain">{account.domain}</span> + <span className="arc top"></span> + <span className="arc bottom"></span> + </li> + ) + }) + + + return ( + <div className="accounts" style={style}> + <ul> + {items} + </ul> + <ButtonToolbar> + {plusminusButtons} + {expandButton} + </ButtonToolbar> + </div> + ) + } + + +} diff --git a/www/app/components/main_panel/email_section.js b/www/app/components/main_panel/email_section.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/www/app/components/main_panel/email_section.js diff --git a/www/app/components/main_panel/index.js b/www/app/components/main_panel/index.js new file mode 100644 index 00000000..910d58b6 --- /dev/null +++ b/www/app/components/main_panel/index.js @@ -0,0 +1,57 @@ +// +// The main panel manages the current account and the list of available accounts +// +// It displays multiple sections, one for each service. +// + +import React from 'react' +import App from 'app' +import Login from 'components/login' + +import './main_panel.less' +import AccountList from './account_list' +import UserSection from './user_section' + +export default class MainPanel extends React.Component { + + static get defaultProps() {return{ + initialAccount: null + }} + + constructor(props) { + super(props) + this.state = { + account: null, + accounts: [] + } + this.activateAccount = this.activateAccount.bind(this) + } + + componentWillMount() { + if (this.props.initialAccount) { + this.setState({ + account: this.props.initialAccount, + accounts: [this.props.initialAccount] + }) + } + } + + activateAccount(account) { + this.setState({ + account: account, + accounts: [account] + }) + } + + render() { + return ( + <div className="main-panel"> + <AccountList account={this.state.account} accounts={this.state.accounts} onSelect={this.activateAccount} /> + <div className="body"> + <UserSection account={this.state.account} onLogin={this.activateAccount} onLogout={this.activateAccount}/> + </div> + </div> + ) + } + +} diff --git a/www/app/components/main_panel/main_panel.less b/www/app/components/main_panel/main_panel.less new file mode 100644 index 00000000..3172bba5 --- /dev/null +++ b/www/app/components/main_panel/main_panel.less @@ -0,0 +1,208 @@ +// The space around account entries: +@accounts-padding: 8px; +@accounts-width: 200px; + +// +// LAYOUT +// + +.main-panel { + position: absolute; + height: 100%; + width: 100%; + display: flex; + flex-direction: row; + + > .body { + flex: 1 1 auto; + overflow: auto; + } + + .accounts { + flex: 0 0 auto; + overflow-y: auto; + overflow-x: hidden; + + display: flex; + flex-direction: column; + ul { + flex: 1 1 1000px; + } + .btn-toolbar { + flex: 0 0 auto; + } + + } + +} + +// +// Style +// + +.main-panel > .body { + padding: 20px; +} + +.main-panel .accounts { + background-color: #333; + width: @accounts-width; + padding: @accounts-padding; + padding-right: 0px; +} + +.main-panel .accounts ul { + list-style: none; + margin: 0; + padding: 0; +} + +.main-panel .accounts li { + position: relative; + cursor: pointer; + color: white; + padding: 15px; + background-color: #444; + margin-bottom: @accounts-padding; + border-top-left-radius: @accounts-padding; + border-bottom-left-radius: @accounts-padding; + z-index: 100; +} + +.main-panel .accounts li span.domain { + display: block; + font-weight: bold; + //margin-left: 40px; +} + +.main-panel .accounts li span.username { + display: block; + //margin-left: 40px; + //line-height: 7px; + //margin-bottom: 4px; +} + +/*.main-panel .accounts li span.icon { + display: block; + height: 32px; + width: 32px; + background-color: #999; + float: left; +} +*/ + +.main-panel .accounts li.active { + background-color: white; + color: #333; +} + +.main-panel .accounts li.active span.arc { + display: block; + height: @accounts-padding; + width: @accounts-padding; + background-color: white; + position: absolute; + right: 0; +} + +.main-panel .accounts li.active span.arc.top { + top: 0; + margin-top: -@accounts-padding; +} +.main-panel .accounts li.active span.arc.bottom { + bottom: 0; + margin-bottom: -@accounts-padding; +} +.main-panel .accounts li.active span.arc:after { + display: block; + content: ""; + border-radius: 100%; + height: 0px; + width: 0px; + margin-left: -@accounts-padding; +} +.main-panel .accounts li.active span.arc.top:after { + border: @accounts-padding solid transparent; + border-right: @accounts-padding solid #333; + margin-top: -@accounts-padding; + transform: rotate(45deg); +} +.main-panel .accounts li.active span.arc.bottom:after { + border: @accounts-padding solid #333; +} + +.main-panel .accounts .btn.expander { + margin-right: @accounts-padding; +} + + +// +// SECTIONS +// + +@icon-size: 32px; +@status-size: 24px; +@section-padding: 10px; + +// service sections layout + +.main-panel .service-section { + display: flex; + flex-direction: row; + > .icon { + flex: 0 0 auto; + } + > .body { + flex: 1 1 auto; + } + > .buttons { + flex: 0 0 auto; + } + > .status { + flex: 0 0 auto; + display: flex; + align-items: center; + } +} + +.main-panel .service-section div { + //outline: 1px solid rgba(0,0,0,0.1); +} + +// service sections style + +.main-panel .service-section { + background: #f6f6f6; + border-radius: 4px; + padding: 10px; + &.wide-margin { + padding: 20px 20px 20px 10px; // arbitrary, looks nice + } + > .icon { + padding-right: @section-padding; + img { + width: @icon-size; + height: @icon-size; + } + } + > .body { + h1 { + margin: 0; + padding: 0; + font-size: @icon-size - 10; + line-height: @icon-size; + } + } + > .buttons { + padding-left: 10px; + } + > .status { + padding-left: @section-padding; + width: @section-padding + @status-size; + img { + width: @status-size; + height: @status-size; + } + } +} + diff --git a/www/app/components/main_panel/section_layout.js b/www/app/components/main_panel/section_layout.js new file mode 100644 index 00000000..e7c6f2ab --- /dev/null +++ b/www/app/components/main_panel/section_layout.js @@ -0,0 +1,59 @@ +// +// This is the layout for a service section in the main window. +// It does not do anything except for arrange items using css and html. +// + +import React from 'react' + +export default class SectionLayout extends React.Component { + + static get defaultProps() {return{ + icon: null, + buttons: null, + status: null, + className: "", + style: {} + }} + + constructor(props) { + super(props) + } + + render() { + let className = ["service-section", this.props.className].join(' ') + let status = null + let icon = null + let buttons = null + + if (this.props.status) { + status = ( + <div className="status"> + <img src={'img/' + this.props.status + '.svg' } /> + </div> + ) + } + if (this.props.icon) { + icon = ( + <div className="icon"> + <img src={'img/' + this.props.icon + '.svg'} /> + </div> + ) + } + if (this.props.buttons) + buttons = ( + <div className="buttons"> + {this.props.buttons} + </div> + ) + return( + <div className={className} style={this.props.style}> + {icon} + <div className="body"> + {this.props.children} + </div> + {buttons} + {status} + </div> + ) + } +} diff --git a/www/app/components/main_panel/user_section.js b/www/app/components/main_panel/user_section.js new file mode 100644 index 00000000..0b4ba136 --- /dev/null +++ b/www/app/components/main_panel/user_section.js @@ -0,0 +1,71 @@ +import React from 'react' +import { Button, Glyphicon, Alert } from 'react-bootstrap' +import SectionLayout from './section_layout' +import Login from 'components/login' +import Spinner from 'components/spinner' +import Account from 'models/account' + +import bitmask from 'lib/bitmask' + +export default class UserSection extends React.Component { + + static get defaultProps() {return{ + account: null, + onLogout: null, + onLogin: null + }} + + constructor(props) { + super(props) + this.state = { + error: null, + loading: false + } + this.logout = this.logout.bind(this) + } + + logout() { + this.setState({loading: true}) + this.props.account.logout().then( + account => { + this.setState({error: null, loading: false}) + if (this.props.onLogout) { + this.props.onLogout(account) + } + }, error => { + this.setState({error: error, loading: false}) + } + ) + } + + render () { + let message = null + if (this.state.error) { + // style may be: success, warning, danger, info + message = ( + <Alert bsStyle="danger">{this.state.error}</Alert> + ) + } + + if (this.props.account.authenticated) { + let button = null + if (this.state.loading) { + button = <Button disabled={true}><Spinner /></Button> + } else { + button = <Button onClick={this.logout}>Log Out</Button> + } + return ( + <SectionLayout icon="user" buttons={button} status="on"> + <h1>{this.props.account.address}</h1> + {message} + </SectionLayout> + ) + } else { + return ( + <SectionLayout icon="user" className="wide-margin"> + <Login onLogin={this.props.onLogin} domain={this.props.account.domain} /> + </SectionLayout> + ) + } + } +} diff --git a/www/app/components/main_panel/vpn_section.js b/www/app/components/main_panel/vpn_section.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/www/app/components/main_panel/vpn_section.js |