1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-30 13:41:57 +02:00

Merge branch 'AD-416-account-settings-page-navbar' of bitbucket.org:ascribe/onion into AD-416-account-settings-page-navbar

This commit is contained in:
Tim Daubenschütz 2015-06-22 10:55:43 +02:00
commit cb3349c416
19 changed files with 554 additions and 113 deletions

View File

@ -49,7 +49,7 @@ var config = {
filesToWatch: [
'build/css/*.css',
'build/js/*.js',
'node_modules/react-s3-fineuploader/*.js'
'node_modules/react-s3-fine_uploader/*.js'
]
};
@ -69,7 +69,7 @@ gulp.task('js:build', function() {
bundle(false);
});
gulp.task('serve', ['browser-sync', 'run-server', 'sass:build', 'sass:watch', 'copy'], function() {
gulp.task('serve', ['browser-sync', 'run-server', 'sass:watch', 'copy'], function() {
bundle(true);
});

View File

@ -11,6 +11,7 @@
<script>
window.BASE_URL = '<%= BASE_URL %>';
window.API_ENDPOINT = '<%= API_ENDPOINT %>';
window.SERVER_URL = '<%= SERVER_URL %>';
<% DEBUG && print('window.DEBUG = true'); %>
<% DEBUG && print('window.CREDENTIALS = \'' + CREDENTIALS + '\''); %>
</script>

View File

@ -7,7 +7,8 @@ import UserFetcher from '../fetchers/user_fetcher';
class UserActions {
constructor() {
this.generateActions(
'updateCurrentUser'
'updateCurrentUser',
'deleteCurrentUser'
);
}
@ -20,6 +21,15 @@ class UserActions {
console.log(err);
});
}
logoutCurrentUser() {
UserFetcher.logout()
.then(() => {
this.actions.deleteCurrentUser();
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(UserActions);

View File

@ -85,14 +85,21 @@ let Form = React.createClass({
},
clearErrors(){
for (var ref in this.refs){
if ('clearError' in this.refs[ref]){
if ('clearErrors' in this.refs[ref]){
this.refs[ref].clearErrors();
}
}
this.setState({errors: []});
},
getButtons() {
if (this.state.submitted){
return this.props.spinner;
}
if (this.props.buttons){
return this.props.buttons;
}
let buttons = null;
if (this.state.edited){
buttons = (
<div className="pull-right">
@ -122,8 +129,9 @@ let Form = React.createClass({
});
},
render() {
return (
<form role="form" onSubmit={this.submit}>
<form role="form" className="ascribe-form" onSubmit={this.submit} autoComplete="on">
{this.getErrors()}
{this.renderChildren()}
{this.getButtons()}

View File

@ -3,6 +3,9 @@
import React from 'react';
import ReactAddons from 'react/addons';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
let Property = React.createClass({
propTypes: {
editable: React.PropTypes.bool,
@ -49,6 +52,11 @@ let Property = React.createClass({
isFocused: true
});
},
handleBlur() {
this.setState({
isFocused: false
});
},
handleSuccess(){
this.setState({
isFocused: false,
@ -83,22 +91,35 @@ let Property = React.createClass({
return ReactAddons.addons.cloneWithProps(child, {
value: this.state.value,
onChange: this.handleChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled: !this.props.editable,
ref: 'input'
});
});
},
render() {
let tooltip = <span/>;
if (this.props.tooltip){
tooltip = (
<Tooltip>
{this.props.tooltip}
</Tooltip>);
}
return (
<div
className={'ascribe-settings-wrapper ' + this.getClassName()}
onClick={this.handleFocus}>
<div className="ascribe-settings-property">
{this.state.errors}
<span>{ this.props.label}</span>
{this.renderChildren()}
</div>
onClick={this.handleFocus} onfocus={this.handleFocus}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
<div className="ascribe-settings-property">
{this.state.errors}
<span>{ this.props.label}</span>
{this.renderChildren()}
</div>
</OverlayTrigger>
</div>
);
}

View File

@ -6,19 +6,15 @@ import Router from 'react-router';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import apiUrls from '../constants/api_urls.js';
//import PieceListActions from '../actions/piece_list_actions';
import requests from '../utils/requests';
import Alt from '../alt';
import Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
import NavItem from 'react-bootstrap/lib/NavItem';
import CollapsibleNav from 'react-bootstrap/lib/CollapsibleNav';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import LoginModal from '../components/ascribe_modal/modal_login';
import SignupModal from '../components/ascribe_modal/modal_signup';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import { getLangText } from '../utils/lang_utils';
@ -26,7 +22,7 @@ import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let Header = React.createClass({
//mixins: [Router.Navigation],
mixins: [Router.Navigation],
getInitialState() {
return UserStore.getState();
@ -41,53 +37,46 @@ let Header = React.createClass({
UserStore.unlisten(this.onChange);
},
handleLogout(){
requests
.get(apiUrls.users_logout)
.then(this.refreshData);
UserActions.logoutCurrentUser();
Alt.flush();
},
onChange(state) {
this.setState(state);
},
refreshData(){
location.reload();
},
render() {
let account = null;
let signup = null;
if (this.state.currentUser.username){
account = (
<DropdownButton eventKey="1" title={this.state.currentUser.username}>
<MenuItemLink to="settings">{getLangText('Account Settings')}</MenuItemLink>
<li className="divider"></li>
<MenuItem eventKey="2" href="/art/faq/">{getLangText('FAQ')}</MenuItem>
<MenuItem eventKey="3" href="/art/terms/">{getLangText('Terms of Service')}</MenuItem>
<MenuItem divider />
<MenuItem eventKey="4" href="#" onClick={this.handleLogout}>{getLangText('Log out')}</MenuItem>
<MenuItemLink to="settings">{getLangText('Account Settings')}</MenuItemLink>
<li className="divider"></li>
<MenuItem eventKey="2" href="/art/faq/">{getLangText('FAQ')}</MenuItem>
<MenuItem eventKey="3" href="/art/terms/">{getLangText('Terms of Service')}</MenuItem>
<MenuItem divider />
<MenuItem eventKey="4" onClick={this.handleLogout}>{getLangText('Log out')}</MenuItem>
</DropdownButton>
);
}
else {
account = (
<LoginModal
button={<NavItem to="pieces">LOGIN</NavItem>}
handleSuccess={this.refreshData}/>);
signup = (
<SignupModal
button={<NavItem to="pieces">SIGNUP</NavItem>} />);
account = <NavItemLink to="login">LOGIN</NavItemLink>;
signup = <NavItemLink to="signup">SIGNUP</NavItemLink>;
}
return (
<Navbar>
<Nav>
<Link className="navbar-brand" to="pieces" path="/?page=1">
let brand = (<Link className="navbar-brand" to="pieces" path="/?page=1">
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</Link>
</Nav>
<Nav right>
{account}
{signup}
</Nav>
</Link>);
return (
<Navbar brand={brand} toggleNavKey={0}>
<CollapsibleNav eventKey={0}>
<Nav navbar />
<Nav navbar right>
{account}
{signup}
</Nav>
</CollapsibleNav>
</Navbar>
);
}

View File

@ -0,0 +1,86 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import apiUrls from '../constants/api_urls';
let LoginContainer = React.createClass({
mixins: [Router.Navigation],
render() {
return (
<div className="ascribe-login-wrapper">
<br/>
<div className="ascribe-login-text ascribe-login-header">
Log in to ascribe...
</div>
<LoginForm />
</div>
);
}
});
let LoginForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(){
let notification = new GlobalNotificationModel('Login successsful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
render() {
return (
<Form
url={apiUrls.users_login}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Log in to ascribe
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='email'
label="Email">
<input
type="email"
placeholder="Enter your email"
autoComplete="on"
required/>
</Property>
<Property
name='password'
label="Password">
<input
type="password"
placeholder="Enter your password"
autoComplete="on"
required/>
</Property>
<hr />
<div className="ascribe-login-text">
Not an ascribe user&#63; Sign up...<br/>
Forgot my password&#63; Rescue me...
</div>
</Form>
);
}
});
export default LoginContainer;

View File

@ -5,73 +5,149 @@ import React from 'react';
import AppConstants from '../constants/application_constants';
import fineUploader from 'fineUploader';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import apiUrls from '../constants/api_urls';
import ReactS3FineUploader from 'ReactS3FineUploader';
let RegisterPiece = React.createClass( {
render() {
return (
<div>
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
autoUpload={true}
debug={false}
objectProperties={{
acl: 'public-read',
bucket: 'ascribe0'
}}
request={{
endpoint: 'https://ascribe0.s3.amazonaws.com',
accessKey: 'AKIAIVCZJ33WSCBQ3QDA'
}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/'
}}
uploadSuccess={{
params: {
isBrowserPreviewCapable: fineUploader.supportedFeatures.imagePreviews
}
}}
cors={{
expected: true
}}
chunking={{
enabled: true
}}
resume={{
enabled: true
}}
retry={{
enableAuto: false
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete'
}}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
session={{
endpoint: null
}}
messages={{
unsupportedBrowser: '<h3>Upload is not functional in IE7 as IE7 has no support for CORS!</h3>'
}}
formatFileName={(name) => {// fix maybe
if (name !== undefined && name.length > 26) {
name = name.slice(0, 15) + '...' + name.slice(-15);
}
return name;
}}
multiple={true}/>
<div className="row ascribe-row">
<div className="col-md-6">
<FileUploader />
</div>
<div className="col-md-6">
<LoginForm />
</div>
</div>
);
}
});
export default RegisterPiece;
let FileUploader = React.createClass( {
render() {
return (
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
autoUpload={true}
debug={false}
objectProperties={{
acl: 'public-read',
bucket: 'ascribe0'
}}
request={{
endpoint: 'https://ascribe0.s3.amazonaws.com',
accessKey: 'AKIAIVCZJ33WSCBQ3QDA'
}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/'
}}
uploadSuccess={{
params: {
isBrowserPreviewCapable: fineUploader.supportedFeatures.imagePreviews
}
}}
cors={{
expected: true
}}
chunking={{
enabled: true
}}
resume={{
enabled: true
}}
retry={{
enableAuto: false
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete'
}}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
session={{
endpoint: null
}}
messages={{
unsupportedBrowser: '<h3>Upload is not functional in IE7 as IE7 has no support for CORS!</h3>'
}}
formatFileName={(name) => {// fix maybe
if (name !== undefined && name.length > 26) {
name = name.slice(0, 15) + '...' + name.slice(-15);
}
return name;
}}
multiple={true}/>
);
}
});
let LoginForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(){
let notification = new GlobalNotificationModel('Login successsful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
render() {
return (
<Form
url={apiUrls.users_login}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Log in to ascribe
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='email'
label="Email">
<input
type="email"
placeholder="Enter your email"
autoComplete="on"
required/>
</Property>
<Property
name='password'
label="Password">
<input
type="password"
placeholder="Enter your password"
autoComplete="on"
required/>
</Property>
<hr />
<div className="ascribe-login-text">
Not an ascribe user&#63; Sign up...<br/>
Forgot my password&#63; Rescue me...
</div>
</Form>
);
}
});
export default RegisterPiece;

View File

@ -0,0 +1,135 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputCheckbox from './ascribe_forms/input_checkbox';
import apiUrls from '../constants/api_urls';
let SignupContainer = React.createClass({
mixins: [Router.Navigation],
getInitialState(){
return ({
submitted: false,
message: null
});
},
handleSuccess(message){
this.setState({
submitted: true,
message: message
});
},
render() {
if (this.state.submitted){
return (
<div className="ascribe-login-wrapper">
<br/>
<div className="ascribe-login-text ascribe-login-header">
{this.state.message}
</div>
</div>
);
}
return (
<div className="ascribe-login-wrapper">
<br/>
<div className="ascribe-login-text ascribe-login-header">
Welcome to ascribe...
</div>
<SignupForm handleSuccess={this.handleSuccess}/>
</div>
);
}
});
let SignupForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(response){
let notificationText = 'Sign up successful';
let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.props.handleSuccess('We sent an email to your address ' + response.user.email + ', please confirm.');
},
render() {
let tooltipPassword = 'Your password must be at least 10 characters.\n ' +
'This password is securing your digital property like a bank account.\n ' +
'Store it in a safe place!';
return (
<Form
ref='form'
url={apiUrls.users_signup}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Sign up to ascribe
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='email'
label="Email">
<input
type="email"
placeholder="Enter your email"
autoComplete="on"
required/>
</Property>
<Property
name='password'
label="Password"
tooltip={tooltipPassword}>
<input
type="password"
placeholder="Enter your password"
autoComplete="on"
required/>
</Property>
<Property
name='password_confirm'
label="Confirm Password"
tooltip={tooltipPassword}>
<input
type="password"
placeholder="Enter your password once again"
autoComplete="on"
required/>
</Property>
<Property
name='promo_code'
label="Promocode">
<input
type="password"
placeholder="Enter a promocode here (Optional)"/>
</Property>
<hr />
<InputCheckbox
name='terms'
required="required"
label={
<div>
I agree to the&nbsp;
<a href="/terms" target="_blank"> Terms of Service</a>
</div>}/>
</Form>
);
}
});
export default SignupContainer;

View File

@ -1,7 +1,7 @@
'use strict';
import requests from '../utils/requests';
import apiUrls from '../constants/api_urls';
let UserFetcher = {
/**
@ -10,6 +10,10 @@ let UserFetcher = {
*/
fetchOne() {
return requests.get('user');
},
logout() {
return requests.get(apiUrls.users_logout);
}
};

View File

@ -6,11 +6,14 @@ import Router from 'react-router';
import AscribeApp from './components/ascribe_app';
import PieceList from './components/piece_list';
import EditionContainer from './components/edition_container';
import LoginContainer from './components/login_container';
import SignupContainer from './components/signup_container';
import PasswordResetContainer from './components/password_reset_container';
import SettingsContainer from './components/settings_container';
import AppConstants from './constants/application_constants';
import RegisterPiece from './components/register_piece';
//import LoginModalHandler from './components/login_modal_handler';
let Route = Router.Route;
let Redirect = Router.Redirect;
@ -19,6 +22,8 @@ let baseUrl = AppConstants.baseUrl;
let routes = (
<Route name="app" path={baseUrl} handler={AscribeApp}>
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="pieces" path="collection" handler={PieceList} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />

View File

@ -13,6 +13,9 @@ class UserStore {
onUpdateCurrentUser(user) {
this.currentUser = user;
}
onDeleteCurrentUser() {
this.currentUser = {};
}
}
export default alt.createStore(UserStore, 'UserStore');

View File

@ -89,6 +89,9 @@ class Requests {
}
get(url, params) {
if (url === undefined){
throw new Error('Url undefined');
}
let paramsCopy = this._merge(params);
let newUrl = this.prepareUrl(url, paramsCopy, true);
return this.request('get', newUrl);

2
node_modules/react-s3-fineuploader generated vendored

@ -1 +1 @@
Subproject commit 2161e28e1bf5f69719c8b376ecb24fd3334806ba
Subproject commit e2890cf3d28d010abc04e0de3b218afe8c949596

View File

@ -27,7 +27,6 @@
"compact": false
}
],
"reactify",
"browserify-shim"
]
},

79
sass/ascribe_login.scss Normal file
View File

@ -0,0 +1,79 @@
$break-small: 764px;
.ascribe-btn-login {
padding: 1.5em;
font-weight: 500;
text-align: center;
background-color: rgba(2, 182, 163, 1);
color: white;
font-size: 1.2em;
border-radius: 0;
width: 100%;
border:none;
&:hover {
color: white;
background-color: rgba(2, 182, 163, 0.8);
border: none;
}
&:active, &:focus {
color: white;
background-color: rgba(2, 182, 163, 0.6);
border: none;
}
}
.ascribe-btn-login-spinner{
background-color: rgba(2, 182, 163, 0.4);
padding: 0.8em;
img {
height: 3em;
}
&:hover {
background-color: rgba(2, 182, 163, 0.4);
}
&:active, &:focus {
background-color: rgba(2, 182, 163, 0.4);
}
}
.ascribe-login-wrapper {
width: 80%;
margin: 0 auto;
max-width: 600px;
@media screen and (max-width: $break-small) {
width: 100%;
}
}
.ascribe-login-text {
font-size: 1em;
padding: 0 0 1em 0;
margin-left: 0.4em;
}
.ascribe-login-header {
font-size: 2em;
padding: 0 0 0.5em 0;
}
.ascribe-form {
hr {
color: rgba(0, 0, 0, 0.2);
border: none;
height: 1px;
background-color: rgba(0, 0, 0, 0.2);
margin-top: 0;
}
}
%vertical-align {
position: relative;
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}

View File

@ -0,0 +1,7 @@
$break-small: 764px;
.ascribe-row {
max-width: 600px;
margin: 0 auto
}

View File

@ -7,7 +7,7 @@
border-left: 3px solid rgba(0,0,0,0);
&:last-child {
&div:last-of-type {
border-bottom: 1px solid rgba(0,0,0,.2);
}
}

View File

@ -9,6 +9,7 @@ $BASE_URL: '<%= BASE_URL %>';
@import '../node_modules/react-datepicker/dist/react-datepicker';
@import './ascribe-fonts/style';
@import './ascribe-fonts/ascribe-fonts';
@import 'ascribe_login';
@import 'ascribe_table';
@import 'ascribe_accordion_list';
@import 'ascribe_piece_list_bulk_modal';
@ -17,6 +18,7 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'ascribe_textarea';
@import 'ascribe_media_player';
@import 'ascribe-global-notification';
@import 'ascribe_piece_register';
@import 'offset_right';
@import 'ascribe_settings';
@import 'ascribe_react_s3_fineuploader';
@ -31,10 +33,12 @@ body {
}
.navbar-default {
border: none;
border-left:0;
border-right:0;
margin-bottom: 1.5em;
border-top:0;
border-color: #CCC
}
.navbar-right {
@ -50,6 +54,17 @@ body {
color: $ascribe-color;
}
.tooltip-inner{
max-width: 300px;
padding: 3px 8px;
color: #000;
text-align: center;
text-decoration: none;
background-color: #FCFAFA;
border-radius: 0;
border: 1px solid;
}
/* Taken from http://stackoverflow.com/a/20548578 */
.vcenter {
display: inline-block;