Skip to content

Commit 07988cb

Browse files
dognose24claude
andauthored
Stats: modernize the VideoPress video details page (Automattic#112499)
* Stats: split video details into a dedicated page modeled on post details Move the videodetails module out of the summary switch into its own stats-video-detail page component, keeping the existing URL. Adds a video details header card (title, published date, thumbnail linking to the media library) and the shared stats breadcrumb trail. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: add period navigation to the video details chart Replace the old single-metric StatsVideoSummary with a VideoSummary section modeled on stats-post-summary: a date label with previous/next arrows and a Days/Weeks/Months/Years segmented control driving the statsVideo query. Daily data is paged 30 bars at a time like the post chart. Removes the now-unused stats-video-summary component. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: add metric highlight tabs to the video details chart Fetch all four statsVideo series (views, impressions, watch time, retention rate) and render them as selectable cards below the chart, matching the post details layout from the JA designs. Card totals are computed over the dates visible in the chart; retention rate uses a views-weighted average. The chart's statType now lives in page state, seeded from the statType query param so existing links keep working. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: modernize the video details Embedded pages module Replace the legacy StatModuleVideoDetails list with an Embedded pages card matching the rest of the redesigned video details page. Pages still display as URLs since the stats/video endpoint only returns embed URLs; showing post titles needs the API to return post IDs/titles. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: zero-fill the video details chart series The stats/video endpoint only returns buckets from the video's first activity onwards, so a young video yields one or two bars, which the is-chart-tabs layout (max-width bars + space-between) then scatters away from the axis labels. Fill gaps between buckets, extend the day view backwards to a full 30-bar window per the designs, and add the has-less-than-three-bars fallback class used by the traffic chart. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: align the video details chart with the real stats/video semantics A backend investigation confirmed stats/video/:id treats period as a fixed trailing-window selector (day yields only 2 daily buckets, year yields ~13 monthly buckets) rather than a bucket granularity, and that its statType only recognizes watch_time and impressions — views falls back to the plays column and there is no retention series at all. Rework the summary accordingly: fetch the 30-day daily window for the Days/Weeks views and the 365-day monthly window for Months/Years, and aggregate buckets client-side. Views now covers the same 30-day window and plays metric as the Traffic Videos module and the All videos page. Retention rate is derived per bucket and in total with the canonical complete_stats formula, (watch time / plays) / video duration, using the media item duration — replacing the bogus fourth query whose response was silently the plays series. Also harden the statsVideo normalizer against the endpoint's empty-window response shape, and drop the now-unneeded zero-fill and paging logic. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: trim the video details daily window to the trailing 30 days The stats/video endpoint's month window spans 31 days inclusive, one more than the 30-day window the Videos module and All videos page show, so the Views total was off by the oldest day's plays. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: keep video chart month labels on one line The chart's x-axis labels have a fixed 42px width, so 'MMM YYYY' wraps and collides with the metric cards below. Show 'MMM' like the legacy year view did — the full month and year remain in the bar tooltip and the date heading — and key bar selection by startDate since bare month labels can repeat within a 13-month window. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: bucket video weekly stats Monday-based to match the date heading The date heading (stats-date-label) renders weeks Monday-Sunday, while the weekly aggregation grouped by the locale's week start (Sunday in en), so a selected bar's tooltip range was one day off from the heading. Group by isoWeek, which matches the heading's convention on every Monday key regardless of locale. Also center capped chart bars in their slots (space-around) so bars line up with the mathematically positioned x-axis labels when there are only a few buckets. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: show full month-year video chart labels like post details Restore the 'MMM YYYY' month labels — the post details chart uses the same format and its second wrapped line is absorbed by the spacing below the chart, so match that spacing above the metric cards instead of shortening the label. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: stretch the video details header and embeds cards to full width The Card base carries margin: 0 auto; inside the page's flex column the auto inline margins shrink-to-fit and center the card instead of letting align-items: stretch apply, so the header and embeds cards rendered narrower than the chart section between them. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: track the video details screen in the stats navigation history recordCurrentScreen silently ignores screens missing from possibleBackLinks, and videodetails was never registered (a gap dating back to the summary-page implementation), so the breadcrumb trail treated the previous screen as current and dropped the Videos crumb — or showed only the Stats root on direct entry. Register videodetails like postDetails (tracked, never rendered as a crumb) and record it in a layout effect so it lands in the history before the breadcrumb-trail hook's passive effect reads it. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: address review feedback on the video details page Include watch-time bucket keys in the chart bucket union, treat missing embeds data as loading so the empty state doesn't flash before the deferred request starts, and cover the statsVideo normalizer's empty-window response shapes with unit tests. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: degrade the video details page gracefully in Odyssey Stats The Odyssey stats-app proxy (Jetpack stats-admin REST controller) has no media or settings routes, so those requests 404 in wp-admin. Gate the shared stats shell's QuerySiteSettings the same way as QuerySiteFeatures (the request 404s on every Odyssey stats page today), and hide the video details header card in Odyssey while its media item is unavailable instead of showing a loading state forever. The media request is still made, so the card and the derived retention rate light up automatically once the proxy learns the media route. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: drop the video details page's hard dependency on the media API The header card and the retention rate metric both required the media item (title, date, thumbnail, and duration), but the Odyssey stats-app proxy has no media route, leaving the page degraded in wp-admin until a Jetpack release ships one. Remove the header card and the retention card/series so the page works identically everywhere, and keep the media request only as a best-effort enhancement for the breadcrumb title (matching the legacy page), falling back to a static label. Revert this commit to restore both once Automattic/jetpack#50412 (or the retention series from STATS-312) is available. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: render video details media-derived UI only when the data exists Restore the header card and the Retention rate card, but field-adaptive instead of hard-required: the header card renders once the media item loads (never in Odyssey until its proxy has a media route), the date and thumbnail only when present, and the Retention rate card and series only when the video duration is available — the metric row divides evenly whether three or four cards render. The breadcrumb keeps the media title with a static fallback. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: drop the thumbnail from the video details header card The VideoPress poster comes in arbitrary aspect ratios and sizes, which the card layout handled poorly; keep the card to the title and published date. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: source the video title and upload date from the statsVideo post The stats/video/:id response includes the video's attachment post, so the breadcrumb and header card can take the title and published date from data the page already fetches — and since the Odyssey stats-app proxy forwards stats routes, both now work in wp-admin too. The media item is only consulted as a fallback and for the video duration (retention rate), which remains adaptive. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: polish the video details layout Pin the period switcher to the right edge when the period header wraps on narrow widths, add breathing room between the header and the chart, and stretch the metric cards to the row height so they stay uniform when a label or value wraps inside one of them. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: remove the video details retention rate card The retention rate needs the video duration, which only exists on the media item — unavailable in Odyssey Stats where the stats-app proxy has no media route. Drop the card and its derived series for now; it can return once the backend provides a retention series (STATS-312) or the proxy learns a media route (Automattic/jetpack#50412). Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: avoid flashing the video chart empty state when switching windows QuerySiteStats defers its initial request, so on the first render after switching to the Months/Years window the requesting flag is still false while the data is missing, and the chart briefly rendered its empty state — resizing the layout — before the fetch started. Treat a missing selected series as loading, matching the embeds card. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: stop the video chart and metric cards from shifting layout Nothing hides the chart while StatsModulePlaceholder shows inside .is-summary-chart (the .is-chart-tabs.is-loading rule targets the outer wrapper), so during loading the placeholder and the empty chart stacked to double height and the page jumped when data arrived — pin the section height and hide the chart while loading. Also lay the metric cards out with a fixed-column grid: the 50%-minus-gap flex basis could round past 100% and wrap cards one per row. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: stack video metric cards one per row on small screens The single-column rule still targeted the old flex layout (flex-basis has no effect on grid items), so narrow viewports kept two columns. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: pad the video metric cards off the viewport edges on narrow screens The page container only adds side padding above the medium breakpoint, so the cards sat flush against the browser edge below it. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: truncate long breadcrumb labels with an ellipsis The ellipsis rules on the current-crumb label never applied: each crumb wraps in an unclassed span that is the actual flex item, so nothing let the label shrink, and the label also lacked white-space: nowrap. Give the wrapper a class with min-width: 0 and complete the truncation rules — long video/post titles now ellipsize instead of overflowing. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: keep the header Jetpack logo from shrinking with long titles The logo sits in JetpackTitle's HStack as a flex item with the default flex-shrink: 1, so it compressed alongside a truncating breadcrumb title; the previous pin targeted a logo inside the breadcrumbs nav, where none renders. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: show exact zero and exact one hours watched on the video cards 'value > 1' rendered both 0 and exactly 1 as '< 1.0' — zero hours reading as 'less than one' is misleading. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: zero out the video details heading margin for wp-admin wp-admin core styles give h4 a 1.33em vertical margin in Odyssey Stats. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: restore the video details card loading placeholder The card lost its shimmer when the title source moved to the statsVideo response and started rendering nothing until data arrived, pushing the chart down when it appeared. Reserve the card with shimmering title/date lines while neither source has responded, and keep hiding it only when the response confirms there is no title. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: pin the embeds card heading margin for wp-admin Same as the details card heading: wp-admin core styles give h4 a 1.33em vertical margin in Odyssey Stats. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 44095c7 commit 07988cb

16 files changed

Lines changed: 984 additions & 203 deletions

File tree

client/my-sites/stats/components/stats-main/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function StatsBreadcrumbs( { items }: { items: BreadcrumbItem[] } ) {
7676
<span className="stats-breadcrumbs__current">{ STATS_HEADER_TITLE }</span>
7777
) }
7878
{ restItems.map( ( item, index ) => (
79-
<span key={ index }>
79+
<span key={ index } className="stats-breadcrumbs__item">
8080
<span className="stats-breadcrumbs__separator"> / </span>
8181
{ item.to ? (
8282
<a
@@ -138,7 +138,8 @@ export default function StatsMain( {
138138
return (
139139
<Main { ...props } className={ clsx( 'stats-main', 'color-scheme', customTheme, className ) }>
140140
{ ! isWPAdminAndNotSimpleSite && <QuerySiteFeatures siteIds={ [ siteId ] } /> }
141-
<QuerySiteSettings siteId={ siteId } />
141+
{ /* The Odyssey stats-app proxy has no settings route, so the request would 404. */ }
142+
{ ! isWPAdminAndNotSimpleSite && <QuerySiteSettings siteId={ siteId } /> }
142143
<Page
143144
// Restore a stable styling hook lost when @wordpress/admin-ui 2.x moved Page
144145
// internals to CSS Modules. Stats SCSS overrides target `.admin-ui-page`.

client/my-sites/stats/controller.jsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const loadStatsPostDetail = () =>
2626
import(
2727
/* webpackChunkName: "async-load-calypso-my-sites-stats-stats-post-detail" */ './stats-post-detail'
2828
);
29+
const loadStatsVideoDetail = () =>
30+
import(
31+
/* webpackChunkName: "async-load-calypso-my-sites-stats-stats-video-detail" */ './stats-video-detail'
32+
);
2933
const loadCommentFollows = () =>
3034
import(
3135
/* webpackChunkName: "async-load-calypso-my-sites-stats-comment-follows" */ './comment-follows'
@@ -335,8 +339,21 @@ export function summary( context, next ) {
335339
);
336340
const period = rangeOfPeriod( activeFilter.period, date );
337341

338-
const extraProps =
339-
context.params.module === 'videodetails' ? { postId: parseInt( queryOptions.post, 10 ) } : {};
342+
// Video details has its own dedicated page, modeled on the single post page.
343+
if ( context.params.module === 'videodetails' ) {
344+
context.primary = (
345+
<StatsPageLoader>
346+
<AsyncLoad
347+
require={ loadStatsVideoDetail }
348+
placeholder={ PageLoading }
349+
postId={ parseInt( queryOptions.post, 10 ) }
350+
period={ period }
351+
context={ context }
352+
/>
353+
</StatsPageLoader>
354+
);
355+
return next();
356+
}
340357

341358
// The option is used for stats queries only.
342359
const statsQueryOptions = pick( queryOptions, [ 'num', 'summarize', 'geoMode', 'viewType' ] );
@@ -356,7 +373,6 @@ export function summary( context, next ) {
356373
dateRange={ dateRange }
357374
context={ context }
358375
period={ period }
359-
{ ...extraProps }
360376
/>
361377
</StatsPageLoader>
362378
);

client/my-sites/stats/hooks/use-stats-navigation-history.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const possibleBackLinks: { [ key: string ]: string | null } = {
3030
postList: '{adminUrl}edit.php',
3131
emailsummary: '/stats/{period}/emails/',
3232
postDetails: null, // Last item in the history, the text is not displayed anywhere but this is used to track the item in history stack.
33+
videodetails: null, // Same as postDetails: tracked in the history stack, never rendered as a crumb.
3334
};
3435

3536
const SUPPORTED_QUERY_PARAMS: string[] = [
@@ -158,6 +159,7 @@ export const useStatsNavigationHistory = (
158159
postList: translate( 'Post List' ),
159160
emailsummary: translate( 'Emails' ),
160161
postDetails: null, // Last item in the history, the text is not displayed anywhere but this is used to track the item in history stack.
162+
videodetails: null,
161163
} ),
162164
[]
163165
);
@@ -288,6 +290,7 @@ export const useStatsBreadcrumbTrail = (
288290
postList: translate( 'Post List' ),
289291
emailsummary: translate( 'Emails' ),
290292
postDetails: null,
293+
videodetails: null,
291294
} ),
292295
[]
293296
);

client/my-sites/stats/stats-summary/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ class StatsSummaryChart extends Component {
9999
label: tabLabel + label,
100100
};
101101

102-
// The StatsPostSummary has been modernized to fresh styling.
103-
const isModernized = 'post' === type;
102+
// The post and video summaries have been modernized to fresh styling.
103+
const isModernized = 'post' === type || 'video' === type;
104104

105105
return isModernized ? (
106106
<div className={ clsx( 'is-summary-chart', { 'is-loading': isLoading } ) }>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useTranslate } from 'i18n-calypso';
2+
import { useEffect, useLayoutEffect } from 'react';
3+
import titlecase from 'to-title-case';
4+
import QueryMedia from 'calypso/components/data/query-media';
5+
import Main from 'calypso/my-sites/stats/components/stats-main';
6+
import {
7+
useStatsBreadcrumbTrail,
8+
recordCurrentScreen,
9+
} from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
10+
import { useSelector } from 'calypso/state';
11+
import getMediaItem from 'calypso/state/selectors/get-media-item';
12+
import { getSiteStatsNormalizedData } from 'calypso/state/stats/lists/selectors';
13+
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
14+
import PageViewTracker from '../stats-page-view-tracker';
15+
import VideoDetailsCard from './video-details-card';
16+
import VideoEmbedsCard from './video-embeds-card';
17+
import VideoSummary from './video-summary';
18+
19+
import './style.scss';
20+
21+
interface StatsVideoDetailProps {
22+
postId: number;
23+
period: {
24+
period: string;
25+
};
26+
context: {
27+
query: Record< string, string >;
28+
};
29+
}
30+
31+
interface VideoMediaItem {
32+
title?: string;
33+
date?: string;
34+
/** Video duration in seconds. */
35+
length?: number;
36+
}
37+
38+
interface VideoStatsPost {
39+
post_title?: string;
40+
post_date?: string;
41+
}
42+
43+
export default function StatsVideoDetail( { postId, period, context }: StatsVideoDetailProps ) {
44+
const translate = useTranslate();
45+
const siteId = useSelector( getSelectedSiteId );
46+
// The video title and upload date come from the attachment post included in
47+
// the statsVideo response — available in both Calypso and Odyssey (the
48+
// stats-app proxy forwards stats routes). Mirrors VideoSummary's default
49+
// Days/Weeks query, which is always fetched first.
50+
const videoStatsData = useSelector(
51+
( state ) =>
52+
getSiteStatsNormalizedData( state, siteId, 'statsVideo', {
53+
postId,
54+
statType: 'views',
55+
period: 'month',
56+
} ) as { post?: VideoStatsPost | null } | null
57+
);
58+
const videoStatsPost = videoStatsData?.post ?? null;
59+
// The media item is only needed for the video duration (retention rate);
60+
// the request 404s harmlessly in Odyssey, where the stats-app proxy has no
61+
// media route, and the retention card is simply omitted.
62+
const media = useSelector(
63+
( state ) => getMediaItem( state, siteId, postId ) as VideoMediaItem | null
64+
);
65+
const breadcrumbTrail = useStatsBreadcrumbTrail();
66+
const statType = context.query.statType ?? null;
67+
68+
useEffect( () => {
69+
window.scrollTo( 0, 0 );
70+
}, [] );
71+
72+
// Must run before useStatsBreadcrumbTrail's passive effect reads the
73+
// navigation history, so the trail treats this screen (not the previous
74+
// one) as the current entry to exclude.
75+
useLayoutEffect( () => {
76+
recordCurrentScreen( 'videodetails', {
77+
queryParams: context.query,
78+
period: period.period,
79+
} );
80+
}, [ context.query, period.period ] );
81+
82+
const videoTitle = videoStatsPost?.post_title || media?.title || null;
83+
const videoDate = videoStatsPost?.post_date || media?.date || null;
84+
// Loading = neither source has responded yet; once statsVideo answers, a
85+
// missing post means there is genuinely no title and the card hides.
86+
const isVideoInfoLoading = ! videoStatsData && ! media;
87+
88+
return (
89+
<Main
90+
fullWidthLayout
91+
breadcrumbs={ [
92+
...breadcrumbTrail.map( ( item ) => ( {
93+
label: item.label,
94+
to: item.url ?? undefined,
95+
} ) ),
96+
{ label: videoTitle || translate( 'Video details', { textOnly: true } ) },
97+
] }
98+
>
99+
<PageViewTracker
100+
path={ `/stats/${ period.period }/videodetails/:site` }
101+
title={ `Stats > ${ titlecase( period.period ) } > Videodetails` }
102+
/>
103+
{ siteId && <QueryMedia siteId={ siteId } mediaId={ postId } /> }
104+
<div className="stats stats-summary-view">
105+
<div
106+
id="my-stats-content"
107+
className="stats-summary-view stats-summary__positioned stats-video-detail"
108+
>
109+
<VideoDetailsCard
110+
title={ videoTitle }
111+
date={ videoDate }
112+
isLoading={ isVideoInfoLoading }
113+
/>
114+
<VideoSummary postId={ postId } initialStatType={ statType } />
115+
<VideoEmbedsCard postId={ postId } />
116+
</div>
117+
</div>
118+
</Main>
119+
);
120+
}

0 commit comments

Comments
 (0)