Skip to content

Commit

Permalink
Stats: Flesh out highlight card components (#94084)
Browse files Browse the repository at this point in the history
* Replace custom code with standard function (Intl)

* Add missing return invocation

* Refactor number manipulation into a lib module

* Add CountCard as an exported component

* Add tooltip and note functionality to CountCard

* Remove unused note prop from CountComparisonCard

* Use CountCard where appropriate

Replace unnecessary use of CountComparisonCards

* Simplify tooltip for CountCard

* Simplify tooltip for CountComparisonCard

* Add explicit percentage formatting

* Undo unnecessary change

* Make precise small percentages toggleable

* Remove unused props

* Update count comparison card tooltip logic

Always render the tooltip, but only render the trend in the tooltip if there's a difference

* Update stories to align with existing components

* Update CountCard usage on WordAds dashboard

---------

Co-authored-by: Jason Moon <jsnmoon@users.noreply.github.com>
  • Loading branch information
jsnmoon and jsnmoon authored Sep 5, 2024
1 parent 0b5c336 commit a20f087
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 246 deletions.
9 changes: 6 additions & 3 deletions client/my-sites/stats/all-time-highlights-section/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import {
Card,
ComponentSwapper,
formattedNumber,
percentCalculator,
ShortenedNumber,
DotPager,
} from '@automattic/components';
import {
formatPercentage,
percentCalculator,
} from '@automattic/components/src/highlight-cards/lib/numbers';
import { eye } from '@automattic/components/src/icons';
import { Icon, people, postContent, starEmpty, commentContent } from '@wordpress/icons';
import clsx from 'clsx';
Expand Down Expand Up @@ -152,8 +155,8 @@ export default function AllTimeHighlightsSection( {
id: 'views',
header: translate( 'Views' ),
content: <ShortenedNumber value={ viewsBestDayTotal } />,
footer: translate( '%(percent)d%% of views', {
args: { percent: bestViewsEverPercent || 0 },
footer: translate( '%(percent)s of views', {
args: { percent: formatPercentage( bestViewsEverPercent, true ) },
context: 'Stats: Percentage of views',
} ),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
ComponentSwapper,
CountComparisonCard,
CountCard,
MobileHighlightCardListing,
Spinner,
} from '@automattic/components';
Expand Down Expand Up @@ -87,12 +87,12 @@ function SubscriberHighlightsStandard( {
return (
<div className="highlight-cards-list">
{ highlights.map( ( highlight ) => (
<CountComparisonCard
key={ highlight.heading }
<CountCard
heading={ isLoading ? '-' : highlight.heading }
count={ isLoading ? null : highlight.count }
key={ highlight.heading }
showValueTooltip
note={ highlight.note }
value={ isLoading ? null : highlight.count }
/>
) ) }
</div>
Expand Down
7 changes: 3 additions & 4 deletions client/my-sites/stats/stats-subscribers-overview/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CountComparisonCard } from '@automattic/components';
import { CountCard } from '@automattic/components';
import React from 'react';
import useSubscribersOverview from 'calypso/my-sites/stats/hooks/use-subscribers-overview';

Expand All @@ -15,12 +15,11 @@ const SubscribersOverview: React.FC< SubscribersOverviewProps > = ( { siteId } )
{ overviewData.map( ( { count, heading }, index ) => {
return (
// TODO: Communicate loading vs error state to the user.
<CountComparisonCard
<CountCard
key={ index }
heading={ heading }
count={ isLoading || isError ? null : count }
value={ isLoading || isError ? null : count }
showValueTooltip
icon={ false }
/>
);
} ) }
Expand Down
2 changes: 1 addition & 1 deletion client/my-sites/stats/wordads/highlights-section.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function HighlightsListing( { highlights } ) {
<CountCard
key={ highlight.id }
heading={ highlight.heading }
icon={ highlight.svgIcon }
icon={ <Icon icon={ highlight.svgIcon } /> }
value={ highlight.value }
/>
) ) }
Expand Down
55 changes: 17 additions & 38 deletions packages/components/src/highlight-cards/annual-highlight-cards.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { comment, Icon, paragraph, people, postContent, starEmpty } from '@wordpress/icons';
import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
import { translate, useTranslate } from 'i18n-calypso';
import ComponentSwapper from '../component-swapper';
import CountComparisonCard from './count-comparison-card';
import CountCard from './count-card';
import HighlightCardsHeading from './highlight-cards-heading';
import MobileHighlightCardListing from './mobile-highlight-cards';

Expand All @@ -28,54 +28,33 @@ type AnnualHighlightCardsProps = {
navigation?: React.ReactNode;
};

function AnnualHighlightsMobile( { counts }: AnnualHighlightsProps ) {
const translate = useTranslate();

const highlights = [
function getCardProps( counts: AnnualHighlightCounts ) {
return [
{ heading: translate( 'Posts' ), count: counts?.posts, icon: postContent },
{ heading: translate( 'Words' ), count: counts?.words, icon: paragraph },
{ heading: translate( 'Likes' ), count: counts?.likes, icon: starEmpty },
{ heading: translate( 'Comments' ), count: counts?.comments, icon: comment },
{ heading: translate( 'Subscribers' ), count: counts?.followers, icon: people },
];
}

return <MobileHighlightCardListing highlights={ highlights } />;
function AnnualHighlightsMobile( { counts }: AnnualHighlightsProps ) {
return <MobileHighlightCardListing highlights={ getCardProps( counts ) } />;
}

function AnnualHighlightsStandard( { counts }: AnnualHighlightsProps ) {
const translate = useTranslate();
const props = getCardProps( counts );
return (
<div className="highlight-cards-list">
<CountComparisonCard
heading={ translate( 'Posts' ) }
icon={ <Icon icon={ postContent } /> }
count={ counts?.posts ?? null }
showValueTooltip
/>
<CountComparisonCard
heading={ translate( 'Words' ) }
icon={ <Icon icon={ paragraph } /> }
count={ counts?.words ?? null }
showValueTooltip
/>
<CountComparisonCard
heading={ translate( 'Likes' ) }
icon={ <Icon icon={ starEmpty } /> }
count={ counts?.likes ?? null }
showValueTooltip
/>
<CountComparisonCard
heading={ translate( 'Comments' ) }
icon={ <Icon icon={ comment } /> }
count={ counts?.comments ?? null }
showValueTooltip
/>
<CountComparisonCard
heading={ translate( 'Subscribers' ) }
icon={ <Icon icon={ people } /> }
count={ counts?.followers ?? null }
showValueTooltip
/>
{ props.map( ( { count, heading, icon }, index ) => (
<CountCard
key={ index }
heading={ heading }
value={ count }
icon={ <Icon icon={ icon } /> }
showValueTooltip
/>
) ) }
</div>
);
}
Expand Down
87 changes: 56 additions & 31 deletions packages/components/src/highlight-cards/count-card.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,67 @@
import { Icon } from '@wordpress/icons';
import { useMemo } from 'react';
import BaseCard from './base-card';
import clsx from 'clsx';
import { useRef, useState } from 'react';
import { Card } from '../';
import Popover from '../popover';
import { formatNumber } from './lib/numbers';

interface CountCardProps {
heading: React.ReactNode;
icon: JSX.Element;
value: number | string;
heading?: React.ReactNode;
icon?: JSX.Element;
note?: string;
showValueTooltip?: boolean;
value: number | string | null;
}

function useDisplayValue( value: CountCardProps[ 'value' ] ) {
return useMemo( () => {
if ( typeof value === 'string' ) {
return value;
}
if ( typeof value === 'number' ) {
return value.toLocaleString();
}
return '-';
}, [ value ] );
function TooltipContent( { value }: CountCardProps ) {
return (
<div className="highlight-card-tooltip-content">
<span className="highlight-card-tooltip-counts">
{ formatNumber( value as number, false ) }
</span>
</div>
);
}

export default function CountCard( { heading, icon, value }: CountCardProps ) {
const displayValue = useDisplayValue( value );
export default function CountCard( {
heading,
icon,
note,
value,
showValueTooltip,
}: CountCardProps ) {
const textRef = useRef( null );
const [ isTooltipVisible, setTooltipVisible ] = useState( false );

// Tooltips are used to show the full number instead of the shortened number.
// Non-numeric values are not shown in the tooltip.
const shouldShowTooltip = showValueTooltip && typeof value === 'number';

return (
<BaseCard
heading={
<>
<div className="highlight-card-icon">
<Icon icon={ icon } />
</div>
<div className="highlight-card-title">{ heading }</div>
</>
}
>
<div className="highlight-card-count">
<span className="highlight-card-count-value">{ displayValue }</span>
<Card className="highlight-card">
{ icon && <div className="highlight-card-icon">{ icon }</div> }
{ heading && <div className="highlight-card-heading">{ heading }</div> }
<div
className={ clsx( 'highlight-card-count', {
'is-pointer': showValueTooltip,
} ) }
onMouseEnter={ () => setTooltipVisible( true ) }
onMouseLeave={ () => setTooltipVisible( false ) }
>
<span className="highlight-card-count-value" ref={ textRef }>
{ typeof value === 'number' ? formatNumber( value, true ) : value }
</span>
</div>
</BaseCard>
{ shouldShowTooltip && (
<Popover
className="tooltip tooltip--darker highlight-card-tooltip"
isVisible={ isTooltipVisible }
position="bottom right"
context={ textRef.current }
>
<TooltipContent value={ value } />
{ note && <div className="highlight-card-tooltip-note">{ note }</div> }
</Popover>
) }
</Card>
);
}
79 changes: 18 additions & 61 deletions packages/components/src/highlight-cards/count-comparison-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,19 @@ import { arrowDown, arrowUp, Icon } from '@wordpress/icons';
import clsx from 'clsx';
import { useRef, useState } from 'react';
import { Card } from '../';
import importedFormatNumber, {
DEFAULT_LOCALE,
STANDARD_FORMATTING_OPTIONS,
COMPACT_FORMATTING_OPTIONS,
} from '../number-formatters/lib/format-number';
import Popover from '../popover';
import { formatNumber, formatPercentage, subtract, percentCalculator } from './lib/numbers';

type CountComparisonCardProps = {
count: number | null;
heading?: React.ReactNode;
icon?: React.ReactNode;
onClick?: ( event: MouseEvent ) => void;
previousCount?: number | null;
previousCount: number | null;
showValueTooltip?: boolean | null;
note?: string;
compact?: boolean;
};

function formatNumber( number: number | null, isShortened = true ) {
return importedFormatNumber(
number,
DEFAULT_LOCALE,
isShortened ? COMPACT_FORMATTING_OPTIONS : STANDARD_FORMATTING_OPTIONS
);
}

function subtract( a: number | null, b: number | null | undefined ): number | null {
return a === null || b === null || b === undefined ? null : a - b;
}

export function percentCalculator( part: number | null, whole: number | null | undefined ) {
if ( part === null || whole === null || whole === undefined ) {
return null;
}
// Handle NaN case.
if ( part === 0 && whole === 0 ) {
return 0;
}
const answer = ( part / whole ) * 100;
// Handle Infinities.
return Math.abs( answer ) === Infinity ? 100 : Math.round( answer );
}

type TrendComparisonProps = {
count: number | null;
previousCount?: number | null;
Expand All @@ -61,7 +31,7 @@ export function TrendComparison( { count, previousCount }: TrendComparisonProps
return null;
}

return (
return Math.abs( difference ) === 0 ? null : (
<span
className={ clsx( 'highlight-card-difference', {
'highlight-card-difference--positive': difference < 0,
Expand All @@ -73,38 +43,28 @@ export function TrendComparison( { count, previousCount }: TrendComparisonProps
{ difference > 0 && <Icon size={ 18 } icon={ arrowUp } /> }
</span>
{ percentage !== null && (
<span className="highlight-card-difference-absolute-percentage"> { percentage }%</span>
<span className="highlight-card-difference-absolute-percentage">
{ ' ' }
{ formatPercentage( percentage ) }
</span>
) }
</span>
);
}

function TooltipContent( { count, previousCount, icon, heading }: CountComparisonCardProps ) {
if ( previousCount ) {
const difference = subtract( count, previousCount ) as number;
return (
<div className="highlight-card-tooltip-content">
<div className="highlight-card-tooltip-counts">
{ formatNumber( count, false ) }
{ ' ' }
{ difference !== 0 && difference !== null && (
<span className="highlight-card-tooltip-count-difference">
({ difference < 0 ? '-' : '+' }
{ formatNumber( Math.abs( difference as number ), false ) })
</span>
) }
</div>
</div>
);
}

function TooltipContent( { count, previousCount }: CountComparisonCardProps ) {
const difference = subtract( count, previousCount ) as number;
return (
<div className="highlight-card-tooltip-content">
<span className="highlight-card-tooltip-label">
{ icon && <span className="highlight-card-tooltip-icon">{ icon }</span> }
{ heading && <span className="highlight-card-tooltip-heading">{ heading }</span> }
</span>
<span className="highlight-card-tooltip-counts">{ formatNumber( count ) }</span>
<div className="highlight-card-tooltip-counts">
{ formatNumber( count, false ) }
{ ' ' }
{ difference !== 0 && difference !== null && (
<span className="highlight-card-tooltip-count-difference">
({ formatNumber( difference, false, true ) })
</span>
) }
</div>
</div>
);
}
Expand All @@ -115,12 +75,10 @@ export default function CountComparisonCard( {
icon,
heading,
showValueTooltip,
note = '',
compact = false,
}: CountComparisonCardProps ) {
const textRef = useRef( null );
const [ isTooltipVisible, setTooltipVisible ] = useState( false );

return (
<Card className="highlight-card" compact={ compact }>
{ icon && <div className="highlight-card-icon">{ icon }</div> }
Expand Down Expand Up @@ -149,7 +107,6 @@ export default function CountComparisonCard( {
icon={ icon }
heading={ heading }
/>
{ note && <div className="highlight-card-tooltip-note">{ note }</div> }
</Popover>
) }
</div>
Expand Down
Loading

0 comments on commit a20f087

Please sign in to comment.