Skip to content

Commit

Permalink
A4A: Add Hosting section. (#88755)
Browse files Browse the repository at this point in the history
* Convert filter search component as reusable component.

* Move listing section as a shared component.

* Move useProductAndPlans as a shared hook.

* Make listing section icon as optional.

* Add Pressable filter support in useProductAndPlans hook.

* Add Hosting section skeleton.

* Remove unused CSS codes.

* Implement Hosting card.

* Fix explore button redirect.

* Add WPCOM and Pressable section.

* Address PR comments.
  • Loading branch information
jkguidaven authored Mar 27, 2024
1 parent 9c4a81e commit aefd48c
Show file tree
Hide file tree
Showing 26 changed files with 686 additions and 151 deletions.
17 changes: 17 additions & 0 deletions client/a8c-for-agencies/components/filter-search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Search from 'calypso/components/search';

import './style.scss';

type Props = {
label: string;
onSearch: ( value: string ) => void;
onClick?: () => void;
};

export default function FilterSearch( { onSearch, onClick, label }: Props ) {
return (
<div className="a4a-filter-search">
<Search onClick={ onClick } onSearch={ onSearch } placeholder={ label } compact hideFocus />
</div>
);
}
28 changes: 28 additions & 0 deletions client/a8c-for-agencies/components/filter-search/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/mixins";

.a4a-filter-search {
flex-basis: 100%;

@include break-xlarge {
flex-basis: 360px;
}

.search {
&.is-open {
height: 46px;

@include breakpoint-deprecated( ">660px" ) {
height: 33px;
}
}
margin-block-end: 0;
border: 1px solid var(--color-neutral-10);
}

.search__input.form-text-input[type="search"] {
font-size: rem(13px);
font-weight: 400;
color: var(--color-text);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const PRODUCT_FILTER_ALL = '';
export const PRODUCT_FILTER_PLANS = 'plans';
export const PRODUCT_FILTER_PRODUCTS = 'products';
export const PRODUCT_FILTER_PRESSABLE_PLANS = 'pressable-plans';
export const PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS = 'woocommerce-extensions';
export const PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS = 'vaultpress-backup-addons';
14 changes: 14 additions & 0 deletions client/a8c-for-agencies/sections/marketplace/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import MarketplaceSidebar from '../../components/sidebar-menu/marketplace';
import AssignLicense from './assign-license';
import Checkout from './checkout';
import HostingOverview from './hosting-overview';
import PressableOverview from './pressable-overview';
import DownloadProducts from './primary/download-products';
import ProductsOverview from './products-overview';
import WpcomOverview from './wpcom-overview';

export const marketplaceContext: Callback = () => {
page.redirect( A4A_MARKETPLACE_PRODUCTS_LINK );
Expand All @@ -26,6 +28,18 @@ export const marketplaceHostingContext: Callback = ( context, next ) => {
next();
};

export const marketplacePressableContext: Callback = ( context, next ) => {
context.secondary = <MarketplaceSidebar path={ context.path } />;
context.primary = <PressableOverview />;
next();
};

export const marketplaceWpcomContext: Callback = ( context, next ) => {
context.secondary = <MarketplaceSidebar path={ context.path } />;
context.primary = <WpcomOverview />;
next();
};

export const checkoutContext: Callback = ( context, next ) => {
context.secondary = <MarketplaceSidebar path={ context.path } />;
context.primary = <Checkout />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { isProductMatch } from 'calypso/jetpack-cloud/sections/partner-portal/pr
import { useSelector } from 'calypso/state';
import { getAssignedPlanAndProductIDsForSite } from 'calypso/state/partner-portal/licenses/selectors';
import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types';
import isPressableHostingProduct from '../../../lib/is-pressable-hosting-product';
import {
PRODUCT_FILTER_ALL,
PRODUCT_FILTER_PLANS,
PRODUCT_FILTER_PRESSABLE_PLANS,
PRODUCT_FILTER_PRODUCTS,
PRODUCT_FILTER_VAULTPRESS_BACKUP_ADDONS,
PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS,
} from '../constants';
import { isPressableHostingProduct } from '../lib/hosting';
import type { SiteDetails } from '@automattic/data-stores';

// Plans and Products that we can merged into 1 card.
Expand Down Expand Up @@ -64,6 +65,12 @@ const getProductsAndPlansByFilter = (
allProductsAndPlans?.filter( ( { family_slug } ) => isWooCommerceProduct( family_slug ) ) ||
[]
);
case PRODUCT_FILTER_PRESSABLE_PLANS:
return (
allProductsAndPlans?.filter( ( { family_slug } ) =>
isPressableHostingProduct( family_slug )
) || []
);
}

return allProductsAndPlans || [];
Expand Down Expand Up @@ -174,6 +181,10 @@ export default function useProductAndPlans( {
PRODUCT_FILTER_WOOCOMMERCE_EXTENSIONS,
filteredProductsAndBundles
),
pressablePlans: getProductsAndPlansByFilter(
PRODUCT_FILTER_PRESSABLE_PLANS,
filteredProductsAndBundles
),
suggestedProductSlugs,
};
}, [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { TranslateResult, useTranslate } from 'i18n-calypso';
import { useMemo } from 'react';

/**
* Returns hosting name and description with given product slug.
* @param slug
* @returns
*/
export default function useHostingDescription( slug: string ): {
name: TranslateResult;
description: TranslateResult;
} {
const translate = useTranslate();

return useMemo( () => {
let description = '';
let name = '';

switch ( slug ) {
case 'pressable-hosting':
name = translate( 'Pressable' );
description = translate(
'9 custom plans built for agencies that include an intuitive control panel, easy site migration, staging environments, performance tools, and flexible upgrades & downgrades. '
);
break;
case 'wpcom-hosting':
name = translate( 'Wordpress.com' );
description = translate(
'Unbeatable uptime, unmetered bandwidth, and everything you need to streamline your development process, baked in. Perfect uptime. Fastest WP Bench score. A+ SSL grade.'
);
break;
}

return {
name,
description,
};
}, [ slug, translate ] );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Button } from '@automattic/components';
import { formatCurrency } from '@automattic/format-currency';
import { useTranslate } from 'i18n-calypso';
import { useDispatch } from 'calypso/state';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types';
import { getHostingLogo, getHostingPageUrl } from '../../lib/hosting';
import useHostingDescription from '../hooks/use-hosting-description';

import './style.scss';
type Props = {
plan: APIProductFamilyProduct;
};

export default function HostingCard( { plan }: Props ) {
const translate = useTranslate();
const dispatch = useDispatch();

const { name, description } = useHostingDescription( plan.family_slug );

const onExploreClick = () => {
dispatch(
recordTracksEvent( 'calypso_marketplace_hosting_overview_explore_plan_click', {
hosting: plan.family_slug,
} )
);
};

return (
<div className="hosting-card">
<div className="hosting-card__header">{ getHostingLogo( plan.family_slug ) }</div>

<div className="hosting-card__price">
<b className="hosting-card__price-value">
{ translate( 'Starting at %(price)s', {
args: { price: formatCurrency( Number( plan.amount ), plan.currency ) },
} ) }
</b>
<div className="hosting-card__price-interval">
{ plan.price_interval === 'day' && translate( 'USD per plan per day' ) }
{ plan.price_interval === 'month' && translate( 'USD per plan per month' ) }
</div>
</div>

<p className="hosting-card__description">{ description }</p>

<Button
className="hosting-card__explore-button"
href={ getHostingPageUrl( plan.family_slug ) }
onClick={ onExploreClick }
primary
>
{ translate( 'Explore %(hosting)s plans', {
args: {
hosting: name,
},
comment: '%(hosting)s is the name of the hosting provider.',
} ) }
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

.hosting-card {
padding: 1.5rem;
border: 1px solid var(--color-neutral-10);
border-radius: 4px;
user-select: none;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}

.hosting-card__price-value {
font-size: 1.5rem;
font-weight: 600;
line-height: 1.1;
}

.hosting-card__price-interval {
font-size: 0.75rem;
line-height: 1.1;
font-weight: 400;
}

.hosting-card__description {
font-size: 0.875rem;
line-height: 1.1;
font-weight: 400;
margin: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { SiteDetails } from '@automattic/data-stores';
import { useTranslate } from 'i18n-calypso';
import { useCallback, useMemo, useState } from 'react';
import FilterSearch from 'calypso/a8c-for-agencies/components/filter-search';
import { useDispatch } from 'calypso/state';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import useProductAndPlans from '../../hooks/use-product-and-plans';
import { getCheapestPlan } from '../../lib/hosting';
import ListingSection from '../../listing-section';
import HostingCard from '../hosting-card';

import './style.scss';

interface Props {
selectedSite?: SiteDetails | null;
}

export default function HostingList( { selectedSite }: Props ) {
const translate = useTranslate();
const dispatch = useDispatch();

const [ productSearchQuery, setProductSearchQuery ] = useState< string >( '' );

const { isLoadingProducts, pressablePlans } = useProductAndPlans( {
selectedSite,
productSearchQuery,
} );

const cheapestPressablePlan = useMemo(
() => ( pressablePlans.length ? getCheapestPlan( pressablePlans ) : null ),
[ pressablePlans ]
);

const cheapestWPCOMPlan = cheapestPressablePlan
? { ...cheapestPressablePlan, family_slug: 'wpcom-hosting' }
: null; // FIXME: Need to fetch from API

const onProductSearch = useCallback(
( value: string ) => {
setProductSearchQuery( value );
dispatch(
recordTracksEvent( 'calypso_a4a_marketplace_hosting_overview_search_submit', { value } )
);
},
[ dispatch ]
);

if ( isLoadingProducts ) {
return (
<div className="hosting-list">
<div className="hosting-list__placeholder" />
</div>
);
}

return (
<div className="hosting-list">
<div className="hosting-list__actions">
<FilterSearch label={ translate( 'Search products' ) } onSearch={ onProductSearch } />
</div>

<ListingSection
title={ translate( 'Hosting' ) }
description={ translate(
'Mix and match powerful security, performance, and growth tools for your sites.'
) }
isTwoColumns
>
{ cheapestPressablePlan && <HostingCard plan={ cheapestPressablePlan } /> }
{ cheapestWPCOMPlan && <HostingCard plan={ cheapestWPCOMPlan } /> }
</ListingSection>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.hosting-list__placeholder {
@include placeholder( --color-neutral-10 );
height: 43px;
}

.hosting-list__actions {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 16px 0 32px;
justify-content: space-between;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import MobileSidebarNavigation from 'calypso/a8c-for-agencies/components/sidebar
import { A4A_MARKETPLACE_CHECKOUT_LINK } from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants';
import useShoppingCart from '../hooks/use-shopping-cart';
import ShoppingCart from '../shopping-cart';
import HostingList from './hosting-list';

export default function Hosting() {
const translate = useTranslate();
Expand Down Expand Up @@ -42,7 +43,9 @@ export default function Hosting() {
</LayoutHeader>
</LayoutTop>

<LayoutBody>Hosting here</LayoutBody>
<LayoutBody>
<HostingList />
</LayoutBody>
</Layout>
);
}
11 changes: 11 additions & 0 deletions client/a8c-for-agencies/sections/marketplace/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
A4A_MARKETPLACE_ASSIGN_LICENSE_LINK,
A4A_MARKETPLACE_CHECKOUT_LINK,
A4A_MARKETPLACE_HOSTING_LINK,
A4A_MARKETPLACE_HOSTING_PRESSABLE_LINK,
A4A_MARKETPLACE_HOSTING_WPCOM_LINK,
A4A_MARKETPLACE_LINK,
A4A_MARKETPLACE_PRODUCTS_LINK,
A4A_MARKETPLACE_DOWNLOAD_PRODUCTS_LINK,
Expand All @@ -13,15 +15,24 @@ import {
checkoutContext,
marketplaceContext,
marketplaceHostingContext,
marketplacePressableContext,
marketplaceProductsContext,
downloadProductsContext,
marketplaceWpcomContext,
} from './controller';

export default function () {
// FIXME: check access, TOS consent, valid payment method, all sites context and partner key selection if needed
page( A4A_MARKETPLACE_LINK, marketplaceContext, makeLayout, clientRender );
page( A4A_MARKETPLACE_PRODUCTS_LINK, marketplaceProductsContext, makeLayout, clientRender );
page( A4A_MARKETPLACE_HOSTING_LINK, marketplaceHostingContext, makeLayout, clientRender );
page(
A4A_MARKETPLACE_HOSTING_PRESSABLE_LINK,
marketplacePressableContext,
makeLayout,
clientRender
);
page( A4A_MARKETPLACE_HOSTING_WPCOM_LINK, marketplaceWpcomContext, makeLayout, clientRender );
page( A4A_MARKETPLACE_CHECKOUT_LINK, checkoutContext, makeLayout, clientRender );
page( A4A_MARKETPLACE_ASSIGN_LICENSE_LINK, assignLicenseContext, makeLayout, clientRender );
page( A4A_MARKETPLACE_DOWNLOAD_PRODUCTS_LINK, downloadProductsContext, makeLayout, clientRender );
Expand Down
Loading

0 comments on commit aefd48c

Please sign in to comment.