Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Themes: Add site-selector modal to theme sheet #5354

Merged
merged 3 commits into from
May 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions client/my-sites/theme/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import ActivatingTheme from 'components/data/activating-theme';
import ThanksModal from 'my-sites/themes/thanks-modal';
import QueryCurrentTheme from 'components/data/query-current-theme';
import { getCurrentTheme } from 'state/themes/current-theme/selectors';
import ThemesSiteSelectorModal from 'my-sites/themes/themes-site-selector-modal';
import actionLabels from 'my-sites/themes/action-labels';

const ThemeSheet = React.createClass( {
displayName: 'ThemeSheet',
Expand Down Expand Up @@ -59,6 +61,14 @@ const ThemeSheet = React.createClass( {
return { section: 'overview' };
},

getInitialState() {
return { selectedAction: null };
},

hideSiteSelectorModal() {
this.setState( { selectedAction: null } );
},

onBackClick() {
page.back();
},
Expand All @@ -73,12 +83,19 @@ const ThemeSheet = React.createClass( {
this.props.signup( this.props );
} else if ( this.isActive() ) {
this.props.customize( this.props, this.props.selectedSite );
// TODO: use site picker if no selected site
} else if ( isPremium( this.props ) ) {
// TODO: check theme is not already purchased
this.props.purchase( this.props, this.props.selectedSite, 'showcase-sheet' );
this.selectSiteAndDispatch( 'purchase' );
} else {
this.selectSiteAndDispatch( 'activate' );
}
},

selectSiteAndDispatch( action ) {
if ( this.props.selectedSite ) {
this.props[ action ]( this.props, this.props.selectedSite, 'showcase-sheet' );
} else {
this.props.activate( this.props, this.props.selectedSite, 'showcase-sheet' );
this.setState( { selectedAction: action } );
}
},

Expand Down Expand Up @@ -234,7 +251,7 @@ const ThemeSheet = React.createClass( {
}

const section = this.validateSection( this.props.section );
const priceElement = <span className="themes__sheet-action-bar-cost" dangerouslySetInnerHTML={ { __html: this.props.price } } />;
const priceElement = <span className="themes__sheet-action-bar-cost">{ this.props.price }</span>;
const siteID = this.props.selectedSite && this.props.selectedSite.ID;

return (
Expand All @@ -247,6 +264,15 @@ const ThemeSheet = React.createClass( {
source={ 'details' }
clearActivated={ this.props.clearActivated }/>
</ActivatingTheme>
{ this.state.selectedAction && <ThemesSiteSelectorModal
name={ this.state.selectedAction }
label={ actionLabels[ this.state.selectedAction ].label }
header={ actionLabels[ this.state.selectedAction ].header }
selectedTheme={ this.props }
onHide={ this.hideSiteSelectorModal }
action={ this.props[ this.state.selectedAction ] }
sourcePath={ `/theme/${ this.props.id }/${ section }` }
/> }
<div className="themes__sheet-columns">
<div className="themes__sheet-column-left">
<HeaderCake className="themes__sheet-action-bar" onClick={ this.onBackClick }>
Expand Down
2 changes: 2 additions & 0 deletions client/my-sites/themes/action-labels.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @ssr-ready **/

/**
* Internal dependencies
*/
Expand Down
1 change: 1 addition & 0 deletions client/my-sites/themes/multi-site.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const ThemesMultiSite = React.createClass( {
selectedTheme={ this.state.selectedTheme }
onHide={ this.hideSiteSelectorModal }
action={ bindActionCreators( Action[ this.state.selectedAction ], dispatch ) }
sourcePath={ '/design' }
/> }
</Main>
);
Expand Down
6 changes: 4 additions & 2 deletions client/my-sites/themes/themes-site-selector-modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const ThemesSiteSelectorModal = React.createClass( {
header: React.PropTypes.string.isRequired,
selectedTheme: PropTypes.object.isRequired,
onHide: PropTypes.func,
action: PropTypes.func
action: PropTypes.func,
// Will be prepended to site slug for a redirect on selection
sourcePath: PropTypes.string.isRequired,
},

redirectAndCallAction( site ) {
Expand All @@ -29,7 +31,7 @@ const ThemesSiteSelectorModal = React.createClass( {
*/
defer( () => {
trackClick( 'site selector', this.props.name );
page( '/design/' + site.slug );
page( this.props.sourcePath + '/' + site.slug );
this.props.action( this.props.selectedTheme, site );
} );
},
Expand Down
2 changes: 1 addition & 1 deletion client/state/themes/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function receiveThemeDetails( theme ) {
themeId: theme.id,
themeName: theme.name,
themeAuthor: theme.author,
themePrice: theme.price ? theme.price.display : undefined,
themePrice: theme.price,
themeScreenshot: theme.screenshot,
themeDescription: theme.description,
themeDescriptionLong: theme.description_long,
Expand Down
23 changes: 21 additions & 2 deletions client/state/themes/theme-details/selectors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
/** @ssr-ready **/

export function getThemeDetails( state, id ) {
const theme = state.themes.themeDetails.get( id );
return theme ? theme.toJS() : {};
let theme = state.themes.themeDetails.get( id );
theme = theme ? theme.toJS() : {};
if ( theme.price ) {
theme.price = formatPrice( theme.price );
}
return theme;
}

// Convert price to format used by v1.2 themes API to fit with existing components.
// TODO (seear): remove when v1.2 theme details endpoint is added
function formatPrice( price ) {
if ( price.value === 0 ) {
// Free themes have plaintext string
return price.display;
}
// Premium themes have markup in price.display, so create our own plaintext
return price.value.toLocaleString( 'default', {
style: 'currency',
currency: price.currency,
minimumFractionDigits: 0,
} );
}
11 changes: 2 additions & 9 deletions server/isomorphic-routing/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* Internal dependencies
*/
import i18n from 'lib/mixins/i18n';
import { serverRender } from 'render';
import { createReduxStore } from 'state';
import { setSection as setSectionMiddlewareFactory } from '../../client/controller';
Expand All @@ -11,22 +10,21 @@ export function serverRouter( expressApp, setUpRoute, section ) {
expressApp.get(
route,
setUpRoute,
setUpI18n,
combineMiddlewares(
setSectionMiddlewareFactory( section ),
...middlewares
),
serverRender
);
}
};
}

function combineMiddlewares( ...middlewares ) {
return function( req, res, next ) {
req.context = getEnhancedContext( req );
applyMiddlewares( req.context, middlewares );
next();
}
};
}

// TODO: Maybe merge into getDefaultContext().
Expand All @@ -52,8 +50,3 @@ function compose( ...functions ) {
next => f( composed ) // eslint-disable-line no-unused-vars
), () => {} );
}

function setUpI18n( req, res, next ) {
i18n.initialize();
next();
}
10 changes: 9 additions & 1 deletion server/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ var config = require( 'config' ),
utils = require( 'bundler/utils' ),
sectionsModule = require( '../../client/sections' ),
serverRouter = require( 'isomorphic-routing' ).serverRouter,
serverRender = require( 'render' ).serverRender;
serverRender = require( 'render' ).serverRender,
i18n = require( 'lib/mixins/i18n' );

var HASH_LENGTH = 10,
URL_BASE_PATH = '/calypso',
Expand All @@ -37,6 +38,13 @@ sections.forEach( function( section ) {
} );
} );

// TODO: Move into an `app.get()` route handler (e.g. next to language setting),
// and pass the bootstrap locale as a parameter.
// However, this requires that i18n doesn't use global state to store the locale
// anymore (but e.g. a Redux subtree), or all users will get the same locale for
// SSR'd content that the first user had set after a deploy.
i18n.initialize();

/**
* Generates a hash of a files contents to be used as a version parameter on asset requests.
* @param {String} path Path to file we want to hash
Expand Down
1 change: 1 addition & 0 deletions server/pragma-checker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var IGNORED_MODULES = [
'lib/mixins/i18n', // ignore this until we make it work properly on the server
'lib/mixins/i18n/localize', // ignore this until we make it work properly on the server
'my-sites/themes/thanks-modal', // stubbed on the server until we develop an isomorphic version
'my-sites/themes/themes-site-selector-modal' // stubbed on the server until we develop an isomorphic version
];

function PragmaCheckPlugin( options ) {
Expand Down
3 changes: 2 additions & 1 deletion webpack.config.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ module.exports = {
new webpack.NormalModuleReplacementPlugin( /^lib\/analytics$/, 'lodash/noop' ), // Depends on BOM
new webpack.NormalModuleReplacementPlugin( /^lib\/upgrades\/actions$/, 'lodash/noop' ), // Uses Flux dispatcher
new webpack.NormalModuleReplacementPlugin( /^lib\/route$/, 'lodash/noop' ), // Depends too much on page.js
new webpack.NormalModuleReplacementPlugin( /^my-sites\/themes\/thanks-modal$/, 'components/empty-component' ) // Depends on BOM
new webpack.NormalModuleReplacementPlugin( /^my-sites\/themes\/thanks-modal$/, 'components/empty-component' ), // Depends on BOM
new webpack.NormalModuleReplacementPlugin( /^my-sites\/themes\/themes-site-selector-modal$/, 'components/empty-component' ) // Depends on BOM
],
externals: getExternals()
};