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

Merge branch 'master' into AD-1149-implement-lumenus-the-lumen-mark

This commit is contained in:
Brett Sun 2015-11-24 10:42:15 +01:00
commit efdc5d32e7
41 changed files with 600 additions and 232 deletions

View File

@ -10,6 +10,7 @@ queryParams of the piece_list_store should all be reflected in the url and not a
- Use classNames plugin instead of if-conditional-classes - Use classNames plugin instead of if-conditional-classes
- Instead of using `currentUser && currentUser.email` in an validation that checks whether we user is logged in or now, in the `UserStore` on login we set a boolean property called `isLoggedIn` that can then be used instead of `email` - Instead of using `currentUser && currentUser.email` in an validation that checks whether we user is logged in or now, in the `UserStore` on login we set a boolean property called `isLoggedIn` that can then be used instead of `email`
- Refactor AclProxy to be a generic hide/show element component. Have it take data input and a validation function to assess whether it should show or hide child elements. Move current Acl checks to another place, eg. acl_utils.js. - Refactor AclProxy to be a generic hide/show element component. Have it take data input and a validation function to assess whether it should show or hide child elements. Move current Acl checks to another place, eg. acl_utils.js.
- Convert all fetchers to [alt.js's sources](http://alt.js.org/docs/async/)
# Refactor DONE # Refactor DONE
- Refactor forms to generic-declarative form component ✓ - Refactor forms to generic-declarative form component ✓

View File

@ -1,37 +1,18 @@
'use strict'; 'use strict';
import { altUser } from '../alt'; import { altUser } from '../alt';
import UserFetcher from '../fetchers/user_fetcher';
class UserActions { class UserActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateCurrentUser', 'fetchCurrentUser',
'deleteCurrentUser' 'successFetchCurrentUser',
'logoutCurrentUser',
'successLogoutCurrentUser',
'errorCurrentUser'
); );
} }
fetchCurrentUser() {
UserFetcher.fetchOne()
.then((res) => {
this.actions.updateCurrentUser(res.users[0]);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCurrentUser({});
});
}
logoutCurrentUser() {
UserFetcher.logout()
.then(() => {
this.actions.deleteCurrentUser();
})
.catch((err) => {
console.logGlobal(err);
});
}
} }
export default altUser.createActions(UserActions); export default altUser.createActions(UserActions);

View File

@ -1,29 +1,16 @@
'use strict'; 'use strict';
import { altWhitelabel } from '../alt'; import { altWhitelabel } from '../alt';
import WhitelabelFetcher from '../fetchers/whitelabel_fetcher';
class WhitelabelActions { class WhitelabelActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateWhitelabel' 'fetchWhitelabel',
'successFetchWhitelabel',
'errorWhitelabel'
); );
} }
fetchWhitelabel() {
WhitelabelFetcher.fetch()
.then((res) => {
if(res && res.whitelabel) {
this.actions.updateWhitelabel(res.whitelabel);
} else {
this.actions.updateWhitelabel({});
}
})
.catch((err) => {
console.logGlobal(err);
});
}
} }
export default altWhitelabel.createActions(WhitelabelActions); export default altWhitelabel.createActions(WhitelabelActions);

View File

@ -30,6 +30,7 @@ import GoogleAnalyticsHandler from './third_party/ga';
import RavenHandler from './third_party/raven'; import RavenHandler from './third_party/raven';
import IntercomHandler from './third_party/intercom'; import IntercomHandler from './third_party/intercom';
import NotificationsHandler from './third_party/notifications'; import NotificationsHandler from './third_party/notifications';
import FacebookHandler from './third_party/facebook';
/* eslint-enable */ /* eslint-enable */
initLogging(); initLogging();

View File

@ -6,9 +6,9 @@ import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import ConsignButton from './acls/consign_button'; import ConsignButton from './acls/consign_button';
import EmailButton from './acls/email_button';
import LoanButton from './acls/loan_button'; import LoanButton from './acls/loan_button';
import LoanRequestButton from './acls/loan_request_button'; import LoanRequestButton from './acls/loan_request_button';
import ShareButton from './acls/share_button';
import TransferButton from './acls/transfer_button'; import TransferButton from './acls/transfer_button';
import UnconsignButton from './acls/unconsign_button'; import UnconsignButton from './acls/unconsign_button';
@ -41,7 +41,7 @@ let AclButtonList = React.createClass({
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser.defer();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
@ -90,7 +90,7 @@ let AclButtonList = React.createClass({
return ( return (
<div className={className}> <div className={className}>
<span ref="buttonList" style={buttonsStyle}> <span ref="buttonList" style={buttonsStyle}>
<ShareButton <EmailButton
availableAcls={availableAcls} availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions} pieceOrEditions={pieceOrEditions}
currentUser={currentUser} currentUser={currentUser}

View File

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { InformationTexts } from '../../constants/information_text'; import { AclInformationText } from '../../constants/acl_information_text';
import { replaceSubstringAtIndex, sanitize, intersectLists } from '../../utils/general_utils'; import { replaceSubstringAtIndex, sanitize, intersectLists } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -38,7 +38,7 @@ let AclInformation = React.createClass({
}, },
getInfoText(title, info, example){ getInfoText(title, info, example){
let aim = this.props.aim; const aim = this.props.aim;
if(aim) { if(aim) {
if(aim === 'form') { if(aim === 'form') {
@ -75,7 +75,7 @@ let AclInformation = React.createClass({
}, },
produceInformationBlock() { produceInformationBlock() {
const { titles, informationSentences, exampleSentences } = InformationTexts; const { titles, informationSentences, exampleSentences } = AclInformationText;
const { verbs, aim } = this.props; const { verbs, aim } = this.props;
const availableInformations = intersectLists(verbs, Object.keys(titles)); const availableInformations = intersectLists(verbs, Object.keys(titles));
@ -95,7 +95,13 @@ let AclInformation = React.createClass({
} }
return verbsToDisplay.map((verb) => { return verbsToDisplay.map((verb) => {
return this.getInfoText(getLangText(titles[verb]), getLangText(informationSentences[verb]), getLangText(exampleSentences[verb])); const title = titles[verb];
const informationSentence = informationSentences[verb];
const exampleSentence = exampleSentences[verb];
if (title && informationSentence && exampleSentence) {
return this.getInfoText(getLangText(title), getLangText(informationSentence), getLangText(exampleSentence));
}
}); });
}, },

View File

@ -11,6 +11,8 @@ import ModalWrapper from '../../ascribe_modal/modal_wrapper';
import AppConstants from '../../../constants/application_constants'; import AppConstants from '../../../constants/application_constants';
import { AclInformationText } from '../../../constants/acl_information_text';
export default function ({ action, displayName, title, tooltip }) { export default function ({ action, displayName, title, tooltip }) {
if (AppConstants.aclList.indexOf(action) < 0) { if (AppConstants.aclList.indexOf(action) < 0) {
@ -34,12 +36,11 @@ export default function ({ action, displayName, title, tooltip }) {
className: React.PropTypes.string className: React.PropTypes.string
}, },
// Removes the acl_ prefix and converts to upper case
sanitizeAction() { sanitizeAction() {
if (this.props.buttonAcceptName) { if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName; return this.props.buttonAcceptName;
} }
return action.split('acl_')[1].toUpperCase(); return AclInformationText.titles[action];
}, },
render() { render() {

View File

@ -8,7 +8,7 @@ import { getLangText } from '../../../utils/lang_utils';
export default AclButton({ export default AclButton({
action: 'acl_share', action: 'acl_share',
displayName: 'ShareButton', displayName: 'EmailButton',
title: getLangText('Share artwork'), title: getLangText('Share artwork via email'),
tooltip: getLangText('Share the artwork') tooltip: getLangText("Share the artwork to another user's collection through email")
}); });

View File

@ -21,13 +21,13 @@ let CollapsibleButton = React.createClass({
this.setState({expanded: !this.state.expanded}); this.setState({expanded: !this.state.expanded});
}, },
render() { render() {
let isVisible = (this.state.expanded) ? '' : 'invisible'; let isHidden = (this.state.expanded) ? '' : 'hidden';
return ( return (
<span> <span>
<span onClick={this.handleToggle}> <span onClick={this.handleToggle}>
{this.props.button} {this.props.button}
</span> </span>
<div ref='panel' className={isVisible}> <div ref='panel' className={isHidden}>
{this.props.panel} {this.props.panel}
</div> </div>
</span> </span>

View File

@ -172,7 +172,7 @@ let EditionActionPanel = React.createClass({
editions={[edition]}/> editions={[edition]}/>
<AclInformation <AclInformation
aim="button" aim="button"
verbs={['acl_share', 'acl_consign', 'acl_loan', 'acl_delete']} verbs={['acl_share', 'acl_transfer', 'acl_consign', 'acl_loan', 'acl_delete']}
aclObject={edition.acl}/> aclObject={edition.acl}/>
</ActionPanelButtonListType> </ActionPanelButtonListType>
</Col> </Col>

View File

@ -5,11 +5,33 @@ import React from 'react';
import Form from '../ascribe_forms/form'; import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property'; import Property from '../ascribe_forms/property';
import { replaceSubstringAtIndex } from '../../utils/general_utils';
let HistoryIterator = React.createClass({ let HistoryIterator = React.createClass({
propTypes: { propTypes: {
history: React.PropTypes.array history: React.PropTypes.array
}, },
composeHistoryDescription(historicalEvent) {
if(historicalEvent.length === 3) {
// We want to get the capturing group without the quotes,
// which is why we access the match list at index 1 and not 0
const contractName = historicalEvent[1].match(/\"(.*)\"/)[1];
const historicalEventDescription = replaceSubstringAtIndex(historicalEvent[1], `"${contractName}"`, '');
return (
<span>
{historicalEventDescription}
<a href={historicalEvent[2]} target="_blank">{contractName}</a>
</span>
);
} else if(historicalEvent.length === 2) {
return historicalEvent[1];
} else {
throw new Error('Expected an historical event list with either 3 or 2 items. Got less or more.');
}
},
render() { render() {
return ( return (
<Form> <Form>
@ -20,7 +42,7 @@ let HistoryIterator = React.createClass({
key={i} key={i}
label={ historicalEvent[0] } label={ historicalEvent[0] }
editable={false}> editable={false}>
<pre className="ascribe-pre">{ historicalEvent[1] }</pre> <pre className="ascribe-pre">{this.composeHistoryDescription(historicalEvent)}</pre>
</Property> </Property>
); );
})} })}

View File

@ -7,10 +7,18 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MediaPlayer from './../ascribe_media/media_player'; import MediaPlayer from './../ascribe_media/media_player';
import FacebookShareButton from '../ascribe_social_share/facebook_share_button';
import TwitterShareButton from '../ascribe_social_share/twitter_share_button';
import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy'; import AclProxy from '../acl_proxy';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import { mergeOptions } from '../../utils/general_utils.js';
import { getLangText } from '../../utils/lang_utils.js';
const EMBED_IFRAME_HEIGHT = { const EMBED_IFRAME_HEIGHT = {
video: 315, video: 315,
@ -24,10 +32,17 @@ let MediaContainer = React.createClass({
}, },
getInitialState() { getInitialState() {
return {timerId: null}; return mergeOptions(
UserStore.getState(),
{
timerId: null
});
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
if (!this.props.content.digital_work) { if (!this.props.content.digital_work) {
return; return;
} }
@ -45,19 +60,32 @@ let MediaContainer = React.createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
window.clearInterval(this.state.timerId); window.clearInterval(this.state.timerId);
}, },
onChange(state) {
this.setState(state);
},
render() { render() {
let thumbnail = this.props.content.thumbnail.thumbnail_sizes && this.props.content.thumbnail.thumbnail_sizes['600x600'] ? const { content } = this.props;
this.props.content.thumbnail.thumbnail_sizes['600x600'] : this.props.content.thumbnail.url_safe; // Pieces and editions are joined to the user by a foreign key in the database, so
let mimetype = this.props.content.digital_work.mime; // the information in content will be updated if a user updates their username.
// We also force uniqueness of usernames, so this check is safe to dtermine if the
// content was registered by the current user.
const didUserRegisterContent = this.state.currentUser && (this.state.currentUser.username === content.user_registered);
let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
let mimetype = content.digital_work.mime;
let embed = null; let embed = null;
let extraData = null; let extraData = null;
let isEmbedDisabled = mimetype === 'video' && this.props.content.digital_work.isEncoding !== undefined && this.props.content.digital_work.isEncoding !== 100; let isEmbedDisabled = mimetype === 'video' && content.digital_work.isEncoding !== undefined && content.digital_work.isEncoding !== 100;
if (this.props.content.digital_work.encoding_urls) { if (content.digital_work.encoding_urls) {
extraData = this.props.content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; }); extraData = content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
} }
if (['video', 'audio'].indexOf(mimetype) > -1) { if (['video', 'audio'].indexOf(mimetype) > -1) {
@ -73,7 +101,7 @@ let MediaContainer = React.createClass({
panel={ panel={
<pre className=""> <pre className="">
{'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/' {'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/'
+ this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'} + content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
</pre> </pre>
}/> }/>
); );
@ -83,13 +111,19 @@ let MediaContainer = React.createClass({
<MediaPlayer <MediaPlayer
mimetype={mimetype} mimetype={mimetype}
preview={thumbnail} preview={thumbnail}
url={this.props.content.digital_work.url} url={content.digital_work.url}
extraData={extraData} extraData={extraData}
encodingStatus={this.props.content.digital_work.isEncoding} /> encodingStatus={content.digital_work.isEncoding} />
<p className="text-center"> <p className="text-center">
<span className="ascribe-social-button-list">
<FacebookShareButton />
<TwitterShareButton
text={getLangText('Check out %s ascribed piece', didUserRegisterContent ? 'my latest' : 'this' )} />
</span>
<AclProxy <AclProxy
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || this.props.content.acl.acl_download} show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download}
aclObject={this.props.content.acl} aclObject={content.acl}
aclName="acl_download"> aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank"> <Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
Download .{mimetype} <Glyphicon glyph="cloud-download"/> Download .{mimetype} <Glyphicon glyph="cloud-download"/>

View File

@ -222,7 +222,8 @@ let PieceContainer = React.createClass({
piece={piece}/> piece={piece}/>
<AclInformation <AclInformation
aim="button" aim="button"
verbs={['acl_share', 'acl_create_editions', 'acl_loan', 'acl_delete', 'acl_consign']} verbs={['acl_share', 'acl_transfer', 'acl_create_editions', 'acl_loan', 'acl_delete',
'acl_consign']}
aclObject={piece.acl}/> aclObject={piece.acl}/>
</AclButtonList> </AclButtonList>
</DetailProperty> </DetailProperty>

View File

@ -60,7 +60,7 @@ let LoginForm = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
if(success) { if(success) {
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser(true);
} }
}, },

View File

@ -61,7 +61,7 @@ let SignupForm = React.createClass({
// Refactor this to its own component // Refactor this to its own component
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.'); this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
} else { } else {
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser(true);
} }
}, },

View File

@ -3,12 +3,13 @@
import React from 'react'; import React from 'react';
import Q from 'q'; import Q from 'q';
import { escapeHTML } from '../../utils/general_utils';
import InjectInHeadMixin from '../../mixins/inject_in_head_mixin';
import Panel from 'react-bootstrap/lib/Panel'; import Panel from 'react-bootstrap/lib/Panel';
import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../constants/application_constants.js';
import AppConstants from '../../constants/application_constants';
import { escapeHTML } from '../../utils/general_utils';
import { InjectInHeadUtils } from '../../utils/inject_utils';
/** /**
* This is the component that implements display-specific functionality. * This is the component that implements display-specific functionality.
@ -54,15 +55,13 @@ let Image = React.createClass({
preview: React.PropTypes.string.isRequired preview: React.PropTypes.string.isRequired
}, },
mixins: [InjectInHeadMixin],
componentDidMount() { componentDidMount() {
if(this.props.url) { if(this.props.url) {
this.inject('https://code.jquery.com/jquery-2.1.4.min.js') InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl)
.then(() => .then(() =>
Q.all([ Q.all([
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'), InjectInHeadUtils.inject(AppConstants.shmui.cssUrl),
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js') InjectInHeadUtils.inject(AppConstants.shmui.sdkUrl)
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); })); ]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
} }
}, },
@ -87,10 +86,8 @@ let Audio = React.createClass({
url: React.PropTypes.string.isRequired url: React.PropTypes.string.isRequired
}, },
mixins: [InjectInHeadMixin],
componentDidMount() { componentDidMount() {
this.inject(AppConstants.baseUrl + 'static/thirdparty/audiojs/audiojs/audio.min.js').then(this.ready); InjectInHeadUtils.inject(AppConstants.audiojs.sdkUrl).then(this.ready);
}, },
ready() { ready() {
@ -121,7 +118,7 @@ let Video = React.createClass({
* `false` if we failed to load the external library) * `false` if we failed to load the external library)
* 2) render the cover using the `<Image />` component (because libraryLoaded is null) * 2) render the cover using the `<Image />` component (because libraryLoaded is null)
* 3) on `componentDidMount`, we load the external `css` and `js` resources using * 3) on `componentDidMount`, we load the external `css` and `js` resources using
* the `InjectInHeadMixin`, attaching a function to `Promise.then` to change * the `InjectInHeadUtils`, attaching a function to `Promise.then` to change
* `state.libraryLoaded` to true * `state.libraryLoaded` to true
* 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering * 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering
* a re-render * a re-render
@ -139,20 +136,22 @@ let Video = React.createClass({
encodingStatus: React.PropTypes.number encodingStatus: React.PropTypes.number
}, },
mixins: [InjectInHeadMixin],
getInitialState() { getInitialState() {
return { libraryLoaded: null, videoMounted: false }; return { libraryLoaded: null, videoMounted: false };
}, },
componentDidMount() { componentDidMount() {
Q.all([ Q.all([
this.inject('//vjs.zencdn.net/4.12/video-js.css'), InjectInHeadUtils.inject(AppConstants.videojs.cssUrl),
this.inject('//vjs.zencdn.net/4.12/video.js')]) InjectInHeadUtils.inject(AppConstants.videojs.sdkUrl)])
.then(() => this.setState({libraryLoaded: true})) .then(() => this.setState({libraryLoaded: true}))
.fail(() => this.setState({libraryLoaded: false})); .fail(() => this.setState({libraryLoaded: false}));
}, },
shouldComponentUpdate(nextProps, nextState) {
return nextState.videoMounted === false;
},
componentDidUpdate() { componentDidUpdate() {
if (this.state.libraryLoaded && !this.state.videoMounted) { if (this.state.libraryLoaded && !this.state.videoMounted) {
window.videojs('#mainvideo'); window.videojs('#mainvideo');
@ -178,10 +177,6 @@ let Video = React.createClass({
return html.join('\n'); return html.join('\n');
}, },
shouldComponentUpdate(nextProps, nextState) {
return nextState.videoMounted === false;
},
render() { render() {
if (this.state.libraryLoaded !== null) { if (this.state.libraryLoaded !== null) {
return ( return (

View File

@ -49,7 +49,11 @@ export default function AuthProxyHandler({to, when}) {
}, },
componentDidUpdate() { componentDidUpdate() {
this.redirectConditionally(); // Only refresh this component, when UserSources are not loading
// data from the server
if(!UserStore.isLoading()) {
this.redirectConditionally();
}
}, },
componentWillUnmount() { componentWillUnmount() {

View File

@ -27,7 +27,7 @@ let AccountSettings = React.createClass({
}, },
handleSuccess(){ handleSuccess(){
this.props.loadUser(); this.props.loadUser(true);
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },

View File

@ -46,8 +46,8 @@ let SettingsContainer = React.createClass({
UserStore.unlisten(this.onChange); UserStore.unlisten(this.onChange);
}, },
loadUser(){ loadUser(invalidateCache){
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser(invalidateCache);
}, },
onChange(state) { onChange(state) {

View File

@ -0,0 +1,51 @@
'use strict';
import React from 'react';
import AppConstants from '../../constants/application_constants';
import { InjectInHeadUtils } from '../../utils/inject_utils';
let FacebookShareButton = React.createClass({
propTypes: {
type: React.PropTypes.string
},
getDefaultProps() {
return {
type: 'button'
};
},
componentDidMount() {
/**
* Ideally we would only use FB.XFBML.parse() on the component that we're
* mounting, but doing this when we first load the FB sdk causes unpredictable behaviour.
* The button sometimes doesn't get initialized, likely because FB hasn't properly
* been initialized yet.
*
* To circumvent this, we always have the sdk parse the entire DOM on the initial load
* (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later.
*/
InjectInHeadUtils
.inject(AppConstants.facebook.sdkUrl)
.then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) });
},
shouldComponentUpdate(nextProps) {
return this.props.type !== nextProps.type;
},
render() {
return (
<span
ref="fbShareButton"
className="fb-share-button btn btn-ascribe-social"
data-layout={this.props.type}>
</span>
);
}
});
export default FacebookShareButton;

View File

@ -0,0 +1,55 @@
'use strict';
import React from 'react';
import AppConstants from '../../constants/application_constants';
import { InjectInHeadUtils } from '../../utils/inject_utils';
let TwitterShareButton = React.createClass({
propTypes: {
count: React.PropTypes.string,
counturl: React.PropTypes.string,
hashtags: React.PropTypes.string,
size: React.PropTypes.string,
text: React.PropTypes.string,
url: React.PropTypes.string,
via: React.PropTypes.string
},
getDefaultProps() {
return {
count: 'none',
via: 'ascribeIO'
};
},
componentDidMount() {
InjectInHeadUtils.inject(AppConstants.twitter.sdkUrl).then(this.loadTwitterButton);
},
loadTwitterButton() {
const { count, counturl, hashtags, size, text, url, via } = this.props;
twttr.widgets.createShareButton(url, this.refs.twitterShareButton.getDOMNode(), {
count,
counturl,
hashtags,
size,
text,
via,
dnt: true // Do not track
});
},
render() {
return (
<span
ref="twitterShareButton"
className="btn btn-ascribe-social">
</span>
);
}
});
export default TwitterShareButton;

View File

@ -58,8 +58,14 @@ let FileDragAndDropDialog = React.createClass({
return ( return (
<div className="file-drag-and-drop-dialog present-options"> <div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p> <p>{getLangText('Would you rather')}</p>
{/*
The frontend in live is hosted under /app,
Since `Link` is appending that base url, if its defined
by itself, we need to make sure to not set it at this point.
Otherwise it will be appended twice.
*/}
<Link <Link
to={window.location.pathname} to={`/${window.location.pathname.split('/').pop()}`}
query={queryParamsHash}> query={queryParamsHash}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Hash your work')} {getLangText('Hash your work')}
@ -69,7 +75,7 @@ let FileDragAndDropDialog = React.createClass({
<span> or </span> <span> or </span>
<Link <Link
to={window.location.pathname} to={`/${window.location.pathname.split('/').pop()}`}
query={queryParamsUpload}> query={queryParamsUpload}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')} {getLangText('Upload and hash your work')}

View File

@ -11,7 +11,7 @@ let Footer = React.createClass({
<p className="ascribe-sub-sub-statement"> <p className="ascribe-sub-sub-statement">
<br /> <br />
<a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> | <a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> |
<a href="https://www.ascribe.io/impressum/" target="_blank"> impressum</a> | <a href="https://www.ascribe.io/imprint/" target="_blank"> {getLangText('imprint')}</a> |
<a href="https://www.ascribe.io/terms/" target="_blank"> {getLangText('terms of service')}</a> | <a href="https://www.ascribe.io/terms/" target="_blank"> {getLangText('terms of service')}</a> |
<a href="https://www.ascribe.io/privacy/" target="_blank"> {getLangText('privacy')}</a> <a href="https://www.ascribe.io/privacy/" target="_blank"> {getLangText('privacy')}</a>
</p> </p>

View File

@ -71,7 +71,7 @@ const ROUTES = {
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPLoginContainer)} /> component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPLoginContainer)} />
<Route <Route
path='logout' path='logout'
component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)}/> component={AuthProxyHandler({to: '/', when: 'loggedOut'})(LogoutContainer)} />
<Route <Route
path='signup' path='signup'
component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPSignupContainer)} /> component={AuthProxyHandler({to: '/register_piece', when: 'loggedIn'})(SPSignupContainer)} />

View File

@ -1,10 +1,12 @@
'use strict'; 'use strict';
export const InformationTexts = { export const AclInformationText = {
'titles': { 'titles': {
'acl_consign': 'CONSIGN', 'acl_consign': 'CONSIGN',
'acl_loan': 'LOAN', 'acl_loan': 'LOAN',
'acl_share': 'SHARE', 'acl_loan_request': 'LOAN',
'acl_share': 'EMAIL',
'acl_transfer': 'TRANSFER',
'acl_delete': 'DELETE', 'acl_delete': 'DELETE',
'acl_create_editions': 'CREATE EDITIONS', 'acl_create_editions': 'CREATE EDITIONS',
'acl_unconsign': 'UNCONSIGN', 'acl_unconsign': 'UNCONSIGN',
@ -13,7 +15,8 @@ export const InformationTexts = {
'informationSentences': { 'informationSentences': {
'acl_consign': ' - Lets someone represent you in dealing with the work, under the terms you agree to.', 'acl_consign': ' - Lets someone represent you in dealing with the work, under the terms you agree to.',
'acl_loan': ' - Lets someone use or put the Work on display for a limited amount of time.', 'acl_loan': ' - Lets someone use or put the Work on display for a limited amount of time.',
'acl_share': ' - Lets someone view the Work or Edition, but does not give rights to publish or display it.', 'acl_share': ' - Lets someone view the Work or Edition via email, but does not give rights to publish or display it.',
'acl_transfer': ' - Changes ownership of an Edition. As with a physical piece Work, Transferring ownership of an Edition does not transfer copyright in the Work.',
'acl_delete': ' - Removes the Work from your Wallet. Note that the previous registration and transfer ' + 'acl_delete': ' - Removes the Work from your Wallet. Note that the previous registration and transfer ' +
'history will still exist on the blockchain and cannot be deleted.', 'history will still exist on the blockchain and cannot be deleted.',
'acl_create_editions': ' Lets the artist set a fixed number of editions of a work which can then be transferred, guaranteeing each edition is authentic and from the artist.', 'acl_create_editions': ' Lets the artist set a fixed number of editions of a work which can then be transferred, guaranteeing each edition is authentic and from the artist.',
@ -24,10 +27,11 @@ export const InformationTexts = {
'acl_consign': '(e.g. an artist Consigns 10 Editions of her new Work to a gallery ' + 'acl_consign': '(e.g. an artist Consigns 10 Editions of her new Work to a gallery ' +
'so the gallery can sell them on her behalf, under the terms the artist and the gallery have agreed to)', 'so the gallery can sell them on her behalf, under the terms the artist and the gallery have agreed to)',
'acl_loan': '(e.g. a collector Loans a Work to a gallery for one month for display in the gallery\'s show)', 'acl_loan': '(e.g. a collector Loans a Work to a gallery for one month for display in the gallery\'s show)',
'acl_share': '(e.g. a photographer Shares proofs of a graduation photo with the graduate\'s grandparents)', 'acl_share': '(e.g. a photographer Shares proofs of a graduation photo with the graduate\'s grandparents by email)',
'acl_transfer': '(e.g. a musician Transfers limited edition 1 of 10 of her new album to a very happy fan)',
'acl_delete': '(e.g. an artist uploaded the wrong file and doesn\'t want it cluttering his Wallet, so he Deletes it)', 'acl_delete': '(e.g. an artist uploaded the wrong file and doesn\'t want it cluttering his Wallet, so he Deletes it)',
'acl_create_editions': '(e.g. A company commissions a visual artists to create three limited edition prints for a giveaway)', 'acl_create_editions': '(e.g. A company commissions a visual artists to create three limited edition prints for a giveaway)',
'acl_unconsign': '(e.g. An artist regains full control over their work and releases the consignee of any rights or responsibilities)', 'acl_unconsign': '(e.g. An artist regains full control over their work and releases the consignee of any rights or responsibilities)',
'acl_request_unconsign': '(e.g. An artist submits an unconsign request to a gallery after his exhibition ends, as per their agreement)' 'acl_request_unconsign': '(e.g. An artist submits an unconsign request to a gallery after his exhibition ends, as per their agreement)'
} }
}; };

View File

@ -1,15 +1,19 @@
'use strict'; 'use strict';
let constants = { //const baseUrl = 'http://localhost:8000/api/';
//'baseUrl': 'http://localhost:8000/api/',
//FIXME: referring to a global variable in `window` is not //FIXME: referring to a global variable in `window` is not
// super pro. What if we render stuff on the server? // super pro. What if we render stuff on the server?
// - super-bro - Senor Developer, 14th July 2015 // - super-bro - Senor Developer, 14th July 2015
//'baseUrl': window.BASE_URL, //const baseUrl = window.BASE_URL;
'apiEndpoint': window.API_ENDPOINT, const apiEndpoint = window.API_ENDPOINT;
'serverUrl': window.SERVER_URL, const serverUrl = window.SERVER_URL;
'baseUrl': window.BASE_URL, const baseUrl = window.BASE_URL;
const constants = {
apiEndpoint,
serverUrl,
baseUrl,
'aclList': ['acl_coa', 'acl_consign', 'acl_delete', 'acl_download', 'acl_edit', 'acl_create_editions', 'acl_view_editions', 'aclList': ['acl_coa', 'acl_consign', 'acl_delete', 'acl_download', 'acl_edit', 'acl_create_editions', 'acl_view_editions',
'acl_loan', 'acl_loan_request', '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'], 'acl_withdraw_transfer', 'acl_wallet_submit'],
@ -84,16 +88,40 @@ let constants = {
} }
}, },
// in case of whitelabel customization, we store stuff here
'whitelabel': {},
'raven': {
'url': 'https://0955da3388c64ab29bd32c2a429f9ef4@app.getsentry.com/48351'
},
'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA', 'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA',
'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART', 'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART',
'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV', 'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV',
'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'], 'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'],
'searchThreshold': 500 'searchThreshold': 500,
// in case of whitelabel customization, we store stuff here
'whitelabel': {},
// 3rd party integrations
'jquery': {
'sdkUrl': 'https://code.jquery.com/jquery-2.1.4.min.js'
},
'shmui': {
'sdkUrl': baseUrl + 'static/thirdparty/shmui/jquery.shmui.js',
'cssUrl': baseUrl + 'static/thirdparty/shmui/shmui.css'
},
'audiojs': {
'sdkUrl': baseUrl + 'static/thirdparty/audiojs/audiojs/audio.min.js'
},
'videojs': {
'sdkUrl': '//vjs.zencdn.net/4.12/video.js',
'cssUrl': '//vjs.zencdn.net/4.12/video-js.css'
},
'raven': {
'url': 'https://0955da3388c64ab29bd32c2a429f9ef4@app.getsentry.com/48351'
},
'facebook': {
'appId': '420813844732240',
'sdkUrl': '//connect.facebook.net/en_US/sdk.js'
},
'twitter': {
'sdkUrl': 'https://platform.twitter.com/widgets.js'
}
}; };
export default constants; export default constants;

View File

@ -17,6 +17,7 @@ const languages = {
'%s license': '%s license', '%s license': '%s license',
'Log into': 'Log into', 'Log into': 'Log into',
'Email': 'Email', 'Email': 'Email',
'EMAIL': 'EMAIL',
'Enter your email': 'Enter your email', 'Enter your email': 'Enter your email',
'Password': 'Password', 'Password': 'Password',
'Enter your password': 'Enter your password', 'Enter your password': 'Enter your password',
@ -245,6 +246,7 @@ const languages = {
'%s license': '%s license', '%s license': '%s license',
'Log into': 'Log into', 'Log into': 'Log into',
'Email': 'Email', 'Email': 'Email',
'EMAIL': 'EMAIL',
'Enter your email': 'Enter your email', 'Enter your email': 'Enter your email',
'Password': 'Password', 'Password': 'Password',
'Enter your password': 'Enter your password', 'Enter your password': 'Enter your password',
@ -473,6 +475,7 @@ const languages = {
'%s license': '%s license', '%s license': '%s license',
'Log into': 'Se connecter à', 'Log into': 'Se connecter à',
'Email': 'E-mail', 'Email': 'E-mail',
'EMAIL': 'E-MAIL',
'Enter your email': 'Entrez votre courriel', 'Enter your email': 'Entrez votre courriel',
'Password': 'Mot de passe', 'Password': 'Mot de passe',
'Enter your password': 'Entrez votre mot de passe', 'Enter your password': 'Entrez votre mot de passe',

9
js/fetchers/README.md Normal file
View File

@ -0,0 +1,9 @@
# README
Recently, we've been discovering [alt.js's sources](http://alt.js.org/docs/async/). As it allows us to implement caching as well as avoid a lot of unnecessary boilerplate code, we do not want to write any more NEW `fetcher`s.
So if you need to query an endpoint, or if you make active changes to one of the `fetcher`s, please consider refactoring it to a `source`.
Also, please read the `NAMING_CONVENTIONS.md` file in there first.
Thanks

View File

@ -1,20 +0,0 @@
'use strict';
import requests from '../utils/requests';
import ApiUrls from '../constants/api_urls';
let UserFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*/
fetchOne() {
return requests.get('user');
},
logout() {
return requests.get(ApiUrls.users_logout);
}
};
export default UserFetcher;

View File

@ -1,17 +0,0 @@
'use strict';
import requests from '../utils/requests';
import { getSubdomain } from '../utils/general_utils';
let WhitelabelFetcher = {
/**
* Fetch the custom whitelabel data from the API.
*/
fetch() {
return requests.get('whitelabel_settings', {'subdomain': getSubdomain()});
}
};
export default WhitelabelFetcher;

View File

@ -1,71 +0,0 @@
'use strict';
import Q from 'q';
let mapAttr = {
link: 'href',
script: 'src'
};
let mapTag = {
js: 'script',
css: 'link'
};
let InjectInHeadMixin = {
/**
* Provide functions to inject `<script>` and `<link>` in `<head>`.
* Useful when you have to load a huge external library and
* you don't want to embed everything inside the build file.
*/
isPresent(tag, src) {
let attr = mapAttr[tag];
let query = `head > ${tag}[${attr}="${src}"]`;
return document.querySelector(query);
},
injectTag(tag, src) {
return Q.Promise((resolve, reject) => {
if (InjectInHeadMixin.isPresent(tag, src)) {
resolve();
} else {
let attr = mapAttr[tag];
let element = document.createElement(tag);
if (tag === 'script') {
element.onload = () => resolve();
element.onerror = () => reject();
} else {
resolve();
}
document.head.appendChild(element);
element[attr] = src;
if (tag === 'link') {
element.rel = 'stylesheet';
}
}
});
},
injectStylesheet(src) {
return InjectInHeadMixin.injectTag('link', src);
},
injectScript(src) {
return InjectInHeadMixin.injectTag('source', src);
},
inject(src) {
let ext = src.split('.').pop();
let tag = mapTag[ext];
if (!tag) {
throw new Error(`Cannot inject ${src} in the DOM, cannot guess the tag name from extension "${ext}". Valid extensions are "js" and "css".`);
}
return InjectInHeadMixin.injectTag(tag, src);
}
};
export default InjectInHeadMixin;

View File

@ -0,0 +1,70 @@
# Naming conventions for sources
## Introduction
When using [alt.js's sources](http://alt.js.org/docs/async/), we don't want the source's method to clash with the store/action's method names.
While actions will still be named by the following schema:
```
<verb><ObjectToManipulateInTheStore>
```
e.g.
```
fetchCurrentUser
logoutCurrentUser
fetchApplication
refreshApplicationToken
```
we cannot repeat this for a sources' methods as patterns like this would emerge in the stores:
```javascript
onFetchCurrentUser(invalidateCache) {
this.invalidateCache = invalidateCache;
if(!this.getInstance().isLoading()) {
this.getInstance().fetchCurrentUser(); // does not call a flux "action" but a method in user_source.js - which is confusing
}
}
```
Therefore we're introducing the following naming convention:
## Rules
1. All source methods that perform a data lookup of any kind (be it cached or not), are called `lookup<ObjectToManipulateInTheStore>`. As a mnemonic aid, "You *lookup* something in a *source*".
2. Promise-based callback methods - provided by alt.js - prepend their action, followed by `ObjectToManipulateInTheStore` (e.g. `error<ObjectToManipulateInTheStore>`)
2. For all methods that do not fit 1.), we prepend `perform`.
## Examples
### Examples for Rule 1.)
*HTTP GET'ing the current User*
```javascript
UserActions.fetchCurrentUser
UserStore.onFetchCurrentUser
UserSource.lookupCurrentUser
```
### Examples for Rule 2.)
This talks about the naming in an actual `*_source.js` file:
```javascript
lookupCurrentUser: {
success: UserActions.successFetchCurrentUser, // 'success<ObjectToManipulateInTheStore>'
error: UserActions.errorCurrentUser, // 'error<ObjectToManipulateInTheStore>'
},
```
### Examples for Rule 3.)
*HTTP GET'ing a certain user endpoint, that logs the user out :sad_face:(, as this should not be a GET request anyway)*
```javascript
UserActions.logoutCurrentUser
UserStore.onLogoutCurrentUser
UserSource.performLogoutCurrentUser
```

34
js/sources/user_source.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
import requests from '../utils/requests';
import ApiUrls from '../constants/api_urls';
import UserActions from '../actions/user_actions';
const UserSource = {
lookupCurrentUser: {
remote() {
return requests.get('user');
},
local(state) {
return state.currentUser && state.currentUser.email ? state : {};
},
success: UserActions.successFetchCurrentUser,
error: UserActions.errorCurrentUser,
shouldFetch(state) {
return state.userMeta.invalidateCache || state.currentUser && !state.currentUser.email;
}
},
performLogoutCurrentUser: {
remote() {
return requests.get(ApiUrls.users_logout);
},
success: UserActions.successLogoutCurrentUser,
error: UserActions.errorCurrentUser
}
};
export default UserSource;

View File

@ -0,0 +1,25 @@
'use strict';
import requests from '../utils/requests';
import WhitelabelActions from '../actions/whitelabel_actions';
import { getSubdomain } from '../utils/general_utils';
const WhitelabelSource = {
lookupWhitelabel: {
remote() {
return requests.get('whitelabel_settings', {'subdomain': getSubdomain()});
},
local(state) {
return Object.keys(state.whitelabel).length > 0 ? state : {};
},
success: WhitelabelActions.successFetchWhitelabel,
error: WhitelabelActions.errorWhitelabel,
shouldFetch(state) {
return state.whitelabelMeta.invalidateCache || Object.keys(state.whitelabel).length === 0;
}
}
};
export default WhitelabelSource;

View File

@ -3,19 +3,47 @@
import { altUser } from '../alt'; import { altUser } from '../alt';
import UserActions from '../actions/user_actions'; import UserActions from '../actions/user_actions';
import UserSource from '../sources/user_source';
class UserStore { class UserStore {
constructor() { constructor() {
this.currentUser = {}; this.currentUser = {};
this.userMeta = {
invalidateCache: false,
err: null
};
this.bindActions(UserActions); this.bindActions(UserActions);
this.registerAsync(UserSource);
} }
onUpdateCurrentUser(user) { onFetchCurrentUser(invalidateCache) {
this.userMeta.invalidateCache = invalidateCache;
if(!this.getInstance().isLoading()) {
this.getInstance().lookupCurrentUser();
}
}
onSuccessFetchCurrentUser({users: [user]}) {
this.userMeta.invalidateCache = false;
this.userMeta.err = null;
this.currentUser = user; this.currentUser = user;
} }
onDeleteCurrentUser() {
onLogoutCurrentUser() {
this.getInstance().performLogoutCurrentUser();
}
onSuccessLogoutCurrentUser() {
this.currentUser = {}; this.currentUser = {};
} }
onErrorCurrentUser(err) {
console.logGlobal(err);
this.userMeta.err = err;
}
} }
export default altUser.createStore(UserStore, 'UserStore'); export default altUser.createStore(UserStore, 'UserStore');

View File

@ -2,17 +2,39 @@
import { altWhitelabel } from '../alt'; import { altWhitelabel } from '../alt';
import WhitelabelActions from '../actions/whitelabel_actions'; import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelSource from '../sources/whitelabel_source';
class WhitelabelStore { class WhitelabelStore {
constructor() { constructor() {
this.whitelabel = {}; this.whitelabel = {};
this.whitelabelMeta = {
invalidateCache: false,
err: null
};
this.bindActions(WhitelabelActions); this.bindActions(WhitelabelActions);
this.registerAsync(WhitelabelSource);
} }
onUpdateWhitelabel(whitelabel) { onFetchWhitelabel(invalidateCache) {
this.whitelabelMeta.invalidateCache = invalidateCache;
if(!this.getInstance().isLoading()) {
this.getInstance().lookupWhitelabel();
}
}
onSuccessFetchWhitelabel({ whitelabel }) {
this.whitelabelMeta.invalidateCache = false;
this.whitelabelMeta.err = null;
this.whitelabel = whitelabel; this.whitelabel = whitelabel;
} }
onErrorCurrentUser(err) {
console.logGlobal(err);
this.whitelabelMeta.err = err;
}
} }
export default altWhitelabel.createStore(WhitelabelStore, 'WhitelabelStore'); export default altWhitelabel.createStore(WhitelabelStore, 'WhitelabelStore');

30
js/third_party/facebook.js vendored Normal file
View File

@ -0,0 +1,30 @@
'use strict';
import { altThirdParty } from '../alt';
import EventActions from '../actions/event_actions';
import AppConstants from '../constants/application_constants'
class FacebookHandler {
constructor() {
this.bindActions(EventActions);
}
onApplicationWillBoot(settings) {
// Callback function that FB's sdk will call when it's finished loading
// See https://developers.facebook.com/docs/javascript/quickstart/v2.5
window.fbAsyncInit = () => {
FB.init({
appId: AppConstants.facebook.appId,
// Force FB to parse everything on first load to make sure all the XFBML components are initialized.
// If we don't do this, we can run into issues with components on the first load who are not be
// initialized.
xfbml: true,
version: 'v2.5',
cookie: false
});
};
}
}
export default altThirdParty.createStore(FacebookHandler, 'FacebookHandler');

View File

@ -23,4 +23,4 @@
* @type {bool} Is drag and drop available on this browser * @type {bool} Is drag and drop available on this browser
*/ */
export const dragAndDropAvailable = 'draggable' in document.createElement('div') && export const dragAndDropAvailable = 'draggable' in document.createElement('div') &&
!/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent); !/Mobile|Android|Slick\/|Kindle|BlackBerry|Opera Mini|Opera Mobi/i.test(navigator.userAgent);

67
js/utils/inject_utils.js Normal file
View File

@ -0,0 +1,67 @@
'use strict';
import Q from 'q';
let mapAttr = {
link: 'href',
script: 'src'
};
let mapTag = {
js: 'script',
css: 'link'
};
let tags = {};
function injectTag(tag, src) {
if(!tags[src]) {
tags[src] = Q.Promise((resolve, reject) => {
let attr = mapAttr[tag];
let element = document.createElement(tag);
if (tag === 'script') {
element.onload = resolve;
element.onerror = reject;
} else {
resolve();
}
document.head.appendChild(element);
element[attr] = src;
if (tag === 'link') {
element.rel = 'stylesheet';
}
});
}
return tags[src];
}
function injectStylesheet(src) {
return injectTag('link', src);
}
function injectScript(src) {
return injectTag('source', src);
}
function inject(src) {
let ext = src.split('.').pop();
let tag = mapTag[ext];
if (!tag) {
throw new Error(`Cannot inject ${src} in the DOM, cannot guess the tag name from extension "${ext}". Valid extensions are "js" and "css".`);
}
return injectTag(tag, src);
}
export const InjectInHeadUtils = {
/**
* Provide functions to inject `<script>` and `<link>` in `<head>`.
* Useful when you have to load a huge external library and
* you don't want to embed everything inside the build file.
*/
injectStylesheet,
injectScript,
inject
};

View File

@ -0,0 +1,10 @@
.ascribe-social-button-list {
margin-right: 10px;
}
.btn-ascribe-social {
height: 20px;
width: 56px;
box-sizing: content-box; /* We want to ignore padding in these calculations as we use the sdk buttons' height and width */
padding: 1px 0;
}

View File

@ -31,6 +31,7 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'offset_right'; @import 'offset_right';
@import 'ascribe_settings'; @import 'ascribe_settings';
@import 'ascribe_slides_container'; @import 'ascribe_slides_container';
@import 'ascribe_social_share';
@import 'ascribe_property'; @import 'ascribe_property';
@import 'ascribe_form'; @import 'ascribe_form';
@import 'ascribe_panel'; @import 'ascribe_panel';