From 29af3b0fe0e2cd755a7377e207ebb0db8071722f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 10 Sep 2018 21:28:16 +0200 Subject: [PATCH] Checklist: Unify WordPress.com checklist and banner (#26764) This PR continues separation of state and declarative structure work outlined in #26216. Inspired by suggestions from ockham, the Checklist is declared once in idiomatic React JSX, and can be rendered as a checklist of tasks or a banner (see `viewMode` prop). * `ChecklistShow`, responsible only for rendering the WordPress.com checklist, has been removed. * The array of task props in `onboardingChecklist` has been removed, and is now JSX in `WpcomChecklist`. * `ChecklistBanner` has been removed and replaced by ``. * `ChecklistMain` renders the `WpcomChecklist` and some surrounding UI. Clean up its usage of task status for calculating completion. * Remove the `mergeObjectIntoArrayById` util which is no longer needed and related tests --- assets/stylesheets/_components.scss | 2 +- client/components/checklist/index.js | 65 ++-- .../checklist/checklist-show/index.jsx | 113 ------- client/my-sites/checklist/main.jsx | 52 +-- .../my-sites/checklist/onboardingChecklist.js | 176 ---------- client/my-sites/checklist/test/util.js | 80 ----- client/my-sites/checklist/util.js | 26 -- .../wpcom-checklist/checklist-banner/index.js | 154 +++++++++ .../checklist-banner/style.scss | 0 .../wpcom-checklist/checklist-banner/task.js | 67 ++++ .../checklist/wpcom-checklist/index.js | 300 ++++++++++++++++++ .../my-sites/stats/checklist-banner/index.jsx | 229 ------------- client/my-sites/stats/site.jsx | 5 +- 13 files changed, 581 insertions(+), 688 deletions(-) delete mode 100644 client/my-sites/checklist/checklist-show/index.jsx delete mode 100644 client/my-sites/checklist/onboardingChecklist.js delete mode 100644 client/my-sites/checklist/test/util.js delete mode 100644 client/my-sites/checklist/util.js create mode 100644 client/my-sites/checklist/wpcom-checklist/checklist-banner/index.js rename client/my-sites/{stats => checklist/wpcom-checklist}/checklist-banner/style.scss (100%) create mode 100644 client/my-sites/checklist/wpcom-checklist/checklist-banner/task.js create mode 100644 client/my-sites/checklist/wpcom-checklist/index.js delete mode 100644 client/my-sites/stats/checklist-banner/index.jsx diff --git a/assets/stylesheets/_components.scss b/assets/stylesheets/_components.scss index 5931a8c034d95..b11f6b89fd2e5 100644 --- a/assets/stylesheets/_components.scss +++ b/assets/stylesheets/_components.scss @@ -356,6 +356,7 @@ @import 'my-sites/ads/style'; @import 'my-sites/all-sites-icon/style'; @import 'my-sites/checklist/style'; +@import 'my-sites/checklist/wpcom-checklist/checklist-banner/style'; @import 'my-sites/current-site/style'; @import 'my-sites/current-site/sidebar-banner/style'; @import 'my-sites/customize/style'; @@ -447,7 +448,6 @@ @import 'my-sites/activity/activity-log-tasklist/style'; @import 'my-sites/stats/all-time/style'; @import 'my-sites/stats/annual-site-stats/style'; -@import 'my-sites/stats/checklist-banner/style'; @import 'my-sites/stats/geochart/style'; @import 'my-sites/stats/most-popular/style'; @import 'my-sites/stats/post-performance/style'; diff --git a/client/components/checklist/index.js b/client/components/checklist/index.js index 274a7db160ce3..b529cabcb21d4 100644 --- a/client/components/checklist/index.js +++ b/client/components/checklist/index.js @@ -16,37 +16,54 @@ import TaskPlaceholder from 'components/checklist/task-placeholder'; export default class Checklist extends PureComponent { static propTypes = { isPlaceholder: PropTypes.bool, + updateCompletion: PropTypes.func, }; - state = { hideCompleted: false }; - - toggleCompleted = () => - this.setState( ( { hideCompleted } ) => ( { hideCompleted: ! hideCompleted } ) ); + componentDidMount() { + this.notifyCompletion(); + } - renderPlaceholder() { - return ( -
- -
- { times( Children.count( this.props.children ), index => ( - - ) ) } -
-
- ); + componentDidUpdate() { + this.notifyCompletion(); } - render() { - if ( this.props.isPlaceholder ) { - return this.renderPlaceholder(); + notifyCompletion() { + if ( 'function' === typeof this.props.updateCompletion ) { + const [ complete, total ] = this.calculateCompletion(); + this.props.updateCompletion( { complete: complete >= total } ); } + } + calculateCompletion() { const { children } = this.props; - - const count = Children.map( children, child => child.props.completed ).reduce( - ( acc, completed ) => ( true === completed ? acc + 1 : acc ), + const childrenArray = Children.toArray( children ); + const completedCount = childrenArray.reduce( + ( count, task ) => ( true === task.props.completed ? count + 1 : count ), 0 ); + const total = childrenArray.length; + return [ completedCount, total ]; + } + + state = { hideCompleted: false }; + + toggleCompleted = () => + this.setState( ( { hideCompleted } ) => ( { hideCompleted: ! hideCompleted } ) ); + + render() { + const [ completed, total ] = this.calculateCompletion(); + if ( this.props.isPlaceholder ) { + return ( +
+ +
+ { times( total, index => ( + + ) ) } +
+
+ ); + } return (
-
{ children }
+
{ this.props.children }
); } diff --git a/client/my-sites/checklist/checklist-show/index.jsx b/client/my-sites/checklist/checklist-show/index.jsx deleted file mode 100644 index 8f4e7e3618c13..0000000000000 --- a/client/my-sites/checklist/checklist-show/index.jsx +++ /dev/null @@ -1,113 +0,0 @@ -/** @format */ - -/** - * External dependencies - */ -import React, { Fragment, PureComponent } from 'react'; -import { connect } from 'react-redux'; -import { localize } from 'i18n-calypso'; -import { get } from 'lodash'; - -/** - * Internal dependencies - */ -import Checklist from 'components/checklist'; -import Task from 'components/checklist/task'; -import { requestSiteChecklistTaskUpdate } from 'state/checklist/actions'; -import { getSelectedSiteId } from 'state/ui/selectors'; -import getSiteChecklist from 'state/selectors/get-site-checklist'; -import { getSiteSlug } from 'state/sites/selectors'; -import QuerySiteChecklist from 'components/data/query-site-checklist'; -import { getTaskUrls, launchTask, getTasks } from '../onboardingChecklist'; -import { recordTracksEvent } from 'state/analytics/actions'; -import { createNotice } from 'state/notices/actions'; -import { requestGuidedTour } from 'state/ui/guided-tours/actions'; -import QueryPosts from 'components/data/query-posts'; -import { getSitePosts } from 'state/posts/selectors'; - -class ChecklistShow extends PureComponent { - isComplete( taskId ) { - return get( this.props.taskStatuses, [ taskId, 'completed' ], false ); - } - - handleTaskStart = task => () => { - const { requestTour, siteSlug, track, taskUrls } = this.props; - launchTask( { - task: { - ...task, - completed: task.completed || this.isComplete( task.id ), - url: taskUrls[ task.id ] || task.url, - }, - location: 'checklist_show', - requestTour, - siteSlug, - track, - } ); - }; - - handleTaskDismiss = task => () => { - const { notify, siteId, update } = this.props; - - if ( task ) { - notify( 'is-success', 'You completed a task!' ); - update( siteId, task.id ); - } - }; - - render() { - const { siteId, taskStatuses, tasks } = this.props; - - return ( - - { siteId && } - { siteId && ( - - ) } - - { tasks.map( task => ( - - ) ) } - - - ); - } -} - -const mapStateToProps = state => { - const siteId = getSelectedSiteId( state ); - - return { - siteId, - siteSlug: getSiteSlug( state, siteId ), - taskStatuses: get( getSiteChecklist( state, siteId ), [ 'tasks' ] ), - taskUrls: getTaskUrls( getSitePosts( state, siteId ) ), - tasks: getTasks( state, siteId ), - }; -}; - -const mapDispatchToProps = { - track: recordTracksEvent, - notify: createNotice, - requestTour: requestGuidedTour, - update: requestSiteChecklistTaskUpdate, -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( localize( ChecklistShow ) ); diff --git a/client/my-sites/checklist/main.jsx b/client/my-sites/checklist/main.jsx index 17413296b554c..1d55438801aa5 100644 --- a/client/my-sites/checklist/main.jsx +++ b/client/my-sites/checklist/main.jsx @@ -3,7 +3,7 @@ * External dependencies */ import page from 'page'; -import React, { Fragment, PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { localize } from 'i18n-calypso'; import { some } from 'lodash'; @@ -11,7 +11,6 @@ import { some } from 'lodash'; /** * Internal dependencies */ -import ChecklistShow from './checklist-show'; import ChecklistShowShare from './share'; import DocumentHead from 'components/data/document-head'; import EmptyContent from 'components/empty-content'; @@ -22,21 +21,15 @@ import Main from 'components/main'; import PageViewTracker from 'lib/analytics/page-view-tracker'; import QuerySiteChecklist from 'components/data/query-site-checklist'; import SidebarNavigation from 'my-sites/sidebar-navigation'; +import WpcomChecklist from './wpcom-checklist'; import { getCurrentUser } from 'state/current-user/selectors'; import { getSelectedSiteId } from 'state/ui/selectors'; import { getSiteSlug, isJetpackSite, isNewSite } from 'state/sites/selectors'; import { isEnabled } from 'config'; -/** - * Included to fix regression. - * https://github.com/Automattic/wp-calypso/issues/26572 - * @TODO clean up module separation. - */ -import getSiteChecklist from 'state/selectors/get-site-checklist'; -import { mergeObjectIntoArrayById } from './util'; -import { tasks as wpcomTasks } from './onboardingChecklist'; - class ChecklistMain extends PureComponent { + state = { complete: false }; + componentDidMount() { this.maybeRedirectJetpack(); } @@ -45,6 +38,8 @@ class ChecklistMain extends PureComponent { this.maybeRedirectJetpack( prevProps ); } + handleCompletionUpdate = ( { complete } ) => void this.setState( { complete } ); + /** * Redirect Jetpack checklists to /plans/my-plan/:siteSlug * @@ -76,12 +71,12 @@ class ChecklistMain extends PureComponent { } renderHeader() { - const { displayMode, isNewlyCreatedSite, tasks, translate } = this.props; - const completed = tasks && ! some( tasks, { completed: false } ); + const { displayMode, isNewlyCreatedSite, translate } = this.props; + const { complete } = this.state; - if ( completed ) { + if ( complete ) { return ( - + <> - + ); } if ( isNewlyCreatedSite ) { return ( - + <> - + ); } @@ -164,11 +159,11 @@ class ChecklistMain extends PureComponent { { checklistAvailable ? ( - + <> { siteId && } { this.renderHeader() } - - + + ) : ( ) } @@ -182,14 +177,6 @@ export default connect( state => { const isAtomic = isSiteAutomatedTransfer( state, siteId ); const isJetpack = isJetpackSite( state, siteId ); - /** - * Included to fix regression. - * https://github.com/Automattic/wp-calypso/issues/26572 - * @TODO clean up module separation. - */ - const siteChecklist = getSiteChecklist( state, siteId ); - const tasksFromServer = siteChecklist && siteChecklist.tasks; - return { checklistAvailable: ! isAtomic && ( isEnabled( 'jetpack/checklist' ) || ! isJetpack ), isAtomic, @@ -199,12 +186,5 @@ export default connect( state => { siteId, siteSlug: getSiteSlug( state, siteId ), user: getCurrentUser( state ), - - /** - * Included to fix regression. - * https://github.com/Automattic/wp-calypso/issues/26572 - * @TODO clean up module separation. - */ - tasks: tasksFromServer ? mergeObjectIntoArrayById( wpcomTasks, tasksFromServer ) : null, }; } )( localize( ChecklistMain ) ); diff --git a/client/my-sites/checklist/onboardingChecklist.js b/client/my-sites/checklist/onboardingChecklist.js deleted file mode 100644 index 7b7f1c94c4681..0000000000000 --- a/client/my-sites/checklist/onboardingChecklist.js +++ /dev/null @@ -1,176 +0,0 @@ -/** @format */ -/** - * External dependencies - */ -import page from 'page'; -import { isDesktop } from 'lib/viewport'; -import { translate } from 'i18n-calypso'; -import { find } from 'lodash'; -import { getSiteOption } from 'state/sites/selectors'; - -export const tasks = [ - { - id: 'site_created', - title: translate( 'Create your site' ), - description: translate( 'This is where your adventure begins.' ), - completedTitle: translate( 'You created your site' ), - completed: true, - }, - { - id: 'domain_selected', - title: translate( 'Pick a website address' ), - description: translate( 'Choose an address so people can find you on the internet.' ), - completedTitle: translate( 'You picked a website address' ), - completed: true, - image: '/calypso/images/stats/tasks/domains.svg', - }, - { - id: 'blogname_set', - title: translate( 'Give your site a name' ), - description: translate( 'Give your site a descriptive name to entice visitors.' ), - duration: translate( '%d minute', '%d minutes', { count: 1, args: [ 1 ] } ), - completedTitle: translate( 'You updated your site title' ), - completedButtonText: translate( 'Edit' ), - url: '/settings/general/$siteSlug', - image: '/calypso/images/stats/tasks/personalize-your-site.svg', - tour: 'checklistSiteTitle', - }, - { - id: 'site_icon_set', - title: translate( 'Upload a site icon' ), - description: translate( - 'Help people recognize your site in browser tabs — just like the WordPress.com W!' - ), - duration: translate( '%d minute', '%d minutes', { count: 1, args: [ 1 ] } ), - completedTitle: translate( 'You uploaded a site icon' ), - completedButtonText: translate( 'Change' ), - url: '/settings/general/$siteSlug', - image: '/calypso/images/stats/tasks/upload-icon.svg', - tour: 'checklistSiteIcon', - }, - { - id: 'blogdescription_set', - title: translate( 'Create a tagline' ), - description: translate( 'Pique readers’ interest with a little more detail about your site.' ), - duration: translate( '%d minute', '%d minutes', { count: 2, args: [ 2 ] } ), - completedTitle: translate( 'You created a tagline' ), - completedButtonText: translate( 'Change' ), - url: '/settings/general/$siteSlug', - image: '/calypso/images/stats/tasks/create-tagline.svg', - tour: 'checklistSiteTagline', - }, - { - id: 'avatar_uploaded', - title: translate( 'Upload your profile picture' ), - description: translate( - 'Who’s the person behind the site? Personalize your posts and comments with a custom profile picture.' - ), - duration: translate( '%d minute', '%d minutes', { count: 2, args: [ 2 ] } ), - completedTitle: translate( 'You uploaded a profile picture' ), - completedButtonText: translate( 'Change' ), - url: '/me', - image: '/calypso/images/stats/tasks/upload-profile-picture.svg', - tour: 'checklistUserAvatar', - }, - { - id: 'contact_page_updated', - title: translate( 'Personalize your Contact page' ), - description: translate( - 'Encourage visitors to get in touch — a website is for connecting with people.' - ), - duration: translate( '%d minute', '%d minutes', { count: 2, args: [ 2 ] } ), - completedTitle: translate( 'You updated your Contact page' ), - completedButtonText: translate( 'Edit' ), - image: '/calypso/images/stats/tasks/contact.svg', - url: '/post/$siteSlug/2', - tour: 'checklistContactPage', - }, - { - id: 'post_published', - title: translate( 'Publish your first blog post' ), - description: translate( 'Introduce yourself to the world! That’s why you’re here.' ), - duration: translate( '%d minute', '%d minutes', { count: 10, args: [ 10 ] } ), - completedTitle: translate( 'You published your first blog post' ), - completedButtonText: translate( 'Edit' ), - url: '/post/$siteSlug', - image: '/calypso/images/stats/tasks/first-post.svg', - tour: 'checklistPublishPost', - }, - { - id: 'custom_domain_registered', - title: translate( 'Register a custom domain' ), - description: translate( - 'Memorable domain names make it easy for people to remember your address — and search engines love ’em.' - ), - duration: translate( '%d minute', '%d minutes', { count: 2, args: [ 2 ] } ), - completedTitle: translate( 'You registered a custom domain' ), - completedButtonText: translate( 'Change' ), - url: '/domains/add/$siteSlug', - image: '/calypso/images/stats/tasks/custom-domain.svg', - tour: 'checklistDomainRegister', - }, -]; - -export function getTasks( state, siteId ) { - const designType = getSiteOption( state, siteId, 'design_type' ); - - if ( designType === 'blog' ) { - return tasks; - } - - return tasks.filter( task => { - return task.id !== 'avatar_uploaded' && task.id !== 'post_published'; - } ); -} - -export function launchTask( { task, location, requestTour, siteSlug, track } ) { - const checklist_name = 'new_blog'; - const url = task.url && task.url.replace( '$siteSlug', siteSlug ); - const tour = task.tour; - - if ( task.completed ) { - if ( url ) { - page( url ); - } - return; - } - - if ( ! tour && ! url ) { - return; - } - - track( 'calypso_checklist_task_start', { - checklist_name, - step_name: task.id, - location, - } ); - - if ( url ) { - page( url ); - } - - if ( tour && isDesktop() ) { - requestTour( tour ); - } -} - -export function getTaskUrls( posts ) { - const urls = {}; - const firstPost = find( posts, { type: 'post' } ); - const contactPage = find( posts, post => { - return ( - post.type === 'page' && - find( post.metadata, { key: '_headstart_post', value: '_hs_contact_page' } ) - ); - } ); - - if ( firstPost ) { - urls.post_published = '/post/$siteSlug/' + firstPost.ID; - } - - if ( contactPage ) { - urls.contact_page_updated = '/post/$siteSlug/' + contactPage.ID; - } - - return urls; -} diff --git a/client/my-sites/checklist/test/util.js b/client/my-sites/checklist/test/util.js deleted file mode 100644 index fd1f21949850b..0000000000000 --- a/client/my-sites/checklist/test/util.js +++ /dev/null @@ -1,80 +0,0 @@ -/** @format */ - -/** - * Internal dependencies - */ -import { mergeObjectIntoArrayById } from '../util'; - -describe( 'mergeObjectIntoArrayById', () => { - test( 'should produce a new array', () => { - const arr = [ { id: 'a', prop: 'prop' } ]; - const obj = { a: { newProp: 'newProp' } }; - expect( mergeObjectIntoArrayById( arr, obj ) ).not.toBe( arr ); - } ); - - test( 'should produce a new object when merging', () => { - const arr = [ { id: 'a', prop: 'prop' } ]; - const obj = { a: { newProp: 'newProp' } }; - - const result = mergeObjectIntoArrayById( arr, obj ); - expect( result[ 0 ] ).not.toBe( arr[ 0 ] ); - expect( result[ 0 ] ).not.toBe( obj ); - } ); - - test( 'should not replace unchanged objects', () => { - const arr = [ { id: 'a', prop: 'prop' }, { id: 'b' } ]; - const obj = { a: { newProp: 'newProp' } }; - - expect( mergeObjectIntoArrayById( arr, obj )[ 1 ] ).toBe( arr[ 1 ] ); - } ); - - test( 'should overwrite existing props', () => { - const arr = [ { id: 'a', prop: 'prop' } ]; - const obj = { a: { prop: 'updated' } }; - - expect( mergeObjectIntoArrayById( arr, obj ) ).toEqual( [ - { - id: 'a', - prop: 'updated', - }, - ] ); - } ); - - test( 'should keep existing props', () => { - const arr = [ { id: 'a', prop: 'prop', untouched: 'stay-the-same' } ]; - const obj = { a: { prop: 'updated' } }; - - expect( mergeObjectIntoArrayById( arr, obj ) ).toEqual( [ - { - id: 'a', - prop: 'updated', - untouched: 'stay-the-same', - }, - ] ); - } ); - - test( 'should add new props', () => { - const arr = [ { id: 'a', prop: 'prop' } ]; - const obj = { a: { newProp: 'add me' } }; - - expect( mergeObjectIntoArrayById( arr, obj ) ).toEqual( [ - { - id: 'a', - prop: 'prop', - newProp: 'add me', - }, - ] ); - } ); - - test( 'should ignore object keys not present in the array', () => { - const arr = [ { id: 'a', prop: 'prop' } ]; - const obj = { c: { ignore: 'me' } }; - - expect( mergeObjectIntoArrayById( arr, obj ) ).toEqual( [ - { - id: 'a', - prop: 'prop', - }, - ] ); - } ); -} ); diff --git a/client/my-sites/checklist/util.js b/client/my-sites/checklist/util.js deleted file mode 100644 index 499f3cde6ba63..0000000000000 --- a/client/my-sites/checklist/util.js +++ /dev/null @@ -1,26 +0,0 @@ -/** @format */ - -/** - * Update an array of objects with an id property with an object of objects keyed by id. - * - * Given an array of objects with an id property, update and add properties from the provided object - * keyed by id. - * - * @example - * mergeObjectIntoArrayById( - * [ { id: 'a', prop: 'stale', keep: 'keep' } ], - * { - * a: { prop: 'fresh', add: 'new' } - * } - * ) - * // results in… - * [ { id: 'a', prop: 'fresh', keep: 'keep', add: 'new' } ], - * - * @param {Array} arr An array of objects, each of which must contain an `id` property - * @param {Object} obj An object whose keys match ids and values are objects or properties to update - * @return {Array} A new array with updated objects - * - */ -export function mergeObjectIntoArrayById( arr, obj ) { - return arr.map( item => ( obj[ item.id ] ? { ...item, ...obj[ item.id ] } : item ) ); -} diff --git a/client/my-sites/checklist/wpcom-checklist/checklist-banner/index.js b/client/my-sites/checklist/wpcom-checklist/checklist-banner/index.js new file mode 100644 index 0000000000000..c4f851df0f936 --- /dev/null +++ b/client/my-sites/checklist/wpcom-checklist/checklist-banner/index.js @@ -0,0 +1,154 @@ +/** @format */ +/** + * External dependencies + */ +import Gridicon from 'gridicons'; +import PropTypes from 'prop-types'; +import React, { Children, Component } from 'react'; +import store from 'store'; +import { connect } from 'react-redux'; +import { find, get, reduce } from 'lodash'; +import { localize } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import Button from 'components/button'; +import Card from 'components/card'; +import ChecklistBannerTask from './task'; +import ChecklistShowShare from 'my-sites/checklist/share'; +import Gauge from 'components/gauge'; +import isEligibleForDotcomChecklist from 'state/selectors/is-eligible-for-dotcom-checklist'; +import ProgressBar from 'components/progress-bar'; +import { getSelectedSiteId } from 'state/ui/selectors'; +import { getSiteSlug } from 'state/sites/selectors'; + +const storeKeyForNeverShow = 'sitesNeverShowChecklistBanner'; + +export class ChecklistBanner extends Component { + static propTypes = { + isEligibleForDotcomChecklist: PropTypes.bool, + siteSlug: PropTypes.string, + translate: PropTypes.func.isRequired, + }; + + state = { closed: false }; + + handleClose = () => { + const { siteId } = this.props; + const sitesNeverShowBanner = store.get( storeKeyForNeverShow ) || {}; + sitesNeverShowBanner[ `${ siteId }` ] = true; + store.set( storeKeyForNeverShow, sitesNeverShowBanner ); + + this.setState( { closed: true } ); + + this.props.track( 'calypso_checklist_banner_close', { + site_id: siteId, + } ); + }; + + canShow() { + if ( ! this.props.isEligibleForDotcomChecklist ) { + return false; + } + + if ( this.state.closed ) { + return false; + } + + const sitesNeverShowBanner = store.get( storeKeyForNeverShow ); + if ( get( sitesNeverShowBanner, String( this.props.siteId ) ) === true ) { + return false; + } + + return true; + } + + render() { + const { translate } = this.props; + + if ( ! this.canShow() ) { + return null; + } + + const childrenArray = Children.toArray( this.props.children ); + const total = childrenArray.length; + const completeCount = reduce( + childrenArray, + ( sum, child ) => ( !! child.props.completed ? sum + 1 : sum ), + 0 + ); + const isFinished = completeCount >= total; + const completePercentage = Math.round( ! total ? 0 : ( completeCount / total ) * 100 ); + + return ( + +
+ { translate( 'setup' ) } + +
+
+ { translate( 'Site setup' ) } + + { translate( '%(percentage)s%% completed', { + args: { percentage: completePercentage }, + } ) } + + { isFinished && ( + + ) } + + +
+ { isFinished ? ( + <> + , + }, + } + ) } + title={ translate( 'Your site is ready to share' ) } + > + + + + + ) : ( + find( childrenArray, child => ! child.props.completed ) + ) } +
+ ); + } +} + +export default connect( state => { + const siteId = getSelectedSiteId( state ); + return { + isEligibleForDotcomChecklist: isEligibleForDotcomChecklist( state, siteId ), + siteId, + siteSlug: getSiteSlug( state, siteId ), + }; +} )( localize( ChecklistBanner ) ); diff --git a/client/my-sites/stats/checklist-banner/style.scss b/client/my-sites/checklist/wpcom-checklist/checklist-banner/style.scss similarity index 100% rename from client/my-sites/stats/checklist-banner/style.scss rename to client/my-sites/checklist/wpcom-checklist/checklist-banner/style.scss diff --git a/client/my-sites/checklist/wpcom-checklist/checklist-banner/task.js b/client/my-sites/checklist/wpcom-checklist/checklist-banner/task.js new file mode 100644 index 0000000000000..5142b5bb35f9e --- /dev/null +++ b/client/my-sites/checklist/wpcom-checklist/checklist-banner/task.js @@ -0,0 +1,67 @@ +/** @format */ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import React, { PureComponent } from 'react'; +import { localize } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import Button from 'components/button'; + +class ChecklistBannerTask extends PureComponent { + static propTypes = { + bannerImageSrc: PropTypes.string, + buttonText: PropTypes.node, + completed: PropTypes.bool, + description: PropTypes.node, + onClick: PropTypes.func, + siteSlug: PropTypes.string, + title: PropTypes.node.isRequired, + translate: PropTypes.func.isRequired, + }; + + render() { + // Banners never render completed Tasks + if ( this.props.completed ) { + return null; + } + + const { bannerImageSrc, description, onClick, siteSlug, title, translate } = this.props; + const { buttonText = translate( 'Do it!' ) } = this.props; + + return ( + <> +
+

{ title }

+

{ description }

+
+ { onClick && ( + + ) } + { this.props.children || + ( siteSlug && ( + + { this.props.translate( 'View your checklist' ) } + + ) ) } +
+
+ { bannerImageSrc && ( + + ) } + + ); + } +} + +export default localize( ChecklistBannerTask ); diff --git a/client/my-sites/checklist/wpcom-checklist/index.js b/client/my-sites/checklist/wpcom-checklist/index.js new file mode 100644 index 0000000000000..aaadb51e6e9ce --- /dev/null +++ b/client/my-sites/checklist/wpcom-checklist/index.js @@ -0,0 +1,300 @@ +/** @format */ +/** + * External dependencies + */ +import page from 'page'; +import PropTypes from 'prop-types'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { compact, find, get, some } from 'lodash'; +import { isDesktop } from 'lib/viewport'; +import { localize } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import Checklist from 'components/checklist'; +import ChecklistBanner from './checklist-banner'; +import ChecklistBannerTask from './checklist-banner/task'; +import getSiteChecklist from 'state/selectors/get-site-checklist'; +import QueryPosts from 'components/data/query-posts'; +import QuerySiteChecklist from 'components/data/query-site-checklist'; +import Task from 'components/checklist/task'; +import { createNotice } from 'state/notices/actions'; +import { getPostsForQuery } from 'state/posts/selectors'; +import { getSelectedSiteId } from 'state/ui/selectors'; +import { getSiteOption, getSiteSlug } from 'state/sites/selectors'; +import { loadTrackingTool, recordTracksEvent } from 'state/analytics/actions'; +import { requestGuidedTour } from 'state/ui/guided-tours/actions'; +import { requestSiteChecklistTaskUpdate } from 'state/checklist/actions'; + +const query = { type: 'any', number: 10, order_by: 'ID', order: 'ASC' }; + +class WpcomChecklist extends PureComponent { + static propTypes = { + createNotice: PropTypes.func.isRequired, + designType: PropTypes.oneOf( [ 'blog', 'page', 'portfolio' ] ), + loadTrackingTool: PropTypes.func.isRequired, + recordTracksEvent: PropTypes.func.isRequired, + requestGuidedTour: PropTypes.func.isRequired, + requestSiteChecklistTaskUpdate: PropTypes.func.isRequired, + siteId: PropTypes.number, + siteSlug: PropTypes.string, + taskStatuses: PropTypes.object, + viewMode: PropTypes.oneOf( [ 'checklist', 'banner' ] ), + }; + + static defaultProps = { + viewMode: 'checklist', + }; + + componentDidMount() { + this.props.loadTrackingTool( 'HotJar' ); + } + + isComplete( taskId ) { + return get( this.props.taskStatuses, [ taskId, 'completed' ], false ); + } + + handleTaskStart = ( { taskId, tourId, url } ) => () => { + if ( ! tourId && ! url ) { + return; + } + + const location = 'banner' === this.props.viewMode ? 'checklist_banner' : 'checklist_show'; + + this.props.recordTracksEvent( 'calypso_checklist_task_start', { + checklist_name: 'jetpack', + location, + step_name: taskId, + } ); + + if ( tourId && ! this.isComplete( taskId ) && isDesktop() ) { + this.props.requestGuidedTour( tourId ); + } + + if ( url ) { + page.show( url ); + } + }; + + handleTaskDismiss = taskId => () => { + const { siteId } = this.props; + + if ( taskId ) { + this.props.createNotice( 'is-success', 'You completed a task!' ); + this.props.requestSiteChecklistTaskUpdate( siteId, taskId ); + } + }; + + render() { + const { + designType, + siteId, + siteSlug, + taskStatuses, + taskUrls, + translate, + viewMode, + } = this.props; + + const ChecklistComponent = 'banner' === viewMode ? ChecklistBanner : Checklist; + const TaskComponent = 'banner' === viewMode ? ChecklistBannerTask : Task; + + return ( + <> + { siteId && } + { siteId && } + + + + + + + + { 'blog' === designType && ( + + ) } + + { 'blog' === designType && ( + + ) } + + + + ); + } +} + +function getContactPage( posts ) { + return find( + posts, + post => + post.type === 'page' && + some( post.metadata, { key: '_headstart_post', value: '_hs_contact_page' } ) + ); +} + +export default connect( + state => { + const siteId = getSelectedSiteId( state ); + const siteSlug = getSiteSlug( state, siteId ); + + const posts = getPostsForQuery( state, siteId, query ); + + const firstPost = find( posts, { type: 'post' } ); + const contactPage = getContactPage( posts ); + + const taskUrls = { + post_published: compact( [ '/post', siteSlug, get( firstPost, [ 'ID' ] ) ] ).join( '/' ), + contact_page_updated: [ '/page', siteSlug, get( contactPage, [ 'ID' ], 2 ) ].join( '/' ), + }; + + return { + designType: getSiteOption( state, siteId, 'design_type' ), + siteId, + siteSlug, + taskStatuses: get( getSiteChecklist( state, siteId ), [ 'tasks' ] ), + taskUrls, + }; + }, + { + createNotice, + loadTrackingTool, + recordTracksEvent, + requestGuidedTour, + requestSiteChecklistTaskUpdate, + } +)( localize( WpcomChecklist ) ); diff --git a/client/my-sites/stats/checklist-banner/index.jsx b/client/my-sites/stats/checklist-banner/index.jsx deleted file mode 100644 index f9ec5d88006ef..0000000000000 --- a/client/my-sites/stats/checklist-banner/index.jsx +++ /dev/null @@ -1,229 +0,0 @@ -/** @format */ - -/** - * External dependencies - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { localize } from 'i18n-calypso'; -import { find, get, noop, reduce } from 'lodash'; -import Gridicon from 'gridicons'; -import store from 'store'; - -/** - * Internal dependencies - */ -import Button from 'components/button'; -import Card from 'components/card'; -import Gauge from 'components/gauge'; -import ProgressBar from 'components/progress-bar'; -import QuerySiteChecklist from 'components/data/query-site-checklist'; -import getSiteChecklist from 'state/selectors/get-site-checklist'; -import { getSiteSlug } from 'state/sites/selectors'; -import { getTaskUrls, launchTask, getTasks } from 'my-sites/checklist/onboardingChecklist'; -import ChecklistShowShare from 'my-sites/checklist/share'; -import { recordTracksEvent } from 'state/analytics/actions'; -import { requestGuidedTour } from 'state/ui/guided-tours/actions'; -import QueryPosts from 'components/data/query-posts'; -import { getSitePosts } from 'state/posts/selectors'; -import isEligibleForDotcomChecklist from 'state/selectors/is-eligible-for-dotcom-checklist'; - -const storeKeyForNeverShow = 'sitesNeverShowChecklistBanner'; - -export class ChecklistBanner extends Component { - static propTypes = { - siteId: PropTypes.number, - siteSlug: PropTypes.string, - }; - - static defaultProps = { - onClick: noop, - }; - - state = { - closed: false, - }; - - handleClick = () => { - const { requestTour, track, siteSlug, taskUrls } = this.props; - const task = this.getTask(); - - launchTask( { - task: { - ...task, - url: taskUrls[ task.id ] || task.url, - }, - location: 'checklist_banner', - requestTour, - siteSlug, - track, - } ); - }; - - handleClose = () => { - const { siteId } = this.props; - const sitesNeverShowBanner = store.get( storeKeyForNeverShow ) || {}; - sitesNeverShowBanner[ `${ siteId }` ] = true; - store.set( storeKeyForNeverShow, sitesNeverShowBanner ); - - this.setState( { closed: true } ); - - this.props.track( 'calypso_checklist_banner_close', { - site_id: siteId, - } ); - }; - - getTask() { - const task = find( - this.props.tasks, - ( { id, completed } ) => ! completed && ! get( this.props.taskStatuses, [ id, 'completed' ] ) - ); - return ( - task || { - id: 'ready-to-share', - title: this.props.translate( 'Your site is ready to share' ), - description: this.props.translate( - 'We did it! You have completed {{a}}all the tasks{{/a}} on our checklist.', - { - components: { - a: , - }, - } - ), - image: '/calypso/images/stats/tasks/ready-to-share.svg', - } - ); - } - - canShow() { - if ( ! this.props.isEligibleForDotcomChecklist ) { - return false; - } - - if ( this.state.closed ) { - return false; - } - - const sitesNeverShowBanner = store.get( storeKeyForNeverShow ); - if ( get( sitesNeverShowBanner, String( this.props.siteId ) ) === true ) { - return false; - } - - return true; - } - - renderShareButtons() { - return ( - - ); - } - - renderTaskButton() { - return ( - - ); - } - - render() { - const { siteId, taskStatuses, translate, tasks } = this.props; - const total = tasks.length; - const completed = reduce( - tasks, - ( count, { id, completed: taskComplete } ) => - taskComplete || get( taskStatuses, [ id, 'completed' ] ) ? count + 1 : count, - 0 - ); - const task = this.getTask(); - const percentage = Math.round( ( completed / total ) * 100 ) || 0; - - if ( ! this.canShow() ) { - return null; - } - - return ( - - { siteId && } - { siteId && ( - - ) } -
- { translate( 'setup' ) } - -
-
- { translate( 'Site setup' ) } - - { translate( '%(percentage)s%% completed', { args: { percentage } } ) } - - { completed === total && ( - - ) } - - -
-
-

{ task && task.title }

-

{ task && task.description }

- { completed === total ? this.renderShareButtons() : this.renderTaskButton() } -
- { task && - task.image && ( - - ) } - { completed === total && ( - - ) } -
- ); - } -} - -const mapStateToProps = ( state, { siteId } ) => { - const siteSlug = getSiteSlug( state, siteId ); - const taskStatuses = get( getSiteChecklist( state, siteId ), [ 'tasks' ] ); - - return { - siteSlug, - taskStatuses, - taskUrls: getTaskUrls( getSitePosts( state, siteId ) ), - tasks: getTasks( state, siteId ), - isEligibleForDotcomChecklist: isEligibleForDotcomChecklist( state, siteId ), - }; -}; - -const mapDispatchToProps = { - track: recordTracksEvent, - requestTour: requestGuidedTour, -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( localize( ChecklistBanner ) ); diff --git a/client/my-sites/stats/site.jsx b/client/my-sites/stats/site.jsx index 69dcc41d13e4d..6b1c50be2a232 100644 --- a/client/my-sites/stats/site.jsx +++ b/client/my-sites/stats/site.jsx @@ -3,7 +3,6 @@ /** * External dependencies */ - import page from 'page'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -33,7 +32,7 @@ import { getSelectedSiteId, getSelectedSiteSlug } from 'state/ui/selectors'; import { getSiteOption, isJetpackSite } from 'state/sites/selectors'; import { recordGoogleEvent } from 'state/analytics/actions'; import PrivacyPolicyBanner from 'blocks/privacy-policy-banner'; -import ChecklistBanner from './checklist-banner'; +import WpcomChecklist from 'my-sites/checklist/wpcom-checklist'; import QuerySiteKeyrings from 'components/data/query-site-keyrings'; import QueryKeyringConnections from 'components/data/query-keyring-connections'; import GoogleMyBusinessStatsNudge from 'blocks/google-my-business-stats-nudge'; @@ -151,7 +150,7 @@ class StatsSite extends Component { slug={ slug } />
- + { config.isEnabled( 'onboarding-checklist' ) && } { config.isEnabled( 'google-my-business' ) && siteId && (