Skip to content

Commit

Permalink
Email Stats: Add Opens and Clicks tooltips (#97729)
Browse files Browse the repository at this point in the history
  • Loading branch information
lezama authored Jan 2, 2025
1 parent 72f4c2f commit dafd93b
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 13 deletions.
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

0 comments on commit dafd93b

Please sign in to comment.