Add introduction banner card into multichannel marketing page (#37110)

This commit is contained in:
Gan Eng Chin 2023-03-20 19:25:01 +08:00 committed by GitHub
commit bebad071a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 484 additions and 82 deletions

View File

@ -8,3 +8,4 @@ export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody';
export { CardHeaderTitle } from './CardHeaderTitle';
export { CardHeaderDescription } from './CardHeaderDescription';
export { CenteredSpinner } from './CenteredSpinner';
export { CreateNewCampaignModal } from './CreateNewCampaignModal';

View File

@ -1,4 +1,6 @@
export { useIntroductionBanner } from './useIntroductionBanner';
export { useInstalledPlugins } from './useInstalledPlugins';
export { useRegisteredChannels } from './useRegisteredChannels';
export { useRecommendedChannels } from './useRecommendedChannels';
export { useCampaignTypes } from './useCampaignTypes';
export { useCampaigns } from './useCampaigns';

View File

@ -27,13 +27,10 @@ type UseCampaignsType = {
/**
* Custom hook to get campaigns.
*
* @param page Page number. First page is `1`.
* @param perPage Page size, i.e. number of records in one page.
* @param page Page number. Default is `1`.
* @param perPage Page size, i.e. number of records in one page. Default is `5`.
*/
export const useCampaigns = (
page: number,
perPage: number
): UseCampaignsType => {
export const useCampaigns = ( page = 1, perPage = 5 ): UseCampaignsType => {
const { data: channels } = useRegisteredChannels();
return useSelect(

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
type UseIntroductionBanner = {
loading: boolean;
isIntroductionBannerDismissed: boolean;
dismissIntroductionBanner: () => void;
};
const OPTION_NAME_BANNER_DISMISSED =
'woocommerce_marketing_overview_multichannel_banner_dismissed';
const OPTION_VALUE_YES = 'yes';
export const useIntroductionBanner = (): UseIntroductionBanner => {
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const dismissIntroductionBanner = () => {
updateOptions( {
[ OPTION_NAME_BANNER_DISMISSED ]: OPTION_VALUE_YES,
} );
recordEvent( 'marketing_multichannel_banner_dismissed', {} );
};
const { loading, data } = useSelect( ( select ) => {
const { getOption, hasFinishedResolution } =
select( OPTIONS_STORE_NAME );
return {
loading: ! hasFinishedResolution( 'getOption', [
OPTION_NAME_BANNER_DISMISSED,
] ),
data: getOption( OPTION_NAME_BANNER_DISMISSED ),
};
}, [] );
return {
loading,
isIntroductionBannerDismissed: data === OPTION_VALUE_YES,
dismissIntroductionBanner,
};
};

View File

@ -7,21 +7,23 @@ import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { useCampaigns } from './useCampaigns';
import { useCampaignTypes } from '~/marketing/hooks';
import { useCampaignTypes, useCampaigns } from '~/marketing/hooks';
import { Campaigns } from './Campaigns';
jest.mock( './useCampaigns', () => ( {
useCampaigns: jest.fn(),
} ) );
jest.mock( '~/marketing/hooks', () => ( {
useCampaigns: jest.fn(),
useCampaignTypes: jest.fn(),
} ) );
jest.mock( './CreateNewCampaignModal', () => ( {
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
} ) );
jest.mock( '~/marketing/components', () => {
const originalModule = jest.requireActual( '~/marketing/components' );
return {
__esModule: true,
...originalModule,
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
};
} );
/**
* Create a test campaign data object.

View File

@ -24,9 +24,11 @@ import {
/**
* Internal dependencies
*/
import { CardHeaderTitle } from '~/marketing/components';
import { useCampaigns } from './useCampaigns';
import { CreateNewCampaignModal } from './CreateNewCampaignModal';
import {
CardHeaderTitle,
CreateNewCampaignModal,
} from '~/marketing/components';
import { useCampaigns } from '~/marketing/hooks';
import './Campaigns.scss';
const tableCaption = __( 'Campaigns', 'woocommerce' );

View File

@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { Fragment, useState } from '@wordpress/element';
import {
Fragment,
useState,
forwardRef,
useImperativeHandle,
useRef,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
Card,
@ -32,72 +38,97 @@ type ChannelsProps = {
onInstalledAndActivated?: () => void;
};
export const Channels: React.FC< ChannelsProps > = ( {
registeredChannels,
recommendedChannels,
onInstalledAndActivated,
} ) => {
const hasRegisteredChannels = registeredChannels.length >= 1;
export type ChannelsRef = {
/**
* State to collapse / expand the recommended channels.
* Initial state is expanded if there are no registered channels in first page load.
* Scroll into the "Add channels" section in the card.
* The section will be expanded, and the "Add channels" button will be in focus.
*/
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
scrollIntoAddChannels: () => void;
};
return (
<Card className="woocommerce-marketing-channels-card">
<CardHeader>
<CardHeaderTitle>
{ __( 'Channels', 'woocommerce' ) }
</CardHeaderTitle>
{ ! hasRegisteredChannels && (
<CardHeaderDescription>
{ __(
'Start by adding a channel to your store',
'woocommerce'
) }
</CardHeaderDescription>
) }
</CardHeader>
export const Channels = forwardRef< ChannelsRef, ChannelsProps >(
(
{ registeredChannels, recommendedChannels, onInstalledAndActivated },
ref
) => {
const hasRegisteredChannels = registeredChannels.length >= 1;
{ /* Registered channels section. */ }
{ registeredChannels.map( ( el, idx ) => {
return (
/**
* State to collapse / expand the recommended channels.
* Initial state is expanded if there are no registered channels in first page load.
*/
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
const addChannelsButtonRef = useRef< HTMLButtonElement >( null );
useImperativeHandle(
ref,
() => ( {
scrollIntoAddChannels: () => {
setExpanded( true );
addChannelsButtonRef.current?.focus();
addChannelsButtonRef.current?.scrollIntoView( {
block: 'center',
} );
},
} ),
[]
);
return (
<Card className="woocommerce-marketing-channels-card">
<CardHeader>
<CardHeaderTitle>
{ __( 'Channels', 'woocommerce' ) }
</CardHeaderTitle>
{ ! hasRegisteredChannels && (
<CardHeaderDescription>
{ __(
'Start by adding a channel to your store',
'woocommerce'
) }
</CardHeaderDescription>
) }
</CardHeader>
{ /* Registered channels section. */ }
{ registeredChannels.map( ( el, idx ) => (
<Fragment key={ el.slug }>
<RegisteredChannelCardBody registeredChannel={ el } />
{ idx !== registeredChannels.length - 1 && (
<CardDivider />
) }
</Fragment>
);
} ) }
) ) }
{ /* Recommended channels section. */ }
{ recommendedChannels.length >= 1 && (
<div>
{ !! hasRegisteredChannels && (
<>
<CardDivider />
<CardBody>
<Button
variant="link"
onClick={ () => setExpanded( ! expanded ) }
>
{ __( 'Add channels', 'woocommerce' ) }
<Icon
icon={
expanded ? chevronUp : chevronDown
{ /* Recommended channels section. */ }
{ recommendedChannels.length >= 1 && (
<div>
{ !! hasRegisteredChannels && (
<>
<CardDivider />
<CardBody>
<Button
ref={ addChannelsButtonRef }
variant="link"
onClick={ () =>
setExpanded( ! expanded )
}
size={ 24 }
/>
</Button>
</CardBody>
</>
) }
{ !! expanded &&
recommendedChannels.map( ( el, idx ) => {
return (
>
{ __( 'Add channels', 'woocommerce' ) }
<Icon
icon={
expanded
? chevronUp
: chevronDown
}
size={ 24 }
/>
</Button>
</CardBody>
</>
) }
{ !! expanded &&
recommendedChannels.map( ( el, idx ) => (
<Fragment key={ el.plugin }>
<SmartPluginCardBody
plugin={ el }
@ -110,10 +141,10 @@ export const Channels: React.FC< ChannelsProps > = ( {
<CardDivider />
) }
</Fragment>
);
} ) }
</div>
) }
</Card>
);
};
) ) }
</div>
) }
</Card>
);
}
);

View File

@ -1 +1,2 @@
export { Channels } from './Channels';
export type { ChannelsRef } from './Channels';

View File

@ -0,0 +1,54 @@
.woocommerce-marketing-introduction-banner {
& > div {
display: flex;
flex-wrap: wrap;
}
.woocommerce-marketing-introduction-banner-content {
flex: 1 0;
margin: 32px 20px 32px 40px;
.woocommerce-marketing-introduction-banner-title {
font-size: 20px;
line-height: 28px;
margin-bottom: $gap-smaller;
}
.woocommerce-marketing-introduction-banner-features {
color: $gray-700;
svg {
fill: $studio-woocommerce-purple-50;
}
}
.woocommerce-marketing-introduction-banner-buttons {
margin-top: $gap;
}
}
.woocommerce-marketing-introduction-banner-illustration {
flex: 0 0 270px;
background: linear-gradient(90deg, rgba(247, 237, 247, 0) 5.31%, rgba(196, 152, 217, 0.12) 77.75%),
linear-gradient(90deg, rgba(247, 237, 247, 0) 22%, rgba(196, 152, 217, 0.12) 84.6%);
.woocommerce-marketing-introduction-banner-image-placeholder {
width: 100%;
height: 100%;
background: center / contain no-repeat;
}
.woocommerce-marketing-introduction-banner-close-button {
position: absolute;
top: $gap-small;
right: $gap;
padding: 0;
}
img {
display: block;
width: 100%;
height: 100%;
}
}
}

View File

@ -0,0 +1,152 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { Card, Flex, FlexItem, FlexBlock, Button } from '@wordpress/components';
import { Icon, trendingUp, megaphone, closeSmall } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { CreateNewCampaignModal } from '~/marketing/components';
import {
useRegisteredChannels,
useRecommendedChannels,
} from '~/marketing/hooks';
import './IntroductionBanner.scss';
import wooIconUrl from './woo.svg';
import illustrationUrl from './illustration.svg';
type IntroductionBannerProps = {
onDismissClick: () => void;
onAddChannelsClick: () => void;
};
export const IntroductionBanner = ( {
onDismissClick,
onAddChannelsClick,
}: IntroductionBannerProps ) => {
const [ isModalOpen, setModalOpen ] = useState( false );
const { data: dataRegistered } = useRegisteredChannels();
const { data: dataRecommended } = useRecommendedChannels();
const showCreateCampaignButton = !! dataRegistered?.length;
/**
* Boolean to display the "Add channels" button in the introduction banner.
*
* This depends on the number of registered channels,
* because if there are no registered channels,
* the Channels card will not have the "Add channels" toggle button,
* and it does not make sense to display the "Add channels" button in this introduction banner
* that will do nothing upon click.
*
* If there are registered channels and recommended channels,
* the Channels card will display the "Add channels" toggle button,
* and clicking on the "Add channels" button in this introduction banner
* will scroll to the button in Channels card.
*/
const showAddChannelsButton =
!! dataRegistered?.length && !! dataRecommended?.length;
return (
<Card className="woocommerce-marketing-introduction-banner">
<div className="woocommerce-marketing-introduction-banner-content">
<div className="woocommerce-marketing-introduction-banner-title">
{ __(
'Reach new customers and increase sales without leaving WooCommerce',
'woocommerce'
) }
</div>
<Flex
className="woocommerce-marketing-introduction-banner-features"
direction="column"
gap={ 1 }
expanded={ false }
>
<FlexItem>
<Flex>
<Icon icon={ trendingUp } />
<FlexBlock>
{ __(
'Reach customers on other sales channels',
'woocommerce'
) }
</FlexBlock>
</Flex>
</FlexItem>
<FlexItem>
<Flex>
<Icon icon={ megaphone } />
<FlexBlock>
{ __(
'Advertise with marketing campaigns',
'woocommerce'
) }
</FlexBlock>
</Flex>
</FlexItem>
<FlexItem>
<Flex>
<img
src={ wooIconUrl }
alt={ __( 'WooCommerce logo', 'woocommerce' ) }
width="24"
height="24"
/>
<FlexBlock>
{ __( 'Built by WooCommerce', 'woocommerce' ) }
</FlexBlock>
</Flex>
</FlexItem>
</Flex>
{ ( showCreateCampaignButton || showAddChannelsButton ) && (
<Flex
className="woocommerce-marketing-introduction-banner-buttons"
justify="flex-start"
>
{ showCreateCampaignButton && (
<Button
variant="primary"
onClick={ () => {
setModalOpen( true );
} }
>
{ __( 'Create a campaign', 'woocommerce' ) }
</Button>
) }
{ showAddChannelsButton && (
<Button
variant="secondary"
onClick={ onAddChannelsClick }
>
{ __( 'Add channels', 'woocommerce' ) }
</Button>
) }
</Flex>
) }
{ isModalOpen && (
<CreateNewCampaignModal
onRequestClose={ () => setModalOpen( false ) }
/>
) }
</div>
<div className="woocommerce-marketing-introduction-banner-illustration">
<Button
isSmall
className="woocommerce-marketing-introduction-banner-close-button"
onClick={ onDismissClick }
>
<Icon icon={ closeSmall } />
</Button>
<div
className="woocommerce-marketing-introduction-banner-image-placeholder"
style={ {
backgroundImage: `url("${ illustrationUrl }")`,
} }
/>
</div>
</Card>
);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
export { IntroductionBanner } from './IntroductionBanner';

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 256 153" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<title>WooCommerce Logo</title>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m23.759 0h208.38c13.187 0 23.863 10.675 23.863 23.863v79.542c0 13.187-10.675 23.863-23.863 23.863h-74.727l10.257 25.118-45.109-25.118h-98.695c-13.187 0-23.863-10.675-23.863-23.863v-79.542c-0.10466-13.083 10.571-23.863 23.758-23.863z" fill="#7f54b3"/>
<path d="m14.578 21.75c1.4569-1.9772 3.6423-3.0179 6.5561-3.226 5.3073-0.41626 8.3252 2.0813 9.0537 7.4927 3.226 21.75 6.7642 40.169 10.511 55.259l22.79-43.395c2.0813-3.9545 4.6829-6.0358 7.8049-6.2439 4.5789-0.3122 7.3886 2.6016 8.5333 8.7415 2.6016 13.841 5.9317 25.6 9.8862 35.59 2.7057-26.433 7.2846-45.476 13.737-57.236 1.561-2.9138 3.8504-4.3707 6.8683-4.5789 2.3935-0.20813 4.5789 0.52033 6.5561 2.0813 1.9772 1.561 3.0179 3.5382 3.226 5.9317 0.10406 1.8732-0.20813 3.4341-1.0407 4.9951-4.0585 7.4927-7.3886 20.085-10.094 37.567-2.6016 16.963-3.5382 30.179-2.9138 39.649 0.20813 2.6016-0.20813 4.8911-1.2488 6.8683-1.2488 2.2894-3.122 3.5382-5.5154 3.7463-2.7057 0.20813-5.5154-1.0406-8.2211-3.8504-9.678-9.8862-17.379-24.663-22.998-44.332-6.7642 13.32-11.759 23.311-14.985 29.971-6.1398 11.759-11.343 17.795-15.714 18.107-2.8098 0.20813-5.2033-2.1854-7.2846-7.1805-5.3073-13.633-11.031-39.961-17.171-78.985-0.41626-2.7057 0.20813-5.0992 1.665-6.9724zm223.64 16.338c-3.7463-6.5561-9.2618-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0.10406-8.1171-1.665-14.881-4.9951-20.501zm-13.112 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93659-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0 2.7057-0.20813 5.3073-0.72846 7.7008zm-52.033-28.826c-3.7463-6.5561-9.3659-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0-8.1171-1.665-14.881-4.9951-20.501zm-13.216 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93658-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0.10406 2.7057-0.20813 5.3073-0.72846 7.7008z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { useRef } from '@wordpress/element';
import { useUser } from '@woocommerce/data';
/**
@ -10,19 +11,28 @@ import '~/marketing/data';
import '~/marketing/data-multichannel';
import { CenteredSpinner } from '~/marketing/components';
import {
useIntroductionBanner,
useCampaigns,
useRegisteredChannels,
useRecommendedChannels,
useCampaignTypes,
} from '~/marketing/hooks';
import { getAdminSetting } from '~/utils/admin-settings';
import { IntroductionBanner } from './IntroductionBanner';
import { Campaigns } from './Campaigns';
import { Channels } from './Channels';
import { Channels, ChannelsRef } from './Channels';
import { InstalledExtensions } from './InstalledExtensions';
import { DiscoverTools } from './DiscoverTools';
import { LearnMarketing } from './LearnMarketing';
import './MarketingOverviewMultichannel.scss';
export const MarketingOverviewMultichannel: React.FC = () => {
const {
loading: loadingIntroductionBanner,
isIntroductionBannerDismissed,
dismissIntroductionBanner,
} = useIntroductionBanner();
const { loading: loadingCampaigns, meta: metaCampaigns } = useCampaigns();
const {
loading: loadingCampaignTypes,
data: dataCampaignTypes,
@ -36,8 +46,11 @@ export const MarketingOverviewMultichannel: React.FC = () => {
const { loading: loadingRecommended, data: dataRecommended } =
useRecommendedChannels();
const { currentUserCan } = useUser();
const channelsRef = useRef< ChannelsRef >( null );
if (
loadingIntroductionBanner ||
( loadingCampaigns && metaCampaigns?.total === undefined ) ||
( loadingCampaignTypes && ! dataCampaignTypes ) ||
( loadingRegistered && ! dataRegistered ) ||
( loadingRecommended && ! dataRecommended )
@ -45,6 +58,11 @@ export const MarketingOverviewMultichannel: React.FC = () => {
return <CenteredSpinner />;
}
const shouldShowCampaigns = !! (
dataRegistered?.length &&
( isIntroductionBannerDismissed || metaCampaigns?.total )
);
const shouldShowExtensions =
getAdminSetting( 'allowMarketplaceSuggestions', false ) &&
currentUserCan( 'install_plugins' );
@ -56,10 +74,19 @@ export const MarketingOverviewMultichannel: React.FC = () => {
return (
<div className="woocommerce-marketing-overview-multichannel">
{ !! dataRegistered?.length && <Campaigns /> }
{ ! isIntroductionBannerDismissed && (
<IntroductionBanner
onDismissClick={ dismissIntroductionBanner }
onAddChannelsClick={ () => {
channelsRef.current?.scrollIntoAddChannels();
} }
/>
) }
{ shouldShowCampaigns && <Campaigns /> }
{ !! ( dataRegistered && dataRecommended ) &&
!! ( dataRegistered.length || dataRecommended.length ) && (
<Channels
ref={ channelsRef }
registeredChannels={ dataRegistered }
recommendedChannels={ dataRecommended }
onInstalledAndActivated={ refetch }

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add introduction banner to multichannel marketing page.