Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
735d0ca
Stats: split video details into a dedicated page modeled on post details
dognose24 Jul 10, 2026
3cbcffb
Stats: add period navigation to the video details chart
dognose24 Jul 10, 2026
6b711dc
Stats: add metric highlight tabs to the video details chart
dognose24 Jul 10, 2026
b171e3c
Stats: modernize the video details Embedded pages module
dognose24 Jul 10, 2026
59fe4ce
Stats: zero-fill the video details chart series
dognose24 Jul 10, 2026
db3e820
Stats: align the video details chart with the real stats/video semantics
dognose24 Jul 10, 2026
47187c6
Stats: trim the video details daily window to the trailing 30 days
dognose24 Jul 10, 2026
8f81611
Stats: keep video chart month labels on one line
dognose24 Jul 10, 2026
ed7f7f5
Stats: bucket video weekly stats Monday-based to match the date heading
dognose24 Jul 10, 2026
a71f21c
Stats: show full month-year video chart labels like post details
dognose24 Jul 10, 2026
b6cca06
Stats: stretch the video details header and embeds cards to full width
dognose24 Jul 10, 2026
6872b8d
Stats: track the video details screen in the stats navigation history
dognose24 Jul 10, 2026
5ad6257
Merge remote-tracking branch 'origin/trunk' into update/stats-299-vid…
dognose24 Jul 10, 2026
2a6dfa6
Stats: address review feedback on the video details page
dognose24 Jul 10, 2026
a70a3cf
Stats: degrade the video details page gracefully in Odyssey Stats
dognose24 Jul 10, 2026
ef2d3f8
Stats: drop the video details page's hard dependency on the media API
dognose24 Jul 10, 2026
6bc09e4
Stats: render video details media-derived UI only when the data exists
dognose24 Jul 10, 2026
693f12c
Stats: drop the thumbnail from the video details header card
dognose24 Jul 10, 2026
8d42f5d
Stats: source the video title and upload date from the statsVideo post
dognose24 Jul 10, 2026
bcba10c
Stats: polish the video details layout
dognose24 Jul 10, 2026
4bfb091
Stats: remove the video details retention rate card
dognose24 Jul 10, 2026
71241af
Stats: avoid flashing the video chart empty state when switching windows
dognose24 Jul 10, 2026
c535a2b
Stats: stop the video chart and metric cards from shifting layout
dognose24 Jul 10, 2026
3127e16
Stats: stack video metric cards one per row on small screens
dognose24 Jul 10, 2026
327fad0
Stats: pad the video metric cards off the viewport edges on narrow sc…
dognose24 Jul 10, 2026
4e2bb4c
Stats: truncate long breadcrumb labels with an ellipsis
dognose24 Jul 10, 2026
2b5612e
Stats: keep the header Jetpack logo from shrinking with long titles
dognose24 Jul 10, 2026
7488356
Stats: show exact zero and exact one hours watched on the video cards
dognose24 Jul 10, 2026
144ef4d
Stats: zero out the video details heading margin for wp-admin
dognose24 Jul 10, 2026
e1b4b45
Stats: restore the video details card loading placeholder
dognose24 Jul 10, 2026
6c99d8c
Stats: pin the embeds card heading margin for wp-admin
dognose24 Jul 10, 2026
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
22 changes: 19 additions & 3 deletions client/my-sites/stats/controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const loadStatsPostDetail = () =>
import(
/* webpackChunkName: "async-load-calypso-my-sites-stats-stats-post-detail" */ './stats-post-detail'
);
const loadStatsVideoDetail = () =>
import(
/* webpackChunkName: "async-load-calypso-my-sites-stats-stats-video-detail" */ './stats-video-detail'
);
const loadCommentFollows = () =>
import(
/* webpackChunkName: "async-load-calypso-my-sites-stats-comment-follows" */ './comment-follows'
Expand Down Expand Up @@ -309,8 +313,21 @@ export function summary( context, next ) {
const isValidRange = isValidEndDate && ! endDate.isBefore( date );
const dateRange = isValidRange ? { startDate: date, endDate: endDate } : null;

const extraProps =
context.params.module === 'videodetails' ? { postId: parseInt( queryOptions.post, 10 ) } : {};
// Video details has its own dedicated page, modeled on the single post page.
if ( context.params.module === 'videodetails' ) {
context.primary = (
<StatsPageLoader>
<AsyncLoad
require={ loadStatsVideoDetail }
placeholder={ PageLoading }
postId={ parseInt( queryOptions.post, 10 ) }
period={ period }
context={ context }
/>
</StatsPageLoader>
);
return next();
}

// The option is used for stats queries only.
const statsQueryOptions = pick( queryOptions, [ 'num', 'summarize', 'geoMode', 'viewType' ] );
Expand All @@ -330,7 +347,6 @@ export function summary( context, next ) {
dateRange={ dateRange }
context={ context }
period={ period }
{ ...extraProps }
/>
</StatsPageLoader>
);
Expand Down
4 changes: 2 additions & 2 deletions client/my-sites/stats/stats-summary/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ class StatsSummaryChart extends Component {
label: tabLabel + label,
};

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

return isModernized ? (
<div className={ clsx( 'is-summary-chart', { 'is-loading': isLoading } ) }>
Expand Down
82 changes: 82 additions & 0 deletions client/my-sites/stats/stats-video-detail/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useTranslate } from 'i18n-calypso';
import { useEffect } from 'react';
import titlecase from 'to-title-case';
import QueryMedia from 'calypso/components/data/query-media';
import Main from 'calypso/my-sites/stats/components/stats-main';
import {
useStatsBreadcrumbTrail,
recordCurrentScreen,
} from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
import { useSelector } from 'calypso/state';
import getMediaItem from 'calypso/state/selectors/get-media-item';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import PageViewTracker from '../stats-page-view-tracker';
import VideoDetailsCard, { VideoMediaItem } from './video-details-card';
import VideoEmbedsCard from './video-embeds-card';
import VideoSummary from './video-summary';

interface StatsVideoDetailProps {
postId: number;
period: {
period: string;
};
context: {
query: Record< string, string >;
};
}

export default function StatsVideoDetail( { postId, period, context }: StatsVideoDetailProps ) {
const translate = useTranslate();
const siteId = useSelector( getSelectedSiteId );
const media = useSelector(
( state ) => getMediaItem( state, siteId, postId ) as VideoMediaItem | null
);
const breadcrumbTrail = useStatsBreadcrumbTrail();
const statType = context.query.statType ?? null;

useEffect( () => {
window.scrollTo( 0, 0 );
}, [] );

useEffect( () => {
recordCurrentScreen( 'videodetails', {
queryParams: context.query,
period: period.period,
} );
}, [ context.query, period.period ] );

const title = media?.title || translate( 'Video', { textOnly: true } );

return (
<Main
fullWidthLayout
breadcrumbs={ [
...breadcrumbTrail.map( ( item ) => ( {
label: item.label,
to: item.url ?? undefined,
} ) ),
{ label: title },
] }
>
<PageViewTracker
path={ `/stats/${ period.period }/videodetails/:site` }
title={ `Stats > ${ titlecase( period.period ) } > Videodetails` }
/>
{ siteId && <QueryMedia siteId={ siteId } mediaId={ postId } /> }
<div className="stats stats-summary-view">
<div
id="my-stats-content"
className="stats-summary-view stats-summary__positioned stats-video-detail"
>
<VideoDetailsCard media={ media } mediaId={ postId } />
<VideoSummary
postId={ postId }
initialStatType={ statType }
videoDuration={ media?.length ?? null }
/>
<VideoEmbedsCard postId={ postId } />
</div>
</div>
</Main>
);
}
247 changes: 247 additions & 0 deletions client/my-sites/stats/stats-video-detail/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
@import "@wordpress/base-styles/breakpoints";
@import "@automattic/typography/styles/fonts";
@import "@automattic/components/src/styles/typography";
@import "@automattic/components/src/styles/mixins";

$card-padding: 24px;
$border-radius: 5px; // stylelint-disable-line scales/radii

@keyframes stats-video-details-card-shine {
to {
background-position-x: -200%;
}
}

.stats-video-detail {
display: flex;
flex-direction: column;
gap: 24px;

// The Card base has `margin: 0 auto`; inside a flex column the auto
// inline margins absorb the free space and shrink the card to its content
// width instead of stretching, so zero them out.
> .card {
margin-inline: 0;
}
}

// The x-axis places labels at equal-slot centers (chart width / bar count),
// but `.is-chart-tabs` caps bars at 156px and lays them out space-between, so
// with few bars (e.g. ~5 weekly buckets) bars drift away from their labels.
// space-around keeps every capped bar centered in its slot.
.stats-video-summary.is-chart-tabs .chart__bars {
justify-content: space-around;
}

.stats-video-details-card {
border-color: var(--studio-gray-5);
border-radius: $border-radius;
display: grid;
font-family: $font-sf-pro-text;
font-size: $font-body-small;
grid-template-columns: minmax(10px, 1.5fr) minmax(0, auto);
grid-template-rows: 26px auto;
grid-template-areas:
"heading thumbnail"
"info thumbnail";
margin-bottom: 0;
max-width: 100%;
max-height: 255px;
padding: $card-padding;
gap: $card-padding;

&.card::after {
display: none;
}

&.is-loading {
.stats-video-details-card__heading,
.stats-video-details-card__info {
position: relative;

&::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(94deg, var(--studio-gray-5) 8%, var(--studio-gray-0) 18%, var(--studio-gray-5) 33%);
border-radius: 4px;
background-size: 200% 100%;
animation: 1.5s stats-video-details-card-shine linear infinite;
}
}
}
}

.stats-video-details-card__heading {
color: var(--studio-gray-100);
font-family: $font-sf-pro-display;
font-size: $font-size-header-small;
font-weight: 500;
grid-area: heading;
line-height: 1.3;
}

.stats-video-details-card__info {
grid-area: info;

.stats-video-details-card__title {
@include stats-section-header;
display: block;
margin-bottom: 4px;
min-height: 40px;
}

.stats-video-details-card__date {
color: var(--studio-gray-60);
font-weight: 400;
line-height: 1.5;
}
}

.stats-video-details-card__thumbnail-link {
grid-area: thumbnail;
display: block;
height: calc(100% + #{$card-padding * 2});
margin: -1 * $card-padding -1 * $card-padding -1 * $card-padding 0;
max-width: 300px;
width: calc(100% + #{$card-padding});
}

.stats-video-details-card__thumbnail {
border-radius: 0 $border-radius $border-radius 0;
display: block;
height: 100%;
object-fit: cover;
width: 100%;
}

.stats-video-embeds-card {
border-color: var(--studio-gray-5);
border-radius: 5px; /* stylelint-disable-line scales/radii */
font-family: $font-sf-pro-text;
font-size: $font-body-small;
padding: 24px;

&.card::after {
display: none;
}
}

.stats-video-embeds-card__heading {
color: var(--studio-gray-100);
font-family: $font-sf-pro-display;
font-size: $font-size-header-small;
font-weight: 500;
line-height: 1.3;
margin-bottom: 16px;
}

.stats-video-embeds-card__list {
list-style: none;
margin: 0;
padding: 0;
}

.stats-video-embeds-card__item {
line-height: 1.5;
margin: 0;
overflow-wrap: anywhere;

+ .stats-video-embeds-card__item {
margin-top: 12px;
}
}

.stats-video-embeds-card__empty {
color: var(--studio-gray-60);
line-height: 1.5;
}

.stats-video-metric-tabs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
list-style: none;
// The chart's 42px-wide x-axis labels wrap onto a second line for
// 'MMM YYYY' month labels (as on the post details page); leave room for
// the overflowing line, matching the spacing below the post details chart.
margin: 40px 0 0;
padding: 0;
}

.stats-video-metric-tabs__item {
margin: 0;
}

.stats-video-metric-tabs__tab {
background: var(--color-surface);
border: 1px solid var(--studio-gray-5);
border-radius: 5px; /* stylelint-disable-line scales/radii */
box-sizing: border-box;
cursor: pointer;
display: flex;
flex-direction: column;
font-family: $font-sf-pro-text;
gap: 12px;
padding: 16px 24px;
text-align: start;
width: 100%;

&:hover {
border-color: var(--studio-gray-30);
}

&.is-selected {
border-color: var(--color-primary);
box-shadow: inset 0 0 0 1px var(--color-primary);
}
}

.stats-video-metric-tabs__header {
align-items: center;
color: var(--studio-gray-100);
display: flex;
font-size: $font-body-small;
gap: 8px;
line-height: 1.5;

svg {
fill: currentColor;
}
}

.stats-video-metric-tabs__value {
@include stats-section-header;
color: var(--studio-gray-100);
}

@media (max-width: $break-medium) {
.stats-video-metric-tabs {
grid-template-columns: repeat(2, 1fr);
}
}

@media (max-width: $break-small) {
.stats-video-metric-tabs {
grid-template-columns: 1fr;
}

.stats-video-details-card {
gap: 12px 0;
grid-template-areas:
"heading heading"
"info thumbnail";
max-height: initial;
}

.stats-video-details-card__thumbnail-link {
height: 84px;
margin: 0;
width: 84px;
max-width: initial;
}

.stats-video-details-card__thumbnail {
border-radius: $border-radius;
}
}
Loading
Loading