Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email Stats: Add Opens and Clicks tooltips #97729

Open
wants to merge 10 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import { useShouldGateStats } from '../../../hooks/use-should-gate-stats';
import StatsModule from '../../../stats-module';
import { StatsEmptyActionEmail } from '../shared';
import StatsCardSkeleton from '../shared/stats-card-skeleton';
import {
TooltipWrapper,
OpensTooltipContent,
ClicksTooltipContent,
hasUniqueMetrics,
EmailStatsItem,
} from './tooltips';
import type { StatsDefaultModuleProps, StatsStateProps } from '../types';

const StatsEmails: React.FC< StatsDefaultModuleProps > = ( {
Expand Down Expand Up @@ -74,7 +81,18 @@ const StatsEmails: React.FC< StatsDefaultModuleProps > = ( {
}
additionalColumns={ {
header: <span>{ translate( 'Opens' ) }</span>,
body: ( item: { opens_rate: number } ) => <span>{ `${ item.opens_rate }%` }</span>,
body: ( item: EmailStatsItem ) => {
const opensUnique = parseInt( String( item.unique_opens ), 10 );
const opens = parseInt( String( item.opens ), 10 );
const hasUniques = hasUniqueMetrics( opensUnique, opens );
return (
<TooltipWrapper
value={ hasUniques ? `${ item.opens_rate }%` : '—' }
item={ item }
TooltipContent={ OpensTooltipContent }
/>
);
},
} }
moduleStrings={ moduleStrings }
period={ period }
Expand All @@ -83,8 +101,21 @@ const StatsEmails: React.FC< StatsDefaultModuleProps > = ( {
mainItemLabel={ translate( 'Latest Emails' ) }
metricLabel={ translate( 'Clicks' ) }
valueField="clicks_rate"
formatValue={ ( value: number ) => `${ value }%` }
showSummaryLink
formatValue={ ( value: number, item: EmailStatsItem ) => {
if ( ! item?.opens ) {
return value;
}
const clicksUnique = parseInt( String( item.unique_clicks ), 10 );
const clicks = parseInt( String( item.clicks ), 10 );
const hasUniques = hasUniqueMetrics( clicksUnique, clicks );
return (
<TooltipWrapper
value={ hasUniques ? `${ item.clicks_rate }%` : '—' }
item={ item }
TooltipContent={ ClicksTooltipContent }
/>
);
} }
className={ className }
hasNoBackground
skipQuery
Expand Down
110 changes: 110 additions & 0 deletions client/my-sites/stats/features/modules/stats-emails/tooltips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Tooltip } from '@automattic/components';
import { useTranslate } from 'i18n-calypso';
import React, { useRef, useState } from 'react';

export interface EmailStatsItem {
unique_opens: number;
opens: number;
opens_rate: number;
unique_clicks: number;
clicks: number;
clicks_rate: number;
total_sends: number;
}

interface TooltipWrapperProps {
value: string;
item: EmailStatsItem;
TooltipContent: React.ComponentType< { item: EmailStatsItem } >;
}

export const TooltipWrapper: React.FC< TooltipWrapperProps > = ( {
value,
item,
TooltipContent,
} ) => {
const triggerRef = useRef< HTMLSpanElement >( null );
const [ showTooltip, setShowTooltip ] = useState( false );

return (
<span className="stats-email__tooltip-wrapper">
<span
ref={ triggerRef }
className="stats-email__tooltip-trigger"
onMouseEnter={ () => setShowTooltip( true ) }
onMouseLeave={ () => setShowTooltip( false ) }
>
{ value }
</span>
<Tooltip position="top" context={ triggerRef.current } isVisible={ showTooltip }>
<TooltipContent item={ item } />
</Tooltip>
</span>
);
};

export const hasUniqueMetrics = ( uniqueValue: number, totalValue: number ) => {
return uniqueValue > 0 && totalValue > 0;
};

export const OpensTooltipContent: React.FC< { item: EmailStatsItem } > = ( { item } ) => {
const translate = useTranslate();
const opensUnique = parseInt( String( item.unique_opens ), 10 );
const opens = parseInt( String( item.opens ), 10 );
const opensRate = parseFloat( String( item.opens_rate ) );
const totalSends = parseInt( String( item.total_sends ), 10 );
const hasUniques = hasUniqueMetrics( opensUnique, opens );

return (
<div className="stats-email__tooltip">
<div>
{ translate( 'Recipients: %(sends)d', {
args: { sends: totalSends },
} ) }
</div>
<div>
{ translate( 'Total opens: %(opens)d', {
args: { opens },
} ) }
</div>
<div>
{ hasUniques
? translate( 'Unique opens: %(uniqueOpens)d (%(openRate).2f%%)', {
args: { uniqueOpens: opensUnique, openRate: opensRate },
} )
: translate( 'Unique opens: —' ) }
</div>
</div>
);
};

export const ClicksTooltipContent: React.FC< { item: EmailStatsItem } > = ( { item } ) => {
const translate = useTranslate();
const clicksUnique = parseInt( String( item.unique_clicks ), 10 );
const clicks = parseInt( String( item.clicks ), 10 );
const clicksRate = parseFloat( String( item.clicks_rate ) );
const totalSends = parseInt( String( item.total_sends ), 10 );
const hasUniques = hasUniqueMetrics( clicksUnique, clicks );

return (
<div className="stats-email__tooltip">
<div>
{ translate( 'Recipients: %(sends)d', {
args: { sends: totalSends },
} ) }
</div>
<div>
{ translate( 'Total clicks: %(clicks)d', {
args: { clicks },
} ) }
</div>
<div>
{ hasUniques
? translate( 'Unique clicks: %(uniqueClicks)d (%(clickRate).2f%%)', {
args: { uniqueClicks: clicksUnique, clickRate: clicksRate },
} )
: translate( 'Unique clicks: —' ) }
</div>
</div>
);
};
38 changes: 32 additions & 6 deletions client/my-sites/stats/stats-email-summary/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import JetpackColophon from 'calypso/components/jetpack-colophon';
import Main from 'calypso/components/main';
import NavigationHeader from 'calypso/components/navigation-header';
import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
import {
TooltipWrapper,
OpensTooltipContent,
ClicksTooltipContent,
} from '../features/modules/stats-emails/tooltips';
import StatsModule from '../stats-module';
import PageViewTracker from '../stats-page-view-tracker';
import statsStringsFactory from '../stats-strings';
Expand Down Expand Up @@ -63,11 +68,18 @@ const StatsEmailSummary = ( { translate, period, siteSlug } ) => {
<span>{ translate( 'Opens' ) }</span>
</>
),
body: ( item ) => (
<>
<span>{ `${ item.opens_rate }%` }</span>
</>
),
body: ( item ) => {
const opensUnique = parseInt( item.unique_opens, 10 );
const opens = parseInt( item.opens, 10 );
const hasUniquesData = opensUnique > 0 || opens === 0;
return (
<TooltipWrapper
value={ hasUniquesData ? `${ item.opens_rate }%` : '—' }
item={ item }
TooltipContent={ OpensTooltipContent }
/>
);
},
} }
path="emails"
moduleStrings={ { ...StatsStrings.emails, title: '' } }
Expand All @@ -78,7 +90,21 @@ const StatsEmailSummary = ( { translate, period, siteSlug } ) => {
hideSummaryLink
metricLabel={ translate( 'Clicks' ) }
valueField="clicks_rate"
formatValue={ ( value ) => `${ value }%` }
formatValue={ ( value, item ) => {
if ( item?.clicks !== undefined ) {
const clicksUnique = parseInt( item.unique_clicks, 10 );
const clicks = parseInt( item.clicks, 10 );
const hasUniquesData = clicksUnique > 0 || clicks === 0;
return (
<TooltipWrapper
value={ hasUniquesData ? `${ item.clicks_rate }%` : '—' }
item={ item }
TooltipContent={ ClicksTooltipContent }
/>
);
}
return <span>{ value }</span>;
} }
listItemClassName="stats__summary--narrow-mobile"
/>
<JetpackColophon />
Expand Down
20 changes: 18 additions & 2 deletions client/state/stats/lists/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -974,19 +974,35 @@ export const normalizers = {
const emailsData = get( data, [ 'posts' ], [] );

return emailsData.map(
( { id, href, date, title, type, opens, clicks, opens_rate, clicks_rate } ) => {
( {
id,
href,
date,
title,
type,
opens,
clicks,
opens_rate,
clicks_rate,
unique_opens,
unique_clicks,
total_sends,
} ) => {
const detailPage = site ? `/stats/email/opens/day/${ id }/${ site.slug }` : null;
return {
id,
href,
date,
label: title,
type,
value: clicks || '0',
value: clicks_rate || '0',
opens: opens || '0',
clicks: clicks || '0',
opens_rate: opens_rate || '0',
clicks_rate: clicks_rate || '0',
unique_opens: unique_opens || '0',
unique_clicks: unique_clicks || '0',
total_sends: total_sends || '0',
page: detailPage,
actions: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const HorizontalBarListItem = ( {
return <ShortenedNumber value={ value } />;
}
if ( formatValue ) {
return formatValue( value );
return formatValue( value, data );
}
return usePlainCard ? value : numberFormat( value, 0 );
};
Expand Down Expand Up @@ -193,6 +193,7 @@ const HorizontalBarListItem = ( {
isStatic={ isStatic }
usePlainCard={ usePlainCard }
isLinkUnderlined={ isLinkUnderlined }
formatValue={ formatValue }
/>
);
} ) }
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/horizontal-bar-list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export type HorizontalBarListItemProps = {
* @property {boolean} hasNoBackground - don't render the background bar and adjust indentation
*/
hasNoBackground?: boolean;
formatValue?: ( value: number ) => string;
/**
* @property {Function} formatValue - function to format the value display. Can optionally receive the full item data.
*/
formatValue?: ( value: number, item?: StatDataObject ) => React.ReactNode;
};

type StatDataObject = {
Expand Down
Loading