From c2d8b781368f6a4641fec1071e28322d63d7ea0e Mon Sep 17 00:00:00 2001 From: ddejongh Date: Thu, 18 Jun 2015 19:03:03 +0200 Subject: [PATCH] settings --- gulpfile.js | 2 +- .../collapsible_paragraph.js | 54 ++++ js/components/ascribe_forms/form.js | 137 +++++++++ js/components/ascribe_forms/property.js | 107 +++++++ js/components/settings_container.js | 263 ++++++------------ js/constants/api_urls.js | 1 + sass/ascribe_settings.scss | 38 ++- 7 files changed, 423 insertions(+), 179 deletions(-) create mode 100644 js/components/ascribe_collapsible/collapsible_paragraph.js create mode 100644 js/components/ascribe_forms/form.js create mode 100644 js/components/ascribe_forms/property.js diff --git a/gulpfile.js b/gulpfile.js index a055e765..5ab28046 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -63,7 +63,7 @@ gulp.task('js:build', function() { bundle(false); }); -gulp.task('serve', ['browser-sync', 'run-server', 'lint:watch', 'sass:build', 'sass:watch', 'copy'], function() { +gulp.task('serve', ['browser-sync', 'run-server', 'sass:build', 'sass:watch', 'copy'], function() { bundle(true); }); diff --git a/js/components/ascribe_collapsible/collapsible_paragraph.js b/js/components/ascribe_collapsible/collapsible_paragraph.js new file mode 100644 index 00000000..5156c306 --- /dev/null +++ b/js/components/ascribe_collapsible/collapsible_paragraph.js @@ -0,0 +1,54 @@ +'use strict'; + +import React from 'react'; + +import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; + +import classNames from 'classnames'; + + +const CollapsibleParagraph = React.createClass({ + + propTypes: { + title: React.PropTypes.string, + children: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), + iconName: React.PropTypes.string + }, + + mixins: [CollapsibleMixin], + + getCollapsibleDOMNode(){ + return React.findDOMNode(this.refs.panel); + }, + + getCollapsibleDimensionValue(){ + return React.findDOMNode(this.refs.panel).scrollHeight; + }, + + handleToggle(e){ + e.preventDefault(); + this.setState({expanded: !this.state.expanded}); + }, + + render() { + let styles = this.getCollapsibleClassSet(); + let text = this.isExpanded() ? '-' : '+'; + return ( +
+
+
+ {text} {this.props.title} +
+
+ {this.props.children} +
+
+
+ ); + } +}); + +export default CollapsibleParagraph; \ No newline at end of file diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js new file mode 100644 index 00000000..8d25531b --- /dev/null +++ b/js/components/ascribe_forms/form.js @@ -0,0 +1,137 @@ +'use strict'; + +import React from 'react'; +import ReactAddons from 'react/addons'; + +import Button from 'react-bootstrap/lib/Button'; + +import requests from '../../utils/requests'; +import AlertDismissable from './alert'; + +let Form = React.createClass({ + propTypes: { + children: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]) + }, + + getInitialState() { + return { + edited: false, + submitted: false, + errors: [] + }; + }, + reset(){ + for (let ref in this.refs){ + if (typeof this.refs[ref].reset === 'function'){ + this.refs[ref].reset(); + } + } + }, + submit(event){ + if (event) { + event.preventDefault(); + } + this.setState({submitted: true}); + this.clearErrors(); + let action = (this.httpVerb && this.httpVerb()) || 'post'; + this[action](); + }, + post(){ + requests + .post(this.props.url, { body: this.getFormData() }) + .then(this.handleSuccess) + .catch(this.handleError); + }, + + getFormData(){ + let data = {}; + for (let ref in this.refs){ + data[this.refs[ref].props.name] = this.refs[ref].state.value; + } + return data; + }, + + handleChangeChild(){ + this.setState({edited: true}); + }, + handleSuccess(response){ + if ('handleSuccess' in this.props){ + this.props.handleSuccess(response); + } + for (var ref in this.refs){ + if ('handleSuccess' in this.refs[ref]){ + this.refs[ref].handleSuccess(); + } + } + this.setState({edited: false}); + }, + handleError(err){ + if (err.json) { + for (var input in err.json.errors){ + if (this.refs && this.refs[input] && this.refs[input].state) { + this.refs[input].setErrors( err.json.errors[input]); + } else { + this.setState({errors: this.state.errors.concat(err.json.errors[input])}); + } + } + } + else { + this.setState({errors: ['Something went wrong, please try again later']}); + } + this.setState({submitted: false}); + }, + clearErrors(){ + for (var ref in this.refs){ + if ('clearError' in this.refs[ref]){ + this.refs[ref].clearErrors(); + } + } + this.setState({errors: []}); + }, + getButtons() { + let buttons = null; + if (this.state.edited){ + buttons = ( +
+ + +
+ ); + + } + return buttons; + }, + getErrors() { + let errors = null; + if (this.state.errors.length > 0){ + errors = this.state.errors.map((error) => { + return ; + }); + } + return errors; + }, + renderChildren() { + return ReactAddons.Children.map(this.props.children, (child) => { + return ReactAddons.addons.cloneWithProps(child, { + handleChange: this.handleChangeChild, + ref: child.props.name + }); + }); + }, + render() { + return ( +
+ {this.getErrors()} + {this.renderChildren()} + {this.getButtons()} +
+ + ); + } +}); + + +export default Form; \ No newline at end of file diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js new file mode 100644 index 00000000..7e27c1fe --- /dev/null +++ b/js/components/ascribe_forms/property.js @@ -0,0 +1,107 @@ +'use strict'; + +import React from 'react'; +import ReactAddons from 'react/addons'; + +let Property = React.createClass({ + propTypes: { + editable: React.PropTypes.bool, + label: React.PropTypes.string, + value: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]), + handleChange: React.PropTypes.func + }, + + getDefaultProps() { + return { + editable: true + }; + }, + + getInitialState() { + return { + initialValue: null, + value: null, + isFocused: false, + errors: null + }; + }, + componentWillReceiveProps(){ + this.setState({ + initialValue: this.refs.input.getDOMNode().defaultValue, + value: this.refs.input.getDOMNode().value + }); + }, + reset(){ + this.setState({value: this.state.initialValue}); + this.refs.input.getDOMNode().value = this.state.initialValue; + }, + + handleChange(event) { + this.props.handleChange(event); + this.setState({value: event.target.value}); + }, + handleFocus() { + this.refs.input.getDOMNode().focus(); + this.setState({ + isFocused: true + }); + }, + handleSuccess(){ + this.setState({ + isFocused: false, + errors: null + }); + }, + setErrors(errors){ + this.setState({ + errors: errors.map((error) => { + return {error}; + }) + }); + }, + clearErrors(){ + this.setState({errors: null}); + }, + getClassName() { + if(!this.props.editable){ + return 'is-fixed'; + } + if (this.state.errors){ + return 'is-error'; + } + if(this.state.isFocused) { + return 'is-focused'; + } else { + return ''; + } + }, + renderChildren() { + return ReactAddons.Children.map(this.props.children, (child) => { + return ReactAddons.addons.cloneWithProps(child, { + value: this.state.value, + onChange: this.handleChange, + disabled: !this.props.editable, + ref: 'input' + }); + }); + }, + render() { + + return ( +
+
+ {this.state.errors} + { this.props.label} + {this.renderChildren()} +
+
+ ); + } +}); + +export default Property; \ No newline at end of file diff --git a/js/components/settings_container.js b/js/components/settings_container.js index 7596fde0..b07c69c0 100644 --- a/js/components/settings_container.js +++ b/js/components/settings_container.js @@ -3,8 +3,6 @@ import React from 'react'; import Router from 'react-router'; -import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; - import UserActions from '../actions/user_actions'; import UserStore from '../stores/user_store'; @@ -14,13 +12,29 @@ import WalletSettingsStore from '../stores/wallet_settings_store'; import GlobalNotificationModel from '../models/global_notification_model'; import GlobalNotificationActions from '../actions/global_notification_actions'; -import Input from 'react-bootstrap/lib/Input'; +import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph'; +import Form from './ascribe_forms/form'; +import Property from './ascribe_forms/property'; + +import apiUrls from '../constants/api_urls'; +import AppConstants from '../constants/application_constants'; -import classNames from 'classnames'; let SettingsContainer = React.createClass({ mixins: [Router.Navigation], + render() { + return ( +
+ + +
+ ); + } +}); + + +let AccountSettings = React.createClass({ getInitialState() { return UserStore.getState(); }, @@ -39,186 +53,55 @@ let SettingsContainer = React.createClass({ }, handleSuccess(){ - this.transitionTo('pieces'); - let notification = new GlobalNotificationModel('password succesfully updated', 'success', 10000); + UserActions.fetchCurrentUser(); + let notification = new GlobalNotificationModel('username succesfully updated', 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, render() { - return ( -
+ if (this.state.currentUser.username) { + return ( - +
+ + + + + + +
- - - - - - - - - - - - -
- ); - } -}); - -const CollapsibleParagraph = React.createClass({ - - propTypes: { - title: React.PropTypes.string, - children: React.PropTypes.oneOfType([ - React.PropTypes.object, - React.PropTypes.array - ]), - iconName: React.PropTypes.string - }, - - mixins: [CollapsibleMixin], - - getCollapsibleDOMNode(){ - return React.findDOMNode(this.refs.panel); - }, - - getCollapsibleDimensionValue(){ - return React.findDOMNode(this.refs.panel).scrollHeight; - }, - - onHandleToggle(e){ - e.preventDefault(); - this.setState({expanded: !this.state.expanded}); - }, - - render() { - let styles = this.getCollapsibleClassSet(); - let text = this.isExpanded() ? '-' : '+'; - - return ( -
-
-
- {text} {this.props.title} -
-
- {this.props.children} -
-
-
- ); - } -}); - -let SettingsProperty = React.createClass({ - - - propTypes: { - label: React.PropTypes.string, - value: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.element - ]), - separator: React.PropTypes.string, - labelClassName: React.PropTypes.string, - valueClassName: React.PropTypes.string - }, - - getDefaultProps() { - return { - separator: ':', - labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-1', - valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-11' - }; - }, - - getInitialState() { - return { - value: '', - isFocused: false - }; - }, - - componentWillReceiveProps() { - this.setState({ - value: this.props.value - }); - }, - - handleChange(event) { - this.setState({value: event.target.value}); - }, - - handleFocus() { - this.refs.input.getDOMNode().focus(); - this.setState({ - isFocused: !this.state.isFocused - }); - }, - - getClassName() { - if(this.state.isFocused) { - return 'is-focused'; - } else { - return ''; + ); + } + else { + return ( + + ); } - }, - - render() { - return ( -
-
- { this.props.label} - -
-
- ); } }); -let AccountSettings = React.createClass({ - propTypes: { - currentUser: React.PropTypes.object - }, - - render() { - - return ( -
- - -
- ); - } -}); let BitcoinWalletSettings = React.createClass({ - propTypes: { - currentUser: React.PropTypes.object - }, - getInitialState() { return WalletSettingsStore.getState(); }, @@ -235,14 +118,44 @@ let BitcoinWalletSettings = React.createClass({ onChange(state) { this.setState(state); }, - render() { - return ( -
- - -
- ); + render() { + if (this.state.walletSettings.btc_public_key) { + return ( + +
+ + + + + + +
+
+ ); + } + else { + return ( + + ); + } } }); diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index e7f9c174..461ed453 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -29,6 +29,7 @@ let apiUrls = { 'users_password_reset': AppConstants.apiEndpoint + 'users/reset_password/', 'users_password_reset_request': AppConstants.apiEndpoint + 'users/request_reset_password/', 'users_signup': AppConstants.apiEndpoint + 'users/', + 'users_username': AppConstants.apiEndpoint + 'users/username/', 'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/' }; diff --git a/sass/ascribe_settings.scss b/sass/ascribe_settings.scss index abdabf03..c3ca7445 100644 --- a/sass/ascribe_settings.scss +++ b/sass/ascribe_settings.scss @@ -13,10 +13,42 @@ } .is-focused { - background-color: rgba(2, 182, 163, 0.1); + background-color: rgba(2, 182, 163, 0.05); border-left: 3px solid rgba(2, 182, 163, 1); } +.is-error { + background-color: rgba(169, 68, 66, 0.05); + border-left: 3px solid rgba(169, 68, 66, 1); + div { + span { + color: rgba(169, 68, 66, 1); + font-size: 0.9em; + margin-right: 1em; + } + span { + + } + input { + color: #666; + } + } +} + +.is-fixed { + cursor: default; + div { + cursor: default; + span { + cursor: default; + } + input { + cursor: default; + color: #666; + } + } +} + .ascribe-settings-property { display: inline-block; width: 100%; @@ -31,7 +63,7 @@ span { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: normal; - font-size: 1em; + font-size: 0.9em; color: rgba(0,0,0,.7); } @@ -40,7 +72,7 @@ font-weight: 400; font-size: 1.1em; width:100%; - margin-top: .7em; + margin-top: .5em; border: 0; background-color: rgba(0,0,0,0); color: #38BAAD;