From 6ffd538ea609a95788da888f99709c599d7b3dc4 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 25 Nov 2019 18:19:04 +0100 Subject: [PATCH] Add support for products on My Plan page and update layout to match mocks --- _inc/client/lib/plans/constants.js | 11 + _inc/client/my-plan/index.jsx | 4 +- _inc/client/my-plan/my-plan-card/README.md | 43 +++ _inc/client/my-plan/my-plan-card/index.jsx | 49 +++ _inc/client/my-plan/my-plan-card/style.scss | 122 +++++++ _inc/client/my-plan/my-plan-header/index.js | 348 +++++++++----------- _inc/client/my-plan/style.scss | 100 +----- images/products/product-jetpack-backup.svg | 1 + 8 files changed, 383 insertions(+), 295 deletions(-) create mode 100644 _inc/client/my-plan/my-plan-card/README.md create mode 100644 _inc/client/my-plan/my-plan-card/index.jsx create mode 100644 _inc/client/my-plan/my-plan-card/style.scss create mode 100644 images/products/product-jetpack-backup.svg diff --git a/_inc/client/lib/plans/constants.js b/_inc/client/lib/plans/constants.js index 5fcae039daae7..57e0f1c3f9e95 100644 --- a/_inc/client/lib/plans/constants.js +++ b/_inc/client/lib/plans/constants.js @@ -41,6 +41,13 @@ export const JETPACK_MONTHLY_PLANS = [ PLAN_JETPACK_PERSONAL_MONTHLY, ]; +export const JETPACK_BACKUP_PRODUCTS = [ + PLAN_JETPACK_BACKUP_DAILY, + PLAN_JETPACK_BACKUP_DAILY_MONTHLY, + PLAN_JETPACK_BACKUP_REALTIME, + PLAN_JETPACK_BACKUP_REALTIME_MONTHLY, +]; + export const PLAN_MONTHLY_PERIOD = 31; export const PLAN_ANNUAL_PERIOD = 365; @@ -119,6 +126,10 @@ export function isNew( plan ) { return includes( NEW_PLANS, plan ); } +export function isJetpackBackup( product ) { + return includes( JETPACK_BACKUP_PRODUCTS, product ); +} + export function getPlanClass( plan ) { switch ( plan ) { case PLAN_JETPACK_FREE: diff --git a/_inc/client/my-plan/index.jsx b/_inc/client/my-plan/index.jsx index f47b223fad7ff..6a5d0e9bee5cc 100644 --- a/_inc/client/my-plan/index.jsx +++ b/_inc/client/my-plan/index.jsx @@ -27,9 +27,7 @@ export class MyPlan extends React.Component { return (
-
- -
+ ` + +```jsx +import React from 'react'; +import MyPlanCard from 'components/my-plan-card'; +import Button from 'components/button'; + +export default class extends React.Component { + render() { + return ( + Manage Payment } + details="Expires on October 27, 2020" + icon="images/plans/plan-personal.svg" + tagLine="Your data is being securely backed up and you have access to priority support." + title="Jetpack Personal" + /> + ); + } +} +``` + +### `` props + +The following props can be passed to the My Plan Card component: + +* `action`: ( element | node ) Action button element or node. +* `isError`: ( bool ) With this flag being set the details string is in an error state (red copy). +* `isPlaceholder`: ( bool ) Flag indicating that the component in is a loading state +* `details`: ( string ) Details about a plan or product, e.g. expiration or auto-renew date like `Expires on October 27, 2020` +* `icon`: ( string ) Plan or product icon path +* `tagLine`: ( string | element | node ) Plan or product tag line. It can be a string, a node or a React element (e.g. ``) +* `title`: ( string | element | node ) Plan or product title. It can be a string, a node or a React element (e.g. ``) diff --git a/_inc/client/my-plan/my-plan-card/index.jsx b/_inc/client/my-plan/my-plan-card/index.jsx new file mode 100644 index 0000000000000..755a4df5b01be --- /dev/null +++ b/_inc/client/my-plan/my-plan-card/index.jsx @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +/** + * Style dependencies + */ +import './style.scss'; + +const MyPlanCard = ( { action, isError, isPlaceholder, details, icon, tagLine, title } ) => { + const cardClassNames = classNames( 'my-plan-card', { + 'is-placeholder': isPlaceholder, + 'has-action-only': action && ! details && ! isPlaceholder, + } ); + const detailsClassNames = classNames( 'my-plan-card__details', { 'is-error': isError } ); + + return ( +
+
+
{ icon && { }
+
+ { title &&

{ title }

} + { tagLine &&

{ tagLine }

} +
+
+ { ( details || action || isPlaceholder ) && ( +
+
{ isPlaceholder ? null : details }
+
{ isPlaceholder ? null : action }
+
+ ) } +
+ ); +}; + +MyPlanCard.propTypes = { + action: PropTypes.oneOfType( [ PropTypes.node, PropTypes.element ] ), + isError: PropTypes.bool, + isPlaceholder: PropTypes.bool, + details: PropTypes.string, + icon: PropTypes.string, + tagLine: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node, PropTypes.element ] ), + title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node, PropTypes.element ] ), +}; + +export default MyPlanCard; diff --git a/_inc/client/my-plan/my-plan-card/style.scss b/_inc/client/my-plan/my-plan-card/style.scss new file mode 100644 index 0000000000000..9e3cad73d85d1 --- /dev/null +++ b/_inc/client/my-plan/my-plan-card/style.scss @@ -0,0 +1,122 @@ +@import '../../scss/mixin_breakpoint'; +@import '../../scss/calypso-colors'; + +// My Plan Card +.my-plan-card { + @include breakpoint( '>960px' ) { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + } +} + +.my-plan-card__primary { + display: flex; + flex-flow: row nowrap; + flex-grow: 1; +} + +.my-plan-card__header { + flex: 1; +} + +.my-plan-card__title { + font-size: 20px; + font-weight: 600; + line-height: 29px; + margin: 6px 0; + color: $gray-dark; +} + +.my-plan-card__tag-line { + font-size: 14px; + font-weight: 400; + line-height: 17px; + margin: 0 0 24px; + + @include breakpoint( '>960px' ) { + margin-bottom: 8px; + } +} + +.my-plan-card__icon { + flex: 0 0 auto; + width: 64px; + height: 64px; + margin: 8px 20px 16px 0; + + @include breakpoint( '<660px' ) { + display: none; + } + + img { + width: 100%; + height: 100%; + } +} + +.my-plan-card__secondary { + position: relative; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: space-between; + padding: 8px 0 0; + + @include breakpoint( '>960px' ) { + flex-flow: column nowrap; + justify-content: center; + align-items: flex-end; + padding: 0 0 0 24px; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: -16px; + right: -16px; + border-top: 1px solid $light-gray-700; + + @include breakpoint( '>480px' ) { + left: -24px; + right: -24px; + } + + @include breakpoint( '>960px' ) { + content: none; + } + } + + .has-action-only & { + padding-top: 0; + justify-content: center; + + &::before { + content: none; + } + } +} + +.my-plan-card__details { + padding-top: 8px; + white-space: nowrap; + color: $gray-darken-10; + + @include breakpoint( '>960px' ) { + padding-top: 0; + } + + &.is-error { + color: $alert-red; + } +} + +.my-plan-card__action { + padding-top: 8px; + white-space: nowrap; + + .has-action-only & { + padding-top: 0; + } +} diff --git a/_inc/client/my-plan/my-plan-header/index.js b/_inc/client/my-plan/my-plan-header/index.js index 7f2fe8fd8bd63..a17072d382e44 100644 --- a/_inc/client/my-plan/my-plan-header/index.js +++ b/_inc/client/my-plan/my-plan-header/index.js @@ -2,240 +2,190 @@ * External dependencies */ import React from 'react'; -import { translate as __ } from 'i18n-calypso'; +import { moment, translate as __ } from 'i18n-calypso'; import { connect } from 'react-redux'; +import { find, filter, isEmpty } from 'lodash'; /** * Internal dependencies */ -import analytics from 'lib/analytics'; +import Card from 'components/card'; import ChecklistCta from './checklist-cta'; import ChecklistProgress from './checklist-progress-card'; -import { getPlanClass } from 'lib/plans/constants'; +import MyPlanCard from '../my-plan-card'; +import UpgradeLink from 'components/upgrade-link'; +import { getPlanClass, isJetpackBackup } from 'lib/plans/constants'; import { getUpgradeUrl, getSiteRawUrl, showBackups } from 'state/initial-state'; +import { getSitePurchases } from 'state/site'; import { imagePath } from 'constants/urls'; -import UpgradeLink from 'components/upgrade-link'; +import PropTypes from 'prop-types'; class MyPlanHeader extends React.Component { - trackChecklistCtaClick = () => - void analytics.tracks.recordEvent( - 'jetpack_myplan_headerchecklistcta_click', - this.props.plan - ? { - plan: this.props.plan, - } - : undefined - ); + getProductProps( productSlug ) { + const { displayBackups, purchases } = this.props; - render() { - const { plan, siteSlug } = this.props; + if ( ! productSlug ) { + return { + isPlaceholder: true, + }; + } - const PlanHeaderCard = props => { - const { title, text, imgSrc, imgAlt } = props; - - return ( -
-
- { -
-
-

{ title }

-

{ text }

- -
-
- ); - }; - - let planCard = ''; - switch ( getPlanClass( plan ) ) { + const purchase = find( purchases, purchaseObj => purchaseObj.product_slug === productSlug ); + const expiration = + purchase && purchase.expiry_date + ? __( 'Expires on %s.', { args: moment( purchase.expiry_date ).format( 'LL' ) } ) + : null; + + switch ( getPlanClass( productSlug ) ) { case 'is-free-plan': - planCard = ( -
-
- { -
-
-

- { __( 'Your plan: Jetpack Free' ) } -

-

- { __( - 'Worried about security? Get backups, automated security fixes and more: {{a}}Upgrade now{{/a}}', - { - components: { - a: ( - - ), - }, - } - ) } -

- -
-
- ); - break; + return { + icon: imagePath + '/plans/plan-free.svg', + tagLine: __( + 'Worried about security? Get backups, automated security fixes and more: {{a}}Upgrade now{{/a}}', + { + components: { + a: ( + + ), + }, + } + ), + title: __( 'Jetpack Free' ), + }; case 'is-personal-plan': - planCard = ( -
-
- { -
-
-

- { __( 'Your plan: Jetpack Personal' ) } -

- { this.props.showBackups ? ( -

- { __( 'Daily backups, spam filtering, and priority support.' ) } -

- ) : ( -

- { __( 'Spam filtering and priority support.' ) } -

- ) } - -
-
- ); - break; + return { + details: expiration, + icon: imagePath + '/plans/plan-personal.svg', + tagLine: displayBackups + ? __( 'Daily backups, spam filtering, and priority support.' ) + : __( 'Spam filtering and priority support.' ), + title: __( 'Jetpack Personal' ), + }; case 'is-premium-plan': - planCard = ( -
-
- { -
-
-

- { __( 'Your plan: Jetpack Premium' ) } -

-

- { __( - 'Full security suite, marketing and revenue automation tools, unlimited video hosting, and priority support.' - ) } -

- -
-
- ); - break; + return { + details: expiration, + icon: imagePath + '/plans/plan-premium.svg', + tagLine: __( + 'Full security suite, marketing and revenue automation tools, unlimited video hosting, and priority support.' + ), + title: __( 'Jetpack Premium' ), + }; case 'is-business-plan': - planCard = ( -
-
- { -
-
-

- { __( 'Your plan: Jetpack Professional' ) } -

-

- { __( - 'Full security suite, marketing and revenue automation tools, unlimited video hosting, unlimited themes, enhanced search, and priority support.' - ) } -

- -
-
- ); - break; + return { + details: expiration, + icon: imagePath + '/plans/plan-business.svg', + tagLine: __( + 'Full security suite, marketing and revenue automation tools, unlimited video hosting, unlimited themes, enhanced search, and priority support.' + ), + title: __( 'Jetpack Professional' ), + }; case 'is-daily-backup-plan': - planCard = ( - - ), - }, - } - ) } - imgSrc={ imagePath + '/plans/plan-free.svg' } - imgAlt={ __( 'Jetpack Daily Backup Plan' ) } - /> - ); - break; + return { + details: expiration, + icon: imagePath + '/products/product-jetpack-backup.svg', + tagLine: __( 'Your data is being securely backed up every day with a 30-day archive.' ), + title: __( 'Jetpack Backup {{em}}Daily{{/em}}', { + components: { + em: , + }, + } ), + }; case 'is-realtime-backup-plan': - planCard = ( - - ), - }, - } - ) } - imgSrc={ imagePath + '/plans/plan-free.svg' } - imgAlt={ __( 'Jetpack Real-time Backup Plan' ) } - /> - ); - break; + return { + details: expiration, + icon: imagePath + '/products/product-jetpack-backup.svg', + tagLine: __( 'Your data is being securely backed up as you edit.' ), + title: __( 'Jetpack Backup {{em}}Real-Time{{/em}}', { + components: { + em: , + }, + } ), + }; default: - planCard = ( -
-
-
-

-

-
-
- ); - break; + return { + isPlaceholder: true, + }; + } + } + + renderPlan() { + const { plan } = this.props; + const planProps = this.getProductProps( plan ); + + return ( + + { this.renderHeader( __( 'My Plan' ) ) } + + + ); + } + + renderProducts() { + const { purchases } = this.props; + const products = filter( purchases, purchase => isJetpackBackup( purchase.product_slug ) ); + + if ( isEmpty( products ) ) { + return null; } + return ( - <> -
{ planCard }
+ + { this.renderHeader( __( 'My Products' ) ) } + { products.map( ( { ID, product_slug } ) => { + const productProps = this.getProductProps( product_slug ); + + return ; + } ) } + + ); + } + + renderHeader( title ) { + return

{ title }

; + } + + render() { + const { plan, siteSlug } = this.props; + + return ( +
+ { this.renderPlan() } + { this.renderProducts() } + + + - +
); } } + +MyPlanHeader.propTypes = { + plan: PropTypes.string, + siteRawUrl: PropTypes.string, + + // From connect HoC + siteSlug: PropTypes.string, + displayBackups: PropTypes.bool, + plansMainTopUpgradeUrl: PropTypes.string, + purchases: PropTypes.array, +}; + export default connect( state => { return { siteSlug: getSiteRawUrl( state ), - showBackups: showBackups( state ), + displayBackups: showBackups( state ), plansMainTopUpgradeUrl: getUpgradeUrl( state, 'plans-main-top' ), + purchases: getSitePurchases( state ), }; } )( MyPlanHeader ); diff --git a/_inc/client/my-plan/style.scss b/_inc/client/my-plan/style.scss index 6507d21d970eb..f9c347206c7e9 100644 --- a/_inc/client/my-plan/style.scss +++ b/_inc/client/my-plan/style.scss @@ -1,49 +1,24 @@ @import '../scss/calypso-colors'; -.jp-landing__plans.dops-card { - padding: 0; -} - .jp-landing__plans { + margin-bottom: 32px; .dops-button { margin-right: 10px; } } -.jp-landing__plan-card-current { - // Force a break by including full-width psuedo-element at correct position. - &::before { - content: ''; - order: 20; - width: 100%; - } - - h3 { - font-size: 1.5em; - width: 100%; // Make sure placeholder appears - order: 10; - } - - .jp-landing__plan-features-text { - font-size: rem( 14px ); - order: 30; - } - - @include breakpoint( ">660px" ) { - display: flex; - flex-wrap: wrap; - - .jp-landing__plan-features-text { - margin-right: 3em; - } - } - +.jp-landing__card-header { + margin-top: 0; + font-size: 15px; + color: $gray-darken-20; } .jp-landing__plan-features-header-checklist-cta-container { order: 30; align-self: center; + display: flex; + justify-content: flex-end; .dops-button { margin-right: 0; @@ -136,67 +111,6 @@ } } -.jp-landing__plan-card { - padding: rem( 32px ); - - @include breakpoint( "<480px" ) { - text-align: center; - } - - @include breakpoint( ">480px" ) { - display: flex; - align-items: center; - flex-wrap: nowrap; - } - - .jp-landing__plan-features-title, - .jp-landing__plan-features-text { - padding: 0; - } - - .jp-landing__plan-features-title { - margin-bottom: rem( 16px ); - } - - .jp-landing__plan-features-text:last-child { - margin-bottom: 0; - } -} - -.jp-landing__plan-card-img { - float: left; - margin-right: rem( 42px ); - - @include breakpoint( "<960px" ) { - margin-right: rem( 16px ); - } - - @include breakpoint( "<480px" ) { - width: 100%; - max-width: 100%; - text-align: center; - } -} - -.jp-landing__plan-icon { - width: rem( 82px ); - position: relative; - left: rem( -3px ); - - @include breakpoint( ">960px" ) { - width: rem( 96px ); - left: rem( 2px ); - } -} - -.jp-landing__plan-card-img.is-placeholder { - width: rem( 120px ); - height: rem( 85px ); - - & + .jp-landing__plan-card-current { - width: 80%; - } -} .jp-landing__plan-features-title.is-placeholder { height: rem( 24px ); diff --git a/images/products/product-jetpack-backup.svg b/images/products/product-jetpack-backup.svg new file mode 100644 index 0000000000000..fdebe39c06a73 --- /dev/null +++ b/images/products/product-jetpack-backup.svg @@ -0,0 +1 @@ +