From 8146923b37b94a422c5ddcb8b65b0fdcfaa95f44 Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 25 Nov 2024 15:14:32 -0800 Subject: [PATCH] A4A: add support for new pressable enterprise plans in marketplace (#96647) * add support for new pressable enterprise plans split into separate tabs * only optionally add enterprise plans tab * undo previous change, update tab name * select the right tab based on existing plan * wrap tab buttons on smaller screens * fix slider in referral mode * use selectedTab instead of selectedCategory * use constants for plan categories * rename function * let getSliderOptions return plans of a specific category * add category directly to objects for clarity * disable standard tab if existing plan is business 150 or higher * give tab container max width of 600px --- .../pressable-overview/constants.ts | 2 + .../lib/get-pressable-plan.ts | 71 +++++++ .../lib/get-slider-options.ts | 8 +- .../plan-selection/filter.tsx | 199 +++++++++++++----- .../plan-selection/style.scss | 33 +++ 5 files changed, 261 insertions(+), 52 deletions(-) diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/constants.ts b/client/a8c-for-agencies/sections/marketplace/pressable-overview/constants.ts index 264aa100feb87..464ead422145e 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/constants.ts +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/constants.ts @@ -1,2 +1,4 @@ export const FILTER_TYPE_INSTALL = 'install'; export const FILTER_TYPE_VISITS = 'visits'; +export const PLAN_CATEGORY_STANDARD = 'standard'; +export const PLAN_CATEGORY_ENTERPRISE = 'enterprise'; diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts index 78e2cf203e5e0..6c7b31a744eb5 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts @@ -1,8 +1,11 @@ +import { PLAN_CATEGORY_STANDARD, PLAN_CATEGORY_ENTERPRISE } from '../constants'; + export type PressablePlan = { slug: string; install: number; visits: number; storage: number; + category: string; }; const PLAN_DATA: Record< string, PressablePlan > = { @@ -11,42 +14,49 @@ const PLAN_DATA: Record< string, PressablePlan > = { install: 1, visits: 50000, storage: 20, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-2': { slug: 'pressable-wp-2', install: 5, visits: 100000, storage: 50, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-3': { slug: 'pressable-wp-3', install: 10, visits: 250000, storage: 80, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-4': { slug: 'pressable-wp-4', install: 25, visits: 500000, storage: 175, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-5': { slug: 'pressable-wp-5', install: 50, visits: 1000000, storage: 250, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-6': { slug: 'pressable-wp-6', install: 75, visits: 1500000, storage: 350, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-wp-7': { slug: 'pressable-wp-7', install: 100, visits: 2000000, storage: 500, + category: PLAN_CATEGORY_STANDARD, }, // New pressable plans @@ -55,60 +65,121 @@ const PLAN_DATA: Record< string, PressablePlan > = { install: 1, visits: 30000, storage: 20, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-growth': { slug: 'pressable-growth', install: 3, visits: 50000, storage: 30, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-advanced': { slug: 'pressable-advanced', install: 5, visits: 75000, storage: 35, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-pro': { slug: 'pressable-pro', install: 10, visits: 150000, storage: 50, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-premium': { slug: 'pressable-premium', install: 20, visits: 400000, storage: 80, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-business': { slug: 'pressable-business', install: 50, visits: 1000000, storage: 200, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-business-80': { slug: 'pressable-business-80', install: 80, visits: 1600000, storage: 275, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-business-100': { slug: 'pressable-business-100', install: 100, visits: 2000000, storage: 325, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-business-120': { slug: 'pressable-business-120', install: 120, visits: 2400000, storage: 375, + category: PLAN_CATEGORY_STANDARD, }, 'pressable-business-150': { slug: 'pressable-business-150', install: 150, visits: 3000000, storage: 450, + category: PLAN_CATEGORY_STANDARD, + }, + + // Pressable Enterprise plans + 'pressable-enterprise-4': { + slug: 'pressable-enterprise-4', + install: 200, + visits: 4000000, + storage: 500, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-5': { + slug: 'pressable-enterprise-5', + install: 250, + visits: 5000000, + storage: 550, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-6': { + slug: 'pressable-enterprise-6', + install: 300, + visits: 6000000, + storage: 600, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-7': { + slug: 'pressable-enterprise-7', + install: 350, + visits: 7000000, + storage: 700, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-8': { + slug: 'pressable-enterprise-8', + install: 400, + visits: 8000000, + storage: 800, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-9': { + slug: 'pressable-enterprise-9', + install: 450, + visits: 9000000, + storage: 900, + category: PLAN_CATEGORY_ENTERPRISE, + }, + 'pressable-enterprise-10': { + slug: 'pressable-enterprise-10', + install: 500, + visits: 10000000, + storage: 1000, + category: PLAN_CATEGORY_ENTERPRISE, }, }; diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-slider-options.ts b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-slider-options.ts index 804f675a903d0..5f327a5a40afa 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-slider-options.ts +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-slider-options.ts @@ -3,14 +3,20 @@ import { FILTER_TYPE_INSTALL } from '../constants'; import { FilterType } from '../types'; import { PressablePlan } from './get-pressable-plan'; -export default function getSliderOptions( type: FilterType, plans: PressablePlan[] ) { +export default function getSliderOptions( + type: FilterType, + plans: PressablePlan[], + category?: string +) { return plans .filter( ( plan ) => plan !== undefined ) + .filter( ( plan ) => category === undefined || plan.category === category ) // Maybe only return plans of a specific category .sort( ( planA, planB ) => planA.install - planB.install ) // Ensure our options are sorted by install count .map( ( plan ) => { return { label: `${ type === FILTER_TYPE_INSTALL ? plan.install : formatNumber( plan.visits ) }`, value: plan.slug, + category: plan.category, }; } ); } diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx index 67f02de7bc08d..eb697e4b2fa45 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx @@ -1,21 +1,31 @@ -import { Button } from '@wordpress/components'; +import { Button, TabPanel } from '@wordpress/components'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import A4ASlider, { Option } from 'calypso/a8c-for-agencies/components/slider'; import { useDispatch } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types'; -import { FILTER_TYPE_INSTALL, FILTER_TYPE_VISITS } from '../constants'; +import { + FILTER_TYPE_INSTALL, + FILTER_TYPE_VISITS, + PLAN_CATEGORY_STANDARD, + PLAN_CATEGORY_ENTERPRISE, +} from '../constants'; import getPressablePlan, { PressablePlan } from '../lib/get-pressable-plan'; import getSliderOptions from '../lib/get-slider-options'; import { FilterType } from '../types'; type Props = { + // Plan details for the plan that's currently selected in the UI selectedPlan: APIProductFamilyProduct | null; + // All available Pressable plans plans: APIProductFamilyProduct[]; + // The users existing Pressable plan if any pressablePlan?: PressablePlan | null; + // Plan selection handler onSelectPlan: ( plan: APIProductFamilyProduct | null ) => void; + // Whether the existing plan is still being loaded isLoading?: boolean; }; @@ -30,16 +40,30 @@ export default function PlanSelectionFilter( { const dispatch = useDispatch(); const [ filterType, setFilterType ] = useState< FilterType >( FILTER_TYPE_INSTALL ); + const [ selectedTab, setSelectedTab ] = useState( PLAN_CATEGORY_STANDARD ); + const [ disableStandardTab, setDisableStandardTab ] = useState( false ); - const options = useMemo( + const standardOptions = useMemo( + () => + getSliderOptions( + filterType, + plans.map( ( plan ) => getPressablePlan( plan.slug ) ), + PLAN_CATEGORY_STANDARD + ), + [ filterType, plans ] + ); + + const enterpriseOptions = useMemo( () => [ ...getSliderOptions( filterType, - plans.map( ( plan ) => getPressablePlan( plan.slug ) ) + plans.map( ( plan ) => getPressablePlan( plan.slug ) ), + PLAN_CATEGORY_ENTERPRISE ), { label: translate( 'More' ), value: null, + category: null, }, ], [ filterType, plans, translate ] @@ -58,9 +82,9 @@ export default function PlanSelectionFilter( { [ dispatch, onSelectPlan, plans ] ); - const selectedOption = options.findIndex( - ( { value } ) => value === ( selectedPlan ? selectedPlan.slug : null ) - ); + const selectedOptionIndex = ( + PLAN_CATEGORY_STANDARD === selectedTab ? standardOptions : enterpriseOptions + ).findIndex( ( { value } ) => value === ( selectedPlan ? selectedPlan.slug : null ) ); const onSelectInstallFilterType = useCallback( () => { setFilterType( FILTER_TYPE_INSTALL ); @@ -82,23 +106,51 @@ export default function PlanSelectionFilter( { : 'a4a-pressable-filter-wrapper-visits'; const wrapperClass = clsx( additionalWrapperClass, 'pressable-overview-plan-selection__filter' ); - const minimum = useMemo( () => { + const getSliderMinimum = useCallback( + ( category: string, categoryOptions: Option[] ) => { + if ( ! pressablePlan ) { + return 0; + } + + // Depending on the category of the existing plan, we might want to show other category slider at the most min or max + if ( + PLAN_CATEGORY_STANDARD === category && + PLAN_CATEGORY_STANDARD !== pressablePlan?.category + ) { + return categoryOptions.length - 1; + } else if ( + PLAN_CATEGORY_ENTERPRISE === category && + PLAN_CATEGORY_ENTERPRISE !== pressablePlan?.category + ) { + return 0; + } + + for ( let i = 0; i < categoryOptions.length; i++ ) { + const plan = getPressablePlan( categoryOptions[ i ].value as string ); + if ( pressablePlan?.install < plan?.install ) { + return i; + } + } + return categoryOptions.length; + }, + [ pressablePlan ] + ); + + useEffect( () => { if ( ! pressablePlan ) { - return 0; + return; } - const allAvailablePlans = plans - .map( ( plan ) => getPressablePlan( plan.slug ) ) - .filter( ( plan ) => plan !== undefined ) - .sort( ( a, b ) => a?.install - b?.install ); + setSelectedTab( pressablePlan.category ?? PLAN_CATEGORY_STANDARD ); - for ( let i = 0; i < allAvailablePlans.length; i++ ) { - if ( pressablePlan?.install < allAvailablePlans[ i ]?.install ) { - return i; - } + // Disable the standard tab if the existing plan is the highest standard plan or higher + if ( + pressablePlan.category !== PLAN_CATEGORY_STANDARD || + pressablePlan.slug === standardOptions[ standardOptions.length - 1 ]?.value + ) { + setDisableStandardTab( true ); } - return allAvailablePlans.length; - }, [ plans, pressablePlan ] ); + }, [ pressablePlan, standardOptions ] ); if ( isLoading ) { return ( @@ -109,39 +161,84 @@ export default function PlanSelectionFilter( { ); } - return ( -
-
-

- { translate( 'Filter by:' ) } -

-
- - - -
+ const FilterByPicker = () => ( +
+

+ { translate( 'Filter by:' ) } +

+
+ + +
+
+ ); - + return ( +
+ + { ( tab ) => { + switch ( tab.name ) { + case PLAN_CATEGORY_STANDARD: + return ( + <> + + + + ); + case PLAN_CATEGORY_ENTERPRISE: + return ( + <> + + + + ); + default: + return null; + } + } } +
); } diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/style.scss b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/style.scss index 2164538c4ac9a..b6b53d7a2d98c 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/style.scss +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/style.scss @@ -303,3 +303,36 @@ .pressable-overview-plan-selection__tooltip { max-width: 400px; } + +.pressable-overview-plan-selection__plan-category-tabpanel { + .components-tab-panel__tabs { + justify-content: center; + flex-wrap: wrap; + border: 1px solid var(--color-neutral-5); + border-radius: 4px; + padding: 7px 0; + margin: 0 auto 32px; + max-width: 600px; + + button { + flex: 1; + margin: 0 7px; + + &:hover { + background-color: initial; + color: inherit; + } + + &:disabled, &[aria-disabled="true"] { + color: var(--color-neutral-20); + } + + &.pressable-overview-plan-selection__plan-category-tab-is-active { + background-color: var(--color-primary-50); + border-color: var(--color-primary-50); + fill: var(--color-text-inverted); + color: var(--color-text-inverted); + } + } + } +}