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

Merge branch 'AD-565-add-landing-page-for-sluice'

This commit is contained in:
vrde 2015-07-13 23:58:15 +02:00
commit 3b1fdba80f
55 changed files with 1372 additions and 1081 deletions

View File

@ -18,7 +18,7 @@
</script>
</head>
<body>
<div id="main" class="container"></div>
<div id="main"></div>
<script src="<%= BASE_URL %>static/js/app.js"></script>
</body>
</html>

View File

@ -7,7 +7,8 @@ import PieceFetcher from '../fetchers/piece_fetcher';
class PieceActions {
constructor() {
this.generateActions(
'updatePiece'
'updatePiece',
'updateProperty'
);
}

View File

@ -14,6 +14,21 @@ class PieceListActions {
}
fetchPieceList(page, pageSize, search, orderBy, orderAsc) {
// To prevent flickering on a pagination request,
// we overwrite the piecelist with an empty list before
// pieceListCount === -1 defines the loading state
this.actions.updatePieceList({
page,
pageSize,
search,
orderBy,
orderAsc,
'pieceList': [],
'pieceListCount': -1
});
// afterwards, we can load the list
return new Promise((resolve, reject) => {
PieceListFetcher
.fetch(page, pageSize, search, orderBy, orderAsc)

View File

@ -1,6 +1,6 @@
'use strict';
require("babel/polyfill");
require('babel/polyfill');
import React from 'react';
import Router from 'react-router';
@ -8,7 +8,8 @@ import Router from 'react-router';
import fetch from 'isomorphic-fetch';
import ApiUrls from './constants/api_urls';
import routes from './routes';
import constants from './constants/application_constants';
import getRoutes from './routes';
import requests from './utils/requests';
let headers = {
@ -28,9 +29,38 @@ requests.defaults({
}
});
Router.run(routes, Router.HistoryLocation, (AscribeApp) => {
React.render(
<AscribeApp />,
document.getElementById('main')
);
});
class AppGateway {
start() {
console.log('start');
let subdomain = window.location.host.split('.')[0];
requests.get('whitelabel_settings', {'subdomain': subdomain})
.then(this.loadSubdomain.bind(this))
.catch(this.loadDefault.bind(this));
}
loadSubdomain(data) {
let settings = data.whitelabel;
constants.whitelabel = settings;
this.load('prize');
}
loadDefault(error) {
console.log('Loading default app, error'. error);
this.load('default');
}
load(type) {
console.log('loading', type);
Router.run(getRoutes(type), Router.HistoryLocation, (App) => {
React.render(
<App />,
document.getElementById('main')
);
});
}
}
let ag = new AppGateway();
ag.start();

View File

@ -22,7 +22,7 @@ let AccordionList = React.createClass({
} else if(this.props.count === 0) {
return (
<div>
<p className="text-center">{getLangText('We could not find any works related to you%s', '...')}</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>
);

View File

@ -8,14 +8,11 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import AccordionListItemEditionWidget from './accordion_list_item_edition_widget';
import AccordionListItemCreateEditions from './accordion_list_item_create_editions';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import PieceListActions from '../../actions/piece_list_actions';
import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
@ -35,20 +32,6 @@ let AccordionListItem = React.createClass({
};
},
componentDidUpdate() {
if(this.props.content.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
this.startPolling();
}
},
componentWillUnmount() {
clearInterval(this.state.pollingIntervalIndex);
},
onChange(state) {
this.setState(state);
},
getGlyphicon(){
if (this.props.content.requestAction){
return (
@ -66,39 +49,32 @@ let AccordionListItem = React.createClass({
});
},
handleEditionCreationSuccess(response) {
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
handleEditionCreationSuccess() {
PieceListActions.updatePropertyForPiece({pieceId: this.props.content.id, key: 'num_editions', value: 0});
this.toggleCreateEditionsDialog();
},
startPolling() {
// start polling until editions are defined
let pollingIntervalIndex = setInterval(() => {
EditionListActions.fetchEditionList(this.props.content.id)
.then((res) => {
clearInterval(this.state.pollingIntervalIndex);
PieceListActions.updatePropertyForPiece({
pieceId: this.props.content.id,
key: 'num_editions',
value: res.editions[0].num_editions
});
EditionListActions.toggleEditionList(this.props.content.id);
})
.catch(() => {
/* Ignore and keep going */
});
}, 5000);
this.setState({
pollingIntervalIndex
onPollingSuccess(pieceId, numEditions) {
PieceListActions.updatePropertyForPiece({
pieceId,
key: 'num_editions',
value: numEditions
});
EditionListActions.toggleEditionList(pieceId);
},
getCreateEditionsDialog() {
if(this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<CreateEditionsForm
pieceId={this.props.content.id}
handleSuccess={this.handleEditionCreationSuccess} />
</div>
);
}
},
render() {
@ -141,7 +117,8 @@ let AccordionListItem = React.createClass({
<AccordionListItemEditionWidget
className="pull-right"
piece={this.props.content}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}/>
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.onPollingSuccess}/>
</div>
</div>
<span style={{'clear': 'both'}}></span>
@ -150,8 +127,9 @@ let AccordionListItem = React.createClass({
</div>
</div>
</div>
{this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog ? <AccordionListItemCreateEditions pieceId={this.props.content.id} handleSuccess={this.handleEditionCreationSuccess}/> : null}
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children}
</div>

View File

@ -1,51 +0,0 @@
'use strict';
import React from 'react';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import apiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
let AccordionListItemCreateEditions = React.createClass({
propTypes: {
pieceId: React.PropTypes.number,
handleSuccess: React.PropTypes.func
},
getFormData(){
return {
piece_id: parseInt(this.props.pieceId, 10)
};
},
render() {
return (
<div className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<Form
ref='form'
url={apiUrls.editions}
getFormData={this.getFormData}
handleSuccess={this.props.handleSuccess}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='num_editions'
label={getLangText('Number of editions')}>
<input
type="number"
placeholder="(e.g. 32)"
min={0}/>
</Property>
</Form>
</div>
);
}
});
export default AccordionListItemCreateEditions;

View File

@ -6,13 +6,16 @@ import classNames from 'classnames';
import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import { getLangText } from '../../utils/lang_utils';
let AccordionListItemEditionWidget = React.createClass({
propTypes: {
className: React.PropTypes.string,
piece: React.PropTypes.object.isRequired,
toggleCreateEditionsDialog: React.PropTypes.func.isRequired
toggleCreateEditionsDialog: React.PropTypes.func.isRequired,
onPollingSuccess: React.PropTypes.func
},
getInitialState() {
@ -55,16 +58,9 @@ let AccordionListItemEditionWidget = React.createClass({
let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false;
if(isEditionListOpen) {
if(typeof this.state.editionList[pieceId] === 'undefined') {
return (
<span className="glyph-ascribe-spool-chunked spin"/>
);
} else {
return (
<span className="glyphicon glyphicon-menu-up" aria-hidden="true" style={{top: 2}}></span>
);
}
return (
<span className="glyphicon glyphicon-menu-up" aria-hidden="true" style={{top: 2}}></span>
);
} else {
return (
<span className="glyphicon glyphicon-menu-down" aria-hidden="true" style={{top: 2}}></span>
@ -76,23 +72,16 @@ let AccordionListItemEditionWidget = React.createClass({
let piece = this.props.piece;
let numEditions = piece.num_editions;
if(numEditions === -1) {
if(numEditions <= 0) {
return (
<button
onClick={this.props.toggleCreateEditionsDialog}
className={classNames('btn', 'btn-default', 'btn-xs', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
+ Editions
</button>
<CreateEditionsButton
label={getLangText('Create editions')}
className="btn-xs pull-right"
piece={piece}
toggleCreateEditionsDialog={this.props.toggleCreateEditionsDialog}
onPollingSuccess={this.props.onPollingSuccess}/>
);
}
else if(numEditions === 0) {
return (
<button disabled className={classNames('btn', 'btn-default', 'btn-xs', this.props.className)}>
Creating Editions <span className="glyph-ascribe-spool-chunked spin"/>
</button>
);
}
else if(numEditions === 1) {
} else if(numEditions === 1) {
let editionMapping = piece && piece.first_edition ? piece.first_edition.edition_number + '/' + piece.num_editions : '';
return (

View File

@ -6,14 +6,14 @@ import Header from '../components/header';
import Footer from '../components/footer';
import GlobalNotification from './global_notification';
let Link = Router.Link;
// let Link = Router.Link;
let RouteHandler = Router.RouteHandler;
let AscribeApp = React.createClass({
render() {
return (
<div>
<div className="container ascribe-default-app">
<Header />
<RouteHandler />
<Footer />

View File

@ -13,25 +13,28 @@ import AppConstants from '../../constants/application_constants';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils.js'
import { getLangText } from '../../utils/lang_utils.js';
import apiUrls from '../../constants/api_urls';
let AclButton = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.array.isRequired,
editions: React.PropTypes.array.isRequired,
pieceOrEditions: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
isPiece(){
return !(this.props.pieceOrEditions.constructor === Array);
},
actionProperties(){
if (this.props.action === 'consign'){
return {
title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork'),
form: <ConsignForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
form: <ConsignForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>,
handleSuccess: this.showNotification
};
}
@ -39,14 +42,14 @@ let AclButton = React.createClass({
return {
title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again'),
form: <UnConsignForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
form: <UnConsignForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>,
handleSuccess: this.showNotification
};
}else if (this.props.action === 'transfer') {
return {
title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork'),
form: <TransferForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
form: <TransferForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>,
handleSuccess: this.showNotification
};
}
@ -54,7 +57,7 @@ let AclButton = React.createClass({
return {
title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time'),
form: <LoanForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
form: <LoanForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>,
handleSuccess: this.showNotification
};
}
@ -62,18 +65,62 @@ let AclButton = React.createClass({
return {
title: getLangText('Share artwork'),
tooltip: getLangText('Share the artwork'),
form: <ShareForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
form: (
<ShareForm
message={this.getShareMessage()}
id={this.getFormDataId()}
url={this.isPiece() ? apiUrls.ownership_shares_pieces : apiUrls.ownership_shares_editions }/>
),
handleSuccess: this.showNotification
};
}
},
showNotification(response){
this.props.handleSuccess();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
getTitlesString(){
if (this.isPiece()){
return '\"' + this.props.pieceOrEditions.title + '\"';
}
else {
return this.props.pieceOrEditions.map(function(edition) {
return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n';
}).join('');
}
},
getFormDataId(){
if (this.isPiece()) {
return {piece_id: this.props.pieceOrEditions.id};
}
else {
return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
},
getShareMessage(){
return (
`
${getLangText('Hi')},
${getLangText('I am sharing')}:
${this.getTitlesString()} ${getLangText('with you')}.
${getLangText('Truly yours')},
${this.props.currentUser.username}
`
);
},
render() {
let shouldDisplay = this.props.availableAcls.indexOf(this.props.action) > -1;
let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
return (
<ModalWrapper

View File

@ -11,9 +11,16 @@ import DeleteButton from '../ascribe_buttons/delete_button';
let AclButtonList = React.createClass({
propTypes: {
className: React.PropTypes.string,
editions: React.PropTypes.array,
editions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
availableAcls: React.PropTypes.array,
handleSuccess: React.PropTypes.func
handleSuccess: React.PropTypes.func,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
getInitialState() {
@ -39,34 +46,36 @@ let AclButtonList = React.createClass({
<AclButton
availableAcls={this.props.availableAcls}
action="transfer"
editions={this.props.editions}
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess}/>
<AclButton
availableAcls={this.props.availableAcls}
action="consign"
editions={this.props.editions}
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="unconsign"
editions={this.props.editions}
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="loan"
editions={this.props.editions}
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="share"
editions={this.props.editions}
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<DeleteButton editions={this.props.editions}/>
<DeleteButton
editions={this.props.editions}/>
{this.props.children}
</div>
);
}

View File

@ -0,0 +1,95 @@
'use strict';
import React from 'react';
import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store';
import { getAvailableAcls } from '../../utils/acl_utils';
import { getLangText } from '../../utils/lang_utils';
import classNames from 'classnames';
let CreateEditionsButton = React.createClass({
propTypes: {
label: React.PropTypes.string,
className: React.PropTypes.string,
piece: React.PropTypes.object.isRequired,
toggleCreateEditionsDialog: React.PropTypes.func.isRequired,
onPollingSuccess: React.PropTypes.func
},
getInitialState() {
return EditionListStore.getState();
},
componentDidMount() {
EditionListStore.listen(this.onChange);
},
componentWillUnmount() {
EditionListStore.unlisten(this.onChange);
clearInterval(this.state.pollingIntervalIndex);
},
onChange(state) {
this.setState(state);
},
componentDidUpdate() {
if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
this.startPolling();
}
},
startPolling() {
// start polling until editions are defined
let pollingIntervalIndex = setInterval(() => {
EditionListActions.fetchEditionList(this.props.piece.id)
.then((res) => {
clearInterval(this.state.pollingIntervalIndex);
this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions);
})
.catch(() => {
/* Ignore and keep going */
});
}, 5000);
this.setState({
pollingIntervalIndex
});
},
render: function () {
let piece = this.props.piece;
let availableAcls = getAvailableAcls(piece);
if (availableAcls.editions || piece.num_editions > 0){
return null;
}
if(piece.num_editions === 0 && typeof this.state.editionList[piece.id] === 'undefined') {
return (
<button
disabled
className={classNames('btn', 'btn-default', this.props.className)}>
{getLangText('Creating editions')} <span className="glyph-ascribe-spool-chunked spin"/>
</button>
);
} else {
return (
<button
className={classNames('btn', 'btn-default', this.props.className)}
onClick={this.props.toggleCreateEditionsDialog}>
{this.props.label}
</button>
);
}
}
});
export default CreateEditionsButton;

View File

@ -41,11 +41,11 @@ let DeleteButton = React.createClass({
let btnDelete = null;
let content = null;
if (availableAcls.indexOf('delete') > -1) {
if (availableAcls.delete) {
content = <EditionDeleteForm editions={ this.props.editions }/>;
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('DELETE')}</Button>;
}
else if (availableAcls.indexOf('del_from_collection') > -1){
else if (availableAcls.unshare){
content = <EditionRemoveFromCollectionForm editions={ this.props.editions }/>;
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>;
}

View File

@ -5,7 +5,6 @@ import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Button from 'react-bootstrap/lib/Button';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import UserActions from '../../actions/user_actions';
@ -22,7 +21,6 @@ import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable';
import EditionHeader from './header';
import EditionFurtherDetails from './further_details';
//import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
@ -38,7 +36,6 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import apiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
@ -76,14 +73,19 @@ let Edition = React.createClass({
content={this.props.edition}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<EditionHeader content={this.props.edition}/>
<div className="ascribe-detail-header">
<EditionDetailProperty label="TITLE" value={<div className="ascribe-detail-title">{this.props.edition.title}</div>} />
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.edition.date_created.slice(0, 4) } />
<hr/>
</div>
<EditionSummary
currentUser={this.state.currentUser}
edition={this.props.edition} />
<CollapsibleParagraph
title={getLangText('Certificate of Authenticity')}
show={this.props.edition.acl.indexOf('coa') > -1}>
show={this.props.edition.acl.acl_coa}>
<CoaDetails
edition={this.props.edition}/>
</CollapsibleParagraph>
@ -112,7 +114,7 @@ let Edition = React.createClass({
<CollapsibleParagraph
title="Notes"
show={(this.state.currentUser.username && true || false) ||
(this.props.edition.acl.indexOf('edit') > -1 || this.props.edition.public_note)}>
(this.props.edition.acl.acl_edit || this.props.edition.public_note)}>
<EditionPersonalNote
currentUser={this.state.currentUser}
handleSuccess={this.props.loadEdition}
@ -124,11 +126,11 @@ let Edition = React.createClass({
<CollapsibleParagraph
title={getLangText('Further Details')}
show={this.props.edition.acl.indexOf('edit') > -1
show={this.props.edition.acl.acl_edit
|| Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data !== null}>
<EditionFurtherDetails
editable={this.props.edition.acl.indexOf('edit') > -1}
editable={this.props.edition.acl.acl_edit}
pieceId={this.props.edition.parent}
extraData={this.props.edition.extra_data}
otherData={this.props.edition.other_data}
@ -168,7 +170,7 @@ let EditionSummary = React.createClass({
if (this.props.edition.status.length > 0){
let statusStr = this.props.edition.status.join().replace(/_/, ' ');
status = <EditionDetailProperty label="STATUS" value={ statusStr }/>;
if (this.props.edition.pending_new_owner && this.props.edition.acl.indexOf('withdraw_transfer') > -1){
if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){
status = (
<Form
url={apiUrls.ownership_transfers_withdraw}
@ -217,7 +219,6 @@ let EditionSummary = React.createClass({
<EditionDetailProperty label={getLangText('ID')} value={ this.props.edition.bitcoin_id } />
<EditionDetailProperty label={getLangText('OWNER')} value={ this.props.edition.owner } />
{this.getStatus()}
<br/>
{this.getActions()}
<hr/>
</div>
@ -303,8 +304,8 @@ let EditionPublicEditionNote = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
let isEditable = this.props.edition.acl.indexOf('edit') > -1;
if (this.props.edition.acl.indexOf('edit') > -1 || this.props.edition.public_note){
let isEditable = this.props.edition.acl.acl_edit;
if (isEditable || this.props.edition.public_note){
return (
<Form
url={apiUrls.note_edition}

View File

@ -1,26 +0,0 @@
'use strict';
import React from 'react';
import EditionDetailProperty from './detail_property';
let Header = React.createClass({
propTypes: {
content: React.PropTypes.object
},
render() {
var titleHtml = <div className="ascribe-detail-title">{this.props.content.title}</div>;
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="TITLE" value={titleHtml} />
<EditionDetailProperty label="BY" value={this.props.content.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.content.date_created.slice(0, 4) } />
<hr/>
</div>
);
}
});
export default Header;

View File

@ -7,13 +7,25 @@ import Col from 'react-bootstrap/lib/Col';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import DetailProperty from './detail_property';
import FurtherDetails from './further_details';
//import UserActions from '../../actions/user_actions';
//import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import PieceActions from '../../actions/piece_actions';
import MediaContainer from './media_container';
import Header from './header';
import EditionDetailProperty from './detail_property';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
/**
* This is the component that implements display-specific functionality
@ -24,25 +36,62 @@ let Piece = React.createClass({
loadPiece: React.PropTypes.func
},
//getInitialState() {
// return UserStore.getState();
//},
//
//componentDidMount() {
// UserStore.listen(this.onChange);
// UserActions.fetchCurrentUser();
//},
//
//componentWillUnmount() {
// UserStore.unlisten(this.onChange);
//},
//
//onChange(state) {
// this.setState(state);
//},
getInitialState() {
return mergeOptions(
UserStore.getState(),
{
showCreateEditionsDialog: false
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
toggleCreateEditionsDialog() {
this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog
});
},
handleEditionCreationSuccess() {
PieceActions.updateProperty({key: 'num_editions', value: 0});
this.toggleCreateEditionsDialog();
},
getCreateEditionsDialog() {
if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.props.piece.id}
handleSuccess={this.handleEditionCreationSuccess} />
<hr/>
</div>
);
} else {
return (<hr/>);
}
},
handlePollingSuccess(pieceId, numEditions) {
PieceActions.updateProperty({
key: 'num_editions',
value: numEditions
});
},
render() {
return (
<Row>
<Col md={6}>
@ -50,22 +99,44 @@ let Piece = React.createClass({
content={this.props.piece}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<Header
content={this.props.piece}/>
<div className="ascribe-detail-header">
<EditionDetailProperty label="TITLE" value={<div className="ascribe-detail-title">{this.props.piece.title}</div>} />
<EditionDetailProperty label="BY" value={this.props.piece.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.piece.date_created.slice(0, 4) } />
{this.props.piece.num_editions > 0 ? <EditionDetailProperty label="NUMBER OF EDITIONS" value={ this.props.piece.num_editions } /> : null}
<hr/>
</div>
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.props.piece.user_registered } />
</div>
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.props.piece.acl}
editions={this.props.piece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.props.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
</AclButtonList>
{this.getCreateEditionsDialog()}
<CollapsibleParagraph
title="Further Details"
show={this.props.piece.acl.indexOf('edit') > -1
show={this.props.piece.acl.acl_edit
|| Object.keys(this.props.piece.extra_data).length > 0
|| this.props.piece.other_data !== null}
defaultExpanded={true}>
<FurtherDetails
editable={this.props.piece.acl.indexOf('edit') > -1}
editable={this.props.piece.acl.acl_edit}
pieceId={this.props.piece.id}
extraData={this.props.piece.extra_data}
otherData={this.props.piece.other_data}
handleSuccess={this.props.loadPiece}/>
</CollapsibleParagraph>
</Col>
</Row>
);

View File

@ -0,0 +1,62 @@
'use strict';
import React from 'react';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import apiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
let CreateEditionsForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func,
pieceId: React.PropTypes.number
},
getFormData(){
return {
piece_id: parseInt(this.props.pieceId, 10)
};
},
handleSuccess(response) {
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
if(this.props.handleSuccess) {
this.props.handleSuccess(response);
}
},
render() {
return (
<Form
ref='form'
url={apiUrls.editions}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='num_editions'
label={getLangText('Number of editions')}>
<input
type="number"
placeholder="(e.g. 32)"
min={0}/>
</Property>
</Form>
);
}
});
export default CreateEditionsForm;

View File

@ -1,62 +1,130 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import Form from './form';
import Property from './property';
import FormPropertyHeader from './form_property_header';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
import SignupModal from '../ascribe_modal/modal_signup';
import PasswordResetRequestModal from '../ascribe_modal/modal_password_request_reset';
import { getLangText } from '../../utils/lang_utils.js';
let LoginForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.users_login;
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool
},
getFormData() {
mixins: [Router.Navigation],
getDefaultProps() {
return {
email: this.refs.email.state.value,
password: this.refs.password.state.value
headerMessage: 'Enter ascribe',
submitMessage: 'Log in',
redirectOnLoggedIn: true,
redirectOnLoginSuccess: true
};
},
renderForm() {
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
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) {
this.transitionTo('pieces');
}
},
handleSuccess(){
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();
/* 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
*/
if(this.props.redirectOnLoginSuccess) {
window.location = AppConstants.baseUrl + 'collection';
}
},
render() {
return (
<form id="login_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_consignee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputText
ref="email"
placeHolder={getLangText('Email')}
required="required"
type="email"
submitted={this.state.submitted}/>
<InputText
ref="password"
placeHolder={getLangText('Password')}
required="required"
type="password"
submitted={this.state.submitted}/>
<div>
{getLangText('Forgot your password')}&#63;
<PasswordResetRequestModal
button={<a className="button" href="#"> {getLangText('Reset password')}</a>}/>
</div>
<div>
{getLangText('Not a member yet')}&#63;
<SignupModal
button={<a className="button" href="#"> {getLangText('Sign up')}</a>}/>
</div>
<ButtonSubmitOrClose
text={getLangText('LOGIN')}
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
<Form
className="ascribe-form-bordered"
ref="loginForm"
url={apiUrls.users_login}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText(this.props.submitMessage)}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span>
}>
<FormPropertyHeader>
<h3>{getLangText(this.props.headerMessage)}</h3>
</FormPropertyHeader>
<Property
name='email'
label={getLangText('Email')}>
<input
type="email"
placeholder={getLangText('Enter your email')}
autoComplete="on"
name="username"
required/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
<input
type="password"
placeholder={getLangText('Enter your password')}
autoComplete="on"
name="password"
required/>
</Property>
</Form>
);
}
});

View File

@ -1,43 +0,0 @@
'use strict';
import React from 'react';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputTextAreaToggable from './input_textarea_toggable';
let EditionNoteForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.note_edition;
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
note: this.refs.personalNote.state.value
};
},
renderForm() {
return (
<form id="personal_note_content" role="form" key="personal_note_content">
<InputTextAreaToggable
ref="personalNote"
className="form-control"
defaultValue={this.props.editions[0].public_note}
rows={3}
editable={this.props.editable}
required=""
onSubmit={this.submit}
/>
</form>
);
}
});
export default EditionNoteForm;

View File

@ -1,43 +0,0 @@
'use strict';
import React from 'react';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputTextAreaToggable from './input_textarea_toggable';
let PersonalNoteForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.note_notes;
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
note: this.refs.personalNote.state.value
};
},
renderForm() {
return (
<form id="personal_note_content" role="form" key="personal_note_content">
<InputTextAreaToggable
ref="personalNote"
className="form-control"
defaultValue={this.props.editions[0].note_from_user}
rows={3}
editable={this.props.editable}
required=""
onSubmit={this.submit}
/>
</form>
);
}
});
export default PersonalNoteForm;

View File

@ -1,51 +0,0 @@
'use strict';
import React from 'react';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import ButtonSubmit from '../ascribe_buttons/button_submit';
import { getLangText } from '../../utils/lang_utils.js'
let PasswordResetForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.users_password_reset;
},
getFormData() {
return {
email: this.props.email,
token: this.props.token,
password: this.refs.password.state.value,
password_confirm: this.refs.password_confirm.state.value
};
},
renderForm() {
return (
<form id="reset_password_modal_content" role="form" onSubmit={this.submit}>
<div>Reset the password for {this.props.email}:</div>
<InputText
ref="password"
placeHolder={getLangText('Choose a password')}
required="required"
type="password"
submitted={this.state.submitted}/>
<InputText
ref="password_confirm"
placeHolder={getLangText('Confirm password')}
required="required"
type="password"
submitted={this.state.submitted}/>
<ButtonSubmit
text={getLangText('RESET PASSWORD')}
submitted={this.state.submitted} />
</form>
);
}
});
export default PasswordResetForm;

View File

@ -1,42 +0,0 @@
'use strict';
import React from 'react';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close';
import { getLangText } from '../../utils/lang_utils.js'
let PasswordResetRequestForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.users_password_reset_request;
},
getFormData() {
return {
email: this.refs.email.state.value
};
},
renderForm() {
return (
<form id="request_reset_password_modal_content" role="form" onSubmit={this.submit}>
<InputText
ref="email"
placeHolder={getLangText('Email')}
required="required"
type="email"
submitted={this.state.submitted}/>
<ButtonSubmitOrClose
text={getLangText('RESET PASSWORD')}
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default PasswordResetRequestForm;

View File

@ -0,0 +1,180 @@
'use strict';
import React from 'react';
import AppConstants from '../../constants/application_constants';
import Form from './form';
import Property from './property';
import FormPropertyHeader from './form_property_header';
import apiUrls from '../../constants/api_urls';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
let RegisterPieceForm = React.createClass({
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
isFineUploaderEditable: React.PropTypes.bool,
children: React.PropTypes.element
},
getDefaultProps() {
return {
headerMessage: getLangText('Register your work'),
submitMessage: getLangText('Register work')
};
},
getInitialState(){
return {
digitalWorkKey: null,
isUploadReady: false
};
},
getFormData(){
return {
digital_work_key: this.state.digitalWorkKey
};
},
submitKey(key){
this.setState({
digitalWorkKey: key
});
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
isReadyForFormSubmission(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
if (files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
},
render() {
return (
<Form
className="ascribe-form-bordered"
ref='form'
url={apiUrls.pieces_list}
getFormData={this.getFormData}
handleSuccess={this.props.handleSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
disabled={!this.state.isUploadReady}>
{this.props.submitMessage}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span>
}>
<FormPropertyHeader>
<h3>{this.props.headerMessage}</h3>
</FormPropertyHeader>
<Property
ignoreFocus={true}>
<FileUploader
submitKey={this.submitKey}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}
editable={this.props.isFineUploaderEditable}/>
</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={0}
required/>
</Property>
{this.props.children}
</Form>
);
}
});
let FileUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
onClick: React.PropTypes.func,
// editable is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files
// before login in
editable: React.PropTypes.bool
},
render() {
return (
<ReactS3FineUploader
onClick={this.props.onClick}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: apiUrls.blob_digitalworks
}}
submitKey={this.props.submitKey}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsEditable={this.props.editable}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}/>
);
}
});
export default RegisterPieceForm;

View File

@ -24,8 +24,9 @@ let EditionRemoveFromCollectionForm = React.createClass({
<p>{getLangText('Are you sure you would like to remove these editions from your collection')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv" onClick={this.submit}>{getLangText('YES, REMOVE')}</button>
<button className="btn btn-ascribe" onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, REMOVE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
);

View File

@ -2,59 +2,76 @@
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close';
import { getLangText } from '../../utils/lang_utils.js'
import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import Button from 'react-bootstrap/lib/Button';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
let ShareForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_shares;
propTypes: {
url: React.PropTypes.string,
id: React.PropTypes.string,
message: React.PropTypes.string,
editions: React.PropTypes.array,
currentUser: React.PropTypes.object,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
share_emails: this.refs.share_emails.state.value,
share_message: this.refs.share_message.state.value
};
getFormData(){
return this.props.id;
},
renderForm() {
let title = this.getTitlesString().join('');
let username = this.props.currentUser.username;
let message =
`${getLangText('Hi')},
${getLangText('I am sharing')} :
${title}${getLangText('with you')}.
${getLangText('Truly yours')},
${username}`;
render() {
return (
<form id="share_modal_content" role="form" key="share_modal_content" onSubmit={this.submit}>
<InputText
ref="share_emails"
placeHolder={getLangText('Comma separated emails')}
required="required"
type="text"
submitted={this.state.submitted}/>
<InputTextArea
ref="share_message"
defaultValue={message}
required=""
/>
<ButtonSubmitOrClose
text={getLangText('SHARE')}
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
<Form
ref='form'
url={this.props.url}
getFormData={this.getFormData}
handleSuccess={this.props.handleSuccess}
buttons={
<div className="modal-footer">
<p className="pull-right">
<Button
className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">SHARE</Button>
<Button
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>CLOSE</Button>
</p>
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>}>
<Property
name='share_emails'
label={getLangText('Emails')}>
<input
type="text"
placeholder={getLangText('Comma separated emails')}
required/>
</Property>
<Property
name='share_message'
label='Personal Message'
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
</Property>
</Form>
);
}
});

View File

@ -1,80 +1,137 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils';
import UserStore from '../../stores/user_store';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from './form';
import Property from './property';
import FormPropertyHeader from './form_property_header';
import InputCheckbox from './input_checkbox';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputCheckbox from './input_checkbox';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close';
import { getLangText } from '../../utils/lang_utils.js'
let SignupForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.users_signup;
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
children: React.PropTypes.element
},
getFormData() {
mixins: [Router.Navigation],
getDefaultProps() {
return {
email: this.refs.email.state.value,
password: this.refs.password.state.value,
password_confirm: this.refs.password_confirm.state.value,
terms: this.refs.terms.state.value,
promo_code: this.refs.promo_code.state.value
headerMessage: 'Welcome to ascribe',
submitMessage: 'Sign up'
};
},
renderForm() {
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
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.transitionTo('pieces');
}
},
handleSuccess(response){
let notificationText = getLangText('Sign up successful');
let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email +
', ' + getLangText('please confirm') + '.');
},
getFormData(){
return {terms: this.refs.form.refs.terms.refs.input.state.value};
},
render() {
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') + '!';
return (
<form id="signup_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_consignee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputText
ref="email"
placeHolder={getLangText('Email')}
required="required"
type="email"
submitted={this.state.submitted}/>
<InputText
ref="password"
placeHolder={getLangText('Choose a password')}
required="required"
type="password"
submitted={this.state.submitted}/>
<InputText
ref="password_confirm"
placeHolder={getLangText('Confirm password')}
required="required"
type="password"
submitted={this.state.submitted}/>
<div>
{getLangText('Your password must be at least 10 characters')}.
{getLangText('This password is securing your digital property like a bank account')}.
{getLangText('Store it in a safe place')}!
</div>
<InputCheckbox
ref="terms"
required="required"
label={
<div>
{getLangText('I agree to the')}&nbsp;
<a href="/terms" target="_blank"> {getLangText('Terms of Service')}</a>
</div>}/>
<InputText
ref="promo_code"
placeHolder={getLangText('Promocode (Optional)')}
required=""
type="text"
submitted={this.state.submitted}/>
<ButtonSubmitOrClose
text={getLangText('JOIN US')}
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
<Form
className="ascribe-form-bordered"
ref='form'
url={apiUrls.users_signup}
handleSuccess={this.handleSuccess}
getFormData={this.getFormData}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
{getLangText(this.props.submitMessage)}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span>
}>
<FormPropertyHeader>
<h3>{getLangText(this.props.headerMessage)}</h3>
</FormPropertyHeader>
<Property
name='email'
label={getLangText('Email')}>
<input
type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
autoComplete="on"
required/>
</Property>
<Property
name='password'
label={getLangText('Password')}
tooltip={tooltipPassword}>
<input
type="password"
placeholder={getLangText('Use a combination of minimum 10 chars and numbers')}
autoComplete="on"
required/>
</Property>
<Property
name='password_confirm'
label={getLangText('Confirm Password')}
tooltip={tooltipPassword}>
<input
type="password"
placeholder={getLangText('Enter your password once again')}
autoComplete="on"
required/>
</Property>
{this.props.children}
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox/>
</Property>
</Form>
);
}
});
export default SignupForm;
export default SignupForm;

View File

@ -1,33 +0,0 @@
'use strict';
import React from 'react';
import ModalWrapper from './modal_wrapper';
import LoginForm from '../ascribe_forms/form_login';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils.js'
let LoginModal = React.createClass({
handleLoginSuccess(){
this.props.handleSuccess();
let notificationText = getLangText('Login successful');
let notification = new GlobalNotificationModel(notificationText, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<ModalWrapper
button={this.props.button}
title={getLangText('Log in to ascribe')}
handleSuccess={this.handleLoginSuccess}
tooltip={getLangText('Log in to ascribe')}>
<LoginForm />
</ModalWrapper>
);
}
});
export default LoginModal;

View File

@ -1,32 +0,0 @@
'use strict';
import React from 'react';
import ModalWrapper from './modal_wrapper';
import SignupForm from '../ascribe_forms/form_signup';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils.js'
let SignupModal = React.createClass({
handleSignupSuccess(response){
let notificationText = getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.';
let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<ModalWrapper
button={this.props.button}
title={getLangText('Create an account')}
handleSuccess={this.handleSignupSuccess}
tooltip={getLangText('Sign up to ascribe')}>
<SignupForm />
</ModalWrapper>
);
}
});
export default SignupModal;

View File

@ -68,9 +68,9 @@ let CoaVerifyForm = React.createClass({
{getLangText('Verify your Certificate of Authenticity')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
</span>
}>
<Property
name='message'

View File

@ -3,23 +3,13 @@
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import UserStore from '../stores/user_store';
import UserActions from '../actions/user_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import FormPropertyHeader from './ascribe_forms/form_property_header';
import apiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants';
import LoginForm from './ascribe_forms/form_login';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let LoginContainer = React.createClass({
propTypes: {
message: React.PropTypes.string,
@ -27,8 +17,6 @@ let LoginContainer = React.createClass({
redirectOnLoginSuccess: React.PropTypes.bool
},
mixins: [Router.Navigation],
getDefaultProps() {
return {
message: getLangText('Enter') + ' ascribe',
@ -37,32 +25,12 @@ let LoginContainer = React.createClass({
};
},
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
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) {
this.transitionTo('pieces');
}
},
render() {
return (
<div className="ascribe-login-wrapper">
<br/>
<LoginForm
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
message={this.props.message} />
<div className="ascribe-login-text">
{getLangText('Not an ascribe user')}&#63; <Link to="signup">{getLangText('Sign up')}...</Link><br/>
@ -74,80 +42,5 @@ let LoginContainer = React.createClass({
});
let LoginForm = React.createClass({
propTypes: {
redirectOnLoginSuccess: React.PropTypes.bool,
message: React.PropTypes.string
},
handleSuccess(){
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();
/* 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
*/
if(this.props.redirectOnLoginSuccess) {
window.location = AppConstants.baseUrl + 'collection';
}
},
render() {
return (
<Form
className="ascribe-form-bordered"
ref="loginForm"
url={apiUrls.users_login}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Enter')} ascribe
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<FormPropertyHeader>
<h3>{this.props.message}</h3>
</FormPropertyHeader>
<Property
name='email'
label={getLangText('Email')}>
<input
type="email"
placeholder={getLangText('Enter your email')}
autoComplete="on"
name="username"
required/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
<input
type="password"
placeholder={getLangText('Enter your password')}
autoComplete="on"
name="password"
required/>
</Property>
</Form>
);
}
});
export default LoginContainer;

View File

@ -39,7 +39,7 @@ let PasswordResetContainer = React.createClass({
return (
<div>
<div className="ascribe-login-text ascribe-login-header">
{getLangText('Reset your ascribe password')}
{getLangText('Reset your password')}
</div>
<PasswordRequestResetForm
handleRequestSuccess={this.handleRequestSuccess}/>
@ -82,9 +82,9 @@ let PasswordRequestResetForm = React.createClass({
{getLangText('Reset your password')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
</span>
}>
<Property
name='email'
@ -129,9 +129,9 @@ let PasswordResetForm = React.createClass({
{getLangText('Reset your password')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
</span>
}>
<Property
name='password'
@ -157,4 +157,4 @@ let PasswordResetForm = React.createClass({
}
});
export default PasswordResetContainer;
export default PasswordResetContainer;

View File

@ -20,7 +20,7 @@ import AppConstants from '../constants/application_constants';
let PieceList = React.createClass({
propTypes: {
query: React.PropTypes.object
redirectTo: React.PropTypes.string
},
mixins: [Router.Navigation, Router.State],
@ -30,7 +30,7 @@ let PieceList = React.createClass({
},
componentDidMount() {
let page = this.props.query.page || 1;
let page = this.getQuery().page || 1;
PieceListStore.listen(this.onChange);
if (this.state.pieceList.length === 0){
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc)
@ -38,6 +38,12 @@ let PieceList = React.createClass({
}
},
componentDidUpdate() {
if (this.props.redirectTo && this.state.pieceListCount === 0) {
this.transitionTo(this.props.redirectTo);
}
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
},
@ -56,6 +62,30 @@ let PieceList = React.createClass({
this.state.orderAsc);
},
getPieceListToolbar() {
if(this.state.pieceListCount > 10) {
return (
<PieceListToolbar
className="ascribe-piece-list-toolbar"
searchFor={this.searchFor} />
);
}
},
getPagination() {
let currentPage = parseInt(this.getQuery().page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
if (this.state.pieceListCount > 10) {
return (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
goToPage={this.paginationGoToPage} />
);
}
},
searchFor(searchTerm) {
PieceListActions.fetchPieceList(1, this.state.pageSize, searchTerm, this.state.orderBy, this.state.orderAsc);
this.transitionTo(this.getPathname(), {page: 1});
@ -67,15 +97,11 @@ let PieceList = React.createClass({
},
render() {
let currentPage = parseInt(this.props.query.page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
return (
<div>
<PieceListToolbar
className="ascribe-piece-list-toolbar"
searchFor={this.searchFor} />
{this.getPieceListToolbar()}
<PieceListBulkModal className="ascribe-piece-list-bulk-modal" />
<AccordionList
className="ascribe-accordion-list"
@ -101,10 +127,7 @@ let PieceList = React.createClass({
);
})}
</AccordionList>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
goToPage={this.paginationGoToPage} />
{this.getPagination()}
</div>
);
}

View File

@ -2,14 +2,10 @@
import React from 'react';
import DatePicker from 'react-datepicker/dist/react-datepicker';
import Router from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
import AppConstants from '../constants/application_constants';
import LicenseActions from '../actions/license_actions';
import LicenseStore from '../stores/license_store';
@ -21,33 +17,44 @@ import UserStore from '../stores/user_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import PropertyCollapsible from './ascribe_forms/property_collapsible';
import FormPropertyHeader from './ascribe_forms/form_property_header';
import RegisterPieceForm from './ascribe_forms/form_register_piece';
//import FormPropertyHeader from './ascribe_forms/form_property_header';
import LoginContainer from './login_container';
import SlidesContainer from './ascribe_slides_container/slides_container';
import apiUrls from '../constants/api_urls';
import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import { mergeOptions } from '../utils/general_utils';
import { getCookie } from '../utils/fetch_api_utils';
import { getLangText } from '../utils/lang_utils';
let RegisterPiece = React.createClass( {
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
canSpecifyEditions: React.PropTypes.bool,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element])
},
mixins: [Router.Navigation],
getDefaultProps() {
return {
canSpecifyEditions: true
};
},
getInitialState(){
return mergeOptions(
LicenseStore.getState(),
UserStore.getState(),
PieceListStore.getState(),
{
digitalWorkKey: null,
uploadStatus: false,
selectedLicense: 0,
isFineUploaderEditable: false
});
@ -97,32 +104,6 @@ let RegisterPiece = React.createClass( {
this.transitionTo('piece', {pieceId: response.piece.id});
},
getFormData(){
return {
digital_work_key: this.state.digitalWorkKey
};
},
submitKey(key){
this.setState({
digitalWorkKey: key
});
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
isReadyForFormSubmission(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
if (files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
},
onLicenseChange(event){
//console.log(this.state.licenses[event.target.selectedIndex].url);
this.setState({selectedLicense: event.target.selectedIndex});
@ -154,6 +135,21 @@ let RegisterPiece = React.createClass( {
return null;
},
getSpecifyEditions() {
if (this.props.canSpecifyEditions) {
return (
<PropertyCollapsible
checkboxLabel={getLangText('Specify editions')}>
<span>{getLangText('Editions')}</span>
<input
type="number"
placeholder="(e.g. 32)"
min={0}/>
</PropertyCollapsible>
);
}
},
changeSlide() {
// only transition to the login store, if user is not logged in
// ergo the currentUser object is not properly defined
@ -170,69 +166,14 @@ let RegisterPiece = React.createClass( {
onFocus={this.changeSlide}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<Form
className="ascribe-form-bordered"
ref='form'
url={apiUrls.pieces_list}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
disabled={!this.state.isUploadReady}>
{getLangText('Register work')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<FormPropertyHeader>
<h3>{getLangText('Register your work')}</h3>
</FormPropertyHeader>
<Property
ignoreFocus={true}>
<FileUploader
submitKey={this.submitKey}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}
editable={this.state.isFineUploaderEditable}/>
</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={0}
required/>
</Property>
<PropertyCollapsible
checkboxLabel={getLangText('Specify editions')}>
<span>{getLangText('Editions')}</span>
<input
type="number"
placeholder="(e.g. 32)"
min={0}/>
</PropertyCollapsible>
<RegisterPieceForm
{...this.props}
isFineUploaderEditable={this.state.isFineUploaderEditable}
handleSuccess={this.handleSuccess}>
{this.getSpecifyEditions()}
{this.props.children}
{this.getLicenses()}
</Form>
</RegisterPieceForm>
</Col>
</Row>
</div>
@ -248,92 +189,4 @@ let RegisterPiece = React.createClass( {
});
let FileUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
onClick: React.PropTypes.func,
// editable is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files
// before login in
editable: React.PropTypes.bool
},
render() {
return (
<ReactS3FineUploader
onClick={this.props.onClick}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: apiUrls.blob_digitalworks
}}
submitKey={this.props.submitKey}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsEditable={this.props.editable}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}/>
);
}
});
let InputDate = React.createClass({
propTypes: {
placeholderText: React.PropTypes.string,
onChange: React.PropTypes.func
},
getInitialState() {
return {
value: null,
value_formatted: null
};
},
handleChange(date) {
this.setState({
value: date,
value_formatted: date.format('YYYY')});
let event = document.createEvent('HTMLEvents');
event.initEvent('click', false, true);
document.dispatchEvent(event);
event.target.value = date;
this.props.onChange(event);
},
render: function () {
return (
<DatePicker
key="example2"
dateFormat="YYYY"
selected={this.state.value}
onChange={this.handleChange}
onBlur={this.props.onBlur}
placeholderText={this.props.placeholderText}/>
);
}
});
export default RegisterPiece;

25
js/components/routes.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import App from './ascribe_app';
import AppConstants from '../constants/application_constants';
let Route = Router.Route;
let Redirect = Router.Redirect;
let baseUrl = AppConstants.baseUrl;
function getRoutes(commonRoutes) {
return (
<Route name="app" path={baseUrl} handler={App}>
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />
{commonRoutes}
</Route>
);
}
export default getRoutes;

View File

@ -1,50 +1,18 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import UserStore from '../stores/user_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import SignupForm from './ascribe_forms/form_signup';
import Property from './ascribe_forms/property';
import FormPropertyHeader from './ascribe_forms/form_property_header';
import InputCheckbox from './ascribe_forms/input_checkbox';
import apiUrls from '../constants/api_urls';
import { getLangText } from '../utils/lang_utils';
let SignupContainer = React.createClass({
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions({
return {
submitted: false,
message: null
}, UserStore.getState());
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
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.transitionTo('pieces');
}
};
},
handleSuccess(message){
@ -60,108 +28,26 @@ let SignupContainer = React.createClass({
<div className="ascribe-login-wrapper">
<br/>
<div className="ascribe-login-text ascribe-login-header">
{this.state.message}
{this.state.message}
</div>
</div>
);
}
return (
<div className="ascribe-login-wrapper">
<br/>
<SignupForm handleSuccess={this.handleSuccess}/>
<SignupForm handleSuccess={this.handleSuccess}>
<Property
name='promo_code'
label={getLangText('Promocode')}>
<input
type="text"
placeholder={getLangText('Enter a promocode here (Optional)')}/>
</Property>
</SignupForm>
</div>
);
}
});
let SignupForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation],
handleSuccess(response){
let notificationText = getLangText('Sign up successful');
let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email +
', ' + getLangText('please confirm') + '.');
},
getFormData(){
return {terms: this.refs.form.refs.terms.refs.input.state.value};
},
render() {
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') + '!';
return (
<Form
className="ascribe-form-bordered"
ref='form'
url={apiUrls.users_signup}
handleSuccess={this.handleSuccess}
getFormData={this.getFormData}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
{getLangText('Sign up to ascribe')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<FormPropertyHeader>
<h3>{getLangText('Welcome to ascribe')}</h3>
</FormPropertyHeader>
<Property
name='email'
label={getLangText('Email')}>
<input
type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
autoComplete="on"
required/>
</Property>
<Property
name='password'
label={getLangText('Password')}
tooltip={tooltipPassword}>
<input
type="password"
placeholder={getLangText('Use a combination of minimum 10 chars and numbers')}
autoComplete="on"
required/>
</Property>
<Property
name='password_confirm'
label={getLangText('Confirm Password')}
tooltip={tooltipPassword}>
<input
type="password"
placeholder={getLangText('Enter your password once again')}
autoComplete="on"
required/>
</Property>
<Property
name='promo_code'
label={getLangText('Promocode')}>
<input
type="text"
placeholder={getLangText('Enter a promocode here (Optional)')}/>
</Property>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox/>
</Property>
</Form>
);
}
});
export default SignupContainer;
export default SignupContainer;

View File

@ -0,0 +1,34 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Hero from './components/hero';
import Header from '../../header';
// import Footer from '../../footer';
import GlobalNotification from '../../global_notification';
let RouteHandler = Router.RouteHandler;
let PrizeApp = React.createClass({
mixins: [Router.State],
render() {
let header = null;
if (this.isActive('pieces')) {
header = null;
}
return (
<div className="whitelabel-prize">
<Hero />
{header}
<RouteHandler />
<GlobalNotification />
<div id="modal" className="container"></div>
</div>
);
}
});
export default PrizeApp;

View File

@ -0,0 +1,18 @@
'use strict';
import React from 'react';
import constants from '../../../../constants/application_constants';
let Hero = React.createClass({
render() {
return (
<div className="hero">
<img className="logo" src={constants.whitelabel.logo} alt="Sluice Art Prize" />
<h1>Sluice Art Prize 2015</h1>
</div>
);
}
});
export default Hero;

View File

@ -0,0 +1,29 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
let Link = Router.Link;
let Landing = React.createClass({
render() {
return (
<div>
<div className="container">
<ButtonGroup className="enter" bsSize="large" vertical block>
<ButtonLink to="signup">
Signup to the prize
</ButtonLink>
Already a user? <Link to="login">log in</Link>
</ButtonGroup>
</div>
</div>
);
}
});
export default Landing;

View File

@ -0,0 +1,27 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import LoginForm from '../../../ascribe_forms/form_login';
let Link = Router.Link;
let LoginContainer = React.createClass({
render() {
return (
<div className="ascribe-login-wrapper">
<LoginForm headerMessage="Log in" />
<div className="ascribe-login-text">
I'm not a user <Link to="signup">Sign up...</Link><br/>
I forgot my password <Link to="password_reset">Rescue me...</Link>
</div>
</div>
);
}
});
export default LoginContainer;

View File

@ -0,0 +1,15 @@
'use strict';
import React from 'react';
import PieceList from '../../../piece_list';
let PrizePieceList = React.createClass({
render() {
return (
<PieceList redirectTo="register_piece" />
);
}
});
export default PrizePieceList;

View File

@ -0,0 +1,50 @@
'use strict';
import React from 'react';
import RegisterPiece from '../../../register_piece';
import Property from '../../../ascribe_forms/property';
import InputTextAreaToggable from '../../../ascribe_forms/input_textarea_toggable';
import InputCheckbox from '../../../ascribe_forms/input_checkbox';
import { getLangText } from '../../../../utils/lang_utils';
let PrizeRegisterPiece = React.createClass({
render() {
return (
<RegisterPiece
headerMessage={getLangText('Submit to the prize')}
submitMessage={getLangText('Submit')}
canSpecifyEditions={false} >
<Property
name='artist_statement'
label={getLangText('Artist statement')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter your statement')}
required="required"/>
</Property>
<Property
name='work_description'
label={getLangText('Work description')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter the description for your work')}
required="required"/>
</Property>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox/>
</Property>
</RegisterPiece>
);
}
});
export default PrizeRegisterPiece;

View File

@ -0,0 +1,44 @@
'use strict';
import React from 'react';
import SignupForm from '../../../ascribe_forms/form_signup';
let SignupContainer = React.createClass({
getInitialState() {
return {
submitted: false,
message: null
};
},
handleSuccess(message){
this.setState({
submitted: true,
message: message
});
},
render() {
if (this.state.submitted){
return (
<div className="ascribe-login-wrapper">
<div className="ascribe-login-text ascribe-login-header">
{this.state.message}
</div>
</div>
);
}
return (
<div className="ascribe-login-wrapper">
<SignupForm
headerMessage="Sign up to the prize"
submitMessage="Sign up"
handleSuccess={this.handleSuccess} />
</div>
);
}
});
export default SignupContainer;

View File

@ -0,0 +1,38 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Landing from './components/landing';
import LoginContainer from './components/login_container';
import SignupContainer from './components/signup_container';
import PasswordResetContainer from '../../../components/password_reset_container';
import PrizeRegisterPiece from './components/register_piece';
import PrizePieceList from './components/piece_list';
import PieceContainer from '../../ascribe_detail/piece_container';
import EditionContainer from '../../ascribe_detail/edition_container';
import App from './app';
import AppConstants from '../../../constants/application_constants';
let Route = Router.Route;
let baseUrl = AppConstants.baseUrl;
function getRoutes(commonRoutes) {
return (
<Route name="app" path={baseUrl} handler={App}>
<Route name="landing" path="/" handler={Landing} />
<Route name="login" path="login" handler={LoginContainer} />
<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} />
<Route name="pieces" path="collection" handler={PrizePieceList} />
<Route name="piece" path="pieces/:pieceId" handler={PieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
</Route>
);
}
export default getRoutes;

View File

@ -12,7 +12,7 @@ let apiUrls = {
'coa_verify': AppConstants.apiEndpoint + 'coa/verify_coa/',
'edition': AppConstants.apiEndpoint + 'editions/${bitcoin_id}/',
'edition_delete': AppConstants.apiEndpoint + 'editions/${edition_id}/',
'edition_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/${edition_id}/',
'edition_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/editions/${edition_id}/',
'editions': AppConstants.apiEndpoint + 'editions/', // this should be moved to the one below
'editions_list': AppConstants.apiEndpoint + 'pieces/${piece_id}/editions/',
'licenses': AppConstants.apiEndpoint + 'ownership/licenses/',
@ -25,7 +25,8 @@ let apiUrls = {
'ownership_loans_confirm': AppConstants.apiEndpoint + 'ownership/loans/confirm/',
'ownership_loans_deny': AppConstants.apiEndpoint + 'ownership/loans/deny/',
'ownership_loans_contract': AppConstants.apiEndpoint + 'ownership/loans/contract/',
'ownership_shares': AppConstants.apiEndpoint + 'ownership/shares/',
'ownership_shares_editions': AppConstants.apiEndpoint + 'ownership/shares/editions/',
'ownership_shares_pieces': AppConstants.apiEndpoint + 'ownership/shares/pieces/',
'ownership_transfers': AppConstants.apiEndpoint + 'ownership/transfers/',
'ownership_transfers_withdraw': AppConstants.apiEndpoint + 'ownership/transfers/withdraw/',
'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/',

View File

@ -10,7 +10,10 @@ let constants = {
'serverUrl': window.SERVER_URL,
'baseUrl': window.BASE_URL,
'aclList': ['edit', 'consign', 'consign_request', 'unconsign', 'unconsign_request', 'transfer',
'loan', 'loan_request', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection']
'loan', 'loan_request', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection'],
// in case of whitelabel cusomization, we store stuff here
'whitelabel': {}
};
export default constants;

View File

@ -210,6 +210,7 @@ const languages = {
'I agree to the Terms of Service': 'I agree to the Terms of Service',
'read': 'read',
'If your email address exists in our database, you will receive a password recovery link in a few minutes.': 'If your email address exists in our database, you will receive a password recovery link in a few minutes.',
'REGISTREE': 'REGISTREE',
},
'de': {
'ID': 'ID',
@ -420,6 +421,7 @@ const languages = {
'I agree to the Terms of Service': 'I agree to the Terms of Service',
'read': 'read',
'If your email address exists in our database, you will receive a password recovery link in a few minutes.': 'If your email address exists in our database, you will receive a password recovery link in a few minutes.',
'REGISTREE': 'REGISTREE',
},
'fr': {
'ID': 'ID',
@ -630,6 +632,7 @@ const languages = {
'I agree to the Terms of Service': 'I agree to the Terms of Service',
'read': 'read',
'If your email address exists in our database, you will receive a password recovery link in a few minutes.': 'Si votre adresse électronique existe dans notre base de données, vous recevrez un lien de récupération de mot de passe dans quelques minutes.',
'REGISTREE': 'REGISTREE',
}
};

View File

@ -3,7 +3,9 @@
import React from 'react';
import Router from 'react-router';
import AscribeApp from './components/ascribe_app';
import getPrizeRoutes from './components/whitelabel/prize/routes';
import getDefaultRoutes from './components/routes';
import PieceList from './components/piece_list';
import PieceContainer from './components/ascribe_detail/piece_container';
import EditionContainer from './components/ascribe_detail/edition_container';
@ -15,15 +17,13 @@ import PasswordResetContainer from './components/password_reset_container';
import SettingsContainer from './components/settings_container';
import CoaVerifyContainer from './components/coa_verify_container';
import AppConstants from './constants/application_constants';
import RegisterPiece from './components/register_piece';
let Route = Router.Route;
let Redirect = Router.Redirect;
let baseUrl = AppConstants.baseUrl;
let routes = (
<Route name="app" path={baseUrl} handler={AscribeApp}>
const COMMON_ROUTES = (
<Route>
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="pieces" path="collection" handler={PieceList} />
@ -33,10 +33,21 @@ let routes = (
<Route name="register_piece" path="register_piece" handler={RegisterPiece} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />
</Route>
);
export default routes;
function getRoutes(type) {
let routes = null;
console.log(type)
if (type === 'prize') {
routes = getPrizeRoutes(COMMON_ROUTES);
} else {
routes = getDefaultRoutes(COMMON_ROUTES);
}
return routes;
}
export default getRoutes;

View File

@ -19,6 +19,7 @@ class PieceListStore {
* the number of items the resource actually - without pagination - provides.
*/
this.pieceList = [];
// -1 specifies that the store is currently loading
this.pieceListCount = -1;
this.page = 1;
this.pageSize = 10;

View File

@ -13,6 +13,14 @@ class PieceStore {
onUpdatePiece(piece) {
this.piece = piece;
}
onUpdateProperty({key, value}) {
if(this.piece && key in this.piece) {
this.piece[key] = value;
} else {
throw new Error('There is no piece defined in PieceStore or the piece object does not have the property you\'re looking for.');
}
}
}
export default alt.createStore(PieceStore, 'PieceStore');

View File

@ -97,7 +97,7 @@ $ascribe-accordion-list-font: 'Source Sans Pro';
}
}
border-left: 3px solid rgba(0,0,0,0);
border-top: 1px solid rgba(0,0,0,.1);
//border-top: 1px solid rgba(0,0,0,.1);
border-bottom: 1px solid rgba(0,0,0,.05);
}
tbody {

5
sass/ascribe_app.scss Normal file
View File

@ -0,0 +1,5 @@
.ascribe-default-app {
background-color: #FDFDFD;
border-radius: 0;
padding-top: 70px;
}

View File

@ -11,6 +11,7 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'ascribe_theme';
@import './ascribe-fonts/style';
@import './ascribe-fonts/ascribe-fonts';
@import 'ascribe_app';
@import 'ascribe_login';
@import 'ascribe_table';
@import 'ascribe_accordion_list';
@ -28,10 +29,11 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'ascribe_slides_container';
@import 'ascribe_form';
body {
background-color: #FDFDFD;
border-radius: 0;
margin-top: 70px;
@import 'whitelabel/index';
html, body {
height: 100%;
}
html {
@ -42,6 +44,10 @@ hr {
margin-bottom: 15px;
}
#main {
height: 100%;
}
.hidden {
display: none;
}

View File

@ -0,0 +1 @@
@import 'prize/index';

View File

@ -0,0 +1 @@
@import 'landing'

View File

@ -0,0 +1,16 @@
.whitelabel-prize {
.hero {
overflow: hidden;
.logo {
float: left;
padding-right: 2em;
}
}
.enter {
}
}