1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

Merge remote-tracking branch 'origin/master' into AD-538-users-and-even-devs-are-unsure-wh

Conflicts:
	package.json
This commit is contained in:
Tim Daubenschütz 2015-10-20 18:11:41 +02:00
commit 8d0e9a42fc
84 changed files with 1264 additions and 863 deletions

View File

@ -22,7 +22,7 @@
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-mount-set-state": [1, "allow-in-func"],
"react/no-did-update-set-state": 1,
"react/no-multi-comp": 0,
"react/no-unknown-property": 1,

View File

@ -5,6 +5,7 @@ import Q from 'q';
import PieceListFetcher from '../fetchers/piece_list_fetcher';
class PieceListActions {
constructor() {
this.generateActions(
@ -21,17 +22,16 @@ class PieceListActions {
this.actions.updatePieceList({
page,
pageSize,
search,
orderBy,
orderAsc,
filterBy,
search: '',
pieceList: [],
pieceListCount: -1,
unfilteredPieceListCount: -1
});
// afterwards, we can load the list
return Q.Promise((resolve, reject) => {
PieceListFetcher
.fetch(page, pageSize, search, orderBy, orderAsc, filterBy)

View File

@ -13,7 +13,7 @@ class UserActions {
}
fetchCurrentUser() {
return UserFetcher.fetchOne()
UserFetcher.fetchOne()
.then((res) => {
this.actions.updateCurrentUser(res.users[0]);
})
@ -24,7 +24,7 @@ class UserActions {
}
logoutCurrentUser() {
return UserFetcher.logout()
UserFetcher.logout()
.then(() => {
this.actions.deleteCurrentUser();
})

View File

@ -3,7 +3,8 @@
require('babel/polyfill');
import React from 'react';
import Router from 'react-router';
import { Router, Redirect } from 'react-router';
import history from './history';
/* eslint-disable */
import fetch from 'isomorphic-fetch';
@ -46,7 +47,6 @@ requests.defaults({
}
});
class AppGateway {
start() {
let settings;
@ -68,22 +68,36 @@ class AppGateway {
load(settings) {
let type = 'default';
let subdomain = 'www';
let redirectRoute = (<Redirect from="/" to="/collection" />);
if (settings) {
type = settings.type;
subdomain = settings.subdomain;
}
// www and cc do not have a landing page
if(subdomain && subdomain !== 'cc') {
redirectRoute = null;
}
// Adds a client specific class to the body for whitelabel styling
window.document.body.classList.add('client--' + subdomain);
// Send the applicationWillBoot event to the third-party stores
EventActions.applicationWillBoot(settings);
window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => {
React.render(
<App />,
document.getElementById('main')
);
EventActions.routeDidChange();
});
// `history.listen` is called on every route change, which is perfect for
// us in that case.
history.listen(EventActions.routeDidChange);
React.render((
<Router history={history}>
{redirectRoute}
{getRoutes(type, subdomain)}
</Router>
), document.getElementById('main'));
// Send the applicationDidBoot event to the third-party stores
EventActions.applicationDidBoot(settings);
}
}

View File

@ -9,21 +9,49 @@ let AccordionList = React.createClass({
className: React.PropTypes.string,
children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired,
loadingElement: React.PropTypes.element,
count: React.PropTypes.number
count: React.PropTypes.number,
itemList: React.PropTypes.arrayOf(React.PropTypes.object),
search: React.PropTypes.string,
searchFor: React.PropTypes.func
},
clearSearch() {
this.props.searchFor('');
},
render() {
const { search } = this.props;
if(this.props.itemList && this.props.itemList.length > 0) {
return (
<div className={this.props.className}>
{this.props.children}
</div>
);
} else if(this.props.count === 0) {
} else if(this.props.count === 0 && !search) {
return (
<div className="ascribe-accordion-list-placeholder">
<p className="text-center">{getLangText('We could not find any works related to you...')}</p>
<p className="text-center">{getLangText('To register one, click')} <a href="register_piece">{getLangText('here')}</a>!</p>
<p className="text-center">
{getLangText('We could not find any works related to you...')}
</p>
<p className="text-center">
{getLangText('To register one, click')}
<a href="register_piece">{getLangText('here')}</a>!
</p>
</div>
);
} else if(this.props.count === 0 && search) {
return (
<div className="ascribe-accordion-list-placeholder">
<p className="text-center">
{getLangText('We could not find any works related to you...')}
</p>
<p className="text-center">
{getLangText('You\'re filtering by the search keyword: \'%s\' ', search)}
</p>
<p className="text-center">
<button className="btn btn-sm btn-default" onClick={this.clearSearch}>{getLangText('Clear search')}</button>
</p>
</div>
);
} else {

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
let AccordionListItem = React.createClass({
propTypes: {
@ -18,10 +18,7 @@ let AccordionListItem = React.createClass({
])
},
mixins: [Router.Navigation],
render() {
return (
<div className="row">
<div className={this.props.className}>

View File

@ -1,14 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import AccordionListItem from './accordion_list_item';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let AccordionListItemPiece = React.createClass({
propTypes: {
@ -24,26 +22,15 @@ let AccordionListItemPiece = React.createClass({
badge: React.PropTypes.object
},
mixins: [Router.Navigation],
getLinkData() {
let { piece } = this.props;
if(piece && piece.first_edition) {
return `/editions/${piece.first_edition.bitcoin_id}`;
if(this.props.piece && this.props.piece.first_edition) {
return {
to: 'edition',
params: {
editionId: this.props.piece.first_edition.bitcoin_id
}
};
} else {
return {
to: 'piece',
params: {
pieceId: this.props.piece.id
return `/pieces/${piece.id}`;
}
};
}
},
render() {
@ -51,11 +38,11 @@ let AccordionListItemPiece = React.createClass({
<AccordionListItem
className={this.props.className}
thumbnail={
<Link {...this.getLinkData()}>
<Link to={this.getLinkData()}>
<img src={this.props.piece.thumbnail.url_safe}/>
</Link>}
heading={
<Link {...this.getLinkData()}>
<Link to={this.getLinkData()}>
<h1>{this.props.piece.title}</h1>
</Link>}
subheading={

View File

@ -110,7 +110,7 @@ let AccordionListItemTableEditions = React.createClass({
showExpandOption = true;
}
let transition = new TransitionModel('edition', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() );
let transition = new TransitionModel('editions', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() );
let columnList = [
new ColumnModel(

View File

@ -1,22 +1,29 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Header from '../components/header';
import Footer from '../components/footer';
import GlobalNotification from './global_notification';
import getRoutes from '../routes';
let RouteHandler = Router.RouteHandler;
let AscribeApp = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
render() {
let { children, routes } = this.props;
return (
<div className="container ascribe-default-app">
<Header routes={getRoutes()} />
<RouteHandler />
<Header routes={routes} />
{/* Routes are injected here */}
{children}
<Footer />
<GlobalNotification />
<div id="modal" className="container"></div>

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Button from 'react-bootstrap/lib/Button';
@ -24,8 +23,6 @@ let DeleteButton = React.createClass({
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation],
render() {
let availableAcls;
let btnDelete;

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link, History } from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
@ -22,7 +22,7 @@ import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import EditionFurtherDetails from './further_details';
import FurtherDetails from './further_details';
import EditionActionPanel from './edition_action_panel';
@ -33,17 +33,18 @@ import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
/**
* This is the component that implements display-specific functionality
*/
let Edition = React.createClass({
propTypes: {
edition: React.PropTypes.object,
loadEdition: React.PropTypes.func
loadEdition: React.PropTypes.func,
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return UserStore.getState();
@ -148,12 +149,13 @@ let Edition = React.createClass({
show={this.props.edition.acl.acl_edit
|| Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data.length > 0}>
<EditionFurtherDetails
<FurtherDetails
editable={this.props.edition.acl.acl_edit}
pieceId={this.props.edition.parent}
extraData={this.props.edition.extra_data}
otherData={this.props.edition.other_data}
handleSuccess={this.props.loadEdition}/>
handleSuccess={this.props.loadEdition}
location={this.props.location}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('SPOOL Details')}>
@ -257,7 +259,7 @@ let CoaDetails = React.createClass({
{getLangText('Download')} <Glyphicon glyph="cloud-download"/>
</button>
</a>
<Link to="coa_verify">
<Link to="/coa_verify">
<button className="btn btn-default btn-xs">
{getLangText('Verify')} <Glyphicon glyph="check"/>
</button>

View File

@ -9,12 +9,17 @@ import Edition from './edition';
import AppConstants from '../../constants/application_constants';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let EditionContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return EditionStore.getState();
},
@ -62,10 +67,13 @@ let EditionContainer = React.createClass({
render() {
if(this.state.edition && this.state.edition.title) {
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', '));
return (
<Edition
edition={this.state.edition}
loadEdition={this.loadEdition}/>
loadEdition={this.loadEdition}
location={this.props.location}/>
);
} else {
return (

View File

@ -23,7 +23,8 @@ let FurtherDetails = React.createClass({
pieceId: React.PropTypes.number,
extraData: React.PropTypes.object,
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func
handleSuccess: React.PropTypes.func,
location: React.PropTypes.object
},
getInitialState() {
@ -85,7 +86,8 @@ let FurtherDetails = React.createClass({
overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
multiple={true}/>
multiple={true}
location={this.props.location}/>
</Form>
</Col>
</Row>

View File

@ -20,7 +20,8 @@ let FurtherDetailsFileuploader = React.createClass({
submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool,
multiple: React.PropTypes.bool
multiple: React.PropTypes.bool,
location: React.PropTypes.object
},
getDefaultProps() {
@ -88,7 +89,8 @@ let FurtherDetailsFileuploader = React.createClass({
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple}/>
multiple={this.props.multiple}
location={this.props.location}/>
</Property>
);
}

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store';
@ -40,13 +40,17 @@ import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -134,7 +138,7 @@ let PieceContainer = React.createClass({
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
getCreateEditionsDialog() {
@ -213,6 +217,8 @@ let PieceContainer = React.createClass({
render() {
if(this.state.piece && this.state.piece.title) {
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
return (
<Piece
piece={this.state.piece}
@ -267,7 +273,8 @@ let PieceContainer = React.createClass({
pieceId={this.state.piece.id}
extraData={this.state.piece.extra_data}
otherData={this.state.piece.other_data}
handleSuccess={this.loadPiece}/>
handleSuccess={this.loadPiece}
location={this.props.location}/>
</CollapsibleParagraph>
</Piece>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import ContractListActions from '../../actions/contract_list_actions';
import ContractListStore from '../../stores/contract_list_store';
@ -25,7 +25,7 @@ let ContractAgreementForm = React.createClass({
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -57,7 +57,8 @@ let ContractAgreementForm = React.createClass({
let notification = 'Contract agreement send';
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
getFormData(){
@ -138,7 +139,7 @@ let ContractAgreementForm = React.createClass({
<div>
<p className="text-center">
{getLangText('No contracts uploaded yet, please go to the ')}
<a href="settings">{getLangText('settings page')}</a>
<a href="contract_settings">{getLangText('contract settings page')}</a>
{getLangText(' and create them.')}
</p>
</div>

View File

@ -28,7 +28,8 @@ let CreateContractForm = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
getInitialState() {
@ -86,7 +87,8 @@ let CreateContractForm = React.createClass({
areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload}/>
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
</Property>
<Property
name='name'

View File

@ -130,6 +130,9 @@ let LoanForm = React.createClass({
src={contract.blob.url_safe}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
<a href={contract.blob.url_safe} target="_blank">
<span className="glyphicon glyphicon-download" aria-hidden="true"></span> {getLangText('Download contract')}
</a>
{/* We still need to send the server information that we're accepting */}
<InputCheckbox
style={{'display': 'none'}}

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -24,10 +24,10 @@ let LoginForm = React.createClass({
submitMessage: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -44,10 +44,6 @@ let LoginForm = React.createClass({
componentDidMount() {
UserStore.listen(this.onChange);
let { redirect } = this.getQuery();
if (redirect && redirect !== 'login'){
this.transitionTo(redirect, null, this.getQuery());
}
},
componentWillUnmount() {
@ -56,65 +52,19 @@ let LoginForm = React.createClass({
onChange(state) {
this.setState(state);
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email && this.props.redirectOnLoggedIn) {
// FIXME: hack to redirect out of the dispatch cycle
let { redirectAuthenticated } = this.getQuery();
if ( redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
window.setTimeout(() => this.transitionTo('pieces'), 0);
}
},
handleSuccess(){
handleSuccess({ success }){
let notification = new GlobalNotificationModel('Login successful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
// register_piece is waiting for a login success as login_container and it is wrapped
// in a slides_container component.
// The easiest way to check if the user was successfully logged in is to fetch the user
// in the user store (which is obviously only possible if the user is logged in), since
// register_piece is listening to the changes of the user_store.
UserActions.fetchCurrentUser()
.then(() => {
if(this.props.redirectOnLoginSuccess) {
/* Taken from http://stackoverflow.com/a/14916411 */
/*
We actually have to trick the Browser into showing the "save password" dialog
as Chrome expects the login page to be reloaded after the login.
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
Until then, we redirect the HARD way, but reloading the whole page using window.location
*/
let { redirectAuthenticated } = this.getQuery();
if ( redirectAuthenticated) {
window.location = AppConstants.baseUrl + redirectAuthenticated;
if(success) {
UserActions.fetchCurrentUser();
}
else {
window.location = AppConstants.baseUrl + 'collection';
}
} else if(this.props.onLogin) {
// In some instances we want to give a callback to an outer container,
// to show that the one login action the user triggered actually went through.
// We can not do this by listening on a store's state as it wouldn't really tell us
// if the user did log in or was just fetching the user's data again
this.props.onLogin();
}
})
.catch((err) => {
console.logGlobal(err);
});
},
render() {
let email = this.getQuery().email || null;
let email = this.props.location.query.email || null;
return (
<Form
className="ascribe-form-bordered"

View File

@ -29,7 +29,8 @@ let RegisterPieceForm = React.createClass({
onLoggedOut: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool
disabled: React.PropTypes.bool,
location: React.PropTypes.object
},
getDefaultProps() {
@ -113,7 +114,8 @@ let RegisterPieceForm = React.createClass({
isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/>
enableLocalHashing={enableLocalHashing}
location={this.props.location}/>
</Property>
<Property
name='artist_name'

View File

@ -1,11 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import { getLangText } from '../../utils/lang_utils';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -14,20 +15,19 @@ import Form from './form';
import Property from './property';
import InputCheckbox from './input_checkbox';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
let SignupForm = React.createClass({
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
children: React.PropTypes.element
children: React.PropTypes.element,
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -35,16 +35,13 @@ let SignupForm = React.createClass({
submitMessage: getLangText('Sign up')
};
},
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
let { redirect } = this.getQuery();
if (redirect && redirect !== 'signup'){
this.transitionTo(redirect, null, this.getQuery());
}
},
componentWillUnmount() {
@ -53,45 +50,23 @@ let SignupForm = React.createClass({
onChange(state) {
this.setState(state);
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email) {
let { redirectAuthenticated } = this.getQuery();
if ( redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
window.setTimeout(() => this.transitionTo('pieces'));
}
},
handleSuccess(response){
handleSuccess(response) {
if (response.user) {
let notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
// Refactor this to its own component
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
}
else if (response.redirect) {
let { redirectAuthenticated } = this.getQuery();
if ( redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
this.transitionTo('pieces');
} else {
UserActions.fetchCurrentUser();
}
},
getFormData() {
if (this.getQuery().token){
return {token: this.getQuery().token};
if (this.props.location.query.token){
return {token: this.props.location.query.token};
}
return null;
},
@ -100,7 +75,9 @@ let SignupForm = React.createClass({
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' +
getLangText('This password is securing your digital property like a bank account') + '.\n ' +
getLangText('Store it in a safe place') + '!';
let email = this.getQuery().email || null;
let email = this.props.location.query.email || null;
return (
<Form
className="ascribe-form-bordered"

View File

@ -46,7 +46,8 @@ let InputFineUploader = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
getInitialState() {
@ -106,7 +107,8 @@ let InputFineUploader = React.createClass({
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload}/>
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
);
}
});

View File

@ -103,14 +103,17 @@ let Video = React.createClass({
* ReactJS is responsible for DOM manipulation but VideoJS updates the DOM
* to install itself to display the video, therefore ReactJS complains that we are
* changing the DOM under its feet.
* The component supports a fall-back to HTML5 video tag.
*
* What we do is the following:
* 1) set `state.ready = false`
* 2) render the cover using the `<Image />` component (because ready is false)
* 1) set `state.libraryLoaded = null` (state.libraryLoaded can be in three states: `null`
* if we don't know anything about it, `true` if the external library has been loaded,
* `false` if we failed to load the external library)
* 2) render the cover using the `<Image />` component (because libraryLoaded is null)
* 3) on `componentDidMount`, we load the external `css` and `js` resources using
* the `InjectInHeadMixin`, attaching a function to `Promise.then` to change
* `state.ready` to true
* 4) when the promise is succesfully resolved, we change `state.ready` triggering
* `state.libraryLoaded` to true
* 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering
* a re-render
* 5) the new render calls `prepareVideoHTML` to get the raw HTML of the video tag
* (that will be later processed and expanded by VideoJS)
@ -129,18 +132,19 @@ let Video = React.createClass({
mixins: [InjectInHeadMixin],
getInitialState() {
return { ready: false, videoMounted: false };
return { libraryLoaded: null, videoMounted: false };
},
componentDidMount() {
Q.all([
this.inject('//vjs.zencdn.net/4.12/video-js.css'),
this.inject('//vjs.zencdn.net/4.12/video.js')
]).then(this.ready);
this.inject('//vjs.zencdn.net/4.12/video.js')])
.then(() => this.setState({libraryLoaded: true}))
.fail(() => this.setState({libraryLoaded: false}));
},
componentDidUpdate() {
if (this.state.ready && !this.state.videoMounted) {
if (this.state.libraryLoaded && !this.state.videoMounted) {
window.videojs('#mainvideo');
/* eslint-disable */
this.setState({videoMounted: true});
@ -149,11 +153,9 @@ let Video = React.createClass({
},
componentWillUnmount() {
if (this.state.videoMounted) {
window.videojs('#mainvideo').dispose();
},
ready() {
this.setState({ready: true, videoMounted: false});
}
},
prepareVideoHTML() {
@ -171,7 +173,7 @@ let Video = React.createClass({
},
render() {
if (this.state.ready) {
if (this.state.libraryLoaded !== null) {
return (
<div dangerouslySetInnerHTML={{__html: this.prepareVideoHTML() }}/>
);

View File

@ -1,11 +1,10 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let PaginationButton = React.createClass({
propTypes: {
@ -43,7 +42,7 @@ let PaginationButton = React.createClass({
if (this.isInRange(page)) {
anchor = (
<Link to="pieces"
<Link to="/collection"
query={{page}}
onClick={this.props.goToPage(page)}>
{directionDisplay}

View File

@ -4,16 +4,16 @@ import React from 'react';
import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget';
import PieceListToolbarOrderWidget from './piece_list_toolbar_order_widget';
import SearchBar from '../search_bar';
import AppConstants from '../../constants/application_constants';
import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { getLangText } from '../../utils/lang_utils';
let PieceListToolbar = React.createClass({
propTypes: {
className: React.PropTypes.string,
searchFor: React.PropTypes.func,
searchQuery: React.PropTypes.string,
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string,
@ -39,11 +39,6 @@ let PieceListToolbar = React.createClass({
])
},
searchFor() {
let searchTerm = this.refs.search.getInputDOMNode().value;
this.props.searchFor(searchTerm);
},
getFilterWidget(){
if (this.props.filterParams){
return (
@ -55,6 +50,7 @@ let PieceListToolbar = React.createClass({
}
return null;
},
getOrderWidget(){
if (this.props.orderParams){
return (
@ -68,24 +64,21 @@ let PieceListToolbar = React.createClass({
},
render() {
let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>;
const { className, children, searchFor, searchQuery } = this.props;
return (
<div className={this.props.className}>
<div className={className}>
<div className="row">
<div className="col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<div className="row">
<span className="pull-left">
{this.props.children}
</span>
<span className="pull-right search-bar ascribe-input-glyph">
<Input
type='text'
ref="search"
placeholder={getLangText('Search%s', '...')}
onChange={this.searchFor}
addonAfter={searchIcon} />
{children}
</span>
<SearchBar
className="pull-right search-bar ascribe-input-glyph"
searchFor={searchFor}
searchQuery={searchQuery}
threshold={AppConstants.searchThreshold}/>
<span className="pull-right">
{this.getOrderWidget()}
{this.getFilterWidget()}

View File

@ -0,0 +1,107 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import UserStore from '../../../stores/user_store';
import UserActions from '../../../actions/user_actions';
import AppConstants from '../../../constants/application_constants';
const { object } = React.PropTypes;
const WHEN_ENUM = ['loggedIn', 'loggedOut'];
/**
* Can be used in combination with `Route` as an intermediate Handler
* between the actual component we want to display dependent on a certain state
* that is required to display that component.
*
* @param {string} options.to Any type of route path that is defined in routes.js
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
*/
export default function AuthProxyHandler({to, when}) {
// validate `when`, must be contained in `WHEN_ENUM`.
// Throw an error otherwise.
if(WHEN_ENUM.indexOf(when) === -1) {
let whenValues = WHEN_ENUM.join(', ');
throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`);
}
return (Component) => {
return React.createClass({
propTypes: {
location: object
},
mixins: [History],
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentDidUpdate() {
this.redirectConditionally();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
redirectConditionally() {
const { query } = this.props.location;
const { redirectAuthenticated, redirect } = query;
// The user of this handler specifies with `when`, what kind of status
// needs to be checked to conditionally do - if that state is `true` -
// a redirect.
//
// So if when === 'loggedIn', we're checking if the user is logged in (and
// vice versa)
let exprToValidate = when === 'loggedIn' ?
this.state.currentUser && this.state.currentUser.email :
this.state.currentUser && !this.state.currentUser.email;
// and redirect if `true`.
if(exprToValidate) {
window.setTimeout(() => this.history.replaceState(null, to, query));
// Otherwise there can also be the case that the backend
// wants to redirect the user to a specific route when the user is logged out already
} else if(!exprToValidate && when === 'loggedIn' && redirect) {
delete query.redirect;
window.setTimeout(() => this.history.replaceState(null, '/' + redirect, query));
} else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly.
*
* While we're getting rid of `query.redirect` explicitly in the
* above `else if` statement, here it's sufficient to just call
* `baseUrl` + `redirectAuthenticated`, as it gets rid of queries as well.
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
},
onChange(state) {
this.setState(state);
},
render() {
return (
<Component {...this.props}/>
);
}
});
};
}

View File

@ -96,7 +96,11 @@ let AccountSettings = React.createClass({
title={getLangText('Account')}
defaultExpanded={true}>
{content}
<AclProxy
aclObject={this.props.whitelabel}
aclName="acl_view_settings_copyright_association">
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
</AclProxy>
{profile}
</CollapsibleParagraph>
);

View File

@ -23,10 +23,15 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils';
let ContractSettings = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState(){
return mergeOptions(
ContractListStore.getState(),
@ -82,6 +87,8 @@ let ContractSettings = React.createClass({
let privateContracts = this.getPrivateContracts();
let createPublicContractForm = null;
setDocumentTitle(getLangText('Contracts settings'));
if(publicContracts.length === 0) {
createPublicContractForm = (
<CreateContractForm
@ -89,7 +96,8 @@ let ContractSettings = React.createClass({
fileClassToUpload={{
singular: 'new contract',
plural: 'new contracts'
}}/>
}}
location={this.props.location}/>
);
}
@ -114,7 +122,9 @@ let ContractSettings = React.createClass({
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_public_contract">
<ContractSettingsUpdateButton contract={contract}/>
<ContractSettingsUpdateButton
contract={contract}
location={this.props.location}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
@ -144,7 +154,8 @@ let ContractSettings = React.createClass({
fileClassToUpload={{
singular: getLangText('new contract'),
plural: getLangText('new contracts')
}}/>
}}
location={this.props.location}/>
{privateContracts.map((contract, i) => {
return (
<ActionPanel
@ -156,7 +167,9 @@ let ContractSettings = React.createClass({
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_private_contract">
<ContractSettingsUpdateButton contract={contract}/>
<ContractSettingsUpdateButton
contract={contract}
location={this.props.location}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"

View File

@ -20,7 +20,8 @@ import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({
propTypes: {
contract: React.PropTypes.object
contract: React.PropTypes.object,
location: React.PropTypes.object
},
submitFile(file) {
@ -90,7 +91,7 @@ let ContractSettingsUpdateButton = React.createClass({
}}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile}
/>
location={this.props.location}/>
);
}
});

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
@ -16,6 +15,8 @@ import APISettings from './api_settings';
import AclProxy from '../acl_proxy';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
let SettingsContainer = React.createClass({
@ -25,8 +26,6 @@ let SettingsContainer = React.createClass({
React.PropTypes.element])
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
@ -56,6 +55,8 @@ let SettingsContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Account settings'));
if (this.state.currentUser && this.state.currentUser.username) {
return (
<div className="settings-container">

View File

@ -1,96 +1,40 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import ReactAddons from 'react/addons';
import React from 'react/addons';
import { History } from 'react-router';
import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs';
let State = Router.State;
let Navigation = Router.Navigation;
const { arrayOf, element, bool, shape, string, object } = React.PropTypes;
let SlidesContainer = React.createClass({
const SlidesContainer = React.createClass({
propTypes: {
children: React.PropTypes.arrayOf(React.PropTypes.element),
forwardProcess: React.PropTypes.bool.isRequired,
children: arrayOf(element),
forwardProcess: bool.isRequired,
glyphiconClassNames: React.PropTypes.shape({
pending: React.PropTypes.string,
complete: React.PropTypes.string
})
glyphiconClassNames: shape({
pending: string,
complete: string
}),
location: object
},
mixins: [State, Navigation],
mixins: [History],
getInitialState() {
// handle queryParameters
let queryParams = this.getQuery();
let slideNum = -1;
let startFrom = -1;
// We can actually need to check if slide_num is present as a key in queryParams.
// We do not really care about its value though...
if(queryParams && 'slide_num' in queryParams) {
slideNum = parseInt(queryParams.slide_num, 10);
}
// if slide_num is not set, this will be done in componentDidMount
// the query param 'start_from' removes all slide children before the respective number
// Also, we use the 'in' keyword for the same reason as above in 'slide_num'
if(queryParams && 'start_from' in queryParams) {
startFrom = parseInt(queryParams.start_from, 10);
}
return {
slideNum,
startFrom,
containerWidth: 0,
historyLength: window.history.length
containerWidth: 0
};
},
componentDidMount() {
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery();
// We use 'in' to check if the key is present in the user's browser url bar,
// we do not really care about its value at this point
if(!('slide_num' in queryParams)) {
// we're first requiring all the other possible queryParams and then set
// the specific one we need instead of overwriting them
queryParams.slide_num = 0;
this.replaceWith(this.getPathname(), null, queryParams);
}
// init container width
this.handleContainerResize();
// we're using an event listener on window here,
// as it was not possible to listen to the resize events of a dom node
window.addEventListener('resize', this.handleContainerResize);
},
componentWillReceiveProps() {
let queryParams = this.getQuery();
// also check if start_from was updated
// This applies for example when the user tries to submit a already existing piece
// (starting from slide 1 for example) and then clicking on + NEW WORK
if(queryParams && !('start_from' in queryParams)) {
this.setState({
startFrom: -1
});
}
},
componentDidUpdate() {
let queryParams = this.getQuery();
// check if slide_num was defined, and if not then default to 0
this.setSlideNum(queryParams.slide_num);
// Initially, we need to dispatch 'resize' once to render correctly
window.dispatchEvent(new Event('resize'));
},
componentWillUnmount() {
@ -105,80 +49,26 @@ let SlidesContainer = React.createClass({
},
// When the start_from parameter is used, this.setSlideNum can not simply be used anymore.
nextSlide() {
let nextSlide = this.state.slideNum + 1;
this.setSlideNum(nextSlide);
nextSlide(additionalQueryParams) {
const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0;
let nextSlide = slideNum + 1;
this.setSlideNum(nextSlide, additionalQueryParams);
},
// We let every one from the outsite set the page number of the slider,
// though only if the slideNum is actually in the range of our children-list.
setSlideNum(slideNum) {
// we do not want to overwrite other queryParams
let queryParams = this.getQuery();
// slideNum can in some instances be not a number,
// therefore we have to parse it to one and make sure that its not NaN
slideNum = parseInt(slideNum, 10);
// if slideNum is not a number (even after we parsed it to one) and there has
// never been a transition to another slide (this.state.slideNum ==== -1 indicates that)
// then we want to "replace" (in this case append) the current url with ?slide_num=0
if(isNaN(slideNum) && this.state.slideNum === -1) {
slideNum = 0;
queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams);
this.setState({slideNum: slideNum});
return;
// slideNum always represents the future state. So if slideNum and
// this.state.slideNum are equal, there is no sense in redirecting
} else if(slideNum === this.state.slideNum) {
return;
// if slideNum is within the range of slides and none of the previous cases
// where matched, we can actually do transitions
} else if(slideNum >= 0 || slideNum < this.customChildrenCount()) {
if(slideNum !== this.state.slideNum - 1) {
// Bootstrapping the component, getInitialState is called once to save
// the tabs history length.
// In order to know if we already pushed a new state on the history stack or not,
// we're comparing the old history length with the new one and if it didn't change then
// we push a new state on it ONCE (ever).
// Otherwise, we're able to use the browsers history.forward() method
// to keep the stack clean
if(this.props.forwardProcess) {
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else {
if(this.state.historyLength === window.history.length) {
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else {
window.history.forward();
}
}
}
this.setState({
slideNum: slideNum
});
} else {
throw new Error('You\'re calling a page number that is out of range.');
}
setSlideNum(nextSlideNum, additionalQueryParams = {}) {
let queryParams = Object.assign(this.props.location.query, additionalQueryParams);
queryParams.slide_num = nextSlideNum;
this.history.pushState(null, this.props.location.pathname, queryParams);
},
// breadcrumbs are defined as attributes of the slides.
// To extract them we have to read the DOM element's attributes
extractBreadcrumbs() {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
let breadcrumbs = [];
ReactAddons.Children.map(this.props.children, (child, i) => {
if(child && i >= this.state.startFrom && child.props['data-slide-title']) {
React.Children.map(this.props.children, (child, i) => {
if(child && i >= startFrom && child.props['data-slide-title']) {
breadcrumbs.push(child.props['data-slide-title']);
}
});
@ -191,9 +81,11 @@ let SlidesContainer = React.createClass({
// Therefore React.Children.count does not work anymore and we
// need to implement our own method.
customChildrenCount() {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
let count = 0;
React.Children.forEach(this.props.children, (child, i) => {
if(i >= this.state.startFrom) {
if(i >= startFrom) {
count++;
}
});
@ -212,7 +104,7 @@ let SlidesContainer = React.createClass({
return (
<SlidesContainerBreadcrumbs
breadcrumbs={breadcrumbs}
slideNum={this.state.slideNum}
slideNum={parseInt(this.props.location.query.slide_num, 10) || 0}
numOfSlides={breadcrumbs.length}
containerWidth={this.state.containerWidth}
glyphiconClassNames={this.props.glyphiconClassNames}/>
@ -225,12 +117,13 @@ let SlidesContainer = React.createClass({
// Since we need to give the slides a width, we need to call ReactAddons.addons.cloneWithProps
// Also, a key is nice to have!
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child, i) => {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
return React.Children.map(this.props.children, (child, i) => {
// since the default parameter of startFrom is -1, we do not need to check
// if its actually present in the url bar, as it will just not match
if(child && i >= this.state.startFrom) {
return ReactAddons.addons.cloneWithProps(child, {
if(child && i >= startFrom) {
return React.addons.cloneWithProps(child, {
className: 'ascribe-slide',
style: {
width: this.state.containerWidth
@ -246,7 +139,7 @@ let SlidesContainer = React.createClass({
},
render() {
let spacing = this.state.containerWidth * this.state.slideNum;
let spacing = this.state.containerWidth * parseInt(this.props.location.query.slide_num, 10) || 0;
let translateXValue = 'translateX(' + (-1) * spacing + 'px)';
/*

View File

@ -35,14 +35,7 @@ export class TransitionModel {
this.callback = callback;
}
toReactRouterLinkProps(queryValue) {
let props = {
to: this.to,
params: {}
};
props.params[this.queryKey] = queryValue;
return props;
toReactRouterLink(queryValue) {
return '/' + this.to + '/' + queryValue;
}
}

View File

@ -1,11 +1,10 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import { ColumnModel } from './models/table_models';
let Link = Router.Link;
let TableItemWrapper = React.createClass({
propTypes: {
@ -15,8 +14,6 @@ let TableItemWrapper = React.createClass({
onClick: React.PropTypes.func
},
mixins: [Router.Navigation],
render() {
return (
<tr onClick={this.props.onClick}>
@ -35,18 +32,13 @@ let TableItemWrapper = React.createClass({
);
} else {
let linkProps = column.transition.toReactRouterLinkProps(this.props.columnContent[column.transition.valueKey]);
/**
* If a transition is defined in columnContent, then we can use
* Router.Navigation.transitionTo to redirect the user
* programmatically
*/
let linkString = column.transition.toReactRouterLink(this.props.columnContent[column.transition.valueKey]);
return (
<td key={i} className={column.className}>
<Link
to={linkString}
className={'ascribe-table-item-column'}
onClick={column.transition.callback}
{...linkProps}>
onClick={column.transition.callback}>
<TypeElement {...typeElementProps} />
</Link>
</td>

View File

@ -12,6 +12,7 @@ import { getLangText } from '../../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({
propTypes: {
className: React.PropTypes.string,
onDrop: React.PropTypes.func.isRequired,
onDragOver: React.PropTypes.func,
onInactive: React.PropTypes.func,
@ -40,7 +41,8 @@ let FileDragAndDrop = React.createClass({
plural: React.PropTypes.string
}),
allowedExtensions: React.PropTypes.string
allowedExtensions: React.PropTypes.string,
location: React.PropTypes.object
},
handleDragOver(event) {
@ -107,6 +109,7 @@ let FileDragAndDrop = React.createClass({
},
handleOnClick() {
let evt;
// when multiple is set to false and the user already uploaded a piece,
// do not propagate event
if(this.props.dropzoneInactive) {
@ -118,16 +121,18 @@ let FileDragAndDrop = React.createClass({
return;
}
// Firefox only recognizes the simulated mouse click if bubbles is set to true,
// but since Google Chrome propagates the event much further than needed, we
// need to stop propagation as soon as the event is created
var evt = new MouseEvent('click', {
try {
evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
} catch(e) {
// For browsers that do not support the new MouseEvent syntax
evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
}
evt.stopPropagation();
this.refs.fileinput.getDOMNode().dispatchEvent(evt);
},
@ -142,7 +147,8 @@ let FileDragAndDrop = React.createClass({
fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions
allowedExtensions,
location
} = this.props;
// has files only is true if there are files that do not have the status deleted or canceled
@ -158,10 +164,10 @@ let FileDragAndDrop = React.createClass({
<div className="file-drag-and-drop-hashing-dialog">
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<p>
<a onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</a>
<a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a>
</p>
<ProgressBar
now={Math.ceil(this.props.hashingProgress)}
now={Math.ceil(hashingProgress)}
label="%(percent)s%"
className="ascribe-progress-bar"/>
</div>
@ -179,7 +185,8 @@ let FileDragAndDrop = React.createClass({
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload}/>
fileClassToUpload={fileClassToUpload}
location={location}/>
<FileDragAndDropPreviewIterator
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
@ -188,12 +195,23 @@ let FileDragAndDrop = React.createClass({
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
{/*
Opera doesn't trigger simulated click events
if the targeted input has `display:none` set.
Which means we need to set its visibility to hidden
instead of using `display:none`.
See:
- http://stackoverflow.com/questions/12880604/jquery-triggerclick-not-working-on-opera-if-the-element-is-not-displayed
*/}
<input
multiple={multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
visibility: 'hidden',
position: 'absolute',
top: 0,
height: 0,
width: 0
}}

View File

@ -1,11 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import { getLangText } from '../../../utils/lang_utils';
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
let Link = Router.Link;
let FileDragAndDropDialog = React.createClass({
propTypes: {
@ -19,13 +20,24 @@ let FileDragAndDropDialog = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
mixins: [Router.State],
getDragDialog(fileClass) {
if(dragAndDropAvailable) {
return [
<p>{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p>
];
} else {
return null;
}
},
render() {
const queryParams = this.getQuery();
const queryParams = this.props.location.query;
if(this.props.hasFiles) {
return null;
@ -38,11 +50,13 @@ let FileDragAndDropDialog = React.createClass({
let queryParamsUpload = Object.assign({}, queryParams);
queryParamsUpload.method = 'upload';
let { location } = this.props;
return (
<div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p>
<Link
to={this.getPath()}
to={location.pathname}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
@ -52,7 +66,7 @@ let FileDragAndDropDialog = React.createClass({
<span> or </span>
<Link
to={this.getPath()}
to={location.pathname}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
@ -64,8 +78,7 @@ let FileDragAndDropDialog = React.createClass({
if(this.props.multipleFiles) {
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag %s here', this.props.fileClassToUpload.plural)}</p>
<p>{getLangText('or')}</p>
{this.getDragDialog(this.props.fileClassToUpload.plural)}
<span
className="btn btn-default"
onClick={this.props.onClick}>
@ -78,8 +91,7 @@ let FileDragAndDropDialog = React.createClass({
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}</p>
<p>{getLangText('or')}</p>
{this.getDragDialog(this.props.fileClassToUpload.singular)}
<span
className="btn btn-default"
onClick={this.props.onClick}>

View File

@ -2,7 +2,6 @@
import React from 'react/addons';
import fineUploader from 'fineUploader';
import Router from 'react-router';
import Q from 'q';
import S3Fetcher from '../../fetchers/s3_fetcher';
@ -127,10 +126,10 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
])
},
]),
mixins: [Router.State],
location: React.PropTypes.object
},
getDefaultProps() {
return {
@ -649,7 +648,7 @@ let ReactS3FineUploader = React.createClass({
//
// In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload')
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') {
let convertedFilePromises = [];
@ -830,7 +829,7 @@ let ReactS3FineUploader = React.createClass({
isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true;
@ -859,12 +858,20 @@ let ReactS3FineUploader = React.createClass({
enableLocalHashing,
fileClassToUpload,
validation,
fileInputElement
fileInputElement,
location
} = this.props;
// Here we initialize the template that has been either provided from the outside
// or the default input that is FileDragAndDrop.
return React.createElement(fileInputElement, {
multiple,
areAssetsDownloadable,
areAssetsEditable,
onInactive,
enableLocalHashing,
fileClassToUpload,
location,
onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile,
@ -872,14 +879,8 @@ let ReactS3FineUploader = React.createClass({
handlePauseFile: this.handlePauseFile,
handleResumeFile: this.handleResumeFile,
handleCancelHashing: this.handleCancelHashing,
multiple: multiple,
areAssetsDownloadable: areAssetsDownloadable,
areAssetsEditable: areAssetsEditable,
onInactive: onInactive,
dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress,
enableLocalHashing: enableLocalHashing,
fileClassToUpload: fileClassToUpload,
allowedExtensions: this.getAllowedExtensions()
});
}

View File

@ -1,7 +1,6 @@
'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';
@ -12,12 +11,13 @@ import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import ApiUrls from '../constants/api_urls';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({
mixins: [Router.Navigation],
render() {
setDocumentTitle(getLangText('Verify your Certificate of Authenticity'));
return (
<div className="ascribe-login-wrapper">
<br/>
@ -45,8 +45,6 @@ let CoaVerifyContainer = React.createClass({
let CoaVerifyForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(response){
let notification = null;
if (response.verdict) {

View File

@ -1,15 +1,15 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
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 NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import NavItem from 'react-bootstrap/lib/NavItem';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AclProxy from './acl_proxy';
@ -33,11 +33,9 @@ import { getLangText } from '../utils/lang_utils';
let Header = React.createClass({
propTypes: {
showAddWork: React.PropTypes.bool,
routes: React.PropTypes.element
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
mixins: [Router.State],
getDefaultProps() {
return {
showAddWork: true
@ -134,38 +132,61 @@ let Header = React.createClass({
ref='dropdownbutton'
eventKey="1"
title={this.state.currentUser.username}>
<MenuItemLink
eventKey="2"
to="settings"
<LinkContainer
to="/settings"
onClick={this.onMenuItemClick}>
<MenuItem
eventKey="2">
{getLangText('Account Settings')}
</MenuItemLink>
</MenuItem>
</LinkContainer>
<AclProxy
aclObject={this.state.currentUser.acl}
aclName="acl_view_settings_contract">
<MenuItemLink
to="contract_settings"
<LinkContainer
to="/contract_settings"
onClick={this.onMenuItemClick}>
<MenuItem
eventKey="2">
{getLangText('Contract Settings')}
</MenuItemLink>
</MenuItem>
</LinkContainer>
</AclProxy>
<MenuItem divider />
<MenuItemLink eventKey="3" to="logout">{getLangText('Log out')}</MenuItemLink>
<LinkContainer
to="/logout">
<MenuItem
eventKey="3">
{getLangText('Log out')}
</MenuItem>
</LinkContainer>
</DropdownButton>
);
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
}
else {
account = <NavItemLink to="login">{getLangText('LOGIN')}</NavItemLink>;
signup = <NavItemLink to="signup">{getLangText('SIGNUP')}</NavItemLink>;
account = (
<LinkContainer
to="/login">
<NavItem>
{getLangText('LOGIN')}
</NavItem>
</LinkContainer>
);
signup = (
<LinkContainer
to="/signup">
<NavItem>
{getLangText('SIGNUP')}
</NavItem>
</LinkContainer>
);
}
return (
<div>
<Navbar
brand={
this.getLogo()
}
brand={this.getLogo()}
toggleNavKey={0}
fixedTop={true}>
<CollapsibleNav eventKey={0}>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MenuItem from 'react-bootstrap/lib/MenuItem';
@ -14,8 +14,6 @@ import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let HeaderNotifications = React.createClass({
@ -39,7 +37,7 @@ let HeaderNotifications = React.createClass({
this.setState(state);
},
onMenuItemClick(event) {
onMenuItemClick() {
/*
This is a hack to make the dropdown close after clicking on an item
The function just need to be defined
@ -158,23 +156,13 @@ let NotificationListItem = React.createClass({
},
getLinkData() {
let { pieceOrEdition } = this.props;
if (this.isPiece()) {
return {
to: 'piece',
params: {
pieceId: this.props.pieceOrEdition.id
}
};
return `/pieces/${pieceOrEdition.id}`;
} else {
return {
to: 'edition',
params: {
editionId: this.props.pieceOrEdition.bitcoin_id
return `/editions/${pieceOrEdition.bitcoin_id}`;
}
};
}
},
onClick(event){
@ -184,7 +172,7 @@ let NotificationListItem = React.createClass({
getNotificationText(){
let numNotifications = null;
if (this.props.notification.length > 1){
numNotifications = <div>+ {this.props.notification.length - 1} more...</div>;
numNotifications = <div>+ {this.props.notification.length - 1} {getLangText('more...')}</div>;
}
return (
<div className="notification-action">
@ -196,7 +184,7 @@ let NotificationListItem = React.createClass({
render() {
if (this.props.pieceOrEdition) {
return (
<Link {...this.getLinkData()} onClick={this.onClick}>
<Link to={this.getLinkData()} onClick={this.onClick}>
<div className="row notification-wrapper">
<div className="col-xs-4 clear-paddings">
<div className="thumbnail-wrapper">

View File

@ -1,13 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import LoginForm from './ascribe_forms/form_login';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
import { setDocumentTitle } from '../utils/dom_utils';
let LoginContainer = React.createClass({
@ -15,7 +14,8 @@ let LoginContainer = React.createClass({
message: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
onLogin: React.PropTypes.func,
location: React.PropTypes.object
},
getDefaultProps() {
@ -27,16 +27,19 @@ let LoginContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Log in'));
return (
<div className="ascribe-login-wrapper">
<LoginForm
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
message={this.props.message}
onLogin={this.props.onLogin}/>
onLogin={this.props.onLogin}
location={this.props.location}/>
<div className="ascribe-login-text">
{getLangText('Not an ascribe user')}&#63; <Link to="signup">{getLangText('Sign up')}...</Link><br/>
{getLangText('Forgot my password')}&#63; <Link to="password_reset">{getLangText('Rescue me')}...</Link>
{getLangText('Not an ascribe user')}&#63; <Link to="/signup">{getLangText('Sign up')}...</Link><br/>
{getLangText('Forgot my password')}&#63; <Link to="/password_reset">{getLangText('Rescue me')}...</Link>
</div>
</div>
);

View File

@ -1,38 +1,33 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import UserActions from '../actions/user_actions';
import { alt, altWhitelabel, altUser, altThirdParty } from '../alt';
import AppConstants from '../constants/application_constants';
let baseUrl = AppConstants.baseUrl;
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let LogoutContainer = React.createClass({
mixins: [Router.Navigation, Router.State],
mixins: [History],
componentDidMount() {
UserActions.logoutCurrentUser()
.then(() => {
UserActions.logoutCurrentUser();
alt.flush();
altWhitelabel.flush();
altUser.flush();
altThirdParty.flush();
// kill intercom (with fire)
window.Intercom('shutdown');
this.replaceWith(baseUrl);
})
.catch((err) => {
console.logGlobal(err);
});
},
render() {
setDocumentTitle(getLangText('Log out'));
return null;
}
});

View File

@ -13,7 +13,7 @@ import { sanitizeList } from '../utils/general_utils';
let NavRoutesLinks = React.createClass({
propTypes: {
routes: React.PropTypes.element,
routes: React.PropTypes.arrayOf(React.PropTypes.object),
userAcl: React.PropTypes.object
},
@ -33,15 +33,15 @@ let NavRoutesLinks = React.createClass({
return;
}
let links = node.props.children.map((child, j) => {
let links = node.childRoutes.map((child, j) => {
let childrenFn = null;
let { aclName, headerTitle, name, children } = child.props;
let { aclName, headerTitle, path, childRoutes } = child;
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass childrenFn as false
if(child.props.children && child.props.children.length > 0) {
if(child.childRoutes && child.childRoutes.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
@ -58,7 +58,7 @@ let NavRoutesLinks = React.createClass({
aclObject={this.props.userAcl}>
<NavRoutesLinksLink
headerTitle={headerTitle}
routeName={name}
routePath={'/' + path}
depth={i}
children={childrenFn}/>
</AclProxy>
@ -68,7 +68,7 @@ let NavRoutesLinks = React.createClass({
<NavRoutesLinksLink
key={j}
headerTitle={headerTitle}
routeName={name}
routePath={'/' + path}
depth={i}
children={childrenFn}/>
);
@ -88,7 +88,7 @@ let NavRoutesLinks = React.createClass({
return (
<Nav {...this.props}>
{this.extractLinksFromRoutes(routes, userAcl, 0)}
{this.extractLinksFromRoutes(routes[0], userAcl, 0)}
</Nav>
);
}

View File

@ -3,13 +3,16 @@
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import NavItem from 'react-bootstrap/lib/NavItem';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
let NavRoutesLinksLink = React.createClass({
propTypes: {
headerTitle: React.PropTypes.string,
routeName: React.PropTypes.string,
routePath: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
@ -20,10 +23,10 @@ let NavRoutesLinksLink = React.createClass({
},
render() {
let { children, headerTitle, depth, routeName } = this.props;
let { children, headerTitle, depth, routePath } = this.props;
// if the route has children, we're returning a DropdownButton that will get filled
// with MenuItemLinks
// with MenuItems
if(children) {
return (
<DropdownButton title={headerTitle}>
@ -33,13 +36,17 @@ let NavRoutesLinksLink = React.createClass({
} else {
if(depth === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
// returning a DropdownButton matching MenuItem
return (
<MenuItemLink to={routeName}>{headerTitle}</MenuItemLink>
<LinkContainer to={routePath}>
<MenuItem>{headerTitle}</MenuItem>
</LinkContainer>
);
} else if(depth === 0) {
return (
<NavItemLink to={routeName}>{headerTitle}</NavItemLink>
<LinkContainer to={routePath}>
<NavItem>{headerTitle}</NavItem>
</LinkContainer>
);
} else {
return null;

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
@ -10,10 +10,13 @@ import ApiUrls from '../constants/api_urls';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let PasswordResetContainer = React.createClass({
mixins: [Router.Navigation],
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return {isRequested: false};
@ -24,12 +27,14 @@ let PasswordResetContainer = React.createClass({
},
render() {
if (this.props.query.email && this.props.query.token) {
let { location } = this.props;
if (location.query.email && location.query.token) {
return (
<div>
<PasswordResetForm
email={this.props.query.email}
token={this.props.query.token}/>
email={location.query.email}
token={location.query.token}/>
</div>
);
}
@ -71,6 +76,8 @@ let PasswordRequestResetForm = React.createClass({
},
render() {
setDocumentTitle(getLangText('Reset your password'));
return (
<Form
ref="form"
@ -112,7 +119,7 @@ let PasswordResetForm = React.createClass({
token: React.PropTypes.string
},
mixins: [Router.Navigation],
mixins: [History],
getFormData() {
return {
@ -122,7 +129,7 @@ let PasswordResetForm = React.createClass({
},
handleSuccess() {
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
let notification = new GlobalNotificationModel(getLangText('password successfully updated'), 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import PieceListStore from '../stores/piece_list_store';
import PieceListActions from '../actions/piece_list_actions';
@ -24,6 +24,7 @@ import AppConstants from '../constants/application_constants';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let PieceList = React.createClass({
@ -33,10 +34,11 @@ let PieceList = React.createClass({
customSubmitButton: React.PropTypes.element,
filterParams: React.PropTypes.array,
orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string
orderBy: React.PropTypes.string,
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -60,7 +62,7 @@ let PieceList = React.createClass({
},
componentDidMount() {
let page = this.getQuery().page || 1;
let page = this.props.location.query.page || 1;
PieceListStore.listen(this.onChange);
EditionListStore.listen(this.onChange);
@ -75,7 +77,7 @@ let PieceList = React.createClass({
componentDidUpdate() {
if (this.props.redirectTo && this.state.unfilteredPieceListCount === 0) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.transitionTo(this.props.redirectTo, this.getQuery()));
window.setTimeout(() => this.history.pushState(null, this.props.redirectTo, this.props.location.query), 0);
}
},
@ -100,7 +102,7 @@ let PieceList = React.createClass({
},
getPagination() {
let currentPage = parseInt(this.getQuery().page, 10) || 1;
let currentPage = parseInt(this.props.location.query.page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
if (this.state.pieceListCount > 10) {
@ -116,7 +118,7 @@ let PieceList = React.createClass({
searchFor(searchTerm) {
PieceListActions.fetchPieceList(1, this.state.pageSize, searchTerm, this.state.orderBy,
this.state.orderAsc, this.state.filterBy);
this.transitionTo(this.getPathname(), {page: 1});
this.history.pushState(null, this.props.location.pathname, {page: 1});
},
applyFilterBy(filterBy){
@ -140,7 +142,7 @@ let PieceList = React.createClass({
// we have to redirect the user always to page one as it could be that there is no page two
// for filtered pieces
this.transitionTo(this.getPathname(), {page: 1});
this.history.pushState(null, this.props.location.pathname, {page: 1});
},
applyOrderBy(orderBy) {
@ -152,11 +154,14 @@ let PieceList = React.createClass({
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
let AccordionListItemType = this.props.accordionListItemType;
setDocumentTitle(getLangText('Collection'));
return (
<div>
<PieceListToolbar
className="ascribe-piece-list-toolbar"
searchFor={this.searchFor}
searchQuery={this.state.search}
filterParams={this.props.filterParams}
orderParams={this.props.orderParams}
filterBy={this.state.filterBy}
@ -177,6 +182,7 @@ let PieceList = React.createClass({
orderBy={this.state.orderBy}
orderAsc={this.state.orderAsc}
search={this.state.search}
searchFor={this.searchFor}
page={this.state.page}
pageSize={this.state.pageSize}
loadingElement={loadingElement}>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
@ -20,12 +20,9 @@ import GlobalNotificationActions from '../actions/global_notification_actions';
import PropertyCollapsible from './ascribe_forms/property_collapsible';
import RegisterPieceForm from './ascribe_forms/form_register_piece';
import LoginContainer from './login_container';
import SlidesContainer from './ascribe_slides_container/slides_container';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let RegisterPiece = React.createClass( {
@ -37,10 +34,11 @@ let RegisterPiece = React.createClass( {
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element,
React.PropTypes.string
])
]),
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getDefaultProps() {
return {
@ -60,10 +58,10 @@ let RegisterPiece = React.createClass( {
},
componentDidMount() {
WhitelabelActions.fetchWhitelabel();
PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
@ -98,7 +96,7 @@ let RegisterPiece = React.createClass( {
this.state.filterBy
);
this.transitionTo('piece', {pieceId: response.piece.id});
this.history.pushState(null, `/pieces/${response.piece.id}`);
},
getSpecifyEditions() {
@ -117,53 +115,22 @@ let RegisterPiece = React.createClass( {
}
},
// basically redirects to the second slide (index: 1), when the user is not logged in
onLoggedOut() {
// only transition to the login store, if user is not logged in
// ergo the currentUser object is not properly defined
if(this.state.currentUser && !this.state.currentUser.email) {
this.refs.slidesContainer.setSlideNum(1);
}
},
onLogin() {
// once the currentUser object from UserStore is defined (eventually the user was transitioned
// to the login form via the slider and successfully logged in), we can direct him back to the
// register_piece slide
if(this.state.currentUser && this.state.currentUser.email) {
window.history.back();
}
},
render() {
setDocumentTitle(getLangText('Register a new piece'));
return (
<SlidesContainer
ref="slidesContainer"
forwardProcess={false}>
<div
onClick={this.onLoggedOut}
onFocus={this.onLoggedOut}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<RegisterPieceForm
{...this.props}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleSuccess}
onLoggedOut={this.onLoggedOut}>
location={this.props.location}>
{this.props.children}
{this.getSpecifyEditions()}
</RegisterPieceForm>
</Col>
</Row>
</div>
<div>
<LoginContainer
message={getLangText('Please login before ascribing your work%s', '...')}
redirectOnLoggedIn={false}
redirectOnLoginSuccess={false}
onLogin={this.onLogin}/>
</div>
</SlidesContainer>
);
}
});

140
js/components/search_bar.js Normal file
View File

@ -0,0 +1,140 @@
'use strict';
import React from 'react';
import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { getLangText } from '../utils/lang_utils';
const { func, string, number } = React.PropTypes;
const SearchBar = React.createClass({
propTypes: {
// a function that accepts a string as a search query and updates the
// propagated `searchQuery` after successfully retrieving the
// request from the server
searchFor: func.isRequired,
searchQuery: string.isRequired,
className: string,
// the number of milliseconds the component
// should wait before requesting search results from the server
threshold: number.isRequired
},
getInitialState() {
return {
timer: null,
searchQuery: '',
loading: false
};
},
componentDidUpdate(prevProps) {
const searchQueryProps = this.props.searchQuery;
const searchQueryPrevProps = prevProps.searchQuery;
const searchQueryState = this.state.searchQuery;
const { loading } = this.state;
/**
* 1. Condition: `loading` must be true, which implies that `evaluateTimer`,
* has already been called
*
* AND
*
* (
* 2. Condition: `searchQueryProps` and `searchQueryState` are true and equal
* (which means that the search query has been propagated to the inner
* fetch method of `fetchPieceList`, which in turn means that a fetch
* has completed)
*
* OR
*
* 3. Condition: `searchQueryProps` and `searchQueryState` can be any value (`true` or
* `false`, as long as they're equal). This case only occurs when the user
* has entered a `searchQuery` and deletes the query in one go, reseting
* `searchQueryProps` to empty string ('' === false) again.
* )
*/
const firstCondition = !!loading;
const secondCondition = searchQueryProps && searchQueryState && searchQueryProps === searchQueryState;
const thirdCondition = !searchQueryPrevProps && searchQueryProps === searchQueryState;
if(firstCondition && (secondCondition || thirdCondition)) {
this.setState({ loading: false });
}
},
componentWillReceiveProps(nextProps) {
/**
* This enables the `PieceListStore` to override the state
* of that component in case someone is changing the `searchQuery` on
* another component.
*
* Like how it's being done in the 'Clear search' dialog.
*/
if(this.props.searchQuery !== nextProps.searchQuery || !this.state.searchQuery) {
this.setState({ searchQuery: nextProps.searchQuery });
}
},
startTimer(searchQuery) {
const { timer } = this.state;
const { threshold } = this.props;
// The timer waits for the specified threshold time in milliseconds
// and then calls `evaluateTimer`.
// If another letter has been called in the mean time (timespan < `threshold`),
// the present timer gets cleared and a new one is added to `this.state`.
// This means that `evaluateTimer`, will only be called when the threshold has actually
// passed,
clearTimeout(timer); // apparently `clearTimeout` can be called with null, without throwing errors
const newTimer = setTimeout(this.evaluateTimer(searchQuery), threshold);
this.setState({ timer: newTimer });
},
evaluateTimer(searchQuery) {
return () => {
this.setState({ timer: null, loading: true }, () => {
// search for the query
this.props.searchFor(searchQuery);
});
};
},
handleChange({ target: { value }}) {
// On each letter entry we're updating the state of the component
// and start a timer, which we're also pushing to the state
// of the component
this.startTimer(value);
this.setState({ searchQuery: value });
},
render() {
let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>;
const { className } = this.props;
const { loading, searchQuery } = this.state;
if(loading) {
searchIcon = <span className="glyph-ascribe-spool-chunked ascribe-color spin"/>;
}
return (
<span className={className}>
<Input
type='text'
value={searchQuery}
placeholder={getLangText('Search%s', '...')}
onChange={this.handleChange}
addonAfter={searchIcon} />
</span>
);
}
});
export default SearchBar;

View File

@ -1,15 +1,19 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import SignupForm from './ascribe_forms/form_signup';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let Link = Router.Link;
let SignupContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return {
submitted: false,
@ -25,6 +29,8 @@ let SignupContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Sign up'));
if (this.state.submitted){
return (
<div className="ascribe-login-wrapper">
@ -37,9 +43,11 @@ let SignupContainer = React.createClass({
}
return (
<div className="ascribe-login-wrapper">
<SignupForm handleSuccess={this.handleSuccess} />
<SignupForm
handleSuccess={this.handleSuccess}
location={this.props.location}/>
<div className="ascribe-login-text">
{getLangText('Already an ascribe user')}&#63; <Link to="login">{getLangText('Log in')}...</Link><br/>
{getLangText('Already an ascribe user')}&#63; <Link to="/login">{getLangText('Log in')}...</Link><br/>
</div>
</div>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import StarRating from 'react-star-rating';
import PieceListActions from '../../../../../actions/piece_list_actions';
@ -21,12 +21,9 @@ import GlobalNotificationActions from '../../../../../actions/global_notificatio
import AclProxy from '../../../../acl_proxy';
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
let Link = Router.Link;
let AccordionListItemPrize = React.createClass({
propTypes: {
@ -85,7 +82,7 @@ let AccordionListItemPrize = React.createClass({
return (
<div id="list-rating" className="pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
<Link to={`/pieces/${this.props.content.id}`}>
<StarRating
ref='rating'
name="prize-rating"
@ -108,7 +105,7 @@ let AccordionListItemPrize = React.createClass({
// jury and no rating yet
return (
<div className="react-rating-caption pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
<Link to={`/pieces/${this.props.content.id}`}>
{getLangText('Submit your rating')}
</Link>
</div>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import Moment from 'moment';
import StarRating from 'react-star-rating';
@ -40,8 +40,8 @@ import DetailProperty from '../../../../ascribe_detail/detail_property';
import ApiUrls from '../../../../../constants/api_urls';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let Link = Router.Link;
/**
* This is the component that implements resource/data specific functionality
@ -113,12 +113,22 @@ let PieceContainer = React.createClass({
// Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted
let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) ||
(this.state.currentUser.is_judge && !this.state.piece.selected )) ?
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : this.state.piece.artist_name;
null : this.state.piece.artist_name;
// Only show the artist email if you are a judge and the piece is shortlisted
let artistEmail = (this.state.currentUser.is_judge && this.state.piece.selected ) ?
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } /> : null;
if (artistName === null) {
setDocumentTitle(this.state.piece.title);
} else {
setDocumentTitle([artistName, this.state.piece.title].join(', '));
}
if (artistName === null) {
artistName = <span className="glyphicon glyphicon-eye-close" aria-hidden="true"/>;
}
return (
<Piece
piece={this.state.piece}
@ -169,12 +179,12 @@ let NavigationHeader = React.createClass({
return (
<div style={{marginBottom: '1em'}}>
<div className="row no-margin">
<Link className="disable-select" to='piece' params={{pieceId: nav.prev_index ? nav.prev_index : this.props.piece.id}}>
<Link className="disable-select" to={`/pieces/${ nav.prev_index || this.props.piece.id }`}>
<span className="glyphicon glyphicon-chevron-left pull-left link-ascribe" aria-hidden="true">
{getLangText('Previous')}
</span>
</Link>
<Link className="disable-select" to='piece' params={{pieceId: nav.next_index ? nav.next_index : this.props.piece.id}}>
<Link className="disable-select" to={`/pieces/${ nav.next_index || this.props.piece.id }`}>
<span className="pull-right link-ascribe">
{getLangText('Next')}
<span className="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>

View File

@ -1,14 +1,16 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions';
@ -17,7 +19,7 @@ import { getLangText } from '../../../../utils/lang_utils';
let Landing = React.createClass({
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -44,7 +46,7 @@ let Landing = React.createClass({
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.replaceWith('pieces'), 0);
window.setTimeout(() => this.history.replaceState(null, '/collection'), 0);
}
},
@ -52,16 +54,20 @@ let Landing = React.createClass({
if (this.state.prize && this.state.prize.active){
return (
<ButtonGroup className="enter" bsSize="large" vertical>
<ButtonLink to="signup">
<LinkContainer to="/signup">
<Button>
{getLangText('Sign up to submit')}
</ButtonLink>
</Button>
</LinkContainer>
<p>
{getLangText('or, already an ascribe user?')}
</p>
<ButtonLink to="login">
<LinkContainer to="/login">
<Button>
{getLangText('Log in to submit')}
</ButtonLink>
</Button>
</LinkContainer>
</ButtonGroup>
);
}
@ -74,9 +80,11 @@ let Landing = React.createClass({
<p>
{getLangText('or, already an ascribe user?')}
</p>
<ButtonLink to="login">
<LinkContainer to="/login">
<Button>
{getLangText('Log in')}
</ButtonLink>
</Button>
</LinkContainer>
</ButtonGroup>
);
},

View File

@ -1,29 +1,35 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import LoginForm from '../../../ascribe_forms/form_login';
import { getLangText } from '../../../../utils/lang_utils';
let Link = Router.Link;
import { setDocumentTitle } from '../../../../utils/dom_utils';
let LoginContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
render() {
setDocumentTitle(getLangText('Log in'));
return (
<div className="ascribe-login-wrapper">
<LoginForm
headerMessage={getLangText('Log in with ascribe')} />
headerMessage={getLangText('Log in with ascribe')}
location={this.props.location}/>
<div
className="ascribe-login-text">
{getLangText('I\'m not a user') + ' '}
<Link to="signup">{getLangText('Sign up...')}</Link>
<Link to="/signup">{getLangText('Sign up...')}</Link>
<br/>
{getLangText('I forgot my password') + ' '}
<Link to="password_reset">{getLangText('Rescue me...')}</Link>
<Link to="/password_reset">{getLangText('Rescue me...')}</Link>
</div>
</div>
);

View File

@ -9,13 +9,21 @@ import UserStore from '../../../../stores/user_store';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize';
import { mergeOptions } from '../../../../utils/general_utils';
import { getLangText } from '../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils';
let PrizePieceList = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return mergeOptions(
PrizeStore.getState(),
@ -42,9 +50,11 @@ let PrizePieceList = React.createClass({
getButtonSubmit() {
if (this.state.prize && this.state.prize.active && !this.state.currentUser.is_jury){
return (
<ButtonLink to="register_piece">
<LinkContainer to="/register_piece">
<Button>
{getLangText('Submit to prize')}
</ButtonLink>
</Button>
</LinkContainer>
);
}
else if (this.state.prize && this.state.currentUser.is_judge){
@ -54,6 +64,8 @@ let PrizePieceList = React.createClass({
},
render() {
setDocumentTitle(getLangText('Collection'));
let orderParams = ['artist_name', 'title'];
if (this.state.currentUser.is_jury) {
orderParams = ['rating', 'title'];
@ -65,12 +77,13 @@ let PrizePieceList = React.createClass({
<div>
<PieceList
ref="list"
redirectTo="register_piece"
redirectTo="/register_piece"
accordionListItemType={AccordionListItemPrize}
orderParams={orderParams}
orderBy={this.state.currentUser.is_jury ? 'rating' : null}
filterParams={[]}
customSubmitButton={this.getButtonSubmit()}/>
customSubmitButton={this.getButtonSubmit()}
location={this.props.location}/>
</div>
);
}

View File

@ -11,6 +11,7 @@ import InputTextAreaToggable from '../../../ascribe_forms/input_textarea_toggabl
import InputCheckbox from '../../../ascribe_forms/input_checkbox';
import { getLangText } from '../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils';
let PrizeRegisterPiece = React.createClass({
@ -32,6 +33,8 @@ let PrizeRegisterPiece = React.createClass({
},
render() {
setDocumentTitle(getLangText('Submit to the prize'));
if(this.state.prize && this.state.prize.active){
return (
<RegisterPiece

View File

@ -24,6 +24,7 @@ import AppConstants from '../../../../constants/application_constants';
import ApiUrls from '../../../../constants/api_urls';
import { getLangText } from '../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils';
let Settings = React.createClass({
@ -45,6 +46,8 @@ let Settings = React.createClass({
},
render() {
setDocumentTitle(getLangText('Account settings'));
let prizeSettings = null;
if (this.state.currentUser.is_admin){
prizeSettings = <PrizeSettings />;

View File

@ -4,8 +4,13 @@ import React from 'react';
import SignupForm from '../../../ascribe_forms/form_signup';
import { getLangText } from '../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../utils/dom_utils';
let SignupContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return {
submitted: false,
@ -21,6 +26,8 @@ let SignupContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Sign up'));
if (this.state.submitted){
return (
<div className="ascribe-login-wrapper">
@ -35,7 +42,8 @@ let SignupContainer = React.createClass({
<SignupForm
headerMessage={getLangText('Create account for submission')}
submitMessage={getLangText('Sign up')}
handleSuccess={this.handleSuccess} />
handleSuccess={this.handleSuccess}
location={this.props.location}/>
</div>
);
}

View File

@ -1,38 +1,44 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Hero from './components/prize_hero';
import Header from '../../header';
import Footer from '../../footer';
import GlobalNotification from '../../global_notification';
import getRoutes from './prize_routes';
import { getSubdomain } from '../../../utils/general_utils';
let RouteHandler = Router.RouteHandler;
let PrizeApp = React.createClass({
mixins: [Router.State],
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
history: React.PropTypes.object,
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
render() {
const { history, routes } = this.props;
let header = null;
let subdomain = getSubdomain();
let ROUTES = getRoutes(null, subdomain);
// The second element of routes is always the active component object, where we can
// extract the path.
let path = routes[1] ? routes[1].path : null;
if (this.isActive('landing') || this.isActive('login') || this.isActive('signup')) {
// if the path of the current activeRoute is not defined, then this is the IndexRoute
if (!path || history.isActive('/login') || history.isActive('/signup')) {
header = <Hero />;
} else {
header = <Header showAddWork={false} routes={ROUTES}/>;
header = <Header showAddWork={false} routes={routes}/>;
}
return (
<div className={'container ascribe-prize-app client--' + subdomain}>
{header}
<RouteHandler />
{this.props.children}
<GlobalNotification />
<div id="modal" className="container"></div>
<Footer />

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Route, IndexRoute } from 'react-router';
import Landing from './components/prize_landing';
import LoginContainer from './components/prize_login_container';
@ -17,28 +17,42 @@ import CoaVerifyContainer from '../../../components/coa_verify_container';
import ErrorNotFoundPage from '../../../components/error_not_found_page';
import App from './prize_app';
import AppConstants from '../../../constants/application_constants';
let Route = Router.Route;
let NotFoundRoute = Router.NotFoundRoute;
let baseUrl = AppConstants.baseUrl;
import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler';
function getRoutes() {
return (
<Route name="app" path={baseUrl} handler={App}>
<Route name="landing" path={baseUrl} handler={Landing} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="register_piece" path="register_piece" handler={PrizeRegisterPiece} headerTitle="+ NEW WORK" />
<Route name="pieces" path="collection" handler={PrizePieceList} headerTitle="COLLECTION" />
<Route name="piece" path="pieces/:pieceId" handler={PrizePieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<NotFoundRoute name="notFound" handler={ErrorNotFoundPage} />
<Route path='/' component={App}>
<IndexRoute component={Landing} />
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route
path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route
path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizeRegisterPiece)}
headerTitle='+ NEW WORK'/>
<Route
path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizePieceList)}
headerTitle='COLLECTION'/>
<Route path='pieces/:pieceId' component={PrizePieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
);
}

View File

@ -8,9 +8,13 @@ import LicenseActions from '../../../../../actions/license_actions';
import LicenseStore from '../../../../../stores/license_store';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
let CCRegisterPiece = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return mergeOptions(
@ -78,11 +82,13 @@ let CCRegisterPiece = React.createClass({
},
render() {
setDocumentTitle(getLangText('Register a new piece'));
return (
<RegisterPiece
enableLocalHashing={false}
headerMessage={getLangText('Register under a Creative Commons license')}
submitMessage={getLangText('Submit')}>
submitMessage={getLangText('Submit')}
location={this.props.location}>
{this.getLicenses()}
</RegisterPiece>
);

View File

@ -3,7 +3,9 @@
import React from 'react';
import classNames from 'classnames';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import WhitelabelActions from '../../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../../stores/whitelabel_store';
@ -37,29 +39,21 @@ let CylandSubmitButton = React.createClass({
},
render() {
let piece = this.props.piece;
let startFrom = 1;
// In the Cyland register page a user has to complete three steps.
// Since every one of those steps is atomic a user should always be able to continue
// where he left of.
// This is why we start the process form slide 1/2 if the user has already finished
// it in another session.
if(piece && piece.extra_data && Object.keys(piece.extra_data).length > 0) {
startFrom = 2;
}
const { piece, className } = this.props;
return (
<ButtonLink
to="register_piece"
<LinkContainer
to="/register_piece"
query={{
'slide_num': 0,
'start_from': startFrom,
'piece_id': this.props.piece.id
}}
className={classNames('btn', 'btn-default', 'btn-xs', this.props.className)}>
'start_from': 1,
'piece_id': piece.id
}}>
<Button
className={classNames('btn', 'btn-default', 'btn-xs', className)}>
{getLangText('Submit to Cyland')}
</ButtonLink>
</Button>
</LinkContainer>
);
}
});

View File

@ -18,10 +18,12 @@ import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container';
import AppConstants from '../../../../../../constants/application_constants';
import { getLangText } from '../../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../../utils/dom_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
let CylandPieceContainer = React.createClass({
propTypes: {
location: React.PropTypes.object,
params: React.PropTypes.object
},
@ -60,6 +62,8 @@ let CylandPieceContainer = React.createClass({
render() {
if(this.state.piece && this.state.piece.title) {
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
return (
<WalletPieceContainer
piece={this.state.piece}
@ -72,7 +76,8 @@ let CylandPieceContainer = React.createClass({
<CylandAdditionalDataForm
piece={this.state.piece}
disabled={!this.state.piece.acl.acl_edit}
isInline={true} />
isInline={true}
location={this.props.location}/>
</CollapsibleParagraph>
</WalletPieceContainer>
);

View File

@ -26,7 +26,8 @@ let CylandAdditionalDataForm = React.createClass({
handleSuccess: React.PropTypes.func,
piece: React.PropTypes.object.isRequired,
disabled: React.PropTypes.bool,
isInline: React.PropTypes.bool
isInline: React.PropTypes.bool,
location: React.PropTypes.object
},
getDefaultProps() {
@ -140,7 +141,8 @@ let CylandAdditionalDataForm = React.createClass({
isReadyForFormSubmission={formSubmissionValidation.fileOptional}
pieceId={piece.id}
otherData={piece.other_data}
multiple={true}/>
multiple={true}
location={this.props.location}/>
</Form>
);
} else {

View File

@ -1,22 +1,26 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import WhitelabelActions from '../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../stores/whitelabel_store';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let CylandLanding = React.createClass({
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -43,11 +47,13 @@ let CylandLanding = React.createClass({
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.replaceWith('pieces'), 0);
window.setTimeout(() => this.history.replaceState(null, '/collection'), 0);
}
},
render() {
setDocumentTitle('CYLAND MediaArtLab');
return (
<div className="container ascribe-form-wrapper">
<div className="row">
@ -67,17 +73,21 @@ let CylandLanding = React.createClass({
<p>
{getLangText('Existing ascribe user?')}
</p>
<ButtonLink to="login">
<LinkContainer to="/login">
<Button>
{getLangText('Log in')}
</ButtonLink>
</Button>
</LinkContainer>
</div>
<div className="col-sm-6">
<p>
{getLangText('Do you need an account?')}
</p>
<ButtonLink to="signup">
<LinkContainer to="/signup">
<Button>
{getLangText('Sign up')}
</ButtonLink>
</Button>
</LinkContainer>
</div>
</div>
</div>

View File

@ -9,9 +9,14 @@ import UserStore from '../../../../../stores/user_store';
import CylandAccordionListItem from './cyland_accordion_list/cyland_accordion_list_item';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let CylandPieceList = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return UserStore.getState();
},
@ -30,10 +35,12 @@ let CylandPieceList = React.createClass({
},
render() {
setDocumentTitle(getLangText('Collection'));
return (
<div>
<PieceList
redirectTo="register_piece"
redirectTo="/register_piece?slide_num=0"
accordionListItemType={CylandAccordionListItem}
filterParams={[{
label: getLangText('Show works I have'),
@ -42,7 +49,7 @@ let CylandPieceList = React.createClass({
label: getLangText('loaned to Cyland')
}]
}]}
/>
location={this.props.location}/>
</div>
);
}

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Moment from 'moment';
@ -34,13 +34,17 @@ import SlidesContainer from '../../../../ascribe_slides_container/slides_contain
import ApiUrls from '../../../../../constants/api_urls';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getAclFormMessage } from '../../../../../utils/form_utils';
let CylandRegisterPiece = React.createClass({
propTypes: {
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getInitialState(){
return mergeOptions(
@ -63,7 +67,7 @@ let CylandRegisterPiece = React.createClass({
UserActions.fetchCurrentUser();
WhitelabelActions.fetchWhitelabel();
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
// Since every step of this register process is atomic,
// we may need to enter the process at step 1 or 2.
@ -101,12 +105,13 @@ let CylandRegisterPiece = React.createClass({
// also start loading the piece for the next step
if(response && response.piece) {
PieceActions.updatePiece({});
PieceActions.updatePiece(response.piece);
}
this.incrementStep();
this.refs.slidesContainer.nextSlide();
this.refs.slidesContainer.nextSlide({ piece_id: response.piece.id });
},
handleAdditionalDataSuccess() {
@ -130,7 +135,8 @@ let CylandRegisterPiece = React.createClass({
this.refreshPieceList();
PieceActions.fetchOne(this.state.piece.id);
this.transitionTo('piece', {pieceId: this.state.piece.id});
this.history.pushState(null, `/pieces/${this.state.piece.id}`);
},
// We need to increase the step to lock the forms that are already filled out
@ -163,7 +169,7 @@ let CylandRegisterPiece = React.createClass({
// basically redirects to the second slide (index: 1), when the user is not logged in
onLoggedOut() {
this.transitionTo('login');
this.history.pushState(null, '/login');
},
render() {
@ -172,6 +178,8 @@ let CylandRegisterPiece = React.createClass({
let datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain = new Moment();
datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain.add(1000, 'years');
setDocumentTitle(getLangText('Register a new piece'));
return (
<SlidesContainer
ref="slidesContainer"
@ -179,7 +187,8 @@ let CylandRegisterPiece = React.createClass({
glyphiconClassNames={{
pending: 'glyphicon glyphicon-chevron-right',
completed: 'glyphicon glyphicon-lock'
}}>
}}
location={this.props.location}>
<div data-slide-title={getLangText('Register work')}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
@ -190,7 +199,8 @@ let CylandRegisterPiece = React.createClass({
submitMessage={getLangText('Submit')}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleRegisterSuccess}
onLoggedOut={this.onLoggedOut} />
onLoggedOut={this.onLoggedOut}
location={this.props.location}/>
</Col>
</Row>
</div>
@ -200,7 +210,8 @@ let CylandRegisterPiece = React.createClass({
<CylandAdditionalDataForm
disabled={this.state.step > 1}
handleSuccess={this.handleAdditionalDataSuccess}
piece={this.state.piece}/>
piece={this.state.piece}
location={this.props.location}/>
</Col>
</Row>
</div>

View File

@ -2,7 +2,10 @@
import React from 'react';
import classNames from 'classnames';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import { getLangText } from '../../../../../../utils/lang_utils';
@ -32,16 +35,18 @@ let IkonotvSubmitButton = React.createClass({
}
return (
<ButtonLink
to="register_piece"
<LinkContainer
to="/register_piece"
query={{
'slide_num': 0,
'start_from': startFrom,
'piece_id': piece.id
}}
}}>
<Button
className={classNames('ascribe-margin-1px', this.props.className)}>
{getLangText('Loan to IkonoTV')}
</ButtonLink>
</Button>
</LinkContainer>
);
}
});

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import Button from 'react-bootstrap/lib/Button';
@ -27,13 +27,13 @@ import Property from '../../../../ascribe_forms/property';
import AppConstants from '../../../../../constants/application_constants';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
let Navigation = Router.Navigation;
let IkonotvContractNotifications = React.createClass({
mixins: [Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -114,7 +114,7 @@ let IkonotvContractNotifications = React.createClass({
handleConfirmSuccess() {
let notification = new GlobalNotificationModel(getLangText('You have accepted the conditions'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
handleDeny() {
@ -127,7 +127,7 @@ let IkonotvContractNotifications = React.createClass({
handleDenySuccess() {
let notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
getCopyrightAssociationForm(){
@ -149,6 +149,8 @@ let IkonotvContractNotifications = React.createClass({
},
render() {
setDocumentTitle(getLangText('Contacts notifications'));
if (this.state.contractAgreementListNotifications &&
this.state.contractAgreementListNotifications.length > 0) {

View File

@ -19,6 +19,7 @@ import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container';
import AppConstants from '../../../../../../constants/application_constants';
import { getLangText } from '../../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../../utils/dom_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
@ -95,6 +96,7 @@ let IkonotvPieceContainer = React.createClass({
}
if(this.state.piece && this.state.piece.title) {
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
return (
<WalletPieceContainer
piece={this.state.piece}

View File

@ -1,19 +1,22 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let IkonotvLanding = React.createClass({
mixins: [Router.Navigation, Router.State],
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return UserStore.getState();
@ -33,22 +36,27 @@ let IkonotvLanding = React.createClass({
},
getEnterButton() {
let redirect = 'login';
let redirect = '/login';
if(this.state.currentUser && this.state.currentUser.email) {
redirect = 'pieces';
redirect = '/collection';
}
else if (this.getQuery() && this.getQuery().redirect) {
redirect = this.getQuery().redirect;
else if (this.props.location.query && this.props.location.query.redirect) {
redirect = '/' + this.props.location.query.redirect;
}
return (
<ButtonLink to={redirect} query={this.getQuery()}>
<LinkContainer to={redirect} query={this.props.location.query}>
<Button>
{getLangText('ENTER TO START')}
</ButtonLink>
</Button>
</LinkContainer>
);
},
render() {
setDocumentTitle('ikonoTV');
return (
<div className="ikonotv-landing">
<header>

View File

@ -9,9 +9,14 @@ import UserStore from '../../../../../stores/user_store';
import IkonotvAccordionListItem from './ikonotv_accordion_list/ikonotv_accordion_list_item';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let IkonotvPieceList = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return UserStore.getState();
},
@ -30,10 +35,12 @@ let IkonotvPieceList = React.createClass({
},
render() {
setDocumentTitle(getLangText('Register a new piece'));
return (
<div>
<PieceList
redirectTo="register_piece"
redirectTo="/register_piece?slide_num=0"
accordionListItemType={IkonotvAccordionListItem}
filterParams={[{
label: getLangText('Show works I have'),
@ -47,7 +54,8 @@ let IkonotvPieceList = React.createClass({
label: getLangText('loaned')
}
]
}]}/>
}]}
location={this.props.location}/>
</div>
);
}

View File

@ -2,7 +2,7 @@
import React from 'react';
import Moment from 'moment';
import Router from 'react-router';
import { History } from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
@ -33,13 +33,13 @@ import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
let IkonotvRegisterPiece = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func,
piece: React.PropTypes.object.isRequired
piece: React.PropTypes.object.isRequired,
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getInitialState(){
return mergeOptions(
@ -61,7 +61,7 @@ let IkonotvRegisterPiece = React.createClass({
// not want to display to the user.
PieceActions.updatePiece({});
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
// Since every step of this register process is atomic,
// we may need to enter the process at step 1 or 2.
@ -102,7 +102,7 @@ let IkonotvRegisterPiece = React.createClass({
PieceActions.updatePiece(response.piece);
}
if (!this.canSubmit()) {
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
}
else {
this.incrementStep();
@ -132,7 +132,7 @@ let IkonotvRegisterPiece = React.createClass({
this.refreshPieceList();
PieceActions.fetchOne(this.state.piece.id);
this.transitionTo('piece', {pieceId: this.state.piece.id});
this.history.pushState(null, `/pieces/${this.state.piece.id}`);
},
// We need to increase the step to lock the forms that are already filled out
@ -165,7 +165,7 @@ let IkonotvRegisterPiece = React.createClass({
// basically redirects to the second slide (index: 1), when the user is not logged in
onLoggedOut() {
this.transitionTo('login');
this.history.pushState(null, '/login');
},
canSubmit() {
@ -245,7 +245,8 @@ let IkonotvRegisterPiece = React.createClass({
glyphiconClassNames={{
pending: 'glyphicon glyphicon-chevron-right',
completed: 'glyphicon glyphicon-lock'
}}>
}}
location={this.props.location}>
<div data-slide-title={getLangText('Register work')}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
@ -256,7 +257,8 @@ let IkonotvRegisterPiece = React.createClass({
submitMessage={getLangText('Register')}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleRegisterSuccess}
onLoggedOut={this.onLoggedOut} />
onLoggedOut={this.onLoggedOut}
location={this.props.location}/>
</Col>
</Row>
</div>

View File

@ -1,16 +0,0 @@
'use strict';
import React from 'react';
import ContractAgreementForm from '../../../../ascribe_forms/form_contract_agreement';
let IkonotvRequestLoan = React.createClass({
render() {
return (
<ContractAgreementForm />
);
}
});
export default IkonotvRequestLoan;

View File

@ -1,43 +1,50 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Header from '../../header';
import Footer from '../../footer';
import GlobalNotification from '../../global_notification';
import getRoutes from './wallet_routes';
import classNames from 'classnames';
import { getSubdomain } from '../../../utils/general_utils';
let RouteHandler = Router.RouteHandler;
let WalletApp = React.createClass({
mixins: [Router.State],
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
history: React.PropTypes.object,
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
render() {
let subdomain = getSubdomain();
let ROUTES = getRoutes(null, subdomain);
let activeRoutes = this.getRoutes().map(elem => 'route--' + elem.name);
let header = null;
if ((this.isActive('landing') || this.isActive('login') || this.isActive('signup') || this.isActive('contract_notifications'))
let subdomain = getSubdomain();
const { history, routes, children } = this.props;
// The second element of routes is always the active component object, where we can
// extract the path.
let path = routes[1] ? routes[1].path : null;
// if the path of the current activeRoute is not defined, then this is the IndexRoute
if ((!path || history.isActive('/login') || history.isActive('/signup') || history.isActive('/contract_notifications'))
&& (['ikonotv', 'cyland']).indexOf(subdomain) > -1) {
header = (
<div className="hero"/>);
header = (<div className="hero"/>);
} else {
header = <Header showAddWork={true} routes={ROUTES} />;
header = <Header showAddWork={true} routes={routes} />;
}
// In react-router 1.0, Routes have no 'name' property anymore. To keep functionality however,
// we split the path by the first occurring slash and take the first splitter.
return (
<div className={classNames('ascribe-wallet-app', activeRoutes)}>
<div className={classNames('ascribe-wallet-app', 'route--' + (path ? path.split('/')[0] : 'landing'))}>
<div className='container'>
{header}
<RouteHandler />
{children}
<GlobalNotification />
<div id="modal" className="container"></div>
<Footer />

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Route, IndexRoute } from 'react-router';
// general components
import CoaVerifyContainer from '../../../components/coa_verify_container';
@ -23,74 +23,130 @@ import CylandPieceList from './components/cyland/cyland_piece_list';
import IkonotvLanding from './components/ikonotv/ikonotv_landing';
import IkonotvPieceList from './components/ikonotv/ikonotv_piece_list';
import IkonotvRequestLoan from './components/ikonotv/ikonotv_request_loan';
import ContractAgreementForm from '../../../components/ascribe_forms/form_contract_agreement';
import IkonotvRegisterPiece from './components/ikonotv/ikonotv_register_piece';
import IkonotvPieceContainer from './components/ikonotv/ikonotv_detail/ikonotv_piece_container';
import IkonotvContractNotifications from './components/ikonotv/ikonotv_contract_notifications';
import CCRegisterPiece from './components/cc/cc_register_piece';
import WalletApp from './wallet_app';
import AppConstants from '../../../constants/application_constants';
import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler';
let Route = Router.Route;
let NotFoundRoute = Router.NotFoundRoute;
let Redirect = Router.Redirect;
let baseUrl = AppConstants.baseUrl;
import WalletApp from './wallet_app';
let ROUTES = {
'cyland': (
<Route name="app" path={baseUrl} handler={WalletApp}>
<Route name="landing" path={baseUrl} handler={CylandLanding} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="register_piece" path="register_piece" handler={CylandRegisterPiece} headerTitle="+ NEW WORK" />
<Route name="pieces" path="collection" handler={CylandPieceList} headerTitle="COLLECTION" />
<Route name="piece" path="pieces/:pieceId" handler={CylandPieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="contract_settings" path="contract_settings" handler={ContractSettings} />
<NotFoundRoute name="notFound" handler={ErrorNotFoundPage} />
<Route path='/' component={WalletApp}>
<IndexRoute component={CylandLanding} />
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route
path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route
path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
<Route
path='contract_settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/>
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(CylandRegisterPiece)}
headerTitle='+ NEW WORK'/>
<Route
path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(CylandPieceList)}
headerTitle='COLLECTION'/>
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='pieces/:pieceId' component={CylandPieceContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
'cc': (
<Route name="app" path={baseUrl} handler={WalletApp}>
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="register_piece" path="register_piece" handler={CCRegisterPiece} headerTitle="+ NEW WORK" />
<Route name="pieces" path="collection" handler={PieceList} headerTitle="COLLECTION" />
<Route name="piece" path="pieces/:pieceId" handler={PieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<NotFoundRoute name="notFound" handler={ErrorNotFoundPage} />
<Route path='/' component={WalletApp}>
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route
path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route
path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
<Route
path='contract_settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/>
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(CCRegisterPiece)}
headerTitle='+ NEW WORK'/>
<Route
path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PieceList)}
headerTitle='COLLECTION'/>
<Route path='pieces/:pieceId' component={PieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
),
'ikonotv': (
<Route name="app" path={baseUrl} handler={WalletApp}>
<Route name="landing" path={baseUrl} handler={IkonotvLanding} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="request_loan" path="request_loan" handler={IkonotvRequestLoan} headerTitle="SEND NEW CONTRACT" aclName="acl_create_contractagreement" />
<Route name="register_piece" path="register_piece" handler={IkonotvRegisterPiece} headerTitle="+ NEW WORK" aclName="acl_create_piece"/>
<Route name="pieces" path="collection" handler={IkonotvPieceList} headerTitle="COLLECTION"/>
<Route name="piece" path="pieces/:pieceId" handler={IkonotvPieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="contract_settings" path="contract_settings" handler={ContractSettings} />
<Route name="contract_notifications" path="contract_notifications" handler={IkonotvContractNotifications} />
<NotFoundRoute name="notFound" handler={ErrorNotFoundPage} />
<Route path='/' component={WalletApp}>
<IndexRoute component={IkonotvLanding} />
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
<Route
path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route
path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
<Route
path='contract_settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/>
<Route
path='request_loan'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractAgreementForm)}
headerTitle='SEND NEW CONTRACT'
aclName='acl_create_contractagreement'/>
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(IkonotvRegisterPiece)}
headerTitle='+ NEW WORK'
aclName='acl_create_piece'/>
<Route
path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(IkonotvPieceList)}
headerTitle='COLLECTION'/>
<Route
path='contract_notifications'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(IkonotvContractNotifications)}/>
<Route path='pieces/:pieceId' component={IkonotvPieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route path='verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
)
};

View File

@ -78,7 +78,8 @@ let constants = {
'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA',
'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART',
'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV',
'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA']
'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'],
'searchThreshold': 500
};
export default constants;

13
js/history.js Normal file
View File

@ -0,0 +1,13 @@
'use strict';
import useBasename from 'history/lib/useBasename';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import AppConstants from './constants/application_constants';
// Remove the trailing slash if present
let baseUrl = AppConstants.baseUrl.replace(/\/$/, '');
export default useBasename(createBrowserHistory)({
basename: baseUrl
});

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Route } from 'react-router';
import getPrizeRoutes from './components/whitelabel/prize/prize_routes';
import getWalletRoutes from './components/whitelabel/wallet/wallet_routes';
@ -25,30 +25,41 @@ import ErrorNotFoundPage from './components/error_not_found_page';
import RegisterPiece from './components/register_piece';
import AppConstants from './constants/application_constants';
let Route = Router.Route;
let NotFoundRoute = Router.NotFoundRoute;
let Redirect = Router.Redirect;
let baseUrl = AppConstants.baseUrl;
import AuthProxyHandler from './components/ascribe_routes/proxy_routes/auth_proxy_handler';
const COMMON_ROUTES = (
<Route name="app" path={baseUrl} handler={App}>
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="logout" path="logout" handler={LogoutContainer} />
<Route name="register_piece" path="register_piece" handler={RegisterPiece} headerTitle="+ NEW WORK" />
<Route name="pieces" path="collection" handler={PieceList} headerTitle="COLLECTION" />
<Route name="piece" path="pieces/:pieceId" handler={PieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="contract_settings" path="contract_settings" handler={ContractSettings} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<NotFoundRoute name="notFound" handler={ErrorNotFoundPage} />
let COMMON_ROUTES = (
<Route path='/' component={App}>
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
<Route
path='register_piece'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(RegisterPiece)}
headerTitle='+ NEW WORK'/>
<Route
path='collection'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PieceList)}
headerTitle='COLLECTION'/>
<Route
path='signup'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
<Route
path='logout'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(LogoutContainer)}/>
<Route path='pieces/:pieceId' component={PieceContainer} />
<Route path='editions/:editionId' component={EditionContainer} />
<Route
path='password_reset'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
<Route
path='settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
<Route
path='contract_settings'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/>
<Route path='coa_verify' component={CoaVerifyContainer} />
<Route path='*' component={ErrorNotFoundPage} />
</Route>
);

View File

@ -1,6 +1,9 @@
'use strict';
import history from '../history';
import { altThirdParty } from '../alt';
import EventActions from '../actions/event_actions';
import NotificationActions from '../actions/notification_actions';
@ -9,7 +12,6 @@ import { getSubdomain } from '../utils/general_utils';
class NotificationsHandler {
constructor() {
this.bindActions(EventActions);
this.loaded = false;
@ -19,6 +21,7 @@ class NotificationsHandler {
if (this.loaded) {
return;
}
let subdomain = getSubdomain();
if (subdomain === 'ikonotv') {
NotificationActions.fetchContractAgreementListNotifications().then(
@ -26,7 +29,7 @@ class NotificationsHandler {
if (res.notifications && res.notifications.length > 0) {
this.loaded = true;
console.log('Contractagreement notifications loaded');
setTimeout(() => window.appRouter.transitionTo('contract_notifications'), 0);
history.pushState(null, '/contract_notifications');
}
}
);

View File

@ -8,8 +8,8 @@ export function getSubdomainSettings(subdomain) {
if(settings.length === 1) {
return settings[0];
} else if(settings.length === 0) {
console.warn('There are no subdomain settings for the subdomain: ' + subdomain);
return appConstants.defaultDomain;
// throw new Error('There are no subdomain settings for the subdomain: ' + subdomain);
} else {
throw new Error('Matched multiple subdomains. Adjust constants file.');
}

9
js/utils/dom_utils.js Normal file
View File

@ -0,0 +1,9 @@
'use strict';
/**
* Set the title in the browser window.
*/
export function setDocumentTitle(title) {
document.title = title;
}

View File

@ -0,0 +1,26 @@
'use strict';
/**
* PLEASE
*
* postfix your function with '-Available'.
*
* Like this:
*
* featureNameAvailable
*
*/
/**
* Even though it is not recommended to check (and maintain) the used browser,
* we're checking the browser's ability to drag and drop with this statement as
* there is no other way of detecting it another way.
*
* See this discussion for clarity:
* - https://github.com/Modernizr/Modernizr/issues/57#issuecomment-35831605
*
* @type {bool} Is drag and drop available on this browser
*/
export const dragAndDropAvailable = 'draggable' in document.createElement('div') &&
!/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent);

View File

@ -57,12 +57,14 @@ class Requests {
});
}
handleError(err) {
handleError(url) {
return (err) => {
if (err instanceof TypeError) {
throw new Error('Server did not respond to the request. (Not even displayed a 500)');
throw new Error('For: ' + url + ' - Server did not respond to the request. (Not even displayed a 500)');
} else {
throw err;
}
};
}
getUrl(url) {
@ -118,7 +120,7 @@ class Requests {
merged.method = verb;
return fetch(url, merged)
.then(this.unpackResponse)
.catch(this.handleError);
.catch(this.handleError(url));
}
get(url, params) {

View File

@ -45,7 +45,7 @@
"bootstrap-sass": "^3.3.4",
"browser-sync": "^2.7.5",
"browserify": "^9.0.8",
"browserify-shim": "^3.8.9",
"browserify-shim": "^3.8.10",
"classnames": "^1.2.2",
"compression": "^1.4.4",
"envify": "^3.4.0",
@ -64,6 +64,8 @@
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.4",
"harmonize": "^1.4.2",
"history": "^1.11.1",
"invariant": "^2.1.1",
"isomorphic-fetch": "^2.0.2",
"jest-cli": "^0.4.0",
"lodash": "^3.9.3",
@ -72,11 +74,11 @@
"opn": "^3.0.2",
"q": "^1.4.1",
"raven-js": "^1.1.19",
"react": "^0.13.2",
"react": "0.13.2",
"react-bootstrap": "0.25.1",
"react-datepicker": "^0.12.0",
"react-router": "^0.13.3",
"react-router-bootstrap": "~0.16.0",
"react-router": "1.0.0-rc1",
"react-router-bootstrap": "^0.19.0",
"react-star-rating": "~1.3.2",
"react-textarea-autosize": "^2.5.2",
"reactify": "^1.1.0",

View File

@ -21,7 +21,6 @@
.notification-contract-pdf, .notification-contract-footer {
width: 100%;
max-width: 750px;
margin: 0 auto;
}
.notification-contract-pdf {
@ -29,7 +28,8 @@
border: 1px solid #cccccc;
width: 100%;
height: 60vh;
margin-bottom: 0;
margin-top: .5em;
margin-bottom: 1em;
.loan-form {
margin-top: .5em;