-
Notifications
You must be signed in to change notification settings - Fork 891
Premium Analytics: add Top posts & pages widget to Storybook #49735
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
08ecfd0
Premium Analytics: add Top posts & pages widget to Storybook
louwie17 21d9843
Premium Analytics: reuse shared withChartTheme decorator in Top Posts…
louwie17 50c145c
Premium Analytics: move widgets dir out of packages into the package …
louwie17 05eb446
Storybook: changelog for premium-analytics widgets discovery path
louwie17 0ec43f8
fix(premium-analytics): keep leaderboard overlay bar tint opaque
adamwoodnz 9d5a932
Premium Analytics: fix Top Posts loading state and changelog entry
louwie17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
projects/js-packages/storybook/changelog/register-premium-analytics-widgets-path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Significance: patch | ||
| Type: changed | ||
| Comment: Register the premium-analytics widgets directory as a story discovery path. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: patch | ||
| Type: added | ||
|
|
||
| Storybook: Add a prop-driven Top posts & pages widget story under Packages/Premium Analytics/Widgets. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
projects/packages/premium-analytics/widgets/top-posts/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { TopPostsWidget, type TopPostsWidgetProps } from './top-posts-widget'; | ||
| export type { TopPostRow } from './types'; |
196 changes: 196 additions & 0 deletions
196
projects/packages/premium-analytics/widgets/top-posts/stories/top-posts-widget.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { withChartTheme } from '../../../packages/widgets-toolkit/src/stories/with-chart-theme'; | ||
| import { TopPostsWidget } from '../top-posts-widget'; | ||
| import type { TopPostRow } from '../types'; | ||
| import type { Meta, StoryObj, Decorator } from '@storybook/react'; | ||
|
|
||
| const meta: Meta< typeof TopPostsWidget > = { | ||
| title: 'Packages/Premium Analytics/Widgets/TopPosts', | ||
| component: TopPostsWidget, | ||
| tags: [ 'autodocs' ], | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| component: | ||
| 'The "Top posts & pages" widget. Renders the most-viewed posts and pages for the period as a leaderboard, with each row linking to the published content. This is a presentational component — it takes already-fetched rows via props and handles the loading, error, empty, and populated states.', | ||
| }, | ||
| }, | ||
| }, | ||
| decorators: [ withChartTheme ], | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj< typeof TopPostsWidget >; | ||
|
|
||
| const mockRows: TopPostRow[] = [ | ||
| { | ||
| label: 'How we cut our build times in half', | ||
| value: 12840, | ||
| previousValue: 9870, | ||
| href: 'https://example.com/cut-build-times-in-half', | ||
| type: 'post', | ||
| }, | ||
| { | ||
| label: 'Pricing', | ||
| value: 9320, | ||
| previousValue: 10110, | ||
| href: 'https://example.com/pricing', | ||
| type: 'page', | ||
| }, | ||
| { | ||
| label: '10 lessons from scaling to a million users', | ||
| value: 7610, | ||
| previousValue: 5400, | ||
| href: 'https://example.com/lessons-scaling-million-users', | ||
| type: 'post', | ||
| }, | ||
| { | ||
| label: 'About us', | ||
| value: 4180, | ||
| previousValue: 4360, | ||
| href: 'https://example.com/about', | ||
| type: 'page', | ||
| }, | ||
| { | ||
| label: 'A practical guide to feature flags', | ||
| value: 2950, | ||
| previousValue: 0, | ||
| href: 'https://example.com/guide-to-feature-flags', | ||
| type: 'post', | ||
| }, | ||
| ]; | ||
|
|
||
| const mockLongLabelRows: TopPostRow[] = [ | ||
| { | ||
| label: | ||
| 'An exhaustively long, keyword-stuffed headline that almost certainly needs to be truncated before it overflows the row', | ||
| value: 8400, | ||
| href: 'https://example.com/very-long-headline-that-needs-truncation', | ||
| type: 'post', | ||
| }, | ||
| { | ||
| label: 'Frequently asked questions about billing, refunds, and account management', | ||
| value: 5120, | ||
| href: 'https://example.com/faq-billing-refunds-account-management', | ||
| type: 'page', | ||
| }, | ||
| { | ||
| label: 'Changelog', | ||
| value: 2010, | ||
| href: 'https://example.com/changelog', | ||
| type: 'page', | ||
| }, | ||
| ]; | ||
|
|
||
| /** | ||
| * Default populated state — a mix of posts and pages ranked by views. | ||
| */ | ||
| export const Default: Story = { | ||
| args: { | ||
| rows: mockRows, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Comparison state — each value shows its change versus the previous period | ||
| * (green for gains, red for losses), driven by each row's `previousValue`. | ||
| * Mirrors the overlay comparison mode of the toolkit's `LeaderboardChart`. | ||
| */ | ||
| export const WithComparison: Story = { | ||
| args: { | ||
| rows: mockRows, | ||
| withComparison: true, | ||
| showLegend: true, | ||
| legendLabels: { | ||
| primary: 'Jun 1 – 18, 2026', | ||
| comparison: 'May 14 – 31, 2026', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Loading state — the chart renders its loading overlay while data is fetched. | ||
| */ | ||
| export const Loading: Story = { | ||
| args: { | ||
| rows: [], | ||
| isLoading: true, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Empty state — no views were recorded for the selected period. | ||
| */ | ||
| export const NoViews: Story = { | ||
| args: { | ||
| rows: [], | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Error state — the report could not be loaded. | ||
| */ | ||
| export const ErrorState: Story = { | ||
| args: { | ||
| isError: true, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Long titles are truncated with an ellipsis so rows stay single-line. | ||
| */ | ||
| export const LongLabels: Story = { | ||
| args: { | ||
| rows: mockLongLabelRows, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a decorator that wraps the story in a fixed-size container so the | ||
| * widget's responsiveness can be inspected at a given width. | ||
| * | ||
| * @param width - The container width (any CSS length). | ||
| * @param [height] - The container height; defaults to `auto`. | ||
| * @return A Storybook decorator. | ||
| */ | ||
| const createSizeDecorator = ( width: string, height = 'auto' ): Decorator => { | ||
| return Story => ( | ||
| <div | ||
| style={ { | ||
| width, | ||
| height, | ||
| border: '1px dashed #ccc', | ||
| borderRadius: '8px', | ||
| padding: '16px', | ||
| background: '#fafafa', | ||
| containerType: 'inline-size', | ||
| containerName: 'widget', | ||
| } } | ||
| > | ||
| <Story /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Medium container (448px / md breakpoint). | ||
| */ | ||
| export const SizeMedium: Story = { | ||
| args: { | ||
| rows: mockRows, | ||
| }, | ||
| decorators: [ createSizeDecorator( '448px' ) ], | ||
| }; | ||
|
|
||
| /** | ||
| * Large container (576px / xl breakpoint). | ||
| */ | ||
| export const SizeLarge: Story = { | ||
| args: { | ||
| rows: mockRows, | ||
| }, | ||
| decorators: [ createSizeDecorator( '576px' ) ], | ||
| }; |
22 changes: 22 additions & 0 deletions
22
projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| .labelLink { | ||
| display: block; | ||
|
|
||
| /* Vertical padding sets the overlay bar height — the bar fills the row, | ||
| whose height is driven by this label (there is no image to size it). */ | ||
| padding-block: var(--wpds-dimension-padding-md, 12px); | ||
|
|
||
| /* Inset the text from the bar's rounded left edge. The chart applies this to | ||
| its default `.label` in overlay mode (`.is-overlay .label`), but a custom | ||
| label element bypasses that rule, so we mirror it here. */ | ||
| padding-inline-start: var(--wpds-dimension-padding-sm, 8px); | ||
| overflow: hidden; | ||
| color: inherit; | ||
| text-decoration: none; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .labelLink:hover, | ||
| .labelLink:focus { | ||
| text-decoration: underline; | ||
| } |
145 changes: 145 additions & 0 deletions
145
projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { Text } from '@wordpress/ui'; | ||
| import { | ||
| LeaderboardChart, | ||
| calculateDelta, | ||
| type LeaderboardChartData, | ||
| type LegendLabels, | ||
| } from '@jetpack-premium-analytics/widgets-toolkit'; | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import styles from './top-posts-widget.module.css'; | ||
| import type { TopPostRow } from './types'; | ||
|
|
||
| export type TopPostsWidgetProps = { | ||
| /** | ||
| * Normalized top-posts rows to render. When omitted, the empty state is shown | ||
| * (unless `isLoading` is set). | ||
| */ | ||
| rows?: TopPostRow[]; | ||
| /** | ||
| * When `true`, the chart renders its loading overlay instead of data. | ||
| */ | ||
| isLoading?: boolean; | ||
| /** | ||
| * When `true`, an error message is rendered in place of the chart. | ||
| */ | ||
| isError?: boolean; | ||
| /** | ||
| * When `true`, render the comparison (previous-period) delta next to each | ||
| * value, using `previousValue` from each row. Mirrors the overlay | ||
| * comparison mode of the toolkit's `LeaderboardChart`. | ||
| */ | ||
| withComparison?: boolean; | ||
| /** | ||
| * When `true`, show the period legend below the chart. Requires | ||
| * `legendLabels` to be meaningful. | ||
| */ | ||
| showLegend?: boolean; | ||
| /** | ||
| * Custom legend labels for the current/comparison periods. | ||
| */ | ||
| legendLabels?: LegendLabels; | ||
| }; | ||
|
|
||
| /** | ||
| * Renders a post/page title as a link that opens in a new tab. The link fills | ||
| * its row so the leaderboard overlay bar gets its height from the label. | ||
| * | ||
| * @param props - Component props. | ||
| * @param props.label - The post/page title. | ||
| * @param props.href - The published URL of the post/page. | ||
| * @return The rendered label link. | ||
| */ | ||
| const TopPostLabel = ( { label, href }: { label: string; href: string } ) => ( | ||
| <a | ||
| className={ styles.labelLink } | ||
| href={ href } | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| title={ label } | ||
| > | ||
| { label } | ||
| </a> | ||
| ); | ||
|
|
||
| /** | ||
| * Maps normalized top-posts rows onto the shape `LeaderboardChart` expects. | ||
| * Current shares are computed relative to the most-viewed row so the overlay | ||
| * bars are proportional. When `withComparison` is set, previous-period shares | ||
| * and per-row deltas are derived from each row's `previousValue`; otherwise | ||
| * the comparison fields are zeroed. | ||
| * | ||
| * @param rows - The normalized top-posts rows. | ||
| * @param withComparison - Whether to derive previous-period shares and deltas. | ||
| * @return The leaderboard chart data. | ||
| */ | ||
| function buildLeaderboardData( rows: TopPostRow[], withComparison: boolean ): LeaderboardChartData { | ||
| // `1` guards against division by zero when every value is 0. | ||
| const maxCurrentViews = Math.max( ...rows.map( row => row.value ), 1 ); | ||
| const maxPreviousViews = Math.max( ...rows.map( row => row.previousValue ?? 0 ), 1 ); | ||
|
|
||
| return rows.map( ( row, index ) => { | ||
| const previousValue = row.previousValue ?? 0; | ||
|
|
||
| return { | ||
| id: `${ index }-${ row.href }`, | ||
| label: <TopPostLabel label={ row.label } href={ row.href } />, | ||
| currentValue: row.value, | ||
| currentShare: ( row.value / maxCurrentViews ) * 100, | ||
| previousValue, | ||
| previousShare: | ||
| withComparison && previousValue > 0 ? ( previousValue / maxPreviousViews ) * 100 : 0, | ||
| delta: withComparison ? calculateDelta( row.value, previousValue ) : 0, | ||
| }; | ||
| } ); | ||
| } | ||
|
|
||
| /** | ||
| * "Top posts & pages" widget. Renders the most-viewed posts and pages for the | ||
| * period as a leaderboard, each row linking to the published content. | ||
| * | ||
| * This is a presentational component: it takes already-fetched rows via props | ||
| * and is responsible only for the loading, error, empty, and populated states. | ||
| * | ||
| * @param props - Component props. | ||
| * @param props.rows - Normalized top-posts rows to render. | ||
| * @param props.isLoading - Whether the chart should render its loading overlay. | ||
| * @param props.isError - Whether to render an error message in place of the chart. | ||
| * @param props.withComparison - Whether to render previous-period deltas. | ||
| * @param props.showLegend - Whether to show the period legend below the chart. | ||
| * @param props.legendLabels - Custom labels for the current/comparison periods. | ||
| * @return The rendered widget. | ||
| */ | ||
| export const TopPostsWidget = ( { | ||
| rows = [], | ||
| isLoading = false, | ||
| isError = false, | ||
| withComparison = false, | ||
| showLegend = false, | ||
| legendLabels, | ||
| }: TopPostsWidgetProps ) => { | ||
| if ( isError ) { | ||
| return <Text>{ __( 'Unable to load top posts.', 'jetpack-premium-analytics' ) }</Text>; | ||
| } | ||
|
|
||
| if ( ! isLoading && rows.length === 0 ) { | ||
| return <Text>{ __( 'No views in this period.', 'jetpack-premium-analytics' ) }</Text>; | ||
| } | ||
|
|
||
| return ( | ||
| <LeaderboardChart | ||
| data={ buildLeaderboardData( rows, withComparison ) } | ||
| loading={ isLoading } | ||
| withComparison={ withComparison } | ||
| withOverlayLabel | ||
| showLegend={ showLegend } | ||
| legendLabels={ legendLabels } | ||
| dataFormat={ { type: 'number', options: { useMultipliers: true, decimals: 0 } } } | ||
|
louwie17 marked this conversation as resolved.
Outdated
|
||
| /> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.