diff --git a/README.md b/README.md index 7bca8c31..36f47954 100644 --- a/README.md +++ b/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,6 +43,7 @@ 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 ===================== diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index 185f6e05..da45d1e8 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -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={
- {new Date(this.props.content.date_created).getFullYear()} + {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} {this.getLicences()}
} buttons={ diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js deleted file mode 100644 index 67f4a430..00000000 --- a/js/components/ascribe_buttons/acl_button.js +++ /dev/null @@ -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: ( - - ), - handleSuccess: this.showNotification - }; - } - if (this.props.action === 'acl_unconsign'){ - return { - title: getLangText('Unconsign artwork'), - tooltip: getLangText('Have the owner manage his sales again'), - form: ( - - ), - handleSuccess: this.showNotification - }; - }else if (this.props.action === 'acl_transfer') { - return { - title: getLangText('Transfer artwork'), - tooltip: getLangText('Transfer the ownership of the artwork'), - form: ( - - ), - 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: ( - ), - 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: ( - ), - handleSuccess: this.showNotification - }; - } - else if (this.props.action === 'acl_share'){ - return { - title: getLangText('Share artwork'), - tooltip: getLangText('Share the artwork'), - form: ( - - ), - 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 ( - - {this.sanitizeAction()} - - } - handleSuccess={aclProps.handleSuccess} - title={aclProps.title}> - {aclProps.form} - - ); - } - return null; - } -}); - -export default AclButton; \ No newline at end of file diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index e87a6407..83495363 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -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 (
- - - - - {this.renderChildren()} @@ -123,4 +122,4 @@ let AclButtonList = React.createClass({ } }); -export default AclButtonList; \ No newline at end of file +export default AclButtonList; diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js new file mode 100644 index 00000000..c247c9ba --- /dev/null +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -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 ( + + + {this.sanitizeAction()} + + } + handleSuccess={handleSuccess} + title={title}> + + + + ); + } + }); +} diff --git a/js/components/ascribe_buttons/acls/consign_button.js b/js/components/ascribe_buttons/acls/consign_button.js new file mode 100644 index 00000000..88c86097 --- /dev/null +++ b/js/components/ascribe_buttons/acls/consign_button.js @@ -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') +}); diff --git a/js/components/ascribe_buttons/acls/loan_button.js b/js/components/ascribe_buttons/acls/loan_button.js new file mode 100644 index 00000000..4b803ceb --- /dev/null +++ b/js/components/ascribe_buttons/acls/loan_button.js @@ -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') +}); diff --git a/js/components/ascribe_buttons/acls/loan_request_button.js b/js/components/ascribe_buttons/acls/loan_request_button.js new file mode 100644 index 00000000..a1ec5f3b --- /dev/null +++ b/js/components/ascribe_buttons/acls/loan_request_button.js @@ -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') +}); diff --git a/js/components/ascribe_buttons/acls/share_button.js b/js/components/ascribe_buttons/acls/share_button.js new file mode 100644 index 00000000..83781aed --- /dev/null +++ b/js/components/ascribe_buttons/acls/share_button.js @@ -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') +}); diff --git a/js/components/ascribe_buttons/acls/transfer_button.js b/js/components/ascribe_buttons/acls/transfer_button.js new file mode 100644 index 00000000..346907ca --- /dev/null +++ b/js/components/ascribe_buttons/acls/transfer_button.js @@ -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') +}); diff --git a/js/components/ascribe_buttons/acls/unconsign_button.js b/js/components/ascribe_buttons/acls/unconsign_button.js new file mode 100644 index 00000000..ce1ed0bc --- /dev/null +++ b/js/components/ascribe_buttons/acls/unconsign_button.js @@ -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') +}); diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 64cbf714..6b38ddf8 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -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({

{this.props.edition.title}

- +
+ multiple={this.props.multiple} /> ); } }); -export default FurtherDetailsFileuploader; \ No newline at end of file +export default FurtherDetailsFileuploader; diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 6ac2f745..d31a205f 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -92,7 +92,7 @@ let MediaContainer = React.createClass({ aclObject={this.props.content.acl} aclName="acl_download"> {embed} diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 3895b8b2..59fbfe5c 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -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({

{this.state.piece.title}

- + {this.state.piece.num_editions > 0 ? : null}
diff --git a/js/components/ascribe_forms/acl_form_factory.js b/js/components/ascribe_forms/acl_form_factory.js new file mode 100644 index 00000000..d5494c2d --- /dev/null +++ b/js/components/ascribe_forms/acl_form_factory.js @@ -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 ( + + ); + } else if (action === 'acl_unconsign') { + return ( + + ); + } else if (action === 'acl_transfer') { + return ( + + ); + } else if (action === 'acl_loan') { + return ( + + ); + } else if (action === 'acl_loan_request') { + return ( + + ); + } else if (action === 'acl_share') { + return ( + + ); + } else { + throw new Error('Your specified action did not match a form.'); + } + } +}); + +export default AclFormFactory; diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index fe15f537..0e3517a2 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -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'; diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js index fe00cebc..aac4c5ea 100644 --- a/js/components/ascribe_forms/form_create_contract.js +++ b/js/components/ascribe_forms/form_create_contract.js @@ -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} /> + uploadMethod={this.props.location.query.method} /> { - 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 ( - ); - } else if(this.props.notifications.action === 'loan_request') { + } else if (this.props.notifications.action === 'loan_request') { return ( - @@ -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 (
@@ -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'> {acceptButtonForm} @@ -168,10 +160,10 @@ let RequestActionForm = React.createClass({ return ( + buttons={this.getButtonForm()} /> ); } }); -export default RequestActionForm; \ No newline at end of file +export default RequestActionForm; diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index f0af2143..948521c0 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -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 ( + uploadMethod={this.props.uploadMethod} + fileClassToUpload={this.props.fileClassToUpload} /> ); } }); -export default InputFineUploader; \ No newline at end of file +export default InputFineUploader; diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 793be538..ac272988 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -181,9 +181,7 @@ let Property = React.createClass({ setErrors(errors){ this.setState({ - errors: errors.map((error) => { - return {error}; - }) + errors: errors.pop() }); }, @@ -255,8 +253,10 @@ let Property = React.createClass({ placement="top" overlay={tooltip}>
- {this.state.errors} - {this.props.label} +

+ {this.props.label} + {this.state.errors} +

{this.renderChildren(style)} {footer}
diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index a7e56fb7..19d6a1c7 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -50,25 +50,35 @@ let Other = React.createClass({ let Image = React.createClass({ propTypes: { - url: React.PropTypes.string.isRequired, + url: React.PropTypes.string, preview: React.PropTypes.string.isRequired }, mixins: [InjectInHeadMixin], componentDidMount() { - this.inject('https://code.jquery.com/jquery-2.1.4.min.js') - .then(() => - Q.all([ - this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), - this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') - ]).then(() => { window.jQuery('.shmui-ascribe').shmui(); })); + if(this.props.url) { + this.inject('https://code.jquery.com/jquery-2.1.4.min.js') + .then(() => + Q.all([ + this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), + this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') + ]).then(() => { window.jQuery('.shmui-ascribe').shmui(); })); + } }, render() { - return ( - - ); + const { url, preview } = this.props; + + if(url) { + return ( + + ); + } else { + return ( + + ); + } } }); @@ -202,26 +212,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 (

We successfully received your video and it is now being encoded.
You can leave this page and check back on the status later.

-
); } 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 (
- +
); } diff --git a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js index 3e5b6495..2eedbd4c 100644 --- a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js +++ b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js @@ -117,7 +117,7 @@ let PieceListBulkModal = React.createClass({
{ return React.createClass({ + displayName: 'AuthProxyHandler', + propTypes: { location: object }, diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js index f3bab156..aa13264a 100644 --- a/js/components/ascribe_settings/contract_settings_update_button.js +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -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 ( + submitFile={this.submitFile} /> ); } }); -export default ContractSettingsUpdateButton; \ No newline at end of file +export default ContractSettingsUpdateButton; diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 95ebd150..0cc7ff5e 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -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 }, clearSelection() { @@ -141,19 +141,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; @@ -189,8 +189,8 @@ let FileDragAndDrop = React.createClass({ hasFiles={hasFiles} onClick={this.handleOnClick} enableLocalHashing={enableLocalHashing} - fileClassToUpload={fileClassToUpload} - location={location}/> + uploadMethod={uploadMethod} + fileClassToUpload={fileClassToUpload} /> {getLangText('Drag %s here', fileClass)}

,

{getLangText('or')}

@@ -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 (

{getLangText('Would you rather')}

{getLangText('Hash your work')} @@ -64,9 +67,9 @@ let FileDragAndDropDialog = React.createClass({ or - + {getLangText('Upload and hash your work')} @@ -75,26 +78,27 @@ let FileDragAndDropDialog = React.createClass({
); } else { - if(this.props.multipleFiles) { + if (multipleFiles) { return ( - {this.getDragDialog(this.props.fileClassToUpload.plural)} + {this.getDragDialog(fileClassToUpload.plural)} - {getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} + onClick={onClick}> + {getLangText('choose %s to upload', fileClassToUpload.plural)} ); } 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 ( - {this.getDragDialog(this.props.fileClassToUpload.singular)} + {this.getDragDialog(fileClassToUpload.singular)} + onClick={onClick}> {dialog} @@ -105,4 +109,4 @@ let FileDragAndDropDialog = React.createClass({ } }); -export default FileDragAndDropDialog; \ No newline at end of file +export default FileDragAndDropDialog; diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index 1547272e..252adabb 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -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 ( + + + {' ' + truncateTextAtCharIndex(uploadedFile.name, 40)} + + ); + } else { + return ( + {getLangText('No file chosen')} + ); + } + }, + + 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 ( - +
+ + {this.getButtonLabel()} + + + {this.getUploadedFileLabel()} +
); } }); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index e3adef58..eaa696d2 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -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() { @@ -314,18 +314,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', @@ -336,7 +345,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) => { @@ -352,16 +361,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); }); }); @@ -370,7 +379,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] = { @@ -386,7 +394,6 @@ let ReactS3FineUploader = React.createClass({ }, onUploadChunkSuccess(id, chunkData, responseJson, xhr) { - let chunks = this.state.chunks; let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte; @@ -428,7 +435,7 @@ 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 @@ -445,23 +452,28 @@ 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.cancelUploads(); let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000); @@ -614,7 +626,6 @@ let ReactS3FineUploader = React.createClass({ } else { throw new Error(getLangText('File upload could not be paused.')); } - }, handleResumeFile(fileId) { @@ -626,6 +637,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) { @@ -665,16 +680,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; @@ -686,7 +699,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 @@ -694,7 +706,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. @@ -714,18 +725,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}); @@ -846,15 +853,13 @@ let ReactS3FineUploader = React.createClass({ }, 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() { @@ -875,9 +880,8 @@ let ReactS3FineUploader = React.createClass({ onInactive, enableLocalHashing, fileClassToUpload, - validation, fileInputElement: FileInputElement, - location } = this.props; + uploadMethod } = this.props; const props = { multiple, @@ -885,8 +889,8 @@ let ReactS3FineUploader = React.createClass({ areAssetsEditable, onInactive, enableLocalHashing, + uploadMethod, fileClassToUpload, - location, onDrop: this.handleUploadFile, filesToUpload: this.state.filesToUpload, handleDeleteFile: this.handleDeleteFile, diff --git a/js/components/register_piece.js b/js/components/register_piece.js index f127c149..43ac7bb7 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -40,12 +40,6 @@ let RegisterPiece = React.createClass( { mixins: [History], - getDefaultProps() { - return { - canSpecifyEditions: true - }; - }, - getInitialState(){ return mergeOptions( UserStore.getState(), diff --git a/js/components/whitelabel/prize/actions/prize_actions.js b/js/components/whitelabel/prize/actions/prize_actions.js deleted file mode 100644 index fcd9e91e..00000000 --- a/js/components/whitelabel/prize/actions/prize_actions.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/js/components/whitelabel/prize/components/prize_register_piece.js b/js/components/whitelabel/prize/components/prize_register_piece.js deleted file mode 100644 index 87a23591..00000000 --- a/js/components/whitelabel/prize/components/prize_register_piece.js +++ /dev/null @@ -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 ( - - - - - - - - - - - {' ' + getLangText('I agree to the Terms of Service the art price') + ' '} - ( - {getLangText('read')} - ) - - - - ); - } - else { - return ( -
-
- {getLangText('The prize is no longer active')} -
-
- ); - } - } -}); - -export default PrizeRegisterPiece; diff --git a/js/components/whitelabel/prize/constants/prize_api_urls.js b/js/components/whitelabel/prize/constants/prize_api_urls.js index 2d35cf19..cb4e2e44 100644 --- a/js/components/whitelabel/prize/constants/prize_api_urls.js +++ b/js/components/whitelabel/prize/constants/prize_api_urls.js @@ -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}/' - }; } diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js new file mode 100644 index 00000000..0c293b15 --- /dev/null +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -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 ( + + + + ); + } else { + return ( + + ); + } + }, + + render() { + const { location } = this.props; + + return ( +
+
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + +
+
+ + + {getLangText('By submitting this form, you agree to the') + ' '} + + {getLangText('Terms of Service')} + + {' of Portfolio Review.'} + + +
+ {this.getSubmitButton()} +
+ ); + } +}); + +export default PRRegisterPieceForm; \ No newline at end of file diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_hero.js b/js/components/whitelabel/prize/portfolioreview/components/pr_hero.js new file mode 100644 index 00000000..a0fa0811 --- /dev/null +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_hero.js @@ -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 ( +
+

Congratulations {currentUser.email}!

+

You have successfully submitted to Portfolio Review 2016

+

See below, your uploaded portfolio:

+
+ ); + } +}); + +export default PRHero; \ No newline at end of file diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js new file mode 100644 index 00000000..cdada68b --- /dev/null +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js @@ -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 ( + + + + + +

+ {getLangText('or, already an ascribe user?')} +

+ + + +
+ ); + } + return ( + + + {getLangText('Sign up to ascribe')} + + +

+ {getLangText('or, already an ascribe user?')} +

+ + + +
+ ); + }, + + getTitle() { + if (this.state.prize && this.state.prize.active){ + return ( +

+ {getLangText('This is the submission page for Portfolio Review 2016.')} +

+ ); + } + return ( +

+ {getLangText('Submissions for Portfolio Review 2016 are now closed.')} +

+ ); + }, + render() { + return ( +
+
+
+

+ {getLangText('Welcome to Portfolio Review 2016')} +

+ {this.getTitle()} + {this.getButtons()} +
+
+
+ ); + } +}); + +export default PRLanding; \ No newline at end of file diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js new file mode 100644 index 00000000..a2a70a97 --- /dev/null +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js @@ -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 ( + + +
+

Portfolio Review

+

{getLangText('Submission closing on %s', ' 22 Dec 2015')}

+

+ {getLangText("You're submitting as %s. ", currentUser.email)} + {getLangText('Change account?')} +

+
+ + + + +
+ ); + } +}); + +export default PRRegisterPiece; \ No newline at end of file diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js new file mode 100644 index 00000000..7e66c43c --- /dev/null +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -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 = ; + } + + return ( +
+ {header} +
+ {children} + + +
+
+ ); + } +}); + +export default PRApp; diff --git a/js/components/whitelabel/prize/prize_routes.js b/js/components/whitelabel/prize/prize_routes.js index 14d8b4d9..1f2c6374 100644 --- a/js/components/whitelabel/prize/prize_routes.js +++ b/js/components/whitelabel/prize/prize_routes.js @@ -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 ( - - +const ROUTES = { + sluice: ( + + + component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPLoginContainer)} /> + component={AuthProxyHandler({to: '/collection', when: 'loggedIn'})(SPSignupContainer)} /> + component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SPSettingsContainer)}/> - + - ); + ), + portfolioreview: ( + + + + + + + + + + + ) +}; + + +function getRoutes(commonRoutes, subdomain) { + if(subdomain in ROUTES) { + return ROUTES[subdomain]; + } else { + throw new Error('Subdomain wasn\'t specified in the wallet app.'); + } } diff --git a/js/components/whitelabel/prize/simple_prize/actions/prize_actions.js b/js/components/whitelabel/prize/simple_prize/actions/prize_actions.js new file mode 100644 index 00000000..dbca1b5d --- /dev/null +++ b/js/components/whitelabel/prize/simple_prize/actions/prize_actions.js @@ -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); \ No newline at end of file diff --git a/js/components/whitelabel/prize/actions/prize_jury_actions.js b/js/components/whitelabel/prize/simple_prize/actions/prize_jury_actions.js similarity index 97% rename from js/components/whitelabel/prize/actions/prize_jury_actions.js rename to js/components/whitelabel/prize/simple_prize/actions/prize_jury_actions.js index 9bd03f59..24cf08e8 100644 --- a/js/components/whitelabel/prize/actions/prize_jury_actions.js +++ b/js/components/whitelabel/prize/simple_prize/actions/prize_jury_actions.js @@ -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'; diff --git a/js/components/whitelabel/prize/actions/prize_rating_actions.js b/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js similarity index 98% rename from js/components/whitelabel/prize/actions/prize_rating_actions.js rename to js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js index 184d84e7..68b5334b 100644 --- a/js/components/whitelabel/prize/actions/prize_rating_actions.js +++ b/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js @@ -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'; diff --git a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js similarity index 87% rename from js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js rename to js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js index caef504b..965b9012 100644 --- a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js @@ -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={
- {new Date(this.props.content.date_created).getFullYear()} + {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}
} buttons={this.getPrizeButtons()} badge={this.getPrizeBadge()}> diff --git a/js/components/whitelabel/prize/components/ascribe_buttons/submit_to_prize_button.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_buttons/submit_to_prize_button.js similarity index 86% rename from js/components/whitelabel/prize/components/ascribe_buttons/submit_to_prize_button.js rename to js/components/whitelabel/prize/simple_prize/components/ascribe_buttons/submit_to_prize_button.js index 409b8aa1..8ceb87ea 100644 --- a/js/components/whitelabel/prize/components/ascribe_buttons/submit_to_prize_button.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_buttons/submit_to_prize_button.js @@ -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: { diff --git a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js similarity index 82% rename from js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js rename to js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js index f2e22412..93ca50f3 100644 --- a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js @@ -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({ -
+

{this.state.piece.title}

- + {artistEmail} {this.getActions()}
@@ -157,7 +162,7 @@ let PieceContainer = React.createClass({ piece={this.state.piece} currentUser={this.state.currentUser}/> }> - + ); } 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 (
- + - + {getLangText('Next')}
+
); } @@ -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({
{Object.keys(this.props.piece.extra_data).map((data) => { let label = data.replace('_', ' '); + const value = this.props.piece.extra_data[data] || 'N/A'; + return ( - ); - } - )} -
+ defaultValue={value}/> + + ); + })} + {}} + setIsUploadReady={() => {}} + isReadyForFormSubmission={() => {}} + editable={false} + overrideForm={true} + pieceId={this.props.piece.id} + otherData={this.props.piece.other_data} + multiple={true} + location={location}/> ); diff --git a/js/components/whitelabel/prize/components/prize_hero.js b/js/components/whitelabel/prize/simple_prize/components/prize_hero.js similarity index 83% rename from js/components/whitelabel/prize/components/prize_hero.js rename to js/components/whitelabel/prize/simple_prize/components/prize_hero.js index b98f407e..8842acf9 100644 --- a/js/components/whitelabel/prize/components/prize_hero.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_hero.js @@ -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({ diff --git a/js/components/whitelabel/prize/components/prize_landing.js b/js/components/whitelabel/prize/simple_prize/components/prize_landing.js similarity index 93% rename from js/components/whitelabel/prize/components/prize_landing.js rename to js/components/whitelabel/prize/simple_prize/components/prize_landing.js index 355b3786..e26a05b5 100644 --- a/js/components/whitelabel/prize/components/prize_landing.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_landing.js @@ -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({ diff --git a/js/components/whitelabel/prize/components/prize_login_container.js b/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js similarity index 83% rename from js/components/whitelabel/prize/components/prize_login_container.js rename to js/components/whitelabel/prize/simple_prize/components/prize_login_container.js index 9a0de06d..e168ca68 100644 --- a/js/components/whitelabel/prize/components/prize_login_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js @@ -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({ diff --git a/js/components/whitelabel/prize/components/prize_piece_list.js b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js similarity index 86% rename from js/components/whitelabel/prize/components/prize_piece_list.js rename to js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js index 7a6a90ac..8e602012 100644 --- a/js/components/whitelabel/prize/components/prize_piece_list.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js @@ -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: { diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js new file mode 100644 index 00000000..ca4e8fa9 --- /dev/null +++ b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js @@ -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 ( +
+ + + + + + + + + + + {' ' + getLangText('I agree to the Terms of Service the art price') + ' '} + ( + {getLangText('read')} + ) + + + + +
+ ); + } + else { + return ( +
+
+ {getLangText('The prize is no longer active')} +
+
+ ); + } + } +}); + +export default PrizeRegisterPiece; diff --git a/js/components/whitelabel/prize/components/prize_settings_container.js b/js/components/whitelabel/prize/simple_prize/components/prize_settings_container.js similarity index 91% rename from js/components/whitelabel/prize/components/prize_settings_container.js rename to js/components/whitelabel/prize/simple_prize/components/prize_settings_container.js index 81d62380..145a9d24 100644 --- a/js/components/whitelabel/prize/components/prize_settings_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_settings_container.js @@ -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({ diff --git a/js/components/whitelabel/prize/components/prize_signup_container.js b/js/components/whitelabel/prize/simple_prize/components/prize_signup_container.js similarity index 86% rename from js/components/whitelabel/prize/components/prize_signup_container.js rename to js/components/whitelabel/prize/simple_prize/components/prize_signup_container.js index 884062da..7a44d521 100644 --- a/js/components/whitelabel/prize/components/prize_signup_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_signup_container.js @@ -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: { diff --git a/js/components/whitelabel/prize/fetchers/prize_fetcher.js b/js/components/whitelabel/prize/simple_prize/fetchers/prize_fetcher.js similarity index 70% rename from js/components/whitelabel/prize/fetchers/prize_fetcher.js rename to js/components/whitelabel/prize/simple_prize/fetchers/prize_fetcher.js index 0bf9fc55..410d63a2 100644 --- a/js/components/whitelabel/prize/fetchers/prize_fetcher.js +++ b/js/components/whitelabel/prize/simple_prize/fetchers/prize_fetcher.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../../../../utils/requests'; +import requests from '../../../../../utils/requests'; let PrizeFetcher = { diff --git a/js/components/whitelabel/prize/fetchers/prize_jury_fetcher.js b/js/components/whitelabel/prize/simple_prize/fetchers/prize_jury_fetcher.js similarity index 88% rename from js/components/whitelabel/prize/fetchers/prize_jury_fetcher.js rename to js/components/whitelabel/prize/simple_prize/fetchers/prize_jury_fetcher.js index 1c5b0a0d..973107b4 100644 --- a/js/components/whitelabel/prize/fetchers/prize_jury_fetcher.js +++ b/js/components/whitelabel/prize/simple_prize/fetchers/prize_jury_fetcher.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../../../../utils/requests'; +import requests from '../../../../../utils/requests'; let PrizeJuryFetcher = { diff --git a/js/components/whitelabel/prize/fetchers/prize_rating_fetcher.js b/js/components/whitelabel/prize/simple_prize/fetchers/prize_rating_fetcher.js similarity index 90% rename from js/components/whitelabel/prize/fetchers/prize_rating_fetcher.js rename to js/components/whitelabel/prize/simple_prize/fetchers/prize_rating_fetcher.js index 33450dd6..38d0576e 100644 --- a/js/components/whitelabel/prize/fetchers/prize_rating_fetcher.js +++ b/js/components/whitelabel/prize/simple_prize/fetchers/prize_rating_fetcher.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../../../../utils/requests'; +import requests from '../../../../../utils/requests'; let PrizeRatingFetcher = { diff --git a/js/components/whitelabel/prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js similarity index 87% rename from js/components/whitelabel/prize/prize_app.js rename to js/components/whitelabel/prize/simple_prize/prize_app.js index aadb0b05..d95d7772 100644 --- a/js/components/whitelabel/prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -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({ diff --git a/js/components/whitelabel/prize/stores/prize_jury_store.js b/js/components/whitelabel/prize/simple_prize/stores/prize_jury_store.js similarity index 96% rename from js/components/whitelabel/prize/stores/prize_jury_store.js rename to js/components/whitelabel/prize/simple_prize/stores/prize_jury_store.js index 69d73e3a..536b8633 100644 --- a/js/components/whitelabel/prize/stores/prize_jury_store.js +++ b/js/components/whitelabel/prize/simple_prize/stores/prize_jury_store.js @@ -1,6 +1,6 @@ 'use strict'; -import { alt } from '../../../../alt'; +import { alt } from '../../../../../alt'; import PrizeJuryActions from '../actions/prize_jury_actions'; diff --git a/js/components/whitelabel/prize/stores/prize_rating_store.js b/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js similarity index 93% rename from js/components/whitelabel/prize/stores/prize_rating_store.js rename to js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js index d67fa603..9f1552bb 100644 --- a/js/components/whitelabel/prize/stores/prize_rating_store.js +++ b/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js @@ -1,6 +1,6 @@ 'use strict'; -import { alt } from '../../../../alt'; +import { alt } from '../../../../../alt'; import PrizeRatingActions from '../actions/prize_rating_actions'; diff --git a/js/components/whitelabel/prize/stores/prize_store.js b/js/components/whitelabel/prize/simple_prize/stores/prize_store.js similarity index 87% rename from js/components/whitelabel/prize/stores/prize_store.js rename to js/components/whitelabel/prize/simple_prize/stores/prize_store.js index 68cc9264..8d9c4bbe 100644 --- a/js/components/whitelabel/prize/stores/prize_store.js +++ b/js/components/whitelabel/prize/simple_prize/stores/prize_store.js @@ -1,6 +1,6 @@ 'use strict'; -import { alt } from '../../../../alt'; +import { alt } from '../../../../../alt'; import PrizeActions from '../actions/prize_actions'; diff --git a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_action_panel.js b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_action_panel.js index 178be7da..bd240126 100644 --- a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_action_panel.js +++ b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_action_panel.js @@ -47,7 +47,7 @@ let WalletActionPanel = React.createClass({

{this.props.piece.title}

- +
} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js index 755e550b..9802e93e 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js @@ -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={
- {new Date(this.props.content.date_created).getFullYear()} + {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}
} buttons={this.getSubmitButtons()}> {this.props.children} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js index 1c61d573..0fe5a025 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js @@ -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({ + isInline={true} /> ); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js index 63863b2d..a7631d95 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js @@ -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} + label={getLangText('Artist Biography')} + hidden={disabled && !piece.extra_data.artist_bio}> + + label={getLangText('Conceptual Overview')} + hidden={disabled && !piece.extra_data.conceptual_overview}> + + + + + multiple={true} /> ); } else { @@ -157,4 +203,4 @@ let CylandAdditionalDataForm = React.createClass({ } }); -export default CylandAdditionalDataForm; \ No newline at end of file +export default CylandAdditionalDataForm; diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index ca755cf4..470da761 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -210,8 +210,7 @@ let CylandRegisterPiece = React.createClass({ 1} handleSuccess={this.handleAdditionalDataSuccess} - piece={this.state.piece} - location={this.props.location}/> + piece={this.state.piece} />
@@ -220,7 +219,12 @@ let CylandRegisterPiece = React.createClass({ - {new Date(this.props.content.date_created).getFullYear()} + {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} } buttons={this.getSubmitButtons()}> {this.props.children} diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index 2ff689fe..a07f29b1 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -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/', diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 0fe5e210..0ada8fe3 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -11,7 +11,7 @@ let constants = { 'serverUrl': window.SERVER_URL, 'baseUrl': window.BASE_URL, '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, @@ -46,6 +46,13 @@ let 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': { diff --git a/js/fetchers/edition_list_fetcher.js b/js/fetchers/edition_list_fetcher.js index b416c595..93e4553d 100644 --- a/js/fetchers/edition_list_fetcher.js +++ b/js/fetchers/edition_list_fetcher.js @@ -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 = { /** diff --git a/js/fetchers/piece_list_fetcher.js b/js/fetchers/piece_list_fetcher.js index 8e58402a..6bd4eb3a 100644 --- a/js/fetchers/piece_list_fetcher.js +++ b/js/fetchers/piece_list_fetcher.js @@ -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 = { /** diff --git a/js/utils/fetch_api_utils.js b/js/utils/fetch_api_utils.js index 3ed964ba..0b2d45dc 100644 --- a/js/utils/fetch_api_utils.js +++ b/js/utils/fetch_api_utils.js @@ -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(); }); -} \ No newline at end of file +} diff --git a/js/utils/file_utils.js b/js/utils/file_utils.js index 964ba7b8..3454404a 100644 --- a/js/utils/file_utils.js +++ b/js/utils/file_utils.js @@ -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); diff --git a/js/utils/form_utils.js b/js/utils/form_utils.js index 7f9cfb07..c15eb067 100644 --- a/js/utils/form_utils.js +++ b/js/utils/form_utils.js @@ -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; -} \ No newline at end of file +} + +function getTitlesStringOfPiece(piece){ + return '\"' + piece.title + '\"'; +} + +function getTitlesStringOfEditions(editions) { + return editions.map(function(edition) { + return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n'; + }).join(''); +} diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 7c13f9b5..e717fa75 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -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'); + } } /** diff --git a/js/utils/lang_utils.js b/js/utils/lang_utils.js index f2e2fa14..ee2d292c 100644 --- a/js/utils/lang_utils.js +++ b/js/utils/lang_utils.js @@ -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); //} diff --git a/js/utils/requests.js b/js/utils/requests.js index 7e9c9a58..a7300634 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -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'); } diff --git a/js/utils/url_utils.js b/js/utils/url_utils.js new file mode 100644 index 00000000..86c8dfc5 --- /dev/null +++ b/js/utils/url_utils.js @@ -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; +} diff --git a/package.json b/package.json index 2b770fd3..63c6d1e0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/sass/ascribe_custom_style.scss b/sass/ascribe_custom_style.scss index 05d39027..98cce937 100644 --- a/sass/ascribe_custom_style.scss +++ b/sass/ascribe_custom_style.scss @@ -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; } diff --git a/sass/ascribe_form.scss b/sass/ascribe_form.scss index 41e17de1..1b265e91 100644 --- a/sass/ascribe_form.scss +++ b/sass/ascribe_form.scss @@ -23,4 +23,4 @@ @media (max-width: 550px) { width: 100%; } -} +} \ No newline at end of file diff --git a/sass/ascribe_property.scss b/sass/ascribe_property.scss index e938a7c3..0bfd5496 100644 --- a/sass/ascribe_property.scss +++ b/sass/ascribe_property.scss @@ -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; } -} +} \ No newline at end of file diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index 7819eda0..49bc70e9 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -177,3 +177,15 @@ height: 12px; } +.upload-button-wrapper { + text-align: left; + + .btn { + font-size: 1em; + margin-right: 1em; + } + + span + .btn { + margin-left: 1em; + } +} \ No newline at end of file diff --git a/sass/whitelabel/prize/index.scss b/sass/whitelabel/prize/index.scss index 5bc75746..24645c8a 100644 --- a/sass/whitelabel/prize/index.scss +++ b/sass/whitelabel/prize/index.scss @@ -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; diff --git a/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss new file mode 100644 index 00000000..d8ccd858 --- /dev/null +++ b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/sass/whitelabel/prize/sluice/sluice_custom_style.scss b/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss similarity index 100% rename from sass/whitelabel/prize/sluice/sluice_custom_style.scss rename to sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss diff --git a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss index 52affdaf..70a5cd18 100644 --- a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss +++ b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss @@ -178,7 +178,7 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; border: none; } - .ascribe-property > span { + .ascribe-property > p > span { color: white; }