1
0
mirror of https://github.com/ascribe/onion.git synced 2025-02-14 21:10:27 +01:00

Strip app down to just be an upload test

This commit is contained in:
Brett Sun 2015-11-19 10:37:50 +01:00
parent 60bf4b12f5
commit 40555e3386
10 changed files with 218 additions and 356 deletions

View File

@ -69,7 +69,7 @@ class AppGateway {
load(settings) {
let type = 'default';
let subdomain = 'www';
let redirectRoute = (<Redirect from="/" to="/collection" />);
let redirectRoute = (<Redirect from="/" to="/register_piece" />);
if (settings) {
type = settings.type;

View File

@ -34,7 +34,11 @@ let RegisterPieceForm = React.createClass({
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
]),
onSingleTestComplete: React.PropTypes.func,
onTestsStart: React.PropTypes.func,
onTestsComplete: React.PropTypes.func
},
getDefaultProps() {
@ -73,36 +77,51 @@ let RegisterPieceForm = React.createClass({
});
},
getUploadTests() {
return [
{
'name': 'Direct to S3',
'endpoint': 'https://ascribe0.s3.amazonaws.com'
},
{
'name': 'Direct to S3 with large chunking',
'endpoint': 'https://ascribe0.s3.amazonaws.com',
'chunkSize': 52428800
},
{
'name': 'Fastly to S3',
'endpoint': 'http://www.ascribe.io.global.prod.fastly.net'
},
{
'name': 'Fastly to S3 with large chunking',
'endpoint': 'http://www.ascribe.io.global.prod.fastly.net',
'chunkSize': 52428800
}
];
},
onTestComplete(testInfo) {
this.props.onSingleTestComplete(testInfo);
const uploadTests = this.getUploadTests();
if (testInfo.name === uploadTests[uploadTests.length - 1].name) {
this.props.onTestsComplete();
}
},
render() {
let currentUser = this.state.currentUser;
let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false;
enableLocalHashing = enableLocalHashing && this.props.enableLocalHashing;
return (
<Form
disabled={this.props.disabled}
className="ascribe-form-bordered"
ref='form'
url={ApiUrls.pieces_list}
handleSuccess={this.props.handleSuccess}
buttons={
<button
type="submit"
className="btn btn-default btn-wide"
disabled={!this.state.isUploadReady || this.props.disabled}>
{this.props.submitMessage}
</button>
}
spinner={
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div>
<div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3>
<h3>Upload test</h3>
</div>
<Property
name="digital_work_key"
editable={this.props.isFineUploaderEditable}
ignoreFocus={true}>
<InputFineUploader
keyRoutine={{
@ -114,40 +133,14 @@ let RegisterPieceForm = React.createClass({
}}
validation={AppConstants.fineUploader.validation.registerWork}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}
uploadMethod={this.props.location.query.method} />
uploadTests={this.getUploadTests()}
onTestsStart={this.props.onTestsStart}
onTestComplete={this.onTestComplete} />
</Property>
<Property
name='artist_name'
label={getLangText('Artist Name')}>
<input
type="text"
placeholder="(e.g. Andy Warhol)"
required/>
</Property>
<Property
name='title'
label={getLangText('Title')}>
<input
type="text"
placeholder="(e.g. 32 Campbell's Soup Cans)"
required/>
</Property>
<Property
name='date_created'
label={getLangText('Year Created')}>
<input
type="number"
placeholder="(e.g. 1962)"
min={1}
required/>
</Property>
{this.props.children}
</Form>
</div>
);
}
});

View File

@ -51,7 +51,12 @@ const InputFineUploader = React.createClass({
fileClassToUpload: shape({
singular: string,
plural: string
})
}),
uploadTests: React.PropTypes.array,
onTestComplete: React.PropTypes.func
},
getDefaultProps() {
@ -142,7 +147,10 @@ const InputFineUploader = React.createClass({
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing}
uploadMethod={this.props.uploadMethod}
fileClassToUpload={this.props.fileClassToUpload} />
fileClassToUpload={this.props.fileClassToUpload}
uploadTests={this.props.uploadTests}
onTestsStart={this.props.onTestsStart}
onTestComplete={this.props.onTestComplete} />
);
}
});

View File

@ -46,65 +46,19 @@ let FileDragAndDropDialog = React.createClass({
if (hasFiles) {
return null;
} else {
if (enableLocalHashing && !uploadMethod) {
const currentQueryParams = getCurrentQueryParams();
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
const queryParamsHash = Object.assign({}, currentQueryParams);
queryParamsHash.method = 'hash';
const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsUpload.method = 'upload';
return (
<div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p>
<Link
to={window.location.pathname}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
</span>
</Link>
<span> or </span>
<Link
to={window.location.pathname}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
</span>
</Link>
</div>
);
} else {
if (multipleFiles) {
return (
<span className="file-drag-and-drop-dialog">
{this.getDragDialog(fileClassToUpload.plural)}
<span
className="btn btn-default"
onClick={onClick}>
{getLangText('choose %s to upload', fileClassToUpload.plural)}
</span>
</span>
);
} else {
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
return (
<span className="file-drag-and-drop-dialog">
{this.getDragDialog(fileClassToUpload.singular)}
<span
className="btn btn-default"
onClick={onClick}>
{dialog}
</span>
</span>
);
}
}
return (
<span className="file-drag-and-drop-dialog">
{this.getDragDialog(fileClassToUpload.singular)}
<span
className="btn btn-default"
onClick={onClick}>
{dialog}
</span>
</span>
);
}
}
});

View File

@ -17,6 +17,7 @@ import { computeHashOfFile } from '../../utils/file_utils';
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
import { startTimer, endTimer } from '../../utils/timer_utils';
let ReactS3FineUploader = React.createClass({
propTypes: {
@ -128,7 +129,14 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
])
]),
uploadTests: React.PropTypes.array,
onTestsStart: React.PropTypes.func,
onTestComplete: React.PropTypes.func
},
getDefaultProps() {
@ -188,6 +196,11 @@ let ReactS3FineUploader = React.createClass({
},
getInitialState() {
const uploadTestDeferreds = [];
this.props.uploadTests.forEach(() => {
uploadTestDeferreds.push(Q.defer());
});
return {
filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
@ -198,7 +211,11 @@ let ReactS3FineUploader = React.createClass({
hashingProgress: -2,
// this is for logging
chunks: {}
chunks: {},
curUploadTest: 0,
uploadTestDeferreds
};
},
@ -226,11 +243,13 @@ let ReactS3FineUploader = React.createClass({
let objectProperties = this.props.objectProperties;
objectProperties.key = this.requestKey;
let request = this.props.request;
return {
autoUpload: this.props.autoUpload,
debug: this.props.debug,
objectProperties: objectProperties, // do a special key handling here
request: this.props.request,
request: request,
signature: this.props.signature,
uploadSuccess: this.props.uploadSuccess,
cors: this.props.cors,
@ -274,10 +293,6 @@ let ReactS3FineUploader = React.createClass({
// Cancel uploads and clear previously selected files on the input element
cancelUploads(id) {
!!id ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
// Reset the file input element to clear the previously selected files so that
// the user can reselect them again.
this.clearFileSelection();
},
clearFileSelection() {
@ -419,6 +434,13 @@ let ReactS3FineUploader = React.createClass({
});
// onError will catch any errors, so we can ignore them here
} else if (!res.error || res.success) {
const completedTime = endTimer() / 1000;
this.props.onTestComplete({
'name': this.props.uploadTests[this.state.curUploadTest].name,
'time': completedTime
});
let files = this.state.filesToUpload;
// Set the state of the completed file to 'upload successful' in order to
@ -432,27 +454,7 @@ let ReactS3FineUploader = React.createClass({
// Only after the blob has been created server-side, we can make the form submittable.
this.createBlob(files[id])
.then(() => {
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitFile) {
this.props.submitFile(files[id]);
} else {
console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
}
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
this.handleDeleteFile(id);
})
.catch(this.onErrorPromiseProxy);
}
@ -470,6 +472,14 @@ let ReactS3FineUploader = React.createClass({
},
onError(id, name, errorReason, xhr) {
const errorTime = endTimer() / 1000;
this.props.onTestComplete({
'name': this.props.uploadTests[this.state.curUploadTest].name,
'time': errorTime,
'progress': this.state.filesToUpload[id] ? this.state.filesToUpload[id].progress : 0,
'error': errorReason
});
console.logGlobal(errorReason, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks,
@ -477,10 +487,12 @@ let ReactS3FineUploader = React.createClass({
});
this.props.setIsUploadReady(true);
this.cancelUploads();
this.cancelUploads(id);
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.state.uploadTestDeferreds[this.state.curUploadTest].resolve();
},
getXhrErrorComment(xhr) {
@ -515,19 +527,6 @@ let ReactS3FineUploader = React.createClass({
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
return true;
},
@ -574,28 +573,9 @@ let ReactS3FineUploader = React.createClass({
onDeleteComplete(id, xhr, isError) {
if(isError) {
this.setStatusOfFile(id, 'online');
let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
} else {
let notification = new GlobalNotificationModel(getLangText('File deleted'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
}
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
this.state.uploadTestDeferreds[this.state.curUploadTest].resolve();
},
handleDeleteFile(fileId) {
@ -655,13 +635,6 @@ let ReactS3FineUploader = React.createClass({
// for submission
this.props.setIsUploadReady(false);
// If multiple set and user already uploaded its work,
// cancel upload
if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) {
this.clearFileSelection();
return;
}
// validate each submitted file if it fits the file size
let validFiles = [];
for(let i = 0; i < files.length; i++) {
@ -690,99 +663,8 @@ let ReactS3FineUploader = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
}
// As mentioned already in the propTypes declaration, in some instances we need to calculate the
// md5 hash of a file locally and just upload a txt file containing that hash.
//
// In the view this only happens when the user is allowed to do local hashing as well
// as when the correct method prop is present ('hash' and not 'upload')
if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') {
const convertedFilePromises = [];
let overallFileSize = 0;
// "files" is not a classical Javascript array but a Javascript FileList, therefore
// we can not use map to convert values
for(let i = 0; i < files.length; i++) {
// for calculating the overall progress of all submitted files
// we'll need to calculate the overall sum of all files' sizes
overallFileSize += files[i].size;
// also, we need to set the files' initial progress value
files[i].progress = 0;
// since the actual computation of a file's hash is an async task ,
// we're using promises to handle that
let hashedFilePromise = computeHashOfFile(files[i]);
convertedFilePromises.push(hashedFilePromise);
}
// To react after the computation of all files, we define the resolvement
// with the all function for iterables and essentially replace all original files
// with their txt representative
Q.all(convertedFilePromises)
.progress(({index, value: {progress, reject}}) => {
// hashing progress has been aborted from outside
// To get out of the executing, we need to call reject from the
// inside of the promise's execution.
// This is why we're passing (along with value) a function that essentially
// just does that (calling reject(err))
//
// In the promises catch method, we're then checking if the interruption
// was due to that error or another generic one.
if(this.state.hashingProgress === -1) {
reject(new Error(getLangText('Hashing canceled')));
}
// update file's progress
files[index].progress = progress;
// calculate weighted average for overall progress of all
// currently hashing files
let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress;
}
// Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100});
})
.then((convertedFiles) => {
// clear hashing progress, since its done
this.setState({ hashingProgress: -2});
// actually replacing all files with their txt-hash representative
files = convertedFiles;
// routine for adding all the files submitted to fineuploader for actual uploading them
// to the server
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
})
.catch((err) => {
// If the error is that hashing has been canceled, we want to display a success
// message instead of a danger message
let typeOfMessage = 'danger';
if(err.message === getLangText('Hashing canceled')) {
typeOfMessage = 'success';
this.setState({ hashingProgress: -2 });
} else {
// if there was a more generic error, we also log it
console.logGlobal(err);
}
let notification = new GlobalNotificationModel(err.message, typeOfMessage, 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
// if we're not hashing the files locally, we're just going to hand them over to fineuploader
// to upload them to the server
} else {
if(files.length > 0) {
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
}
if (files.length > 0) {
this.startTests(files);
}
},
@ -886,6 +768,40 @@ let ReactS3FineUploader = React.createClass({
}
},
startTests(files) {
this.props.onTestsStart(files);
this.props.uploadTests.reduce((deferred, test, testIndex) => {
return deferred.then(() => {
this.startTest(test, files, testIndex);
return this.state.uploadTestDeferreds[testIndex].promise;
});
}, Q.when());
},
startTest(test, files, testIndex) {
const uploaderConfig = this.propsToConfig();
uploaderConfig.request = Object.assign({}, uploaderConfig.request);
uploaderConfig.chunking = Object.assign({}, uploaderConfig.chunking);
if (!!test.endpoint) {
uploaderConfig.request.endpoint = test.endpoint;
}
if (!!test.chunkSize) {
uploaderConfig.chunking.partSize = test.chunkSize;
}
this.setState({
uploader: new fineUploader.s3.FineUploaderBasic(uploaderConfig),
curUploadTest: testIndex
}, () => {
startTimer();
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
});
},
render() {
const {
multiple,

View File

@ -82,7 +82,7 @@ let Header = React.createClass({
return (
<span>
<Link className="icon-ascribe-logo" to="/collection"/>
<a className="icon-ascribe-logo" href="http://www.ascribe.io"/>
</span>
);
},
@ -203,16 +203,15 @@ let Header = React.createClass({
toggleNavKey={0}
fixedTop={true}>
<CollapsibleNav eventKey={0}>
<Nav navbar left>
{this.getPoweredBy()}
</Nav>
<Nav navbar right>
<HeaderNotificationDebug show = {false}/>
{account}
{signup}
<LinkContainer
to="/logout">
<MenuItem
eventKey="1">
{getLangText('Log out')}
</MenuItem>
</LinkContainer>
</Nav>
<HeaderNotifications />
{navRoutesLinks}
</CollapsibleNav>
</Navbar>
</div>

View File

@ -37,10 +37,6 @@ let LoginContainer = React.createClass({
message={this.props.message}
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>
</div>
</div>
);
}

View File

@ -6,18 +6,8 @@ import { History } from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import PieceListStore from '../stores/piece_list_store';
import PieceListActions from '../actions/piece_list_actions';
import UserStore from '../stores/user_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import PropertyCollapsible from './ascribe_forms/property_collapsible';
import RegisterPieceForm from './ascribe_forms/form_register_piece';
import { mergeOptions } from '../utils/general_utils';
@ -43,25 +33,22 @@ let RegisterPiece = React.createClass( {
getInitialState(){
return mergeOptions(
UserStore.getState(),
WhitelabelStore.getState(),
PieceListStore.getState(),
{
selectedLicense: 0,
isFineUploaderActive: false
isFineUploaderActive: false,
uploadInfos: [],
testFileSize: 0,
testStarted: false,
testComplete: false
});
},
componentDidMount() {
PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
@ -75,36 +62,44 @@ let RegisterPiece = React.createClass( {
}
},
handleSuccess(response){
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
// once the user was able to register a piece successfully, we need to make sure to keep
// the piece list up to date
PieceListActions.fetchPieceList(
this.state.page,
this.state.pageSize,
this.state.searchTerm,
this.state.orderBy,
this.state.orderAsc,
this.state.filterBy
);
this.history.pushState(null, `/pieces/${response.piece.id}`);
onSingleTestComplete(uploadInfo) {
this.setState({
uploadInfos: this.state.uploadInfos.concat([uploadInfo])
});
},
getSpecifyEditions() {
if(this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) {
onTestsStart(files) {
this.setState({
testStarted: true,
testFileSize: files[0].size
});
},
onTestsComplete() {
this.setState({
testComplete: true
}, () => {
alert('Tests are complete. Please send the results to brett@ascribe.io');
});
},
getUploadedInfo() {
if (this.state.uploadInfos.length > 0 && this.state.testStarted) {
return (
<PropertyCollapsible
name="num_editions"
checkboxLabel={getLangText('Specify editions')}>
<span>{getLangText('Editions')}</span>
<input
type="number"
placeholder="(e.g. 32)"
min={0}/>
</PropertyCollapsible>
<div style={{'backgroundColor': '#FFF'}}>
<h4>{this.state.testComplete? 'Results:' : 'Test in progress...'}</h4>
For file of size: {this.state.testFileSize}
<ul>
{this.state.uploadInfos.map((uploadInfo) => {
if (!uploadInfo.error) {
return (<li key={uploadInfo.name}><strong>{uploadInfo.name}</strong>: {uploadInfo.time}s</li>);
} else {
return (<li key={uploadInfo.name}><strong style={{'color': 'red'}}>Error</strong>: {uploadInfo.error} after {uploadInfo.time}s on completing {uploadInfo.progress}%</li>);
}
})}
</ul>
<p>Please send these results by screenshot or by copying the values to <a href="mailto:brett@ascribe.io">brett@ascribe.io</a></p>
</div>
);
}
},
@ -118,11 +113,13 @@ let RegisterPiece = React.createClass( {
<RegisterPieceForm
{...this.props}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleSuccess}
location={this.props.location}>
{this.props.children}
{this.getSpecifyEditions()}
</RegisterPieceForm>
isFineUploaderEditable={!this.state.testComplete}
location={this.props.location}
onSingleTestComplete={this.onSingleTestComplete}
onTestsStart={this.onTestsStart}
onTestsComplete={this.onTestsComplete} />
{this.getUploadedInfo()}
</Col>
</Row>
);

View File

@ -32,33 +32,17 @@ let COMMON_ROUTES = (
<Route path='/' component={App}>
<Route
path='login'
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
component={AuthProxyHandler({to: '/register_piece', 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)} />
component={AuthProxyHandler({to: '/register_piece', 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>
);

15
js/utils/timer_utils.js Normal file
View File

@ -0,0 +1,15 @@
'use strict'
var startTime = null;
export function startTimer() {
startTime = new Date();
};
export function endTimer() {
if (startTime) {
const time = new Date() - startTime;
startTime = null;
return time;
}
}