mirror of
https://github.com/ascribe/onion.git
synced 2024-11-14 17:15:08 +01:00
Merge branch 'master' into AD-56-add-social-share-functionality
Conflicts: js/components/ascribe_detail/media_container.js js/components/ascribe_media/media_player.js
This commit is contained in:
commit
339421a4b7
20
README.md
20
README.md
@ -29,6 +29,8 @@ Additionally, to work on the white labeling functionality, you need to edit your
|
||||
127.0.0.1 cyland.localhost.com
|
||||
127.0.0.1 ikonotv.localhost.com
|
||||
127.0.0.1 sluice.localhost.com
|
||||
127.0.0.1 lumenus.localhost.com
|
||||
127.0.0.1 portfolioreview.localhost.com
|
||||
```
|
||||
|
||||
|
||||
@ -41,7 +43,25 @@ For this project, we're using:
|
||||
* We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding))
|
||||
* We don't use camel case for file naming but in everything Javascript related
|
||||
* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword)
|
||||
* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
|
||||
|
||||
Branch names
|
||||
=====================
|
||||
Since we moved to Github, we cannot create branch names automatically with JIRA anymore.
|
||||
To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion.
|
||||
|
||||
```
|
||||
AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
|
||||
```
|
||||
|
||||
where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title.
|
||||
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
|
||||
|
||||
Example
|
||||
-------------
|
||||
**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load <more useless information>`
|
||||
|
||||
**Github branch name:** `AD-1242-caching-solution-for-stores`
|
||||
|
||||
SCSS Code Conventions
|
||||
=====================
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Moment from 'moment';
|
||||
|
||||
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
@ -129,7 +130,7 @@ let AccordionListItemWallet = React.createClass({
|
||||
piece={this.props.content}
|
||||
subsubheading={
|
||||
<div className="pull-left">
|
||||
<span>{new Date(this.props.content.date_created).getFullYear()}</span>
|
||||
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
|
||||
{this.getLicences()}
|
||||
</div>}
|
||||
buttons={
|
||||
|
@ -1,187 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ConsignForm from '../ascribe_forms/form_consign';
|
||||
import UnConsignForm from '../ascribe_forms/form_unconsign';
|
||||
import TransferForm from '../ascribe_forms/form_transfer';
|
||||
import LoanForm from '../ascribe_forms/form_loan';
|
||||
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
|
||||
import ShareForm from '../ascribe_forms/form_share_email';
|
||||
import ModalWrapper from '../ascribe_modal/modal_wrapper';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
|
||||
import { getAclFormMessage } from '../../utils/form_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let AclButton = React.createClass({
|
||||
propTypes: {
|
||||
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
|
||||
availableAcls: React.PropTypes.object.isRequired,
|
||||
pieceOrEditions: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.array
|
||||
]).isRequired,
|
||||
currentUser: React.PropTypes.object,
|
||||
buttonAcceptName: React.PropTypes.string,
|
||||
buttonAcceptClassName: React.PropTypes.string,
|
||||
handleSuccess: React.PropTypes.func.isRequired,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
isPiece(){
|
||||
return this.props.pieceOrEditions.constructor !== Array;
|
||||
},
|
||||
|
||||
actionProperties(){
|
||||
|
||||
let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username);
|
||||
|
||||
if (this.props.action === 'acl_consign'){
|
||||
return {
|
||||
title: getLangText('Consign artwork'),
|
||||
tooltip: getLangText('Have someone else sell the artwork'),
|
||||
form: (
|
||||
<ConsignForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_consigns}/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
}
|
||||
if (this.props.action === 'acl_unconsign'){
|
||||
return {
|
||||
title: getLangText('Unconsign artwork'),
|
||||
tooltip: getLangText('Have the owner manage his sales again'),
|
||||
form: (
|
||||
<UnConsignForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_unconsigns}/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
}else if (this.props.action === 'acl_transfer') {
|
||||
return {
|
||||
title: getLangText('Transfer artwork'),
|
||||
tooltip: getLangText('Transfer the ownership of the artwork'),
|
||||
form: (
|
||||
<TransferForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_transfers}/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
}
|
||||
else if (this.props.action === 'acl_loan'){
|
||||
return {
|
||||
title: getLangText('Loan artwork'),
|
||||
tooltip: getLangText('Loan your artwork for a limited period of time'),
|
||||
form: (<LoanForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={this.isPiece() ? ApiUrls.ownership_loans_pieces : ApiUrls.ownership_loans_editions}/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
}
|
||||
else if (this.props.action === 'acl_loan_request'){
|
||||
return {
|
||||
title: getLangText('Loan artwork'),
|
||||
tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time'),
|
||||
form: (<LoanRequestAnswerForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_loans_pieces_request_confirm}/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
}
|
||||
else if (this.props.action === 'acl_share'){
|
||||
return {
|
||||
title: getLangText('Share artwork'),
|
||||
tooltip: getLangText('Share the artwork'),
|
||||
form: (
|
||||
<ShareForm
|
||||
message={message}
|
||||
id={this.getFormDataId()}
|
||||
url={this.isPiece() ? ApiUrls.ownership_shares_pieces : ApiUrls.ownership_shares_editions }/>
|
||||
),
|
||||
handleSuccess: this.showNotification
|
||||
};
|
||||
} else {
|
||||
throw new Error('Your specified action did not match a form.');
|
||||
}
|
||||
},
|
||||
|
||||
showNotification(response){
|
||||
this.props.handleSuccess();
|
||||
if(response.notification) {
|
||||
let notification = new GlobalNotificationModel(response.notification, 'success');
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
}
|
||||
},
|
||||
|
||||
// plz move to share form
|
||||
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()};
|
||||
}
|
||||
},
|
||||
|
||||
// Removes the acl_ prefix and converts to upper case
|
||||
sanitizeAction() {
|
||||
if (this.props.buttonAcceptName) {
|
||||
return this.props.buttonAcceptName;
|
||||
}
|
||||
return this.props.action.split('acl_')[1].toUpperCase();
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.props.availableAcls){
|
||||
let shouldDisplay = this.props.availableAcls[this.props.action];
|
||||
let aclProps = this.actionProperties();
|
||||
let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
|
||||
return (
|
||||
<ModalWrapper
|
||||
trigger={
|
||||
<button
|
||||
className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
|
||||
{this.sanitizeAction()}
|
||||
</button>
|
||||
}
|
||||
handleSuccess={aclProps.handleSuccess}
|
||||
title={aclProps.title}>
|
||||
{aclProps.form}
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export default AclButton;
|
@ -5,21 +5,25 @@ import React from 'react/addons';
|
||||
import UserActions from '../../actions/user_actions';
|
||||
import UserStore from '../../stores/user_store';
|
||||
|
||||
import AclButton from '../ascribe_buttons/acl_button';
|
||||
import ConsignButton from './acls/consign_button';
|
||||
import LoanButton from './acls/loan_button';
|
||||
import LoanRequestButton from './acls/loan_request_button';
|
||||
import ShareButton from './acls/share_button';
|
||||
import TransferButton from './acls/transfer_button';
|
||||
import UnconsignButton from './acls/unconsign_button';
|
||||
|
||||
import { mergeOptions } from '../../utils/general_utils';
|
||||
|
||||
|
||||
let AclButtonList = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
editions: React.PropTypes.oneOfType([
|
||||
pieceOrEditions: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.array
|
||||
]),
|
||||
availableAcls: React.PropTypes.object,
|
||||
]).isRequired,
|
||||
availableAcls: React.PropTypes.object.isRequired,
|
||||
buttonsStyle: React.PropTypes.object,
|
||||
handleSuccess: React.PropTypes.func,
|
||||
handleSuccess: React.PropTypes.func.isRequired,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
@ -78,7 +82,7 @@ let AclButtonList = React.createClass({
|
||||
const { className,
|
||||
buttonsStyle,
|
||||
availableAcls,
|
||||
editions,
|
||||
pieceOrEditions,
|
||||
handleSuccess } = this.props;
|
||||
|
||||
const { currentUser } = this.state;
|
||||
@ -86,34 +90,29 @@ let AclButtonList = React.createClass({
|
||||
return (
|
||||
<div className={className}>
|
||||
<span ref="buttonList" style={buttonsStyle}>
|
||||
<AclButton
|
||||
<ShareButton
|
||||
availableAcls={availableAcls}
|
||||
action="acl_share"
|
||||
pieceOrEditions={editions}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
currentUser={currentUser}
|
||||
handleSuccess={handleSuccess} />
|
||||
<AclButton
|
||||
<TransferButton
|
||||
availableAcls={availableAcls}
|
||||
action="acl_transfer"
|
||||
pieceOrEditions={editions}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
currentUser={currentUser}
|
||||
handleSuccess={handleSuccess}/>
|
||||
<AclButton
|
||||
<ConsignButton
|
||||
availableAcls={availableAcls}
|
||||
action="acl_consign"
|
||||
pieceOrEditions={editions}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
currentUser={currentUser}
|
||||
handleSuccess={handleSuccess} />
|
||||
<AclButton
|
||||
<UnconsignButton
|
||||
availableAcls={availableAcls}
|
||||
action="acl_unconsign"
|
||||
pieceOrEditions={editions}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
currentUser={currentUser}
|
||||
handleSuccess={handleSuccess} />
|
||||
<AclButton
|
||||
<LoanButton
|
||||
availableAcls={availableAcls}
|
||||
action="acl_loan"
|
||||
pieceOrEditions={editions}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
currentUser={currentUser}
|
||||
handleSuccess={handleSuccess} />
|
||||
{this.renderChildren()}
|
||||
@ -123,4 +122,4 @@ let AclButtonList = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default AclButtonList;
|
||||
export default AclButtonList;
|
||||
|
78
js/components/ascribe_buttons/acls/acl_button.js
Normal file
78
js/components/ascribe_buttons/acls/acl_button.js
Normal file
@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AclProxy from '../../acl_proxy';
|
||||
|
||||
import AclFormFactory from '../../ascribe_forms/acl_form_factory';
|
||||
|
||||
import ModalWrapper from '../../ascribe_modal/modal_wrapper';
|
||||
|
||||
import AppConstants from '../../../constants/application_constants';
|
||||
|
||||
|
||||
export default function ({ action, displayName, title, tooltip }) {
|
||||
if (AppConstants.aclList.indexOf(action) < 0) {
|
||||
console.warn('Your specified aclName did not match a an acl class.');
|
||||
}
|
||||
|
||||
return React.createClass({
|
||||
displayName: displayName,
|
||||
|
||||
propTypes: {
|
||||
availableAcls: React.PropTypes.object.isRequired,
|
||||
buttonAcceptName: React.PropTypes.string,
|
||||
buttonAcceptClassName: React.PropTypes.string,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
email: React.PropTypes.string,
|
||||
pieceOrEditions: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.array
|
||||
]).isRequired,
|
||||
handleSuccess: React.PropTypes.func.isRequired,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
// Removes the acl_ prefix and converts to upper case
|
||||
sanitizeAction() {
|
||||
if (this.props.buttonAcceptName) {
|
||||
return this.props.buttonAcceptName;
|
||||
}
|
||||
return action.split('acl_')[1].toUpperCase();
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
availableAcls,
|
||||
buttonAcceptClassName,
|
||||
currentUser,
|
||||
email,
|
||||
pieceOrEditions,
|
||||
handleSuccess } = this.props;
|
||||
|
||||
return (
|
||||
<AclProxy
|
||||
aclName={action}
|
||||
aclObject={availableAcls}>
|
||||
<ModalWrapper
|
||||
trigger={
|
||||
<button
|
||||
className={classNames('btn', 'btn-default', 'btn-sm', buttonAcceptClassName)}>
|
||||
{this.sanitizeAction()}
|
||||
</button>
|
||||
}
|
||||
handleSuccess={handleSuccess}
|
||||
title={title}>
|
||||
<AclFormFactory
|
||||
action={action}
|
||||
currentUser={currentUser}
|
||||
email={email}
|
||||
pieceOrEditions={pieceOrEditions}
|
||||
showNotification />
|
||||
</ModalWrapper>
|
||||
</AclProxy>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
14
js/components/ascribe_buttons/acls/consign_button.js
Normal file
14
js/components/ascribe_buttons/acls/consign_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_consign',
|
||||
displayName: 'ConsignButton',
|
||||
title: getLangText('Consign artwork'),
|
||||
tooltip: getLangText('Have someone else sell the artwork')
|
||||
});
|
14
js/components/ascribe_buttons/acls/loan_button.js
Normal file
14
js/components/ascribe_buttons/acls/loan_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_loan',
|
||||
displayName: 'LoanButton',
|
||||
title: getLangText('Loan artwork'),
|
||||
tooltip: getLangText('Loan your artwork for a limited period of time')
|
||||
});
|
14
js/components/ascribe_buttons/acls/loan_request_button.js
Normal file
14
js/components/ascribe_buttons/acls/loan_request_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_loan_request',
|
||||
displayName: 'LoanRequestButton',
|
||||
title: getLangText('Loan artwork'),
|
||||
tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time')
|
||||
});
|
14
js/components/ascribe_buttons/acls/share_button.js
Normal file
14
js/components/ascribe_buttons/acls/share_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_share',
|
||||
displayName: 'ShareButton',
|
||||
title: getLangText('Share artwork'),
|
||||
tooltip: getLangText('Share the artwork')
|
||||
});
|
14
js/components/ascribe_buttons/acls/transfer_button.js
Normal file
14
js/components/ascribe_buttons/acls/transfer_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_transfer',
|
||||
displayName: 'TransferButton',
|
||||
title: getLangText('Transfer artwork'),
|
||||
tooltip: getLangText('Transfer the ownership of the artwork')
|
||||
});
|
14
js/components/ascribe_buttons/acls/unconsign_button.js
Normal file
14
js/components/ascribe_buttons/acls/unconsign_button.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './acl_button';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
export default AclButton({
|
||||
action: 'acl_unconsign',
|
||||
displayName: 'UnconsignButton',
|
||||
title: getLangText('Unconsign artwork'),
|
||||
tooltip: getLangText('Have the owner manage his sales again')
|
||||
});
|
@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Link, History } from 'react-router';
|
||||
import Moment from 'moment';
|
||||
|
||||
import Row from 'react-bootstrap/lib/Row';
|
||||
import Col from 'react-bootstrap/lib/Col';
|
||||
@ -85,7 +86,7 @@ let Edition = React.createClass({
|
||||
<hr style={{marginTop: 0}}/>
|
||||
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
|
||||
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
|
||||
<EditionDetailProperty label="DATE" value={ new Date(this.props.edition.date_created).getFullYear() } />
|
||||
<EditionDetailProperty label="DATE" value={Moment(this.props.edition.date_created, 'YYYY-MM-DD').year()} />
|
||||
<hr/>
|
||||
</div>
|
||||
<EditionSummary
|
||||
|
@ -107,7 +107,7 @@ let EditionActionPanel = React.createClass({
|
||||
<AclButtonList
|
||||
className="ascribe-button-list"
|
||||
availableAcls={edition.acl}
|
||||
editions={[edition]}
|
||||
pieceOrEditions={[edition]}
|
||||
handleSuccess={this.handleSuccess}>
|
||||
<AclProxy
|
||||
aclObject={edition.acl}
|
||||
@ -172,4 +172,4 @@ let EditionActionPanel = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default EditionActionPanel;
|
||||
export default EditionActionPanel;
|
||||
|
@ -20,8 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
submitFile: React.PropTypes.func,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
editable: React.PropTypes.bool,
|
||||
multiple: React.PropTypes.bool,
|
||||
location: React.PropTypes.object
|
||||
multiple: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -44,6 +43,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
|
||||
return (
|
||||
<Property
|
||||
name="other_data_key"
|
||||
label="Additional files">
|
||||
<ReactS3FineUploader
|
||||
uploadStarted={this.props.uploadStarted}
|
||||
@ -89,11 +89,10 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
}}
|
||||
areAssetsDownloadable={true}
|
||||
areAssetsEditable={this.props.editable}
|
||||
multiple={this.props.multiple}
|
||||
location={this.props.location}/>
|
||||
multiple={this.props.multiple} />
|
||||
</Property>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default FurtherDetailsFileuploader;
|
||||
export default FurtherDetailsFileuploader;
|
||||
|
@ -125,8 +125,8 @@ let MediaContainer = React.createClass({
|
||||
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download}
|
||||
aclObject={content.acl}
|
||||
aclName="acl_download">
|
||||
<Button bsSize="xsmall" className="ascribe-margin-1px" href={content.digital_work.url} target="_blank">
|
||||
Download <Glyphicon glyph="cloud-download"/>
|
||||
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
|
||||
Download .{mimetype} <Glyphicon glyph="cloud-download"/>
|
||||
</Button>
|
||||
</AclProxy>
|
||||
{embed}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { History } from 'react-router';
|
||||
import Moment from 'moment';
|
||||
|
||||
import PieceActions from '../../actions/piece_actions';
|
||||
import PieceStore from '../../stores/piece_store';
|
||||
@ -201,7 +202,7 @@ let PieceContainer = React.createClass({
|
||||
<AclButtonList
|
||||
className="ascribe-button-list"
|
||||
availableAcls={piece.acl}
|
||||
editions={piece}
|
||||
pieceOrEditions={piece}
|
||||
handleSuccess={this.loadPiece}>
|
||||
<CreateEditionsButton
|
||||
label={getLangText('CREATE EDITIONS')}
|
||||
@ -236,7 +237,7 @@ let PieceContainer = React.createClass({
|
||||
<hr style={{marginTop: 0}}/>
|
||||
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
|
||||
<DetailProperty label="BY" value={this.state.piece.artist_name} />
|
||||
<DetailProperty label="DATE" value={ new Date(this.state.piece.date_created).getFullYear() } />
|
||||
<DetailProperty label="DATE" value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year() } />
|
||||
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null}
|
||||
<hr/>
|
||||
</div>
|
||||
|
128
js/components/ascribe_forms/acl_form_factory.js
Normal file
128
js/components/ascribe_forms/acl_form_factory.js
Normal file
@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ConsignForm from '../ascribe_forms/form_consign';
|
||||
import UnConsignForm from '../ascribe_forms/form_unconsign';
|
||||
import TransferForm from '../ascribe_forms/form_transfer';
|
||||
import LoanForm from '../ascribe_forms/form_loan';
|
||||
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
|
||||
import ShareForm from '../ascribe_forms/form_share_email';
|
||||
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils';
|
||||
|
||||
let AclFormFactory = React.createClass({
|
||||
propTypes: {
|
||||
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
email: React.PropTypes.string,
|
||||
message: React.PropTypes.string,
|
||||
pieceOrEditions: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.array
|
||||
]).isRequired,
|
||||
handleSuccess: React.PropTypes.func,
|
||||
showNotification: React.PropTypes.bool
|
||||
},
|
||||
|
||||
isPiece() {
|
||||
return this.props.pieceOrEditions.constructor !== Array;
|
||||
},
|
||||
|
||||
getFormDataId() {
|
||||
return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
|
||||
},
|
||||
|
||||
showSuccessNotification(response) {
|
||||
if (typeof this.props.handleSuccess === 'function') {
|
||||
this.props.handleSuccess();
|
||||
}
|
||||
|
||||
if (response.notification) {
|
||||
const notification = new GlobalNotificationModel(response.notification, 'success');
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
action,
|
||||
pieceOrEditions,
|
||||
currentUser,
|
||||
email,
|
||||
message,
|
||||
handleSuccess,
|
||||
showNotification } = this.props;
|
||||
|
||||
const formMessage = message || getAclFormMessage({
|
||||
aclName: action,
|
||||
entities: pieceOrEditions,
|
||||
isPiece: this.isPiece(),
|
||||
senderName: currentUser.username
|
||||
});
|
||||
|
||||
if (action === 'acl_consign') {
|
||||
return (
|
||||
<ConsignForm
|
||||
email={email}
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_consigns}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else if (action === 'acl_unconsign') {
|
||||
return (
|
||||
<UnConsignForm
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_unconsigns}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else if (action === 'acl_transfer') {
|
||||
return (
|
||||
<TransferForm
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_transfers}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else if (action === 'acl_loan') {
|
||||
return (
|
||||
<LoanForm
|
||||
email={email}
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={this.isPiece() ? ApiUrls.ownership_loans_pieces
|
||||
: ApiUrls.ownership_loans_editions}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else if (action === 'acl_loan_request') {
|
||||
return (
|
||||
<LoanRequestAnswerForm
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={ApiUrls.ownership_loans_pieces_request_confirm}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else if (action === 'acl_share') {
|
||||
return (
|
||||
<ShareForm
|
||||
message={formMessage}
|
||||
id={this.getFormDataId()}
|
||||
url={this.isPiece() ? ApiUrls.ownership_shares_pieces
|
||||
: ApiUrls.ownership_shares_editions}
|
||||
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
|
||||
);
|
||||
} else {
|
||||
throw new Error('Your specified action did not match a form.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default AclFormFactory;
|
@ -12,7 +12,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
|
||||
import requests from '../../utils/requests';
|
||||
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
import { mergeOptionsWithDuplicates } from '../../utils/general_utils';
|
||||
import { sanitize } from '../../utils/general_utils';
|
||||
|
||||
|
||||
let Form = React.createClass({
|
||||
@ -124,12 +124,12 @@ let Form = React.createClass({
|
||||
getFormData() {
|
||||
let data = {};
|
||||
|
||||
for(let ref in this.refs) {
|
||||
for (let ref in this.refs) {
|
||||
data[this.refs[ref].props.name] = this.refs[ref].state.value;
|
||||
}
|
||||
|
||||
if(typeof this.props.getFormData === 'function') {
|
||||
data = mergeOptionsWithDuplicates(data, this.props.getFormData());
|
||||
if (typeof this.props.getFormData === 'function') {
|
||||
data = Object.assign(data, this.props.getFormData());
|
||||
}
|
||||
|
||||
return data;
|
||||
@ -155,7 +155,7 @@ let Form = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
handleError(err){
|
||||
handleError(err) {
|
||||
if (err.json) {
|
||||
for (let input in err.json.errors){
|
||||
if (this.refs && this.refs[input] && this.refs[input].state) {
|
||||
@ -185,7 +185,7 @@ let Form = React.createClass({
|
||||
this.setState({submitted: false});
|
||||
},
|
||||
|
||||
clearErrors(){
|
||||
clearErrors() {
|
||||
for(let ref in this.refs){
|
||||
if (this.refs[ref] && typeof this.refs[ref].clearErrors === 'function'){
|
||||
this.refs[ref].clearErrors();
|
||||
@ -236,12 +236,12 @@ let Form = React.createClass({
|
||||
},
|
||||
|
||||
renderChildren() {
|
||||
return ReactAddons.Children.map(this.props.children, (child) => {
|
||||
return ReactAddons.Children.map(this.props.children, (child, i) => {
|
||||
if (child) {
|
||||
return ReactAddons.addons.cloneWithProps(child, {
|
||||
handleChange: this.handleChangeChild,
|
||||
ref: child.props.name,
|
||||
|
||||
key: i,
|
||||
// We need this in order to make editable be overridable when setting it directly
|
||||
// on Property
|
||||
editable: child.props.overrideForm ? child.props.editable : !this.props.disabled
|
||||
@ -269,6 +269,83 @@ let Form = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a single ref and returns a human-readable error message
|
||||
* @param {object} refToValidate A customly constructed object to check
|
||||
* @return {oneOfType([arrayOf(string), bool])} Either an error message or false, saying that
|
||||
* everything is valid
|
||||
*/
|
||||
_hasRefErrors(refToValidate) {
|
||||
let errors = Object
|
||||
.keys(refToValidate)
|
||||
.reduce((a, constraintKey) => {
|
||||
const contraintValue = refToValidate[constraintKey];
|
||||
|
||||
if(!contraintValue) {
|
||||
switch(constraintKey) {
|
||||
case 'min' || 'max':
|
||||
a.push(getLangText('The field you defined is not in the valid range'));
|
||||
break;
|
||||
case 'pattern':
|
||||
a.push(getLangText('The value you defined is not matching the valid pattern'));
|
||||
break;
|
||||
case 'required':
|
||||
a.push(getLangText('This field is required'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
return errors.length ? errors : false;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method validates all child inputs of the form.
|
||||
*
|
||||
* As of now, it only considers
|
||||
* - `max`
|
||||
* - `min`
|
||||
* - `pattern`
|
||||
* - `required`
|
||||
*
|
||||
* The idea is to enhance this method everytime we need more thorough validation.
|
||||
* So feel free to add props that additionally should be checked, if they're present
|
||||
* in the input's props.
|
||||
*
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
validate() {
|
||||
this.clearErrors();
|
||||
const validatedFormInputs = {};
|
||||
|
||||
Object
|
||||
.keys(this.refs)
|
||||
.forEach((refName) => {
|
||||
let refToValidate = {};
|
||||
const property = this.refs[refName];
|
||||
const input = property.refs.input;
|
||||
const value = input.getDOMNode().value || input.state.value;
|
||||
const { max,
|
||||
min,
|
||||
pattern,
|
||||
required,
|
||||
type } = input.props;
|
||||
|
||||
refToValidate.required = required ? value : true;
|
||||
refToValidate.pattern = pattern && typeof value === 'string' ? value.match(pattern) : true;
|
||||
refToValidate.max = type === 'number' ? parseInt(value, 10) <= max : true;
|
||||
refToValidate.min = type === 'number' ? parseInt(value, 10) >= min : true;
|
||||
|
||||
const validatedRef = this._hasRefErrors(refToValidate);
|
||||
validatedFormInputs[refName] = validatedRef;
|
||||
});
|
||||
const errorMessagesForRefs = sanitize(validatedFormInputs, (val) => !val);
|
||||
this.handleError({ json: { errors: errorMessagesForRefs } });
|
||||
return !Object.keys(errorMessagesForRefs).length;
|
||||
},
|
||||
|
||||
render() {
|
||||
let className = 'ascribe-form';
|
||||
|
||||
|
@ -28,8 +28,7 @@ let CreateContractForm = React.createClass({
|
||||
fileClassToUpload: React.PropTypes.shape({
|
||||
singular: React.PropTypes.string,
|
||||
plural: React.PropTypes.string
|
||||
}),
|
||||
location: React.PropTypes.object
|
||||
})
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
@ -87,8 +86,7 @@ let CreateContractForm = React.createClass({
|
||||
areAssetsEditable={true}
|
||||
setIsUploadReady={this.setIsUploadReady}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
fileClassToUpload={this.props.fileClassToUpload}
|
||||
location={this.props.location}/>
|
||||
fileClassToUpload={this.props.fileClassToUpload} />
|
||||
</Property>
|
||||
<Property
|
||||
name='name'
|
||||
@ -110,4 +108,4 @@ let CreateContractForm = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default CreateContractForm;
|
||||
export default CreateContractForm;
|
||||
|
@ -26,12 +26,15 @@ let RegisterPieceForm = React.createClass({
|
||||
isFineUploaderActive: React.PropTypes.bool,
|
||||
isFineUploaderEditable: React.PropTypes.bool,
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
children: React.PropTypes.element,
|
||||
onLoggedOut: React.PropTypes.func,
|
||||
|
||||
// For this form to work with SlideContainer, we sometimes have to disable it
|
||||
disabled: React.PropTypes.bool,
|
||||
location: React.PropTypes.object
|
||||
location: React.PropTypes.object,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
])
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -116,7 +119,7 @@ let RegisterPieceForm = React.createClass({
|
||||
onLoggedOut={this.props.onLoggedOut}
|
||||
disabled={!this.props.isFineUploaderEditable}
|
||||
enableLocalHashing={enableLocalHashing}
|
||||
location={this.props.location}/>
|
||||
uploadMethod={this.props.location.query.method} />
|
||||
</Property>
|
||||
<Property
|
||||
name='artist_name'
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AclButton from './../ascribe_buttons/acl_button';
|
||||
import ActionPanel from '../ascribe_panel/action_panel';
|
||||
import Form from './form';
|
||||
|
||||
import LoanRequestButton from '../ascribe_buttons/acls/loan_request_button';
|
||||
import UnconsignButton from '../ascribe_buttons/acls/unconsign_button';
|
||||
|
||||
import ActionPanel from '../ascribe_panel/action_panel';
|
||||
|
||||
import NotificationActions from '../../actions/notification_actions';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
@ -13,9 +16,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
|
||||
import { getAclFormDataId } from '../../utils/form_utils';
|
||||
import { getLangText } from '../../utils/lang_utils.js';
|
||||
|
||||
|
||||
let RequestActionForm = React.createClass({
|
||||
propTypes: {
|
||||
pieceOrEditions: React.PropTypes.oneOfType([
|
||||
@ -27,26 +30,26 @@ let RequestActionForm = React.createClass({
|
||||
handleSuccess: React.PropTypes.func
|
||||
},
|
||||
|
||||
isPiece(){
|
||||
isPiece() {
|
||||
return this.props.pieceOrEditions.constructor !== Array;
|
||||
},
|
||||
|
||||
getUrls() {
|
||||
let urls = {};
|
||||
|
||||
if (this.props.notifications.action === 'consign'){
|
||||
if (this.props.notifications.action === 'consign') {
|
||||
urls.accept = ApiUrls.ownership_consigns_confirm;
|
||||
urls.deny = ApiUrls.ownership_consigns_deny;
|
||||
} else if (this.props.notifications.action === 'unconsign'){
|
||||
} else if (this.props.notifications.action === 'unconsign') {
|
||||
urls.accept = ApiUrls.ownership_unconsigns;
|
||||
urls.deny = ApiUrls.ownership_unconsigns_deny;
|
||||
} else if (this.props.notifications.action === 'loan' && !this.isPiece()){
|
||||
} else if (this.props.notifications.action === 'loan' && !this.isPiece()) {
|
||||
urls.accept = ApiUrls.ownership_loans_confirm;
|
||||
urls.deny = ApiUrls.ownership_loans_deny;
|
||||
} else if (this.props.notifications.action === 'loan' && this.isPiece()){
|
||||
} else if (this.props.notifications.action === 'loan' && this.isPiece()) {
|
||||
urls.accept = ApiUrls.ownership_loans_pieces_confirm;
|
||||
urls.deny = ApiUrls.ownership_loans_pieces_deny;
|
||||
} else if (this.props.notifications.action === 'loan_request' && this.isPiece()){
|
||||
} else if (this.props.notifications.action === 'loan_request' && this.isPiece()) {
|
||||
urls.accept = ApiUrls.ownership_loans_pieces_request_confirm;
|
||||
urls.deny = ApiUrls.ownership_loans_pieces_request_deny;
|
||||
}
|
||||
@ -54,37 +57,28 @@ let RequestActionForm = React.createClass({
|
||||
return urls;
|
||||
},
|
||||
|
||||
getFormData(){
|
||||
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()};
|
||||
}
|
||||
getFormData() {
|
||||
return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
|
||||
},
|
||||
|
||||
showNotification(option, action, owner) {
|
||||
return () => {
|
||||
let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner;
|
||||
|
||||
let notifications = new GlobalNotificationModel(message, 'success');
|
||||
const message = getLangText('You have successfully %s the %s request from %s', getLangText(option), getLangText(action), owner);
|
||||
const notifications = new GlobalNotificationModel(message, 'success');
|
||||
GlobalNotificationActions.appendGlobalNotification(notifications);
|
||||
|
||||
this.handleSuccess();
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
handleSuccess() {
|
||||
if (this.isPiece()){
|
||||
if (this.isPiece()) {
|
||||
NotificationActions.fetchPieceListNotifications();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
NotificationActions.fetchEditionListNotifications();
|
||||
}
|
||||
if(this.props.handleSuccess) {
|
||||
|
||||
if (typeof this.props.handleSuccess === 'function') {
|
||||
this.props.handleSuccess();
|
||||
}
|
||||
},
|
||||
@ -98,21 +92,19 @@ let RequestActionForm = React.createClass({
|
||||
},
|
||||
|
||||
getAcceptButtonForm(urls) {
|
||||
if(this.props.notifications.action === 'unconsign') {
|
||||
if (this.props.notifications.action === 'unconsign') {
|
||||
return (
|
||||
<AclButton
|
||||
<UnconsignButton
|
||||
availableAcls={{'acl_unconsign': true}}
|
||||
action="acl_unconsign"
|
||||
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
|
||||
pieceOrEditions={this.props.pieceOrEditions}
|
||||
currentUser={this.props.currentUser}
|
||||
handleSuccess={this.handleSuccess} />
|
||||
);
|
||||
} else if(this.props.notifications.action === 'loan_request') {
|
||||
} else if (this.props.notifications.action === 'loan_request') {
|
||||
return (
|
||||
<AclButton
|
||||
<LoanRequestButton
|
||||
availableAcls={{'acl_loan_request': true}}
|
||||
action="acl_loan_request"
|
||||
buttonAcceptName="LOAN"
|
||||
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
|
||||
pieceOrEditions={this.props.pieceOrEditions}
|
||||
@ -125,7 +117,7 @@ let RequestActionForm = React.createClass({
|
||||
url={urls.accept}
|
||||
getFormData={this.getFormData}
|
||||
handleSuccess={
|
||||
this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by)
|
||||
this.showNotification('accepted', this.props.notifications.action, this.props.notifications.by)
|
||||
}
|
||||
isInline={true}
|
||||
className='inline pull-right'>
|
||||
@ -140,8 +132,8 @@ let RequestActionForm = React.createClass({
|
||||
},
|
||||
|
||||
getButtonForm() {
|
||||
let urls = this.getUrls();
|
||||
let acceptButtonForm = this.getAcceptButtonForm(urls);
|
||||
const urls = this.getUrls();
|
||||
const acceptButtonForm = this.getAcceptButtonForm(urls);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -150,13 +142,13 @@ let RequestActionForm = React.createClass({
|
||||
isInline={true}
|
||||
getFormData={this.getFormData}
|
||||
handleSuccess={
|
||||
this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by)
|
||||
this.showNotification('denied', this.props.notifications.action, this.props.notifications.by)
|
||||
}
|
||||
className='inline pull-right'>
|
||||
<button
|
||||
type="submit"
|
||||
className='btn btn-danger btn-delete btn-sm ascribe-margin-1px'>
|
||||
{getLangText('REJECT')}
|
||||
{getLangText('REJECT')}
|
||||
</button>
|
||||
</Form>
|
||||
{acceptButtonForm}
|
||||
@ -168,10 +160,10 @@ let RequestActionForm = React.createClass({
|
||||
return (
|
||||
<ActionPanel
|
||||
content={this.getContent()}
|
||||
buttons={this.getButtonForm()}/>
|
||||
buttons={this.getButtonForm()} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default RequestActionForm;
|
||||
export default RequestActionForm;
|
||||
|
@ -3,64 +3,80 @@
|
||||
import React from 'react';
|
||||
|
||||
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
|
||||
import FileDragAndDrop from '../ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop';
|
||||
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
|
||||
let InputFineUploader = React.createClass({
|
||||
|
||||
const { func, bool, object, shape, string, number, arrayOf } = React.PropTypes;
|
||||
|
||||
const InputFineUploader = React.createClass({
|
||||
propTypes: {
|
||||
setIsUploadReady: React.PropTypes.func,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
submitFileName: React.PropTypes.func,
|
||||
setIsUploadReady: func,
|
||||
isReadyForFormSubmission: func,
|
||||
submitFileName: func,
|
||||
fileInputElement: func,
|
||||
|
||||
areAssetsDownloadable: React.PropTypes.bool,
|
||||
areAssetsDownloadable: bool,
|
||||
|
||||
onClick: React.PropTypes.func,
|
||||
keyRoutine: React.PropTypes.shape({
|
||||
url: React.PropTypes.string,
|
||||
fileClass: React.PropTypes.string
|
||||
keyRoutine: shape({
|
||||
url: string,
|
||||
fileClass: string
|
||||
}),
|
||||
createBlobRoutine: React.PropTypes.shape({
|
||||
url: React.PropTypes.string
|
||||
createBlobRoutine: shape({
|
||||
url: string
|
||||
}),
|
||||
validation: React.PropTypes.shape({
|
||||
itemLimit: React.PropTypes.number,
|
||||
sizeLimit: React.PropTypes.string,
|
||||
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
|
||||
validation: shape({
|
||||
itemLimit: number,
|
||||
sizeLimit: string,
|
||||
allowedExtensions: arrayOf(string)
|
||||
}),
|
||||
|
||||
// isFineUploaderActive 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
|
||||
isFineUploaderActive: React.PropTypes.bool,
|
||||
onLoggedOut: React.PropTypes.func,
|
||||
isFineUploaderActive: bool,
|
||||
onLoggedOut: func,
|
||||
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
enableLocalHashing: bool,
|
||||
uploadMethod: string,
|
||||
|
||||
// provided by Property
|
||||
disabled: React.PropTypes.bool,
|
||||
disabled: bool,
|
||||
|
||||
// A class of a file the user has to upload
|
||||
// Needs to be defined both in singular as well as in plural
|
||||
fileClassToUpload: React.PropTypes.shape({
|
||||
singular: React.PropTypes.string,
|
||||
plural: React.PropTypes.string
|
||||
}),
|
||||
location: React.PropTypes.object
|
||||
fileClassToUpload: shape({
|
||||
singular: string,
|
||||
plural: string
|
||||
})
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
fileInputElement: FileDragAndDrop
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
value: null
|
||||
value: null,
|
||||
file: null
|
||||
};
|
||||
},
|
||||
|
||||
submitFile(file){
|
||||
submitFile(file) {
|
||||
this.setState({
|
||||
file,
|
||||
value: file.key
|
||||
});
|
||||
|
||||
if(this.state.value && typeof this.props.onChange === 'function') {
|
||||
this.props.onChange({ target: { value: this.state.value } });
|
||||
}
|
||||
|
||||
if(typeof this.props.submitFileName === 'function') {
|
||||
this.props.submitFileName(file.originalName);
|
||||
}
|
||||
@ -70,7 +86,25 @@ let InputFineUploader = React.createClass({
|
||||
this.refs.fineuploader.reset();
|
||||
},
|
||||
|
||||
createBlobRoutine() {
|
||||
const { fineuploader } = this.refs;
|
||||
const { file } = this.state;
|
||||
|
||||
fineuploader.createBlob(file);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { fileInputElement,
|
||||
keyRoutine,
|
||||
createBlobRoutine,
|
||||
validation,
|
||||
setIsUploadReady,
|
||||
isReadyForFormSubmission,
|
||||
areAssetsDownloadable,
|
||||
onLoggedOut,
|
||||
enableLocalHashing,
|
||||
fileClassToUpload,
|
||||
location } = this.props;
|
||||
let editable = this.props.isFineUploaderActive;
|
||||
|
||||
// if disabled is actually set by property, we want to override
|
||||
@ -82,14 +116,14 @@ let InputFineUploader = React.createClass({
|
||||
return (
|
||||
<ReactS3FineUploader
|
||||
ref="fineuploader"
|
||||
onClick={this.props.onClick}
|
||||
keyRoutine={this.props.keyRoutine}
|
||||
createBlobRoutine={this.props.createBlobRoutine}
|
||||
validation={this.props.validation}
|
||||
fileInputElement={fileInputElement}
|
||||
keyRoutine={keyRoutine}
|
||||
createBlobRoutine={createBlobRoutine}
|
||||
validation={validation}
|
||||
submitFile={this.submitFile}
|
||||
setIsUploadReady={this.props.setIsUploadReady}
|
||||
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
|
||||
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
||||
setIsUploadReady={setIsUploadReady}
|
||||
isReadyForFormSubmission={isReadyForFormSubmission}
|
||||
areAssetsDownloadable={areAssetsDownloadable}
|
||||
areAssetsEditable={editable}
|
||||
signature={{
|
||||
endpoint: AppConstants.serverUrl + 's3/signature/',
|
||||
@ -107,10 +141,10 @@ let InputFineUploader = React.createClass({
|
||||
}}
|
||||
onInactive={this.props.onLoggedOut}
|
||||
enableLocalHashing={this.props.enableLocalHashing}
|
||||
fileClassToUpload={this.props.fileClassToUpload}
|
||||
location={this.props.location}/>
|
||||
uploadMethod={this.props.uploadMethod}
|
||||
fileClassToUpload={this.props.fileClassToUpload} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default InputFineUploader;
|
||||
export default InputFineUploader;
|
||||
|
@ -181,9 +181,7 @@ let Property = React.createClass({
|
||||
|
||||
setErrors(errors){
|
||||
this.setState({
|
||||
errors: errors.map((error) => {
|
||||
return <span className="pull-right" key={error}>{error}</span>;
|
||||
})
|
||||
errors: errors.pop()
|
||||
});
|
||||
},
|
||||
|
||||
@ -255,8 +253,10 @@ let Property = React.createClass({
|
||||
placement="top"
|
||||
overlay={tooltip}>
|
||||
<div className={'ascribe-property ' + this.props.className}>
|
||||
{this.state.errors}
|
||||
<span>{this.props.label}</span>
|
||||
<p>
|
||||
<span className="pull-left">{this.props.label}</span>
|
||||
<span className="pull-right">{this.state.errors}</span>
|
||||
</p>
|
||||
{this.renderChildren(style)}
|
||||
{footer}
|
||||
</div>
|
||||
|
@ -51,23 +51,33 @@ let Other = React.createClass({
|
||||
|
||||
let Image = React.createClass({
|
||||
propTypes: {
|
||||
url: React.PropTypes.string.isRequired,
|
||||
url: React.PropTypes.string,
|
||||
preview: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl)
|
||||
.then(() =>
|
||||
Q.all([
|
||||
InjectInHeadUtils.inject(AppConstants.shmui.cssUrl),
|
||||
InjectInHeadUtils.inject(AppConstants.shmui.sdkUrl)
|
||||
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
|
||||
if(this.props.url) {
|
||||
InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl)
|
||||
.then(() =>
|
||||
Q.all([
|
||||
InjectInHeadUtils.inject(AppConstants.shmui.cssUrl),
|
||||
InjectInHeadUtils.inject(AppConstants.shmui.sdkUrl)
|
||||
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<img className="shmui-ascribe" src={this.props.preview} data-large-src={this.props.url}/>
|
||||
);
|
||||
const { url, preview } = this.props;
|
||||
|
||||
if(url) {
|
||||
return (
|
||||
<img className="shmui-ascribe" src={preview} data-large-src={url}/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img src={preview}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -138,6 +148,10 @@ let Video = React.createClass({
|
||||
.fail(() => this.setState({libraryLoaded: false}));
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.videoMounted === false;
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.libraryLoaded && !this.state.videoMounted) {
|
||||
window.videojs('#mainvideo');
|
||||
@ -163,10 +177,6 @@ let Video = React.createClass({
|
||||
return html.join('\n');
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.videoMounted === false;
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.state.libraryLoaded !== null) {
|
||||
return (
|
||||
@ -197,26 +207,50 @@ let MediaPlayer = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.props.mimetype === 'video' && this.props.encodingStatus !== undefined && this.props.encodingStatus !== 100) {
|
||||
const { mimetype,
|
||||
preview,
|
||||
url,
|
||||
extraData,
|
||||
encodingStatus } = this.props;
|
||||
|
||||
if (mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100) {
|
||||
return (
|
||||
<div className="ascribe-detail-header ascribe-media-player">
|
||||
<p>
|
||||
<em>We successfully received your video and it is now being encoded.
|
||||
<br />You can leave this page and check back on the status later.</em>
|
||||
</p>
|
||||
<ProgressBar now={this.props.encodingStatus}
|
||||
<ProgressBar now={encodingStatus}
|
||||
label="%(percent)s%"
|
||||
className="ascribe-progress-bar" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let Component = resourceMap[this.props.mimetype] || Other;
|
||||
let Component = resourceMap[mimetype] || Other;
|
||||
let componentProps = {
|
||||
preview,
|
||||
url,
|
||||
extraData,
|
||||
encodingStatus
|
||||
};
|
||||
|
||||
// Since the launch of the portfolio whitelabel submission,
|
||||
// we allow the user to specify a thumbnail upon piece-registration.
|
||||
// As the `Component` is chosen according to its filetype but could potentially
|
||||
// have a manually submitted thumbnail, we match if the to `Mediaplayer` submitted thumbnail
|
||||
// is not the generally used fallback `url` (ascribe_spiral.png).
|
||||
//
|
||||
// If this is the case, we disable shmui by deleting the original `url` prop and replace
|
||||
// the assigned component to `Image`.
|
||||
if(!decodeURIComponent(preview).match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/) &&
|
||||
Component === Other) {
|
||||
Component = resourceMap.image;
|
||||
delete componentProps.url;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ascribe-media-player">
|
||||
<Component preview={this.props.preview}
|
||||
url={this.props.url}
|
||||
extraData={this.props.extraData}
|
||||
encodingStatus={this.props.encodingStatus} />
|
||||
<Component {...componentProps}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ let PieceListBulkModal = React.createClass({
|
||||
<div className="row-fluid">
|
||||
<AclButtonList
|
||||
availableAcls={availableAcls}
|
||||
editions={selectedEditions}
|
||||
pieceOrEditions={selectedEditions}
|
||||
handleSuccess={this.handleSuccess}
|
||||
className="text-center ascribe-button-list collapse-group">
|
||||
<DeleteButton
|
||||
@ -136,4 +136,4 @@ let PieceListBulkModal = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default PieceListBulkModal;
|
||||
export default PieceListBulkModal;
|
||||
|
@ -31,6 +31,8 @@ export default function AuthProxyHandler({to, when}) {
|
||||
|
||||
return (Component) => {
|
||||
return React.createClass({
|
||||
displayName: 'AuthProxyHandler',
|
||||
|
||||
propTypes: {
|
||||
location: object
|
||||
},
|
||||
|
@ -20,8 +20,7 @@ import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let ContractSettingsUpdateButton = React.createClass({
|
||||
propTypes: {
|
||||
contract: React.PropTypes.object,
|
||||
location: React.PropTypes.object
|
||||
contract: React.PropTypes.object
|
||||
},
|
||||
|
||||
submitFile(file) {
|
||||
@ -56,7 +55,6 @@ let ContractSettingsUpdateButton = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<ReactS3FineUploader
|
||||
ref="fineuploader"
|
||||
fileInputElement={UploadButton}
|
||||
keyRoutine={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
@ -90,10 +88,9 @@ let ContractSettingsUpdateButton = React.createClass({
|
||||
plural: getLangText('UPDATE')
|
||||
}}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
submitFile={this.submitFile}
|
||||
location={this.props.location}/>
|
||||
submitFile={this.submitFile} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ContractSettingsUpdateButton;
|
||||
export default ContractSettingsUpdateButton;
|
||||
|
@ -27,6 +27,7 @@ let FileDragAndDrop = React.createClass({
|
||||
areAssetsEditable: React.PropTypes.bool,
|
||||
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
uploadMethod: React.PropTypes.string,
|
||||
|
||||
// triggers a FileDragAndDrop-global spinner
|
||||
hashingProgress: React.PropTypes.number,
|
||||
@ -41,8 +42,7 @@ let FileDragAndDrop = React.createClass({
|
||||
plural: React.PropTypes.string
|
||||
}),
|
||||
|
||||
allowedExtensions: React.PropTypes.string,
|
||||
location: React.PropTypes.object
|
||||
allowedExtensions: React.PropTypes.string
|
||||
},
|
||||
|
||||
handleDragOver(event) {
|
||||
@ -137,19 +137,19 @@ let FileDragAndDrop = React.createClass({
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let { filesToUpload,
|
||||
dropzoneInactive,
|
||||
className,
|
||||
hashingProgress,
|
||||
handleCancelHashing,
|
||||
multiple,
|
||||
enableLocalHashing,
|
||||
fileClassToUpload,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
allowedExtensions,
|
||||
location
|
||||
} = this.props;
|
||||
const {
|
||||
filesToUpload,
|
||||
dropzoneInactive,
|
||||
className,
|
||||
hashingProgress,
|
||||
handleCancelHashing,
|
||||
multiple,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
allowedExtensions } = this.props;
|
||||
|
||||
// has files only is true if there are files that do not have the status deleted or canceled
|
||||
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
|
||||
@ -185,8 +185,8 @@ let FileDragAndDrop = React.createClass({
|
||||
hasFiles={hasFiles}
|
||||
onClick={this.handleOnClick}
|
||||
enableLocalHashing={enableLocalHashing}
|
||||
fileClassToUpload={fileClassToUpload}
|
||||
location={location}/>
|
||||
uploadMethod={uploadMethod}
|
||||
fileClassToUpload={fileClassToUpload} />
|
||||
<FileDragAndDropPreviewIterator
|
||||
files={filesToUpload}
|
||||
handleDeleteFile={this.handleDeleteFile}
|
||||
|
@ -3,30 +3,28 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
|
||||
|
||||
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
import { getCurrentQueryParams } from '../../../utils/url_utils';
|
||||
|
||||
let FileDragAndDropDialog = React.createClass({
|
||||
propTypes: {
|
||||
hasFiles: React.PropTypes.bool,
|
||||
multipleFiles: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func,
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
uploadMethod: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func,
|
||||
|
||||
// A class of a file the user has to upload
|
||||
// Needs to be defined both in singular as well as in plural
|
||||
fileClassToUpload: React.PropTypes.shape({
|
||||
singular: React.PropTypes.string,
|
||||
plural: React.PropTypes.string
|
||||
}),
|
||||
|
||||
location: React.PropTypes.object
|
||||
})
|
||||
},
|
||||
|
||||
getDragDialog(fileClass) {
|
||||
if(dragAndDropAvailable) {
|
||||
if (dragAndDropAvailable) {
|
||||
return [
|
||||
<p>{getLangText('Drag %s here', fileClass)}</p>,
|
||||
<p>{getLangText('or')}</p>
|
||||
@ -37,26 +35,31 @@ let FileDragAndDropDialog = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
const queryParams = this.props.location.query;
|
||||
const {
|
||||
hasFiles,
|
||||
multipleFiles,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
onClick } = this.props;
|
||||
|
||||
if(this.props.hasFiles) {
|
||||
if (hasFiles) {
|
||||
return null;
|
||||
} else {
|
||||
if(this.props.enableLocalHashing && !queryParams.method) {
|
||||
if (enableLocalHashing && !uploadMethod) {
|
||||
const currentQueryParams = getCurrentQueryParams();
|
||||
|
||||
let queryParamsHash = Object.assign({}, queryParams);
|
||||
const queryParamsHash = Object.assign({}, currentQueryParams);
|
||||
queryParamsHash.method = 'hash';
|
||||
|
||||
let queryParamsUpload = Object.assign({}, queryParams);
|
||||
const queryParamsUpload = Object.assign({}, currentQueryParams);
|
||||
queryParamsUpload.method = 'upload';
|
||||
|
||||
let { location } = this.props;
|
||||
|
||||
return (
|
||||
<div className="file-drag-and-drop-dialog present-options">
|
||||
<p>{getLangText('Would you rather')}</p>
|
||||
<Link
|
||||
to={location.pathname}
|
||||
to={window.location.pathname}
|
||||
query={queryParamsHash}>
|
||||
<span className="btn btn-default btn-sm">
|
||||
{getLangText('Hash your work')}
|
||||
@ -64,9 +67,9 @@ let FileDragAndDropDialog = React.createClass({
|
||||
</Link>
|
||||
|
||||
<span> or </span>
|
||||
|
||||
|
||||
<Link
|
||||
to={location.pathname}
|
||||
to={window.location.pathname}
|
||||
query={queryParamsUpload}>
|
||||
<span className="btn btn-default btn-sm">
|
||||
{getLangText('Upload and hash your work')}
|
||||
@ -75,26 +78,27 @@ let FileDragAndDropDialog = React.createClass({
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if(this.props.multipleFiles) {
|
||||
if (multipleFiles) {
|
||||
return (
|
||||
<span className="file-drag-and-drop-dialog">
|
||||
{this.getDragDialog(this.props.fileClassToUpload.plural)}
|
||||
{this.getDragDialog(fileClassToUpload.plural)}
|
||||
<span
|
||||
className="btn btn-default"
|
||||
onClick={this.props.onClick}>
|
||||
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)}
|
||||
onClick={onClick}>
|
||||
{getLangText('choose %s to upload', fileClassToUpload.plural)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular);
|
||||
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(this.props.fileClassToUpload.singular)}
|
||||
{this.getDragDialog(fileClassToUpload.singular)}
|
||||
<span
|
||||
className="btn btn-default"
|
||||
onClick={this.props.onClick}>
|
||||
onClick={onClick}>
|
||||
{dialog}
|
||||
</span>
|
||||
</span>
|
||||
@ -105,4 +109,4 @@ let FileDragAndDropDialog = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default FileDragAndDropDialog;
|
||||
export default FileDragAndDropDialog;
|
||||
|
@ -2,24 +2,30 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
|
||||
|
||||
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
||||
|
||||
const { func, array, bool, shape, string } = React.PropTypes;
|
||||
|
||||
let UploadButton = React.createClass({
|
||||
propTypes: {
|
||||
onDrop: React.PropTypes.func.isRequired,
|
||||
filesToUpload: React.PropTypes.array,
|
||||
multiple: React.PropTypes.bool,
|
||||
onDrop: func.isRequired,
|
||||
filesToUpload: array,
|
||||
multiple: bool,
|
||||
|
||||
// For simplification purposes we're just going to use this prop as a
|
||||
// label for the upload button
|
||||
fileClassToUpload: React.PropTypes.shape({
|
||||
singular: React.PropTypes.string,
|
||||
plural: React.PropTypes.string
|
||||
fileClassToUpload: shape({
|
||||
singular: string,
|
||||
plural: string
|
||||
}),
|
||||
|
||||
allowedExtensions: React.PropTypes.string
|
||||
allowedExtensions: string,
|
||||
|
||||
handleCancelFile: func // provided by ReactS3FineUploader
|
||||
},
|
||||
|
||||
handleDrop(event) {
|
||||
@ -37,11 +43,20 @@ let UploadButton = React.createClass({
|
||||
return this.props.filesToUpload.filter((file) => file.status === 'uploading');
|
||||
},
|
||||
|
||||
handleOnClick() {
|
||||
let uploadingFiles = this.getUploadingFiles();
|
||||
getUploadedFile() {
|
||||
return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0];
|
||||
},
|
||||
|
||||
// We only want the button to be clickable if there are no files currently uploading
|
||||
handleOnClick() {
|
||||
const uploadingFiles = this.getUploadingFiles();
|
||||
const uploadedFile = this.getUploadedFile();
|
||||
|
||||
if(uploadedFile) {
|
||||
this.props.handleCancelFile(uploadedFile.id);
|
||||
}
|
||||
if(uploadingFiles.length === 0) {
|
||||
// We only want the button to be clickable if there are no files currently uploading
|
||||
|
||||
// Firefox only recognizes the simulated mouse click if bubbles is set to true,
|
||||
// but since Google Chrome propagates the event much further than needed, we
|
||||
// need to stop propagation as soon as the event is created
|
||||
@ -62,40 +77,61 @@ let UploadButton = React.createClass({
|
||||
// filter invalid files that might have been deleted or canceled...
|
||||
filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter);
|
||||
|
||||
// Depending on wether there is an upload going on or not we
|
||||
// display the progress
|
||||
if(filesToUpload.length > 0) {
|
||||
if(this.getUploadingFiles().length !== 0) {
|
||||
return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%';
|
||||
} else {
|
||||
return fileClassToUpload.singular;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
multiple,
|
||||
fileClassToUpload,
|
||||
allowedExtensions
|
||||
} = this.props;
|
||||
getUploadedFileLabel() {
|
||||
const uploadedFile = this.getUploadedFile();
|
||||
|
||||
if(uploadedFile) {
|
||||
return (
|
||||
<span>
|
||||
<Glyphicon glyph="ok" />
|
||||
{' ' + truncateTextAtCharIndex(uploadedFile.name, 40)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span>{getLangText('No file chosen')}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let { multiple,
|
||||
allowedExtensions } = this.props;
|
||||
|
||||
/*
|
||||
* We do not want a button that submits here.
|
||||
* As UploadButton could be used in forms that want to be submitted independent
|
||||
* of clicking the selector.
|
||||
* Therefore the wrapping component needs to be an `anchor` tag instead of a `button`
|
||||
*/
|
||||
return (
|
||||
<button
|
||||
onClick={this.handleOnClick}
|
||||
className="btn btn-default btn-sm margin-left-2px"
|
||||
disabled={this.getUploadingFiles().length !== 0}>
|
||||
{this.getButtonLabel()}
|
||||
<input
|
||||
multiple={multiple}
|
||||
ref="fileinput"
|
||||
type="file"
|
||||
style={{
|
||||
display: 'none',
|
||||
height: 0,
|
||||
width: 0
|
||||
}}
|
||||
onChange={this.handleDrop}
|
||||
accept={allowedExtensions}/>
|
||||
</button>
|
||||
<div className="upload-button-wrapper">
|
||||
<a
|
||||
onClick={this.handleOnClick}
|
||||
className="btn btn-default btn-sm margin-left-2px"
|
||||
disabled={this.getUploadingFiles().length !== 0}>
|
||||
{this.getButtonLabel()}
|
||||
<input
|
||||
multiple={multiple}
|
||||
ref="fileinput"
|
||||
type="file"
|
||||
style={{
|
||||
display: 'none',
|
||||
height: 0,
|
||||
width: 0
|
||||
}}
|
||||
onChange={this.handleDrop}
|
||||
accept={allowedExtensions}/>
|
||||
</a>
|
||||
{this.getUploadedFileLabel()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -18,7 +18,6 @@ import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp }
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
|
||||
let ReactS3FineUploader = React.createClass({
|
||||
propTypes: {
|
||||
keyRoutine: React.PropTypes.shape({
|
||||
@ -107,11 +106,14 @@ let ReactS3FineUploader = React.createClass({
|
||||
// One solution we found in the process of tackling this problem was to hash
|
||||
// the file in the browser using md5 and then uploading the resulting text document instead
|
||||
// of the actual file.
|
||||
// This boolean essentially enables that behavior
|
||||
//
|
||||
// This boolean and string essentially enable that behavior.
|
||||
// Right now, we determine which upload method to use by appending a query parameter,
|
||||
// which should be passed into 'uploadMethod':
|
||||
// 'hash': upload using the hash
|
||||
// 'upload': upload full file (default if not specified)
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
|
||||
// automatically injected by React-Router
|
||||
query: React.PropTypes.object,
|
||||
uploadMethod: React.PropTypes.oneOf(['hash', 'upload']),
|
||||
|
||||
// A class of a file the user has to upload
|
||||
// Needs to be defined both in singular as well as in plural
|
||||
@ -126,9 +128,7 @@ let ReactS3FineUploader = React.createClass({
|
||||
fileInputElement: React.PropTypes.oneOfType([
|
||||
React.PropTypes.func,
|
||||
React.PropTypes.element
|
||||
]),
|
||||
|
||||
location: React.PropTypes.object
|
||||
])
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -192,11 +192,11 @@ let ReactS3FineUploader = React.createClass({
|
||||
filesToUpload: [],
|
||||
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
|
||||
csrfToken: getCookie(AppConstants.csrftoken),
|
||||
|
||||
|
||||
// -1: aborted
|
||||
// -2: uninitialized
|
||||
hashingProgress: -2,
|
||||
|
||||
|
||||
// this is for logging
|
||||
chunks: {}
|
||||
};
|
||||
@ -298,18 +298,27 @@ let ReactS3FineUploader = React.createClass({
|
||||
resolve(res.key);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.logGlobal(err, false, {
|
||||
files: this.state.filesToUpload,
|
||||
chunks: this.state.chunks
|
||||
});
|
||||
this.onErrorPromiseProxy(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
createBlob(file) {
|
||||
const { createBlobRoutine } = this.props;
|
||||
|
||||
return Q.Promise((resolve, reject) => {
|
||||
window.fetch(this.props.createBlobRoutine.url, {
|
||||
|
||||
// if createBlobRoutine is not defined,
|
||||
// we're progressing right away without posting to S3
|
||||
// so that this can be done manually by the form
|
||||
if(!createBlobRoutine) {
|
||||
// still we warn the user of this component
|
||||
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
|
||||
resolve();
|
||||
}
|
||||
|
||||
window.fetch(createBlobRoutine.url, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@ -320,7 +329,7 @@ let ReactS3FineUploader = React.createClass({
|
||||
body: JSON.stringify({
|
||||
'filename': file.name,
|
||||
'key': file.key,
|
||||
'piece_id': this.props.createBlobRoutine.pieceId
|
||||
'piece_id': createBlobRoutine.pieceId
|
||||
})
|
||||
})
|
||||
.then((res) => {
|
||||
@ -336,16 +345,16 @@ let ReactS3FineUploader = React.createClass({
|
||||
} else if(res.contractblob) {
|
||||
file.s3Url = res.contractblob.url_safe;
|
||||
file.s3UrlSafe = res.contractblob.url_safe;
|
||||
} else if(res.thumbnail) {
|
||||
file.s3Url = res.thumbnail.url_safe;
|
||||
file.s3UrlSafe = res.thumbnail.url_safe;
|
||||
} else {
|
||||
throw new Error(getLangText('Could not find a url to download.'));
|
||||
}
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.logGlobal(err, false, {
|
||||
files: this.state.filesToUpload,
|
||||
chunks: this.state.chunks
|
||||
});
|
||||
this.onErrorPromiseProxy(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@ -354,7 +363,6 @@ let ReactS3FineUploader = React.createClass({
|
||||
/* FineUploader specific callback function handlers */
|
||||
|
||||
onUploadChunk(id, name, chunkData) {
|
||||
|
||||
let chunks = this.state.chunks;
|
||||
|
||||
chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = {
|
||||
@ -370,10 +378,9 @@ let ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
|
||||
|
||||
let chunks = this.state.chunks;
|
||||
let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte;
|
||||
|
||||
|
||||
if(chunks[chunkKey]) {
|
||||
chunks[chunkKey].completed = true;
|
||||
chunks[chunkKey].responseJson = responseJson;
|
||||
@ -412,9 +419,9 @@ let ReactS3FineUploader = React.createClass({
|
||||
if(this.props.submitFile) {
|
||||
this.props.submitFile(files[id]);
|
||||
} else {
|
||||
console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader');
|
||||
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,
|
||||
@ -429,22 +436,27 @@ let ReactS3FineUploader = React.createClass({
|
||||
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.logGlobal(err, false, {
|
||||
files: this.state.filesToUpload,
|
||||
chunks: this.state.chunks
|
||||
});
|
||||
let notification = new GlobalNotificationModel(err.message, 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
});
|
||||
.catch(this.onErrorPromiseProxy);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* We want to channel all errors in this component through one single method.
|
||||
* As fineuploader's `onError` method cannot handle the callback parameters of
|
||||
* a promise we define this proxy method to crunch them into the correct form.
|
||||
*
|
||||
* @param {error} err a plain Javascript error
|
||||
*/
|
||||
onErrorPromiseProxy(err) {
|
||||
this.onError(null, null, err.message);
|
||||
},
|
||||
|
||||
onError(id, name, errorReason) {
|
||||
console.logGlobal(errorReason, false, {
|
||||
files: this.state.filesToUpload,
|
||||
chunks: this.state.chunks
|
||||
});
|
||||
this.props.setIsUploadReady(true);
|
||||
this.state.uploader.cancelAll();
|
||||
|
||||
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
|
||||
@ -597,7 +609,6 @@ let ReactS3FineUploader = React.createClass({
|
||||
} else {
|
||||
throw new Error(getLangText('File upload could not be paused.'));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
handleResumeFile(fileId) {
|
||||
@ -609,6 +620,10 @@ let ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
handleUploadFile(files) {
|
||||
// While files are being uploaded, the form cannot be ready
|
||||
// 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) {
|
||||
@ -647,16 +662,14 @@ let ReactS3FineUploader = React.createClass({
|
||||
// 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 query parameter is present in the url ('hash' and not 'upload')
|
||||
let queryParams = this.props.location.query;
|
||||
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') {
|
||||
|
||||
let convertedFilePromises = [];
|
||||
// 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;
|
||||
@ -668,7 +681,6 @@ let ReactS3FineUploader = React.createClass({
|
||||
// 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
|
||||
@ -676,7 +688,6 @@ let ReactS3FineUploader = React.createClass({
|
||||
// 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.
|
||||
@ -696,18 +707,14 @@ let ReactS3FineUploader = React.createClass({
|
||||
// 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});
|
||||
|
||||
@ -823,20 +830,18 @@ let ReactS3FineUploader = React.createClass({
|
||||
changeSet.status = { $set: status };
|
||||
|
||||
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
|
||||
|
||||
|
||||
this.setState({ filesToUpload });
|
||||
},
|
||||
|
||||
isDropzoneInactive() {
|
||||
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
|
||||
let queryParams = this.props.location.query;
|
||||
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
|
||||
|
||||
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
|
||||
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
getAllowedExtensions() {
|
||||
@ -850,17 +855,16 @@ let ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
let {
|
||||
multiple,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
onInactive,
|
||||
enableLocalHashing,
|
||||
fileClassToUpload,
|
||||
validation,
|
||||
fileInputElement,
|
||||
location
|
||||
} = this.props;
|
||||
const {
|
||||
multiple,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
onInactive,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
validation,
|
||||
fileInputElement } = this.props;
|
||||
|
||||
// Here we initialize the template that has been either provided from the outside
|
||||
// or the default input that is FileDragAndDrop.
|
||||
@ -870,8 +874,8 @@ let ReactS3FineUploader = React.createClass({
|
||||
areAssetsEditable,
|
||||
onInactive,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
location,
|
||||
onDrop: this.handleUploadFile,
|
||||
filesToUpload: this.state.filesToUpload,
|
||||
handleDeleteFile: this.handleDeleteFile,
|
||||
|
File diff suppressed because one or more lines are too long
@ -40,12 +40,6 @@ let RegisterPiece = React.createClass( {
|
||||
|
||||
mixins: [History],
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
canSpecifyEditions: true
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState(){
|
||||
return mergeOptions(
|
||||
UserStore.getState(),
|
||||
|
@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import Q from 'q';
|
||||
|
||||
import PrizeFetcher from '../fetchers/prize_fetcher';
|
||||
|
||||
class PrizeActions {
|
||||
constructor() {
|
||||
this.generateActions(
|
||||
'updatePrize'
|
||||
);
|
||||
}
|
||||
|
||||
fetchPrize() {
|
||||
return Q.Promise((resolve, reject) => {
|
||||
PrizeFetcher
|
||||
.fetch()
|
||||
.then((res) => {
|
||||
this.actions.updatePrize({
|
||||
prize: res.prize
|
||||
});
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.logGlobal(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(PrizeActions);
|
@ -1,91 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import PrizeActions from '../actions/prize_actions';
|
||||
import PrizeStore from '../stores/prize_store';
|
||||
|
||||
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';
|
||||
import { setDocumentTitle } from '../../../../utils/dom_utils';
|
||||
|
||||
|
||||
let PrizeRegisterPiece = React.createClass({
|
||||
getInitialState() {
|
||||
return PrizeStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
PrizeStore.listen(this.onChange);
|
||||
PrizeActions.fetchPrize();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
PrizeStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
render() {
|
||||
setDocumentTitle(getLangText('Submit to the prize'));
|
||||
|
||||
if(this.state.prize && this.state.prize.active){
|
||||
return (
|
||||
<RegisterPiece
|
||||
enableLocalHashing={false}
|
||||
headerMessage={getLangText('Submit to the prize')}
|
||||
submitMessage={getLangText('Submit')}>
|
||||
<Property
|
||||
name='artist_statement'
|
||||
label={getLangText('Artist statement')}
|
||||
editable={true}
|
||||
overrideForm={true}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter your statement')}
|
||||
required />
|
||||
</Property>
|
||||
<Property
|
||||
name='work_description'
|
||||
label={getLangText('Work description')}
|
||||
editable={true}
|
||||
overrideForm={true}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter the description for your work')}
|
||||
required />
|
||||
</Property>
|
||||
<Property
|
||||
name="terms"
|
||||
className="ascribe-property-collapsible-toggle"
|
||||
style={{paddingBottom: 0}}>
|
||||
<InputCheckbox>
|
||||
<span>
|
||||
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '}
|
||||
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
|
||||
{getLangText('read')}
|
||||
</a>)
|
||||
</span>
|
||||
</InputCheckbox>
|
||||
</Property>
|
||||
</RegisterPiece>);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className='row'>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
{getLangText('The prize is no longer active')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default PrizeRegisterPiece;
|
@ -2,6 +2,7 @@
|
||||
|
||||
import AppPrizeConstants from './prize_application_constants';
|
||||
|
||||
|
||||
function getPrizeApiUrls(subdomain) {
|
||||
return {
|
||||
'users_login': AppPrizeConstants.prizeApiEndpoint + subdomain + '/users/login/',
|
||||
@ -18,10 +19,9 @@ function getPrizeApiUrls(subdomain) {
|
||||
'ratings': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/',
|
||||
'rating': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/',
|
||||
'rating_average': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/average/',
|
||||
'select_piece' : AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/select/',
|
||||
'select_piece': AppPrizeConstants.prizeApiEndpoint + subdomain + '/ratings/${piece_id}/select/',
|
||||
'notes': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/',
|
||||
'note': AppPrizeConstants.prizeApiEndpoint + subdomain + '/notes/${piece_id}/'
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,375 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { History } from 'react-router';
|
||||
|
||||
import Form from '../../../../../ascribe_forms/form';
|
||||
import Property from '../../../../../ascribe_forms/property';
|
||||
import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable';
|
||||
|
||||
import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
import InputFineuploader from '../../../../../ascribe_forms/input_fineuploader';
|
||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
||||
|
||||
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
|
||||
|
||||
import AppConstants from '../../../../../../constants/application_constants';
|
||||
import ApiUrls from '../../../../../../constants/api_urls';
|
||||
|
||||
import requests from '../../../../../../utils/requests';
|
||||
|
||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||
import { setCookie } from '../../../../../../utils/fetch_api_utils';
|
||||
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||
|
||||
|
||||
const { object } = React.PropTypes;
|
||||
|
||||
const PRRegisterPieceForm = React.createClass({
|
||||
propTypes: {
|
||||
location: object,
|
||||
history: object,
|
||||
currentUser: object
|
||||
},
|
||||
|
||||
mixins: [History],
|
||||
|
||||
getInitialState(){
|
||||
return {
|
||||
digitalWorkKeyReady: true,
|
||||
thumbnailKeyReady: true,
|
||||
|
||||
// we set this to true, as it is not required
|
||||
supportingMaterialsReady: true,
|
||||
proofOfPaymentReady: true,
|
||||
piece: null,
|
||||
submitted: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* In this method, we're composing all fields on the page
|
||||
* in two steps, first submitting the registration of the piece and
|
||||
* second adding all the additional details
|
||||
*/
|
||||
submit() {
|
||||
if(!this.validateForms()) {
|
||||
return;
|
||||
} else {
|
||||
// disable the submission button right after the user
|
||||
// clicks on it to avoid double submission
|
||||
this.setState({
|
||||
submitted: true
|
||||
});
|
||||
}
|
||||
|
||||
const { currentUser } = this.props;
|
||||
const { registerPieceForm,
|
||||
additionalDataForm,
|
||||
uploadersForm } = this.refs;
|
||||
const { digitalWorkKey,
|
||||
thumbnailKey,
|
||||
supportingMaterials,
|
||||
proofOfPayment } = uploadersForm.refs;
|
||||
const additionalDataFormData = additionalDataForm.getFormData();
|
||||
|
||||
// composing data for piece registration
|
||||
let registerPieceFormData = registerPieceForm.getFormData();
|
||||
registerPieceFormData.digital_work_key = digitalWorkKey.state.value;
|
||||
registerPieceFormData.thumbnail_file = thumbnailKey.state.value;
|
||||
registerPieceFormData.terms = true;
|
||||
|
||||
// submitting the piece
|
||||
requests
|
||||
.post(ApiUrls.pieces_list, { body: registerPieceFormData })
|
||||
.then(({ success, piece, notification }) => {
|
||||
if(success) {
|
||||
this.setState({
|
||||
piece
|
||||
}, () => {
|
||||
supportingMaterials.refs.input.createBlobRoutine();
|
||||
proofOfPayment.refs.input.createBlobRoutine();
|
||||
});
|
||||
|
||||
setCookie(currentUser.email, piece.id);
|
||||
|
||||
return requests.post(ApiUrls.piece_extradata, {
|
||||
body: {
|
||||
extradata: additionalDataFormData,
|
||||
piece_id: piece.id
|
||||
},
|
||||
piece_id: piece.id
|
||||
});
|
||||
} else {
|
||||
const notificationMessage = new GlobalNotificationModel(notification, 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notificationMessage);
|
||||
}
|
||||
})
|
||||
.then(() => this.history.pushState(null, `/pieces/${this.state.piece.id}`))
|
||||
.catch(() => {
|
||||
const notificationMessage = new GlobalNotificationModel(getLangText("Ups! We weren't able to send your submission. Contact: support@ascribe.io"), 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notificationMessage);
|
||||
});
|
||||
},
|
||||
|
||||
validateForms() {
|
||||
const { registerPieceForm,
|
||||
additionalDataForm,
|
||||
uploadersForm } = this.refs;
|
||||
|
||||
const registerPieceFormValidation = registerPieceForm.validate();
|
||||
const additionalDataFormValidation = additionalDataForm.validate();
|
||||
const uploaderFormValidation = uploadersForm.validate();
|
||||
|
||||
return registerPieceFormValidation && additionalDataFormValidation && uploaderFormValidation;
|
||||
},
|
||||
|
||||
getCreateBlobRoutine() {
|
||||
const { piece } = this.state;
|
||||
|
||||
if(piece && piece.id) {
|
||||
return {
|
||||
url: ApiUrls.blob_otherdatas,
|
||||
pieceId: piece.id
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is overloaded so that we can track the ready-state
|
||||
* of each uploader in the component
|
||||
* @param {string} uploaderKey Name of the uploader's key to track
|
||||
*/
|
||||
setIsUploadReady(uploaderKey) {
|
||||
return (isUploadReady) => {
|
||||
this.setState({
|
||||
[uploaderKey]: isUploadReady
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
getSubmitButton() {
|
||||
const { digitalWorkKeyReady,
|
||||
thumbnailKeyReady,
|
||||
supportingMaterialsReady,
|
||||
proofOfPaymentReady,
|
||||
submitted } = this.state;
|
||||
|
||||
if(submitted) {
|
||||
return (
|
||||
<span disabled className="btn btn-default btn-wide btn-spinner">
|
||||
<AscribeSpinner color="dark-blue" size="md" />
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-default btn-wide"
|
||||
disabled={!(digitalWorkKeyReady && thumbnailKeyReady && proofOfPaymentReady && supportingMaterialsReady)}
|
||||
onClick={this.submit}>
|
||||
{getLangText('Submit to Portfolio Review')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const { location } = this.props;
|
||||
|
||||
return (
|
||||
<div className="register-piece--form">
|
||||
<Form
|
||||
buttons={{}}
|
||||
className="ascribe-form-bordered"
|
||||
ref="registerPieceForm">
|
||||
<Property
|
||||
name='artist_name'
|
||||
label={getLangText('Full name')}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="(e.g. Andy Warhol)"
|
||||
required/>
|
||||
</Property>
|
||||
<Property
|
||||
name='title'
|
||||
label={getLangText('Title of the Work')}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="(e.g. 32 Campbell's Soup Cans)"
|
||||
required/>
|
||||
</Property>
|
||||
<Property
|
||||
name='date_created'
|
||||
label={getLangText('Year of creation')}>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="(e.g. 1962)"
|
||||
min={1}
|
||||
required/>
|
||||
</Property>
|
||||
<Property
|
||||
name='artist_statement'
|
||||
label={getLangText("Artist's statement")}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter your statement')}/>
|
||||
</Property>
|
||||
</Form>
|
||||
<Form
|
||||
buttons={{}}
|
||||
className="ascribe-form-bordered"
|
||||
ref="additionalDataForm">
|
||||
<Property
|
||||
name='artist_bio'
|
||||
label={getLangText('Biography')}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter your biography')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='exhibition'
|
||||
label={getLangText('Exhibition / Publication history (optional)')}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter exhibitions and publication history')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='contact_information'
|
||||
label={getLangText('Contact information')}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter your contact information (phone/website)')}/>
|
||||
</Property>
|
||||
</Form>
|
||||
<Form
|
||||
buttons={{}}
|
||||
className="ascribe-form-bordered"
|
||||
ref="uploadersForm">
|
||||
<Property
|
||||
name="digitalWorkKey"
|
||||
label={getLangText('Select the PDF with your work')}>
|
||||
<InputFineuploader
|
||||
fileInputElement={UploadButton}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
|
||||
createBlobRoutine={{
|
||||
url: ApiUrls.blob_digitalworks
|
||||
}}
|
||||
keyRoutine={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
fileClass: 'digitalwork'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
allowedExtensions: ['pdf']
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
singular: getLangText('Select the Portfolio'),
|
||||
plural: getLangText('Select the Portfolios')
|
||||
}}
|
||||
required/>
|
||||
</Property>
|
||||
<Property
|
||||
name="thumbnailKey"
|
||||
label={getLangText('Featured Cover photo')}>
|
||||
<InputFineuploader
|
||||
fileInputElement={UploadButton}
|
||||
createBlobRoutine={{
|
||||
url: ApiUrls.blob_thumbnails
|
||||
}}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
|
||||
keyRoutine={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
fileClass: 'thumbnail'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
singular: getLangText('Select cover photo'),
|
||||
plural: getLangText('Select cover photos')
|
||||
}}
|
||||
required/>
|
||||
</Property>
|
||||
<Property
|
||||
name="supportingMaterials"
|
||||
label={getLangText('Supporting Materials (Optional)')}>
|
||||
<InputFineuploader
|
||||
fileInputElement={UploadButton}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
setIsUploadReady={this.setIsUploadReady('supportingMaterialsReady')}
|
||||
createBlobRoutine={this.getCreateBlobRoutine()}
|
||||
keyRoutine={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
fileClass: 'other_data'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
singular: getLangText('Select supporting material'),
|
||||
plural: getLangText('Select supporting materials')
|
||||
}}/>
|
||||
</Property>
|
||||
<Property
|
||||
name="proofOfPayment"
|
||||
label={getLangText('Proof of payment')}>
|
||||
<InputFineuploader
|
||||
fileInputElement={UploadButton}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
setIsUploadReady={this.setIsUploadReady('proofOfPaymentReady')}
|
||||
createBlobRoutine={this.getCreateBlobRoutine()}
|
||||
keyRoutine={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
fileClass: 'other_data'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
singular: getLangText('Select Screenshot'),
|
||||
plural: getLangText('Select Screenshots')
|
||||
}}
|
||||
required/>
|
||||
</Property>
|
||||
</Form>
|
||||
<Form
|
||||
buttons={{}}
|
||||
className="ascribe-form-bordered">
|
||||
<Property
|
||||
name="terms"
|
||||
className="ascribe-property-collapsible-toggle"
|
||||
style={{paddingBottom: 0}}>
|
||||
<span>
|
||||
{getLangText('By submitting this form, you agree to the') + ' '}
|
||||
<a
|
||||
href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/portfolioreview/tos-portfolioreview.pdf"
|
||||
target="_blank">
|
||||
{getLangText('Terms of Service')}
|
||||
</a>
|
||||
{' of Portfolio Review.'}
|
||||
</span>
|
||||
</Property>
|
||||
</Form>
|
||||
{this.getSubmitButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PRRegisterPieceForm;
|
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../actions/user_actions';
|
||||
|
||||
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
|
||||
|
||||
|
||||
const PRHero = React.createClass({
|
||||
getInitialState() {
|
||||
return UserStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { currentUser } = this.state;
|
||||
|
||||
return (
|
||||
<div className="piece--hero">
|
||||
<h2><Glyphicon glyph="ok" /> Congratulations {currentUser.email}!</h2>
|
||||
<h1>You have successfully submitted to Portfolio Review 2016</h1>
|
||||
<p>See below, your uploaded portfolio:</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PRHero;
|
@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { History } from 'react-router';
|
||||
|
||||
import PrizeActions from '../../simple_prize/actions/prize_actions';
|
||||
import PrizeStore from '../../simple_prize/stores/prize_store';
|
||||
|
||||
import Button from 'react-bootstrap/lib/Button';
|
||||
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
|
||||
|
||||
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
|
||||
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../actions/user_actions';
|
||||
|
||||
import { mergeOptions } from '../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
|
||||
|
||||
const PRLanding = React.createClass({
|
||||
propTypes: {
|
||||
location: React.PropTypes.object
|
||||
},
|
||||
|
||||
mixins: [History],
|
||||
|
||||
getInitialState() {
|
||||
return mergeOptions(
|
||||
PrizeStore.getState(),
|
||||
UserStore.getState()
|
||||
);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const { location } = this.props;
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser();
|
||||
PrizeStore.listen(this.onChange);
|
||||
PrizeActions.fetchPrize();
|
||||
|
||||
if(location && location.query && location.query.redirect) {
|
||||
let queryCopy = JSON.parse(JSON.stringify(location.query));
|
||||
delete queryCopy.redirect;
|
||||
window.setTimeout(() => this.history.replaceState(null, `/${location.query.redirect}`, queryCopy));
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.unlisten(this.onChange);
|
||||
PrizeStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
getButtons() {
|
||||
if (this.state.prize && this.state.prize.active){
|
||||
return (
|
||||
<ButtonGroup className="enter" bsSize="large" vertical>
|
||||
<LinkContainer to="/signup">
|
||||
<Button>
|
||||
{getLangText('Sign up to submit')}
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
|
||||
<p>
|
||||
{getLangText('or, already an ascribe user?')}
|
||||
</p>
|
||||
<LinkContainer to="/login">
|
||||
<Button>
|
||||
{getLangText('Log in to submit')}
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ButtonGroup className="enter" bsSize="large" vertical>
|
||||
<a className="btn btn-default" href="https://www.ascribe.io/app/signup">
|
||||
{getLangText('Sign up to ascribe')}
|
||||
</a>
|
||||
|
||||
<p>
|
||||
{getLangText('or, already an ascribe user?')}
|
||||
</p>
|
||||
<LinkContainer to="/login">
|
||||
<Button>
|
||||
{getLangText('Log in')}
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</ButtonGroup>
|
||||
);
|
||||
},
|
||||
|
||||
getTitle() {
|
||||
if (this.state.prize && this.state.prize.active){
|
||||
return (
|
||||
<p>
|
||||
{getLangText('This is the submission page for Portfolio Review 2016.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
{getLangText('Submissions for Portfolio Review 2016 are now closed.')}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-xs-12 wp-landing-wrapper">
|
||||
<h1>
|
||||
{getLangText('Welcome to Portfolio Review 2016')}
|
||||
</h1>
|
||||
{this.getTitle()}
|
||||
{this.getButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PRLanding;
|
@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { Link, History } from 'react-router';
|
||||
|
||||
import Col from 'react-bootstrap/lib/Col';
|
||||
import Row from 'react-bootstrap/lib/Row';
|
||||
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../actions/user_actions';
|
||||
|
||||
import PRRegisterPieceForm from './pr_forms/pr_register_piece_form';
|
||||
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
import { getCookie } from '../../../../../utils/fetch_api_utils';
|
||||
|
||||
|
||||
const { object } = React.PropTypes;
|
||||
|
||||
const PRRegisterPiece = React.createClass({
|
||||
propTypes: {
|
||||
location: object
|
||||
},
|
||||
|
||||
mixins: [History],
|
||||
|
||||
getInitialState() {
|
||||
return UserStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser();
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
const { currentUser } = this.state;
|
||||
if(currentUser && currentUser.email) {
|
||||
const submittedPieceId = getCookie(currentUser.email);
|
||||
if(submittedPieceId) {
|
||||
this.history.pushState(null, `/pieces/${submittedPieceId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { currentUser } = this.state;
|
||||
const { location } = this.props;
|
||||
|
||||
setDocumentTitle(getLangText('Submit to Portfolio Review'));
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<div className="register-piece--info">
|
||||
<h1>Portfolio Review</h1>
|
||||
<h2>{getLangText('Submission closing on %s', ' 22 Dec 2015')}</h2>
|
||||
<p style={{marginTop: '1em'}}>
|
||||
{getLangText("You're submitting as %s. ", currentUser.email)}
|
||||
<Link to="/logout">{getLangText('Change account?')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<PRRegisterPieceForm
|
||||
location={location}
|
||||
currentUser={currentUser}/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PRRegisterPiece;
|
65
js/components/whitelabel/prize/portfolioreview/pr_app.js
Normal file
65
js/components/whitelabel/prize/portfolioreview/pr_app.js
Normal file
@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import GlobalNotification from '../../../global_notification';
|
||||
|
||||
import Hero from './components/pr_hero';
|
||||
|
||||
import UserStore from '../../../../stores/user_store';
|
||||
import UserActions from '../../../../actions/user_actions';
|
||||
|
||||
import { getSubdomain } from '../../../../utils/general_utils';
|
||||
import { getCookie } from '../../../../utils/fetch_api_utils';
|
||||
|
||||
|
||||
let PRApp = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
]),
|
||||
history: React.PropTypes.object,
|
||||
routes: React.PropTypes.arrayOf(React.PropTypes.object)
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return UserStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { history, children } = this.props;
|
||||
const { currentUser } = this.state;
|
||||
let subdomain = getSubdomain();
|
||||
let header;
|
||||
|
||||
if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) {
|
||||
header = <Hero />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<div className={'container ascribe-prize-app client--' + subdomain}>
|
||||
{children}
|
||||
<GlobalNotification />
|
||||
<div id="modal" className="container"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PRApp;
|
@ -3,58 +3,94 @@
|
||||
import React from 'react';
|
||||
import { Route, IndexRoute } from 'react-router';
|
||||
|
||||
import Landing from './components/prize_landing';
|
||||
import LoginContainer from './components/prize_login_container';
|
||||
import LogoutContainer from '../../../components/logout_container';
|
||||
import SignupContainer from './components/prize_signup_container';
|
||||
import PasswordResetContainer from '../../../components/password_reset_container';
|
||||
import PrizeRegisterPiece from './components/prize_register_piece';
|
||||
import PrizePieceList from './components/prize_piece_list';
|
||||
import PrizePieceContainer from './components/ascribe_detail/prize_piece_container';
|
||||
import EditionContainer from '../../ascribe_detail/edition_container';
|
||||
import SettingsContainer from './components/prize_settings_container';
|
||||
import CoaVerifyContainer from '../../../components/coa_verify_container';
|
||||
import ErrorNotFoundPage from '../../../components/error_not_found_page';
|
||||
import SPLanding from './simple_prize/components/prize_landing';
|
||||
import SPLoginContainer from './simple_prize/components/prize_login_container';
|
||||
import SPSignupContainer from './simple_prize/components/prize_signup_container';
|
||||
import SPRegisterPiece from './simple_prize/components/prize_register_piece';
|
||||
import SPPieceList from './simple_prize/components/prize_piece_list';
|
||||
import SPPieceContainer from './simple_prize/components/ascribe_detail/prize_piece_container';
|
||||
import SPSettingsContainer from './simple_prize/components/prize_settings_container';
|
||||
import SPApp from './simple_prize/prize_app';
|
||||
|
||||
import App from './prize_app';
|
||||
import PRApp from './portfolioreview/pr_app';
|
||||
import PRLanding from './portfolioreview/components/pr_landing';
|
||||
import PRRegisterPiece from './portfolioreview/components/pr_register_piece';
|
||||
|
||||
import EditionContainer from '../../ascribe_detail/edition_container';
|
||||
import LogoutContainer from '../../logout_container';
|
||||
import PasswordResetContainer from '../../password_reset_container';
|
||||
import CoaVerifyContainer from '../../coa_verify_container';
|
||||
import ErrorNotFoundPage from '../../error_not_found_page';
|
||||
|
||||
import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler';
|
||||
|
||||
|
||||
function getRoutes() {
|
||||
return (
|
||||
<Route path='/' component={App}>
|
||||
<IndexRoute component={Landing} />
|
||||
const ROUTES = {
|
||||
sluice: (
|
||||
<Route path='/' component={SPApp}>
|
||||
<IndexRoute component={SPLanding} />
|
||||
<Route
|
||||
path='login'
|
||||
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(LoginContainer)} />
|
||||
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPLoginContainer)} />
|
||||
<Route
|
||||
path='logout'
|
||||
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
|
||||
<Route
|
||||
path='signup'
|
||||
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SignupContainer)} />
|
||||
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPSignupContainer)} />
|
||||
<Route
|
||||
path='password_reset'
|
||||
component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(PasswordResetContainer)} />
|
||||
<Route
|
||||
path='settings'
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SettingsContainer)}/>
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPSettingsContainer)}/>
|
||||
<Route
|
||||
path='register_piece'
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizeRegisterPiece)}
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPRegisterPiece)}
|
||||
headerTitle='+ NEW WORK'/>
|
||||
<Route
|
||||
path='collection'
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PrizePieceList)}
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPPieceList)}
|
||||
headerTitle='COLLECTION'/>
|
||||
|
||||
<Route path='pieces/:pieceId' component={PrizePieceContainer} />
|
||||
<Route path='pieces/:pieceId' component={SPPieceContainer} />
|
||||
<Route path='editions/:editionId' component={EditionContainer} />
|
||||
<Route path='verify' component={CoaVerifyContainer} />
|
||||
<Route path='*' component={ErrorNotFoundPage} />
|
||||
</Route>
|
||||
);
|
||||
),
|
||||
portfolioreview: (
|
||||
<Route path='/' component={PRApp}>
|
||||
<IndexRoute component={PRLanding} />
|
||||
<Route
|
||||
path='register_piece'
|
||||
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(PRRegisterPiece)}
|
||||
headerTitle='+ NEW WORK'/>
|
||||
<Route
|
||||
path='login'
|
||||
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPLoginContainer)} />
|
||||
<Route
|
||||
path='logout'
|
||||
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/>
|
||||
<Route
|
||||
path='signup'
|
||||
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPSignupContainer)} />
|
||||
<Route
|
||||
path='password_reset'
|
||||
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(PasswordResetContainer)} />
|
||||
<Route path='pieces/:pieceId' component={SPPieceContainer} />
|
||||
<Route path='*' component={ErrorNotFoundPage} />
|
||||
</Route>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
function getRoutes(commonRoutes, subdomain) {
|
||||
if(subdomain in ROUTES) {
|
||||
return ROUTES[subdomain];
|
||||
} else {
|
||||
throw new Error('Subdomain wasn\'t specified in the wallet app.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../../alt';
|
||||
|
||||
import PrizeFetcher from '../fetchers/prize_fetcher';
|
||||
|
||||
class PrizeActions {
|
||||
constructor() {
|
||||
this.generateActions(
|
||||
'updatePrize'
|
||||
);
|
||||
}
|
||||
|
||||
fetchPrize() {
|
||||
PrizeFetcher
|
||||
.fetch()
|
||||
.then((res) => {
|
||||
this.actions.updatePrize({
|
||||
prize: res.prize
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.logGlobal(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default alt.createActions(PrizeActions);
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import { alt } from '../../../../../alt';
|
||||
import Q from 'q';
|
||||
|
||||
import PrizeJuryFetcher from '../fetchers/prize_jury_fetcher';
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import { alt } from '../../../../../alt';
|
||||
import Q from 'q';
|
||||
|
||||
import PrizeRatingFetcher from '../fetchers/prize_rating_fetcher';
|
@ -3,26 +3,27 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import StarRating from 'react-star-rating';
|
||||
import Moment from 'moment';
|
||||
|
||||
import PieceListActions from '../../../../../actions/piece_list_actions';
|
||||
import PieceListStore from '../../../../../stores/piece_list_store';
|
||||
import PieceListActions from '../../../../../../actions/piece_list_actions';
|
||||
import PieceListStore from '../../../../../../stores/piece_list_store';
|
||||
|
||||
import PrizeRatingActions from '../../actions/prize_rating_actions';
|
||||
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserStore from '../../../../../../stores/user_store';
|
||||
|
||||
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
|
||||
import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
|
||||
|
||||
import AccordionListItemPiece from '../../../../ascribe_accordion_list/accordion_list_item_piece';
|
||||
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
|
||||
|
||||
import GlobalNotificationModel from '../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
|
||||
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
|
||||
|
||||
import AclProxy from '../../../../acl_proxy';
|
||||
import AclProxy from '../../../../../acl_proxy';
|
||||
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
|
||||
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { mergeOptions } from '../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||
import { mergeOptions } from '../../../../../../utils/general_utils';
|
||||
|
||||
|
||||
let AccordionListItemPrize = React.createClass({
|
||||
@ -182,7 +183,7 @@ let AccordionListItemPrize = React.createClass({
|
||||
artistName={artistName}
|
||||
subsubheading={
|
||||
<div>
|
||||
<span>{new Date(this.props.content.date_created).getFullYear()}</span>
|
||||
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
|
||||
</div>}
|
||||
buttons={this.getPrizeButtons()}
|
||||
badge={this.getPrizeBadge()}>
|
@ -3,10 +3,10 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ModalWrapper from '../../../../ascribe_modal/modal_wrapper';
|
||||
import PieceSubmitToPrizeForm from '../../../../ascribe_forms/form_submit_to_prize';
|
||||
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
|
||||
import PieceSubmitToPrizeForm from '../../../../../ascribe_forms/form_submit_to_prize';
|
||||
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||
|
||||
let SubmitToPrizeButton = React.createClass({
|
||||
propTypes: {
|
@ -6,41 +6,44 @@ import Moment from 'moment';
|
||||
|
||||
import StarRating from 'react-star-rating';
|
||||
|
||||
import PieceActions from '../../../../../actions/piece_actions';
|
||||
import PieceStore from '../../../../../stores/piece_store';
|
||||
import PieceActions from '../../../../../../actions/piece_actions';
|
||||
import PieceStore from '../../../../../../stores/piece_store';
|
||||
|
||||
import PieceListStore from '../../../../../stores/piece_list_store';
|
||||
import PieceListActions from '../../../../../actions/piece_list_actions';
|
||||
import PieceListStore from '../../../../../../stores/piece_list_store';
|
||||
import PieceListActions from '../../../../../../actions/piece_list_actions';
|
||||
|
||||
import PrizeRatingActions from '../../actions/prize_rating_actions';
|
||||
import PrizeRatingStore from '../../stores/prize_rating_store';
|
||||
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserStore from '../../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../../actions/user_actions';
|
||||
|
||||
import Piece from '../../../../../components/ascribe_detail/piece';
|
||||
import Note from '../../../../../components/ascribe_detail/note';
|
||||
import Piece from '../../../../../../components/ascribe_detail/piece';
|
||||
import Note from '../../../../../../components/ascribe_detail/note';
|
||||
|
||||
import AscribeSpinner from '../../../../ascribe_spinner';
|
||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
||||
|
||||
import Form from '../../../../../components/ascribe_forms/form';
|
||||
import Property from '../../../../../components/ascribe_forms/property';
|
||||
import InputTextAreaToggable from '../../../../../components/ascribe_forms/input_textarea_toggable';
|
||||
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph';
|
||||
import Form from '../../../../../../components/ascribe_forms/form';
|
||||
import Property from '../../../../../../components/ascribe_forms/property';
|
||||
import InputTextAreaToggable from '../../../../../../components/ascribe_forms/input_textarea_toggable';
|
||||
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
|
||||
|
||||
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
|
||||
import LoanForm from '../../../../ascribe_forms/form_loan';
|
||||
import ListRequestActions from '../../../../ascribe_forms/list_form_request_actions';
|
||||
import ModalWrapper from '../../../../ascribe_modal/modal_wrapper';
|
||||
import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader';
|
||||
|
||||
import GlobalNotificationModel from '../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
|
||||
import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
|
||||
import LoanForm from '../../../../../ascribe_forms/form_loan';
|
||||
import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions';
|
||||
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
|
||||
|
||||
import DetailProperty from '../../../../ascribe_detail/detail_property';
|
||||
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
|
||||
|
||||
import ApiUrls from '../../../../../constants/api_urls';
|
||||
import { mergeOptions } from '../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
import DetailProperty from '../../../../../ascribe_detail/detail_property';
|
||||
|
||||
import ApiUrls from '../../../../../../constants/api_urls';
|
||||
import { mergeOptions } from '../../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../../utils/dom_utils';
|
||||
|
||||
|
||||
/**
|
||||
@ -48,7 +51,8 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
*/
|
||||
let PieceContainer = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object
|
||||
params: React.PropTypes.object,
|
||||
location: React.PropTypes.object
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
@ -62,6 +66,7 @@ let PieceContainer = React.createClass({
|
||||
PieceStore.listen(this.onChange);
|
||||
PieceActions.fetchOne(this.props.params.pieceId);
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser();
|
||||
|
||||
// Every time we enter the piece detail page, just reset the piece
|
||||
// store as it will otherwise display wrong/old data once the user loads
|
||||
@ -142,10 +147,10 @@ let PieceContainer = React.createClass({
|
||||
<NavigationHeader
|
||||
piece={this.state.piece}
|
||||
currentUser={this.state.currentUser}/>
|
||||
<hr/>
|
||||
|
||||
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
|
||||
<DetailProperty label={getLangText('BY')} value={artistName} />
|
||||
<DetailProperty label={getLangText('DATE')} value={new Date(this.state.piece.date_created).getFullYear()} />
|
||||
<DetailProperty label={getLangText('DATE')} value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year()} />
|
||||
{artistEmail}
|
||||
{this.getActions()}
|
||||
<hr/>
|
||||
@ -157,7 +162,7 @@ let PieceContainer = React.createClass({
|
||||
piece={this.state.piece}
|
||||
currentUser={this.state.currentUser}/>
|
||||
}>
|
||||
<PrizePieceDetails piece={this.state.piece}/>
|
||||
<PrizePieceDetails piece={this.state.piece} location={this.props.location}/>
|
||||
</Piece>
|
||||
);
|
||||
} else {
|
||||
@ -177,24 +182,28 @@ let NavigationHeader = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.props.currentUser && this.props.currentUser.email && this.props.piece && this.props.piece.navigation) {
|
||||
let nav = this.props.piece.navigation;
|
||||
const { currentUser, piece } = this.props;
|
||||
|
||||
if (currentUser && currentUser.email && currentUser.is_judge && currentUser.is_jury &&
|
||||
!currentUser.is_admin && piece && piece.navigation) {
|
||||
let nav = piece.navigation;
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
<div className="row no-margin">
|
||||
<Link className="disable-select" to={`/pieces/${ nav.prev_index || this.props.piece.id }`}>
|
||||
<Link className="disable-select" to={`/pieces/${ nav.prev_index || piece.id }`}>
|
||||
<span className="glyphicon glyphicon-chevron-left pull-left link-ascribe" aria-hidden="true">
|
||||
{getLangText('Previous')}
|
||||
</span>
|
||||
</Link>
|
||||
<Link className="disable-select" to={`/pieces/${ nav.next_index || this.props.piece.id }`}>
|
||||
<Link className="disable-select" to={`/pieces/${ nav.next_index || piece.id }`}>
|
||||
<span className="pull-right link-ascribe">
|
||||
{getLangText('Next')}
|
||||
<span className="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -417,7 +426,8 @@ let PrizePieceRatings = React.createClass({
|
||||
|
||||
let PrizePieceDetails = React.createClass({
|
||||
propTypes: {
|
||||
piece: React.PropTypes.object
|
||||
piece: React.PropTypes.object,
|
||||
location: React.PropTypes.object
|
||||
},
|
||||
|
||||
render() {
|
||||
@ -432,6 +442,8 @@ let PrizePieceDetails = React.createClass({
|
||||
<Form ref='form'>
|
||||
{Object.keys(this.props.piece.extra_data).map((data) => {
|
||||
let label = data.replace('_', ' ');
|
||||
const value = this.props.piece.extra_data[data] || 'N/A';
|
||||
|
||||
return (
|
||||
<Property
|
||||
name={data}
|
||||
@ -440,11 +452,20 @@ let PrizePieceDetails = React.createClass({
|
||||
overrideForm={true}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={this.props.piece.extra_data[data]}/>
|
||||
</Property>);
|
||||
}
|
||||
)}
|
||||
<hr />
|
||||
defaultValue={value}/>
|
||||
</Property>
|
||||
);
|
||||
})}
|
||||
<FurtherDetailsFileuploader
|
||||
submitFile={() => {}}
|
||||
setIsUploadReady={() => {}}
|
||||
isReadyForFormSubmission={() => {}}
|
||||
editable={false}
|
||||
overrideForm={true}
|
||||
pieceId={this.props.piece.id}
|
||||
otherData={this.props.piece.other_data}
|
||||
multiple={true}
|
||||
location={location}/>
|
||||
</Form>
|
||||
</CollapsibleParagraph>
|
||||
);
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import constants from '../../../../constants/application_constants';
|
||||
import constants from '../../../../../constants/application_constants';
|
||||
|
||||
|
||||
let Hero = React.createClass({
|
@ -11,11 +11,11 @@ import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
|
||||
|
||||
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
|
||||
|
||||
import UserStore from '../../../../stores/user_store';
|
||||
import UserActions from '../../../../actions/user_actions';
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../actions/user_actions';
|
||||
|
||||
import { mergeOptions } from '../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../utils/lang_utils';
|
||||
import { mergeOptions } from '../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
|
||||
let Landing = React.createClass({
|
||||
|
@ -3,10 +3,10 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import LoginForm from '../../../ascribe_forms/form_login';
|
||||
import LoginForm from '../../../../ascribe_forms/form_login';
|
||||
|
||||
import { getLangText } from '../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../utils/dom_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
|
||||
|
||||
let LoginContainer = React.createClass({
|
@ -1,10 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PieceList from '../../../piece_list';
|
||||
import PieceList from '../../../../piece_list';
|
||||
|
||||
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 PrizeActions from '../actions/prize_actions';
|
||||
import PrizeStore from '../stores/prize_store';
|
||||
@ -15,9 +15,9 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
|
||||
|
||||
import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize';
|
||||
|
||||
import { mergeOptions } from '../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../utils/dom_utils';
|
||||
import { mergeOptions } from '../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
|
||||
let PrizePieceList = React.createClass({
|
||||
propTypes: {
|
@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import PrizeActions from '../actions/prize_actions';
|
||||
import PrizeStore from '../stores/prize_store';
|
||||
|
||||
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';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
|
||||
|
||||
let PrizeRegisterPiece = React.createClass({
|
||||
propTypes: {
|
||||
location: React.PropTypes.object
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return PrizeStore.getState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
PrizeStore.listen(this.onChange);
|
||||
PrizeActions.fetchPrize();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
PrizeStore.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { location } = this.props;
|
||||
|
||||
setDocumentTitle(getLangText('Submit to the prize'));
|
||||
|
||||
if(this.state.prize && this.state.prize.active){
|
||||
return (
|
||||
<div>
|
||||
<RegisterPiece
|
||||
enableLocalHashing={false}
|
||||
headerMessage={''}
|
||||
submitMessage={getLangText('Submit')}
|
||||
location={location}>
|
||||
<Property
|
||||
name='artist_statement'
|
||||
label={getLangText('Artist statement')}
|
||||
editable={true}
|
||||
overrideForm={true}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter your statement')}
|
||||
required />
|
||||
</Property>
|
||||
<Property
|
||||
name='work_description'
|
||||
label={getLangText('Work description')}
|
||||
editable={true}
|
||||
overrideForm={true}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
placeholder={getLangText('Enter the description for your work')}
|
||||
required />
|
||||
</Property>
|
||||
<Property
|
||||
name="terms"
|
||||
className="ascribe-property-collapsible-toggle"
|
||||
style={{paddingBottom: 0}}>
|
||||
<InputCheckbox>
|
||||
<span>
|
||||
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '}
|
||||
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
|
||||
{getLangText('read')}
|
||||
</a>)
|
||||
</span>
|
||||
</InputCheckbox>
|
||||
</Property>
|
||||
</RegisterPiece>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className='row'>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
{getLangText('The prize is no longer active')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default PrizeRegisterPiece;
|
@ -2,29 +2,29 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import UserStore from '../../../../stores/user_store';
|
||||
import UserActions from '../../../../actions/user_actions';
|
||||
import UserStore from '../../../../../stores/user_store';
|
||||
import UserActions from '../../../../../actions/user_actions';
|
||||
import PrizeActions from '../actions/prize_actions';
|
||||
import PrizeStore from '../stores/prize_store';
|
||||
import PrizeJuryActions from '../actions/prize_jury_actions';
|
||||
import PrizeJuryStore from '../stores/prize_jury_store';
|
||||
|
||||
import SettingsContainer from '../../../ascribe_settings/settings_container';
|
||||
import CollapsibleParagraph from '../../../ascribe_collapsible/collapsible_paragraph';
|
||||
import SettingsContainer from '../../../../ascribe_settings/settings_container';
|
||||
import CollapsibleParagraph from '../../../../ascribe_collapsible/collapsible_paragraph';
|
||||
|
||||
import Form from '../../../ascribe_forms/form';
|
||||
import Property from '../../../ascribe_forms/property';
|
||||
import Form from '../../../../ascribe_forms/form';
|
||||
import Property from '../../../../ascribe_forms/property';
|
||||
|
||||
import ActionPanel from '../../../ascribe_panel/action_panel';
|
||||
import ActionPanel from '../../../../ascribe_panel/action_panel';
|
||||
|
||||
import GlobalNotificationModel from '../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../actions/global_notification_actions';
|
||||
import GlobalNotificationModel from '../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
|
||||
|
||||
import AscribeSpinner from '../../../ascribe_spinner';
|
||||
import ApiUrls from '../../../../constants/api_urls';
|
||||
import AscribeSpinner from '../../../../ascribe_spinner';
|
||||
import ApiUrls from '../../../../../constants/api_urls';
|
||||
|
||||
import { getLangText } from '../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../utils/dom_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
|
||||
|
||||
let Settings = React.createClass({
|
@ -1,10 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import SignupForm from '../../../ascribe_forms/form_signup';
|
||||
import SignupForm from '../../../../ascribe_forms/form_signup';
|
||||
|
||||
import { getLangText } from '../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../utils/dom_utils';
|
||||
import { getLangText } from '../../../../../utils/lang_utils';
|
||||
import { setDocumentTitle } from '../../../../../utils/dom_utils';
|
||||
|
||||
let SignupContainer = React.createClass({
|
||||
propTypes: {
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import requests from '../../../../utils/requests';
|
||||
import requests from '../../../../../utils/requests';
|
||||
|
||||
|
||||
let PrizeFetcher = {
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import requests from '../../../../utils/requests';
|
||||
import requests from '../../../../../utils/requests';
|
||||
|
||||
|
||||
let PrizeJuryFetcher = {
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import requests from '../../../../utils/requests';
|
||||
import requests from '../../../../../utils/requests';
|
||||
|
||||
|
||||
let PrizeRatingFetcher = {
|
@ -2,11 +2,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import Hero from './components/prize_hero';
|
||||
import Header from '../../header';
|
||||
import Footer from '../../footer';
|
||||
import GlobalNotification from '../../global_notification';
|
||||
import Header from '../../../header';
|
||||
import Footer from '../../../footer';
|
||||
import GlobalNotification from '../../../global_notification';
|
||||
|
||||
import { getSubdomain } from '../../../utils/general_utils';
|
||||
import { getSubdomain } from '../../../../utils/general_utils';
|
||||
|
||||
|
||||
let PrizeApp = React.createClass({
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import { alt } from '../../../../../alt';
|
||||
|
||||
import PrizeJuryActions from '../actions/prize_jury_actions';
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import { alt } from '../../../../../alt';
|
||||
|
||||
import PrizeRatingActions from '../actions/prize_rating_actions';
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../../../../alt';
|
||||
import { alt } from '../../../../../alt';
|
||||
|
||||
import PrizeActions from '../actions/prize_actions';
|
||||
|
@ -47,7 +47,7 @@ let WalletActionPanel = React.createClass({
|
||||
<AclButtonList
|
||||
className="text-center ascribe-button-list"
|
||||
availableAcls={availableAcls}
|
||||
editions={this.props.piece}
|
||||
pieceOrEditions={this.props.piece}
|
||||
handleSuccess={this.props.loadPiece}>
|
||||
<AclProxy
|
||||
aclObject={this.props.currentUser.acl}
|
||||
@ -69,4 +69,4 @@ let WalletActionPanel = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default WalletActionPanel;
|
||||
export default WalletActionPanel;
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Moment from 'moment';
|
||||
|
||||
import Piece from '../../../../../components/ascribe_detail/piece';
|
||||
|
||||
@ -39,7 +40,7 @@ let WalletPieceContainer = React.createClass({
|
||||
<hr style={{marginTop: 0}}/>
|
||||
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1>
|
||||
<DetailProperty label="BY" value={this.props.piece.artist_name} />
|
||||
<DetailProperty label="DATE" value={new Date(this.props.piece.date_created).getFullYear()} />
|
||||
<DetailProperty label="DATE" value={Moment(this.props.piece.date_created, 'YYYY-MM-DD').year()} />
|
||||
<hr/>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Moment from 'moment';
|
||||
|
||||
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
|
||||
|
||||
@ -100,7 +101,7 @@ let CylandAccordionListItem = React.createClass({
|
||||
piece={this.props.content}
|
||||
subsubheading={
|
||||
<div className="pull-left">
|
||||
<span>{new Date(this.props.content.date_created).getFullYear()}</span>
|
||||
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
|
||||
</div>}
|
||||
buttons={this.getSubmitButtons()}>
|
||||
{this.props.children}
|
||||
|
@ -33,7 +33,6 @@ import { mergeOptions } from '../../../../../../utils/general_utils';
|
||||
|
||||
let CylandPieceContainer = React.createClass({
|
||||
propTypes: {
|
||||
location: React.PropTypes.object,
|
||||
params: React.PropTypes.object
|
||||
},
|
||||
|
||||
@ -106,8 +105,7 @@ let CylandPieceContainer = React.createClass({
|
||||
<CylandAdditionalDataForm
|
||||
piece={this.state.piece}
|
||||
disabled={!this.state.piece.acl.acl_edit}
|
||||
isInline={true}
|
||||
location={this.props.location}/>
|
||||
isInline={true} />
|
||||
</CollapsibleParagraph>
|
||||
</WalletPieceContainer>
|
||||
);
|
||||
|
@ -26,8 +26,7 @@ let CylandAdditionalDataForm = React.createClass({
|
||||
handleSuccess: React.PropTypes.func,
|
||||
piece: React.PropTypes.object.isRequired,
|
||||
disabled: React.PropTypes.bool,
|
||||
isInline: React.PropTypes.bool,
|
||||
location: React.PropTypes.object
|
||||
isInline: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -78,7 +77,7 @@ let CylandAdditionalDataForm = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
let { piece, isInline, disabled, handleSuccess } = this.props;
|
||||
let { piece, isInline, disabled, handleSuccess, location } = this.props;
|
||||
let buttons, spinner, heading;
|
||||
|
||||
if(!isInline) {
|
||||
@ -122,29 +121,76 @@ let CylandAdditionalDataForm = React.createClass({
|
||||
{heading}
|
||||
<Property
|
||||
name='artist_bio'
|
||||
label={getLangText('Artist Biography')}>
|
||||
label={getLangText('Artist Biography')}
|
||||
hidden={disabled && !piece.extra_data.artist_bio}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.artist_bio}
|
||||
placeholder={getLangText('Enter the artist\'s biography...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='artist_contact_information'
|
||||
label={getLangText('Artist Contact Information')}
|
||||
hidden={disabled && !piece.extra_data.artist_contact_information}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.artist_contact_information}
|
||||
placeholder={getLangText('Enter the artist\'s contact information...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='conceptual_overview'
|
||||
label={getLangText('Conceptual Overview')}>
|
||||
label={getLangText('Conceptual Overview')}
|
||||
hidden={disabled && !piece.extra_data.conceptual_overview}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.conceptual_overview}
|
||||
placeholder={getLangText('Enter a conceptual overview...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='medium'
|
||||
label={getLangText('Medium (technical specifications)')}
|
||||
hidden={disabled && !piece.extra_data.medium}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.medium}
|
||||
placeholder={getLangText('Enter the medium (and other technical specifications)...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='size_duration'
|
||||
label={getLangText('Size / Duration')}
|
||||
hidden={disabled && !piece.extra_data.size_duration}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.size_duration}
|
||||
placeholder={getLangText('Enter the size / duration...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='display_instructions'
|
||||
label={getLangText('Display instructions')}
|
||||
hidden={disabled && !piece.extra_data.display_instructions}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.display_instructions}
|
||||
placeholder={getLangText('Enter the display instructions...')}/>
|
||||
</Property>
|
||||
<Property
|
||||
name='additional_details'
|
||||
label={getLangText('Additional details')}
|
||||
hidden={disabled && !piece.extra_data.additional_details}>
|
||||
<InputTextAreaToggable
|
||||
rows={1}
|
||||
defaultValue={piece.extra_data.additional_details}
|
||||
placeholder={getLangText('Enter additional details...')}/>
|
||||
</Property>
|
||||
<FurtherDetailsFileuploader
|
||||
label={getLangText('Additional files (e.g. still images, pdf)')}
|
||||
uploadStarted={this.uploadStarted}
|
||||
submitFile={this.submitFile}
|
||||
setIsUploadReady={this.setIsUploadReady}
|
||||
isReadyForFormSubmission={formSubmissionValidation.fileOptional}
|
||||
pieceId={piece.id}
|
||||
otherData={piece.other_data}
|
||||
multiple={true}
|
||||
location={this.props.location}/>
|
||||
multiple={true} />
|
||||
</Form>
|
||||
);
|
||||
} else {
|
||||
@ -157,4 +203,4 @@ let CylandAdditionalDataForm = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
export default CylandAdditionalDataForm;
|
||||
export default CylandAdditionalDataForm;
|
||||
|
@ -210,8 +210,7 @@ let CylandRegisterPiece = React.createClass({
|
||||
<CylandAdditionalDataForm
|
||||
disabled={this.state.step > 1}
|
||||
handleSuccess={this.handleAdditionalDataSuccess}
|
||||
piece={this.state.piece}
|
||||
location={this.props.location}/>
|
||||
piece={this.state.piece} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
@ -220,7 +219,12 @@ let CylandRegisterPiece = React.createClass({
|
||||
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
|
||||
<LoanForm
|
||||
loanHeading={getLangText('Loan to Cyland archive')}
|
||||
message={getAclFormMessage('acl_loan', '\"' + this.state.piece.title + '\"', this.state.currentUser.username)}
|
||||
message={getAclFormMessage({
|
||||
aclName: 'acl_loan',
|
||||
entities: this.state.piece,
|
||||
isPiece: true,
|
||||
senderName: this.state.currentUser.username
|
||||
})}
|
||||
id={{piece_id: this.state.piece.id}}
|
||||
url={ApiUrls.ownership_loans_pieces}
|
||||
email={this.state.whitelabel.user}
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Moment from 'moment';
|
||||
|
||||
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
|
||||
|
||||
@ -106,7 +107,7 @@ let IkonotvAccordionListItem = React.createClass({
|
||||
piece={this.props.content}
|
||||
subsubheading={
|
||||
<div className="pull-left">
|
||||
<span>{new Date(this.props.content.date_created).getFullYear()}</span>
|
||||
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
|
||||
</div>}
|
||||
buttons={this.getSubmitButtons()}>
|
||||
{this.props.children}
|
||||
|
@ -14,6 +14,7 @@ let ApiUrls = {
|
||||
'blob_digitalworks': AppConstants.apiEndpoint + 'blob/digitalworks/',
|
||||
'blob_otherdatas': AppConstants.apiEndpoint + 'blob/otherdatas/',
|
||||
'blob_contracts': AppConstants.apiEndpoint + 'blob/contracts/',
|
||||
'blob_thumbnails': AppConstants.apiEndpoint + 'blob/thumbnails/',
|
||||
'coa': AppConstants.apiEndpoint + 'coa/${id}/',
|
||||
'coa_create': AppConstants.apiEndpoint + 'coa/',
|
||||
'coa_verify': AppConstants.apiEndpoint + 'coa/verify_coa/',
|
||||
|
@ -15,7 +15,7 @@ const constants = {
|
||||
serverUrl,
|
||||
baseUrl,
|
||||
'aclList': ['acl_coa', 'acl_consign', 'acl_delete', 'acl_download', 'acl_edit', 'acl_create_editions', 'acl_view_editions',
|
||||
'acl_loan', 'acl_share', 'acl_transfer', 'acl_unconsign', 'acl_unshare', 'acl_view',
|
||||
'acl_loan', 'acl_loan_request', 'acl_share', 'acl_transfer', 'acl_unconsign', 'acl_unshare', 'acl_view',
|
||||
'acl_withdraw_transfer', 'acl_wallet_submit'],
|
||||
|
||||
'version': 0.1,
|
||||
@ -50,6 +50,13 @@ const constants = {
|
||||
'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-logo-black.png',
|
||||
'permissions': ['register', 'edit', 'share', 'del_from_collection'],
|
||||
'type': 'wallet'
|
||||
},
|
||||
{
|
||||
'subdomain': 'portfolioreview',
|
||||
'name': 'Portfolio Review',
|
||||
'logo': 'http://notfoundlogo.de',
|
||||
'permissions': ['register', 'edit', 'share', 'del_from_collection'],
|
||||
'type': 'prize'
|
||||
}
|
||||
],
|
||||
'defaultDomain': {
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import requests from '../utils/requests';
|
||||
|
||||
import { generateOrderingQueryParams } from '../utils/fetch_api_utils';
|
||||
import { mergeOptions } from '../utils/general_utils';
|
||||
import { generateOrderingQueryParams } from '../utils/url_utils';
|
||||
|
||||
let EditionListFetcher = {
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@
|
||||
import requests from '../utils/requests';
|
||||
|
||||
import { mergeOptions } from '../utils/general_utils';
|
||||
import { generateOrderingQueryParams } from '../utils/fetch_api_utils';
|
||||
import { generateOrderingQueryParams } from '../utils/url_utils';
|
||||
|
||||
let PieceListFetcher = {
|
||||
/**
|
||||
|
@ -1,64 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
import Q from 'q';
|
||||
import moment from 'moment';
|
||||
|
||||
import { sanitize } from './general_utils';
|
||||
import AppConstants from '../constants/application_constants';
|
||||
|
||||
// TODO: Create Unittests that test all functions
|
||||
|
||||
/**
|
||||
* Takes a key-value object of this form:
|
||||
*
|
||||
* {
|
||||
* 'page': 1,
|
||||
* 'pageSize': 10
|
||||
* }
|
||||
*
|
||||
* and converts it to a query-parameter, which you can append to your URL.
|
||||
* The return looks like this:
|
||||
*
|
||||
* ?page=1&page_size=10
|
||||
*
|
||||
* CamelCase gets converted to snake_case!
|
||||
*
|
||||
*/
|
||||
export function argsToQueryParams(obj) {
|
||||
|
||||
obj = sanitize(obj);
|
||||
|
||||
return Object
|
||||
.keys(obj)
|
||||
.map((key, i) => {
|
||||
let s = '';
|
||||
|
||||
if(i === 0) {
|
||||
s += '?';
|
||||
} else {
|
||||
s += '&';
|
||||
}
|
||||
|
||||
let snakeCaseKey = key.replace(/[A-Z]/, (match) => '_' + match.toLowerCase());
|
||||
|
||||
return s + snakeCaseKey + '=' + encodeURIComponent(obj[key]);
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string and a boolean and generates a string query parameter for
|
||||
* an API call.
|
||||
*/
|
||||
export function generateOrderingQueryParams(orderBy, orderAsc) {
|
||||
let interpolation = '';
|
||||
|
||||
if(!orderAsc) {
|
||||
interpolation += '-';
|
||||
}
|
||||
|
||||
return interpolation + orderBy;
|
||||
}
|
||||
|
||||
export function status(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
@ -68,14 +16,21 @@ export function status(response) {
|
||||
|
||||
export function getCookie(name) {
|
||||
let parts = document.cookie.split(';');
|
||||
|
||||
|
||||
for(let i = 0; i < parts.length; i++) {
|
||||
if(parts[i].indexOf(AppConstants.csrftoken + '=') > -1) {
|
||||
if(parts[i].indexOf(name + '=') > -1) {
|
||||
return parts[i].split('=').pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setCookie(key, value, days) {
|
||||
const exdate = moment();
|
||||
exdate.add(days, 'days');
|
||||
value = window.escape(value) + ((days === null) ? '' : `; expires= ${exdate.utc()}`);
|
||||
document.cookie = `${key}=${value}`;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Given a url for an image, this method fetches it and returns a promise that resolves to
|
||||
@ -111,4 +66,4 @@ export function fetchImageAsBlob(url) {
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import Q from 'q';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import Moment from 'moment';
|
||||
|
||||
import { getLangText } from './lang_utils';
|
||||
|
||||
@ -37,7 +38,7 @@ export function computeHashOfFile(file) {
|
||||
let spark = new SparkMD5.ArrayBuffer();
|
||||
let fileReader = new FileReader();
|
||||
|
||||
let startTime = new Date();
|
||||
let startTime = new Moment();
|
||||
|
||||
// comment: We should convert this to es6 at some point, however if so please consider that
|
||||
// an arrow function will get rid of the function's scope...
|
||||
@ -53,7 +54,7 @@ export function computeHashOfFile(file) {
|
||||
|
||||
console.info('computed hash %s (took %d s)',
|
||||
fileHash,
|
||||
Math.round(((new Date() - startTime) / 1000) % 60)); // Compute hash
|
||||
Math.round(((new Moment() - startTime) / 1000) % 60)); // Compute hash
|
||||
|
||||
let blobTextFile = makeTextFile(fileHash, file);
|
||||
resolve(blobTextFile);
|
||||
|
@ -3,13 +3,39 @@
|
||||
import { getLangText } from './lang_utils';
|
||||
|
||||
/**
|
||||
* Generates a message for submitting a form
|
||||
* @param {string} aclName Enum name of a acl
|
||||
* @param {string} entities Already computed name of entities
|
||||
* @param {string} senderName Name of the sender
|
||||
* @return {string} Completed message
|
||||
* Get the data ids of the given piece or editions.
|
||||
* @param {boolean} isPiece Is the given entities parameter a piece? (False: array of editions)
|
||||
* @param {(object|object[])} pieceOrEditions Piece or array of editions
|
||||
* @return {(object|object[])} Data IDs of the pieceOrEditions for the form
|
||||
*/
|
||||
export function getAclFormMessage(aclName, entities, senderName) {
|
||||
export function getAclFormDataId(isPiece, pieceOrEditions) {
|
||||
if (isPiece) {
|
||||
return {piece_id: pieceOrEditions.id};
|
||||
} else {
|
||||
return {bitcoin_id: pieceOrEditions.map(function(edition){
|
||||
return edition.bitcoin_id;
|
||||
}).join()};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message for submitting a form
|
||||
* @param {object} options Options object for creating the message:
|
||||
* @param {string} options.aclName Enum name of an acl
|
||||
* @param {(object|object[])} options.entities Piece or array of Editions
|
||||
* @param {boolean} options.isPiece Is the given entities parameter a piece? (False: array of editions)
|
||||
* @param {string} [options.senderName] Name of the sender
|
||||
* @return {string} Completed message
|
||||
*/
|
||||
export function getAclFormMessage(options) {
|
||||
if (!options || options.aclName === undefined || options.isPiece === undefined ||
|
||||
!(typeof options.entities === 'object' || options.entities.constructor === Array)) {
|
||||
throw new Error('You must specify an acl class, entities in the correct format, and entity type');
|
||||
}
|
||||
|
||||
let aclName = options.aclName;
|
||||
let entityTitles = options.isPiece ? getTitlesStringOfPiece(options.entities)
|
||||
: getTitlesStringOfEditions(options.entities);
|
||||
let message = '';
|
||||
|
||||
message += getLangText('Hi');
|
||||
@ -32,7 +58,7 @@ export function getAclFormMessage(aclName, entities, senderName) {
|
||||
}
|
||||
|
||||
message += ':\n';
|
||||
message += entities;
|
||||
message += entityTitles;
|
||||
|
||||
if(aclName === 'acl_transfer' || aclName === 'acl_loan' || aclName === 'acl_consign') {
|
||||
message += getLangText('to you');
|
||||
@ -44,10 +70,22 @@ export function getAclFormMessage(aclName, entities, senderName) {
|
||||
throw new Error('Your specified aclName did not match a an acl class.');
|
||||
}
|
||||
|
||||
message += '\n\n';
|
||||
message += getLangText('Truly yours,');
|
||||
message += '\n';
|
||||
message += senderName;
|
||||
if (options.senderName) {
|
||||
message += '\n\n';
|
||||
message += getLangText('Truly yours,');
|
||||
message += '\n';
|
||||
message += options.senderName;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
function getTitlesStringOfPiece(piece){
|
||||
return '\"' + piece.title + '\"';
|
||||
}
|
||||
|
||||
function getTitlesStringOfEditions(editions) {
|
||||
return editions.map(function(edition) {
|
||||
return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n';
|
||||
}).join('');
|
||||
}
|
||||
|
@ -1,29 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Takes an object and deletes all keys that are
|
||||
*
|
||||
* tagged as false by the passed in filter function
|
||||
* Takes an object and returns a shallow copy without any keys
|
||||
* that fail the passed in filter function.
|
||||
* Does not modify the passed in object.
|
||||
*
|
||||
* @param {object} obj regular javascript object
|
||||
* @return {object} regular javascript object without null values or empty strings
|
||||
*/
|
||||
export function sanitize(obj, filterFn) {
|
||||
if(!filterFn) {
|
||||
if (!filterFn) {
|
||||
// By matching null with a double equal, we can match undefined and null
|
||||
// http://stackoverflow.com/a/15992131
|
||||
filterFn = (val) => val == null || val === '';
|
||||
}
|
||||
|
||||
Object
|
||||
.keys(obj)
|
||||
.map((key) => {
|
||||
if(filterFn(obj[key])) {
|
||||
delete obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
return omitFromObject(obj, filterFn);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,8 +74,8 @@ export function formatText() {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Checks a list of objects for key duplicates and returns a boolean
|
||||
/**
|
||||
* Checks a list of objects for key duplicates and returns a boolean
|
||||
*/
|
||||
function _doesObjectListHaveDuplicates(l) {
|
||||
let mergedList = [];
|
||||
@ -121,35 +113,7 @@ export function mergeOptions(...l) {
|
||||
throw new Error('The objects you submitted for merging have duplicates. Merge aborted.');
|
||||
}
|
||||
|
||||
let newObj = {};
|
||||
|
||||
for(let i = 1; i < l.length; i++) {
|
||||
newObj = _mergeOptions(newObj, _mergeOptions(l[i - 1], l[i]));
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a number of objects even if there're having duplicates.
|
||||
*
|
||||
* DOES NOT RETURN AN ERROR!
|
||||
*
|
||||
* Takes a list of object and merges their keys to one object.
|
||||
* Uses mergeOptions for two objects.
|
||||
* @param {[type]} l [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
export function mergeOptionsWithDuplicates(...l) {
|
||||
// If the objects submitted in the list have duplicates,in their key names,
|
||||
// abort the merge and tell the function's user to check his objects.
|
||||
let newObj = {};
|
||||
|
||||
for(let i = 1; i < l.length; i++) {
|
||||
newObj = _mergeOptions(newObj, _mergeOptions(l[i - 1], l[i]));
|
||||
}
|
||||
|
||||
return newObj;
|
||||
return Object.assign({}, ...l);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,25 +129,6 @@ export function update(a, ...l) {
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
|
||||
* @param obj1
|
||||
* @param obj2
|
||||
* @returns obj3 a new object based on obj1 and obj2
|
||||
* Taken from: http://stackoverflow.com/a/171256/1263876
|
||||
*/
|
||||
function _mergeOptions(obj1, obj2) {
|
||||
let obj3 = {};
|
||||
|
||||
for (let attrname in obj1) {
|
||||
obj3[attrname] = obj1[attrname];
|
||||
}
|
||||
for (let attrname in obj2) {
|
||||
obj3[attrname] = obj2[attrname];
|
||||
}
|
||||
return obj3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML in a string so it can be injected safely using
|
||||
* React's `dangerouslySetInnerHTML`
|
||||
@ -196,14 +141,41 @@ export function escapeHTML(s) {
|
||||
return document.createElement('div').appendChild(document.createTextNode(s)).parentNode.innerHTML;
|
||||
}
|
||||
|
||||
export function excludePropFromObject(obj, propList){
|
||||
let clonedObj = mergeOptions({}, obj);
|
||||
for (let item in propList){
|
||||
if (clonedObj[propList[item]]){
|
||||
delete clonedObj[propList[item]];
|
||||
/**
|
||||
* Returns a copy of the given object's own and inherited enumerable
|
||||
* properties, omitting any keys that pass the given filter function.
|
||||
*/
|
||||
function filterObjOnFn(obj, filterFn) {
|
||||
const filteredObj = {};
|
||||
|
||||
for (let key in obj) {
|
||||
const val = obj[key];
|
||||
if (filterFn == null || !filterFn(val, key)) {
|
||||
filteredObj[key] = val;
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
|
||||
return filteredObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to lodash's _.omit(), this returns a copy of the given object's
|
||||
* own and inherited enumerable properties, omitting any keys that are
|
||||
* in the given array or whose value pass the given filter function.
|
||||
* @param {object} obj Source object
|
||||
* @param {array|function} filter Array of key names to omit or function to invoke per iteration
|
||||
* @return {object} The new object
|
||||
*/
|
||||
export function omitFromObject(obj, filter) {
|
||||
if (filter && filter.constructor === Array) {
|
||||
return filterObjOnFn(obj, (_, key) => {
|
||||
return filter.indexOf(key) >= 0;
|
||||
});
|
||||
} else if (filter && typeof filter === 'function') {
|
||||
return filterObjOnFn(obj, filter);
|
||||
} else {
|
||||
throw new Error('The given filter is not an array or function. Exclude aborted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,15 +22,15 @@ export function getLangText(s, ...args) {
|
||||
let lang = getLang();
|
||||
try {
|
||||
if(lang in languages) {
|
||||
return formatText(languages[lang][s], args);
|
||||
return formatText(languages[lang][s], ...args);
|
||||
} else {
|
||||
// just use the english language
|
||||
return formatText(languages['en-US'][s], args);
|
||||
return formatText(languages['en-US'][s], ...args);
|
||||
}
|
||||
} catch(err) {
|
||||
//if(!(s in languages[lang])) {
|
||||
//console.warn('Language-string is not in constants file. Add: "' + s + '" to the "' + lang + '" language file. Defaulting to keyname');
|
||||
return formatText(s, args);
|
||||
return formatText(s, ...args);
|
||||
//} else {
|
||||
// console.error(err);
|
||||
//}
|
||||
|
@ -2,24 +2,14 @@
|
||||
|
||||
import Q from 'q';
|
||||
|
||||
import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils';
|
||||
|
||||
import AppConstants from '../constants/application_constants';
|
||||
|
||||
import {excludePropFromObject} from '../utils/general_utils';
|
||||
import { getCookie } from '../utils/fetch_api_utils';
|
||||
import { omitFromObject } from '../utils/general_utils';
|
||||
import { argsToQueryParams } from '../utils/url_utils';
|
||||
|
||||
|
||||
class Requests {
|
||||
_merge(defaults, options) {
|
||||
let merged = {};
|
||||
for (let key in defaults) {
|
||||
merged[key] = defaults[key];
|
||||
}
|
||||
for (let key in options) {
|
||||
merged[key] = options[key];
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
unpackResponse(response) {
|
||||
if (response.status >= 500) {
|
||||
throw new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url);
|
||||
@ -112,7 +102,7 @@ class Requests {
|
||||
|
||||
request(verb, url, options) {
|
||||
options = options || {};
|
||||
let merged = this._merge(this.httpOptions, options);
|
||||
let merged = Object.assign({}, this.httpOptions, options);
|
||||
let csrftoken = getCookie(AppConstants.csrftoken);
|
||||
if (csrftoken) {
|
||||
merged.headers['X-CSRFToken'] = csrftoken;
|
||||
@ -124,23 +114,23 @@ class Requests {
|
||||
}
|
||||
|
||||
get(url, params) {
|
||||
if (url === undefined){
|
||||
if (url === undefined) {
|
||||
throw new Error('Url undefined');
|
||||
}
|
||||
let paramsCopy = this._merge(params);
|
||||
let paramsCopy = Object.assign({}, params);
|
||||
let newUrl = this.prepareUrl(url, paramsCopy, true);
|
||||
return this.request('get', newUrl);
|
||||
}
|
||||
|
||||
delete(url, params) {
|
||||
let paramsCopy = this._merge(params);
|
||||
let paramsCopy = Object.assign({}, params);
|
||||
let newUrl = this.prepareUrl(url, paramsCopy, true);
|
||||
return this.request('delete', newUrl);
|
||||
}
|
||||
|
||||
_putOrPost(url, paramsAndBody, method){
|
||||
let paramsCopy = this._merge(paramsAndBody);
|
||||
let params = excludePropFromObject(paramsAndBody, ['body']);
|
||||
_putOrPost(url, paramsAndBody, method) {
|
||||
let paramsCopy = Object.assign({}, paramsAndBody);
|
||||
let params = omitFromObject(paramsAndBody, ['body']);
|
||||
let newUrl = this.prepareUrl(url, params);
|
||||
let body = null;
|
||||
if (paramsCopy && paramsCopy.body) {
|
||||
@ -153,11 +143,11 @@ class Requests {
|
||||
return this._putOrPost(url, params, 'post');
|
||||
}
|
||||
|
||||
put(url, params){
|
||||
put(url, params) {
|
||||
return this._putOrPost(url, params, 'put');
|
||||
}
|
||||
|
||||
patch(url, params){
|
||||
patch(url, params) {
|
||||
return this._putOrPost(url, params, 'patch');
|
||||
}
|
||||
|
||||
|
83
js/utils/url_utils.js
Normal file
83
js/utils/url_utils.js
Normal file
@ -0,0 +1,83 @@
|
||||
'use strict'
|
||||
|
||||
import camelCase from 'camelcase';
|
||||
import decamelize from 'decamelize';
|
||||
import qs from 'qs';
|
||||
|
||||
import { sanitize } from './general_utils';
|
||||
|
||||
// TODO: Create Unittests that test all functions
|
||||
|
||||
/**
|
||||
* Takes a key-value dictionary of this form:
|
||||
*
|
||||
* {
|
||||
* 'page': 1,
|
||||
* 'pageSize': 10
|
||||
* }
|
||||
*
|
||||
* and converts it to a query-parameter, which you can append to your URL.
|
||||
* The return looks like this:
|
||||
*
|
||||
* ?page=1&page_size=10
|
||||
*
|
||||
* CamelCase gets converted to snake_case!
|
||||
*
|
||||
* @param {object} obj Query params dictionary
|
||||
* @return {string} Query params string
|
||||
*/
|
||||
export function argsToQueryParams(obj) {
|
||||
const sanitizedObj = sanitize(obj);
|
||||
const queryParamObj = {};
|
||||
|
||||
Object
|
||||
.keys(sanitizedObj)
|
||||
.forEach((key) => {
|
||||
queryParamObj[decamelize(key)] = sanitizedObj[key];
|
||||
});
|
||||
|
||||
// Use bracket arrayFormat as history.js and react-router use it
|
||||
return '?' + qs.stringify(queryParamObj, { arrayFormat: 'brackets' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current url's query params as an key-val dictionary.
|
||||
* snake_case gets converted to CamelCase!
|
||||
* @return {object} Query params dictionary
|
||||
*/
|
||||
export function getCurrentQueryParams() {
|
||||
return queryParamsToArgs(window.location.search.substring(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given query param string into a key-val dictionary.
|
||||
* snake_case gets converted to CamelCase!
|
||||
* @param {string} queryParamString Query params string
|
||||
* @return {object} Query params dictionary
|
||||
*/
|
||||
export function queryParamsToArgs(queryParamString) {
|
||||
const qsQueryParamObj = qs.parse(queryParamString);
|
||||
const camelCaseParamObj = {};
|
||||
|
||||
Object
|
||||
.keys(qsQueryParamObj)
|
||||
.forEach((key) => {
|
||||
camelCaseParamObj[camelCase(key)] = qsQueryParamObj[key];
|
||||
});
|
||||
|
||||
return camelCaseParamObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string and a boolean and generates a string query parameter for
|
||||
* an API call.
|
||||
*/
|
||||
export function generateOrderingQueryParams(orderBy, orderAsc) {
|
||||
let interpolation = '';
|
||||
|
||||
if(!orderAsc) {
|
||||
interpolation += '-';
|
||||
}
|
||||
|
||||
return interpolation + orderBy;
|
||||
}
|
@ -46,8 +46,10 @@
|
||||
"browser-sync": "^2.7.5",
|
||||
"browserify": "^9.0.8",
|
||||
"browserify-shim": "^3.8.10",
|
||||
"camelcase": "^1.2.1",
|
||||
"classnames": "^1.2.2",
|
||||
"compression": "^1.4.4",
|
||||
"decamelize": "^1.1.1",
|
||||
"envify": "^3.4.0",
|
||||
"eslint": "^0.22.1",
|
||||
"eslint-plugin-react": "^2.5.0",
|
||||
@ -73,6 +75,7 @@
|
||||
"object-assign": "^2.0.0",
|
||||
"opn": "^3.0.2",
|
||||
"q": "^1.4.1",
|
||||
"qs": "^4.0.0",
|
||||
"raven-js": "^1.1.19",
|
||||
"react": "0.13.2",
|
||||
"react-bootstrap": "0.25.1",
|
||||
|
@ -508,7 +508,10 @@ fieldset[disabled] .btn-secondary.active {
|
||||
> pre,
|
||||
> select,
|
||||
> span:not(.glyphicon),
|
||||
> p,
|
||||
> p > span,
|
||||
> textarea {
|
||||
color: $ascribe-dark-blue;
|
||||
font-family: $ascribe--font;
|
||||
font-weight: $ascribe--font-weight-light;
|
||||
}
|
||||
|
@ -23,4 +23,4 @@
|
||||
@media (max-width: 550px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -30,11 +30,14 @@ $ascribe-red-error: rgb(169, 68, 66);
|
||||
border-left: 3px solid rgba($ascribe-red-error, 1);
|
||||
|
||||
> div {
|
||||
> span {
|
||||
color: rgba($ascribe-red-error, 1);
|
||||
font-size: .9em;
|
||||
margin-right: 1em;
|
||||
> p {
|
||||
> span {
|
||||
color: rgba($ascribe-red-error, 1);
|
||||
font-size: .9em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> input,
|
||||
> textarea {
|
||||
@ -86,10 +89,13 @@ $ascribe-red-error: rgb(169, 68, 66);
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: rgba(0, 0, 0, .5);
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
> p {
|
||||
height: 20px;
|
||||
margin-bottom: 0;
|
||||
> span {
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
@ -107,6 +113,11 @@ $ascribe-red-error: rgb(169, 68, 66);
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
> .upload-button-wrapper {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
> input,
|
||||
> pre,
|
||||
> textarea,
|
||||
@ -139,7 +150,7 @@ $ascribe-red-error: rgb(169, 68, 66);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> textarea{
|
||||
> textarea {
|
||||
color: #666;
|
||||
margin-top: 1em;
|
||||
padding: 0;
|
||||
@ -218,4 +229,4 @@ $ascribe-red-error: rgb(169, 68, 66);
|
||||
> span > span {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -177,3 +177,15 @@
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.upload-button-wrapper {
|
||||
text-align: left;
|
||||
|
||||
.btn {
|
||||
font-size: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
span + .btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
@import 'sluice/sluice_custom_style';
|
||||
@import 'simple_prize/simple_prize_custom_style';
|
||||
@import 'portfolioreview/portfolioreview_custom_style';
|
||||
|
||||
.ascribe-prize-app {
|
||||
border-radius: 0;
|
||||
|
@ -0,0 +1,100 @@
|
||||
$pr--nav-fg-prim-color: black;
|
||||
$pr--button-color: $pr--nav-fg-prim-color;
|
||||
|
||||
.client--portfolioreview {
|
||||
padding-top: 0 !important;
|
||||
|
||||
.btn-wide,
|
||||
.btn-default {
|
||||
background-color: $pr--button-color;
|
||||
border-color: $pr--button-color;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&:active:hover,
|
||||
&:active:focus,
|
||||
&:active.focus,
|
||||
&.active:hover,
|
||||
&.active:focus,
|
||||
&.active.focus {
|
||||
background-color: lighten($pr--button-color, 20%);
|
||||
border-color: lighten($pr--button-color, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
.ascribe-property {
|
||||
> p > span:not(> .span),
|
||||
> textarea,
|
||||
> input {
|
||||
color: $pr--nav-fg-prim-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ascribe-property-wrapper:hover {
|
||||
border-left-color: lighten($pr--nav-fg-prim-color, 60%);
|
||||
}
|
||||
|
||||
.is-focused {
|
||||
border-left-color: $pr--nav-fg-prim-color !important;
|
||||
background-color: lighten($pr--nav-fg-prim-color, 95%);
|
||||
}
|
||||
|
||||
.register-piece--info {
|
||||
text-align: center;
|
||||
|
||||
h1, h2 {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 5em;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.register-piece--form {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 3em;
|
||||
|
||||
form {
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.piece--hero {
|
||||
text-align: center;
|
||||
padding: 1em 0 1em 0;
|
||||
margin-bottom: 3em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
|
||||
background-color: white;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ascribe-property {
|
||||
> p > span:not(> .span) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
@ -178,7 +178,7 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ascribe-property > span {
|
||||
.ascribe-property > p > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user