From a15e34ae751572d626d3875c75669f983b60bb84 Mon Sep 17 00:00:00 2001 From: mehidi258 Date: Fri, 17 Jul 2026 22:26:34 +0600 Subject: [PATCH] Add next report date to email subscription pill. --- .../email-reporting/FrequencySelector.js | 94 ++++++++++++------- .../FrequencySelector.stories.js | 11 ++- .../email-reporting/FrequencySelector.test.js | 44 ++++++++- .../email-reporting/getNextReportDate.test.ts | 89 ++++++++++++++++++ .../email-reporting/getNextReportDate.ts | 88 +++++++++++++++++ .../_googlesitekit-frequency-selector.scss | 10 +- 6 files changed, 298 insertions(+), 38 deletions(-) create mode 100644 assets/js/components/email-reporting/getNextReportDate.test.ts create mode 100644 assets/js/components/email-reporting/getNextReportDate.ts diff --git a/assets/js/components/email-reporting/FrequencySelector.js b/assets/js/components/email-reporting/FrequencySelector.js index 65cd92ff477..04fe26bab25 100644 --- a/assets/js/components/email-reporting/FrequencySelector.js +++ b/assets/js/components/email-reporting/FrequencySelector.js @@ -43,6 +43,8 @@ import { EMAIL_REPORT_FREQUENCIES, } from '@/js/googlesitekit/datastore/user/constants'; import { BREAKPOINT_SMALL, useBreakpoint } from '@/js/hooks/useBreakpoint'; +import { getLocale } from '@/js/util'; +import getNextReportDate from './getNextReportDate'; export default function FrequencySelector( { isUserSubscribed, isLoading } ) { const breakpoint = useBreakpoint(); @@ -82,9 +84,30 @@ export default function FrequencySelector( { isUserSubscribed, isLoading } ) { const savedFrequency = useSelect( ( select ) => select( CORE_USER ).getEmailReportingSavedFrequency() ); + const referenceDate = useSelect( ( select ) => + select( CORE_USER ).getReferenceDate() + ); const { setEmailReportingFrequency } = useDispatch( CORE_USER ); + const formattedNextReportDate = useMemo( () => { + if ( ! savedFrequency || ! referenceDate ) { + return null; + } + + const nextReportDate = getNextReportDate( + savedFrequency, + referenceDate, + startOfWeek + ); + + return nextReportDate.toLocaleDateString( getLocale(), { + month: 'short', + day: 'numeric', + year: 'numeric', + } ); + }, [ savedFrequency, referenceDate, startOfWeek ] ); + const weeklyDescription = sprintf( /* translators: %s: localized day-of-week name (e.g. Monday). */ __( 'Sent every %s', 'google-site-kit' ), @@ -128,6 +151,37 @@ export default function FrequencySelector( { isUserSubscribed, isLoading } ) { } } + function renderCurrentSubscriptionPill( selected = false ) { + return ( +
+ + { __( 'Current subscription', 'google-site-kit' ) } + + { formattedNextReportDate && ( + + { sprintf( + /* translators: %s: formatted date, e.g. Jul 14, 2026. */ + __( 'Next report: %s', 'google-site-kit' ), + formattedNextReportDate + ) } + + ) } +
+ ); + } + if ( isLoading ) { return ( @@ -166,20 +220,9 @@ export default function FrequencySelector( { isUserSubscribed, isLoading } ) { className="googlesitekit-frequency-selector__badge-cell" > { reportFrequency === savedFrequency && ( -
- - { __( - 'Current subscription', - 'google-site-kit' - ) } - -
+ + { renderCurrentSubscriptionPill() } + ) } ) ) } @@ -219,27 +262,8 @@ export default function FrequencySelector( { isUserSubscribed, isLoading } ) { { isUserSubscribed && savedFrequency && isMobileBreakpoint && - reportFrequency === savedFrequency && ( -
- - { __( - 'Current subscription', - 'google-site-kit' - ) } - -
- ) } + reportFrequency === savedFrequency && + renderCurrentSubscriptionPill( isSelected ) }
diff --git a/assets/js/components/email-reporting/FrequencySelector.stories.js b/assets/js/components/email-reporting/FrequencySelector.stories.js index ca4bfdf2355..0558c57088f 100644 --- a/assets/js/components/email-reporting/FrequencySelector.stories.js +++ b/assets/js/components/email-reporting/FrequencySelector.stories.js @@ -41,6 +41,7 @@ export default { }, args: { isUserSubscribed: false, + referenceDate: '2026-07-14', savedSettings: {}, clientSettings: { frequency: 'weekly', @@ -50,7 +51,12 @@ export default { }, }; -function Template( { isUserSubscribed, savedSettings, clientSettings } ) { +function Template( { + isUserSubscribed, + savedSettings, + clientSettings, + referenceDate, +} ) { function setupRegistry( registry ) { provideSiteInfo( registry, { startOfWeek: savedSettings?.startOfWeek, @@ -63,6 +69,8 @@ function Template( { isUserSubscribed, savedSettings, clientSettings } ) { registry .dispatch( CORE_USER ) .setEmailReportingSettings( clientSettings ); + + registry.dispatch( CORE_USER ).setReferenceDate( referenceDate ); } return ( @@ -113,6 +121,7 @@ WeeklySelectedSundayStartOfTheWeek.scenario = {}; export const PreviouslySavedFrequency = Template.bind( {} ); PreviouslySavedFrequency.args = { isUserSubscribed: true, + referenceDate: '2026-07-14', savedSettings: { frequency: 'monthly', }, diff --git a/assets/js/components/email-reporting/FrequencySelector.test.js b/assets/js/components/email-reporting/FrequencySelector.test.js index a81ab7cacc0..b3256eede0f 100644 --- a/assets/js/components/email-reporting/FrequencySelector.test.js +++ b/assets/js/components/email-reporting/FrequencySelector.test.js @@ -27,7 +27,7 @@ import FrequencySelector from './FrequencySelector'; function setupRegistry( registry, - { startOfWeek = 1, frequency, savedFrequency } = {} + { startOfWeek = 1, frequency, savedFrequency, referenceDate } = {} ) { provideSiteInfo( registry, { startOfWeek } ); @@ -44,6 +44,10 @@ function setupRegistry( if ( frequency ) { registry.dispatch( CORE_USER ).setEmailReportingFrequency( frequency ); } + + if ( referenceDate ) { + registry.dispatch( CORE_USER ).setReferenceDate( referenceDate ); + } } function renderSelector( registry, props = {} ) { @@ -66,6 +70,7 @@ describe( 'FrequencySelector', () => { beforeEach( () => { registry = createTestRegistry(); + global.innerWidth = 1024; } ); describe( 'Story states (visual + DOM)', () => { @@ -114,6 +119,7 @@ describe( 'FrequencySelector', () => { startOfWeek: 1, frequency: 'weekly', savedFrequency: 'monthly', + referenceDate: '2026-07-14', } ); const { container, containerElement, getByText } = renderSelector( @@ -142,6 +148,9 @@ describe( 'FrequencySelector', () => { // Check that the pill text is correct. expect( getByText( 'Current subscription' ) ).toBeInTheDocument(); + expect( + getByText( 'Next report: Aug 1, 2026' ) + ).toBeInTheDocument(); expect( containerElement ).toMatchSnapshot(); } ); @@ -151,6 +160,7 @@ describe( 'FrequencySelector', () => { startOfWeek: 1, frequency: 'monthly', savedFrequency: 'monthly', + referenceDate: '2026-07-14', } ); const { container, containerElement, getByText } = renderSelector( @@ -182,9 +192,41 @@ describe( 'FrequencySelector', () => { ) ).toBe( true ); expect( monthlyCard.getAttribute( 'aria-checked' ) ).toBe( 'true' ); + expect( + getByText( 'Next report: Aug 1, 2026' ) + ).toBeInTheDocument(); expect( containerElement ).toMatchSnapshot(); } ); + + it( 'Renders next report date in the mobile current subscription pill', () => { + global.innerWidth = 500; + + setupRegistry( registry, { + startOfWeek: 1, + frequency: 'weekly', + savedFrequency: 'monthly', + referenceDate: '2026-07-14', + } ); + + const { container, getByText } = renderSelector( registry, { + isUserSubscribed: true, + } ); + + expect( + container.querySelector( + '.googlesitekit-frequency-selector__badge-row' + ) + ).not.toBeInTheDocument(); + expect( + container.querySelector( + '.googlesitekit-frequency-selector__current-subscription' + ) + ).toBeInTheDocument(); + expect( + getByText( 'Next report: Aug 1, 2026' ) + ).toBeInTheDocument(); + } ); } ); describe( 'Interactions', () => { diff --git a/assets/js/components/email-reporting/getNextReportDate.test.ts b/assets/js/components/email-reporting/getNextReportDate.test.ts new file mode 100644 index 00000000000..f87361b11da --- /dev/null +++ b/assets/js/components/email-reporting/getNextReportDate.test.ts @@ -0,0 +1,89 @@ +/** + * Get next report date tests. + * + * Site Kit by Google, Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import { getDateString } from '@/js/util'; +import getNextReportDate from './getNextReportDate'; + +describe( 'getNextReportDate', () => { + describe( 'weekly frequency', () => { + it.each( [ + [ '2026-07-14', 1, '2026-07-20' ], // Tuesday -> next Monday. + [ '2026-07-12', 0, '2026-07-19' ], // Sunday -> next Sunday. + [ '2026-07-18', 6, '2026-07-25' ], // Saturday -> next Saturday. + ] )( + 'returns %s week start %i as %s', + ( referenceDate, weekStartDay, expectedDate ) => { + const nextReportDate = getNextReportDate( + 'weekly', + referenceDate, + weekStartDay + ); + + expect( getDateString( nextReportDate ) ).toEqual( + expectedDate + ); + } + ); + } ); + + describe( 'monthly frequency', () => { + it.each( [ + [ '2026-01-31', '2026-02-01' ], + [ '2026-07-14', '2026-08-01' ], + [ '2026-12-31', '2027-01-01' ], + ] )( + 'returns next month first day for %s as %s', + ( referenceDate, expectedDate ) => { + const nextReportDate = getNextReportDate( + 'monthly', + referenceDate, + 1 + ); + + expect( getDateString( nextReportDate ) ).toEqual( + expectedDate + ); + } + ); + } ); + + describe( 'quarterly frequency', () => { + it.each( [ + [ '2026-01-15', '2026-04-01' ], + [ '2026-03-31', '2026-04-01' ], + [ '2026-11-20', '2027-01-01' ], + [ '2026-12-31', '2027-01-01' ], + ] )( + 'returns next quarter first day for %s as %s', + ( referenceDate, expectedDate ) => { + const nextReportDate = getNextReportDate( + 'quarterly', + referenceDate, + 1 + ); + + expect( getDateString( nextReportDate ) ).toEqual( + expectedDate + ); + } + ); + } ); +} ); diff --git a/assets/js/components/email-reporting/getNextReportDate.ts b/assets/js/components/email-reporting/getNextReportDate.ts new file mode 100644 index 00000000000..e22a53b5d99 --- /dev/null +++ b/assets/js/components/email-reporting/getNextReportDate.ts @@ -0,0 +1,88 @@ +/** + * Utility to compute the next email report date. + * + * Site Kit by Google, Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import { stringToDate } from '@/js/util'; + +type EmailReportingFrequency = 'weekly' | 'monthly' | 'quarterly'; + +/** + * Gets the next report date for a given frequency. + * + * @since n.e.x.t + * + * @param {EmailReportingFrequency} frequency Email reporting frequency. + * @param {string} referenceDate Date string as `YYYY-MM-DD`. + * @param {number} weekStartDay Week start day index (`0` for Sunday through `6` for Saturday). + * @return {Date} Next report date. + */ +export default function getNextReportDate( + frequency: EmailReportingFrequency, + referenceDate: string, + weekStartDay: number +): Date { + const nextReportDate = stringToDate( referenceDate ); + + switch ( frequency ) { + case 'weekly': { + const normalizedWeekStartDay = + Number.isInteger( weekStartDay ) && + weekStartDay >= 0 && + weekStartDay <= 6 + ? weekStartDay + : 1; + const currentWeekday = nextReportDate.getDay(); + let daysUntilTarget = + ( normalizedWeekStartDay - currentWeekday + 7 ) % 7; + + // Use the next occurrence of the week start day. + if ( 0 === daysUntilTarget ) { + daysUntilTarget = 7; + } + + nextReportDate.setDate( + nextReportDate.getDate() + daysUntilTarget + ); + break; + } + + case 'monthly': + nextReportDate.setMonth( nextReportDate.getMonth() + 1, 1 ); + break; + + case 'quarterly': { + const currentMonth = nextReportDate.getMonth() + 1; + // Month offset within quarter: 0 => first, 1 => second, 2 => third. + const position = ( currentMonth - 1 ) % 3; + const monthsToAdd = 3 - position; + + nextReportDate.setMonth( + nextReportDate.getMonth() + monthsToAdd, + 1 + ); + break; + } + + default: + break; + } + + return nextReportDate; +} diff --git a/assets/sass/components/email-reporting/_googlesitekit-frequency-selector.scss b/assets/sass/components/email-reporting/_googlesitekit-frequency-selector.scss index 21ddff6a614..bdea80bad4b 100644 --- a/assets/sass/components/email-reporting/_googlesitekit-frequency-selector.scss +++ b/assets/sass/components/email-reporting/_googlesitekit-frequency-selector.scss @@ -31,8 +31,11 @@ h3.googlesitekit-frequency-selector-title { background-color: $c-surfaces-surface; border-radius: 4px; color: $c-surfaces-on-surface-variant; + display: flex; + flex-direction: column; + gap: 2px; margin-bottom: $grid-gap-phone; - padding: 4px 0; + padding: 8px 4px; text-align: center; width: 100%; @@ -43,6 +46,11 @@ h3.googlesitekit-frequency-selector-title { } } +// Increase specificity to override body-small typography letter spacing. +.googlesitekit-typography.googlesitekit-typography--body.googlesitekit-typography--small.googlesitekit-frequency-selector__next-report { + letter-spacing: initial; +} + .googlesitekit-frequency-selector__current-subscription--selected { background-color: $c-site-kit-sk-10; color: $c-site-kit-sk-500;