Back

0% Statements 0/34
0% Branches 0/19
0% Functions 0/8
0% Lines 0/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                                                                                                                                                                                                                                                                                     
import { TabPanel } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import useAnalytics from '../../hooks/use-analytics';
import useIsJetpackUserNew from '../../hooks/use-is-jetpack-user-new';
import { MY_JETPACK_SECTION_OVERVIEW, MY_JETPACK_SECTION_PRODUCTS } from './constants';
import { FullWidthSeparator } from './full-width-separator';
import styles from './styles.module.scss';
import { TabContent } from './tab-content';
import { MyJetpackSection } from './types';
import { getMyJetpackSections, isValidMyJetpackSection } from './utils';
import type { ReactNode } from 'react';
 
/**
 * My Jetpack Tab panel component.
 *
 * @param {object}    root0               - Component props.
 * @param {ReactNode} root0.beforeContent - Content to render between the tab separator and tab content.
 * @param {boolean}   root0.productsOnly  - When true, lock to the Products tab and hide the tab strip.
 * @return The rendered component.
 */
export function MyJetpackTabPanel( {
	beforeContent,
	productsOnly = false,
}: {
	beforeContent?: ReactNode;
	productsOnly?: boolean;
} ) {
	const params = useParams();
	const navigate = useNavigate();
	const { recordEvent } = useAnalytics();
	const isNewUser = useIsJetpackUserNew();
	const tabStartTimeRef = useRef< number >( Date.now() );
	const [ tabKey, setTabKey ] = useState( 0 );
	const lastNavigationSourceRef = useRef< 'internal' | 'external' >( 'external' );
 
	// If the tab is not valid, use the default one. In products-only mode we lock to the
	// Products tab (the tab strip itself is hidden via CSS) so the layout chrome — width,
	// background and the separator below the header — is preserved without showing tabs.
	const currentTab = useMemo( () => {
		if ( productsOnly ) {
			return MY_JETPACK_SECTION_PRODUCTS;
		}
		const validTab = isValidMyJetpackSection( params.section );
		return validTab ? params.section : MY_JETPACK_SECTION_OVERVIEW;
	}, [ params.section, productsOnly ] );
	const onTabSelect = useCallback(
		( tabName: string ) => {
			if ( tabName !== params.section ) {
				// Mark this as an internal navigation (user clicked a tab)
				lastNavigationSourceRef.current = 'internal';
 
				// Calculate session duration on previous tab
				const sessionDuration = Math.floor( ( Date.now() - tabStartTimeRef.current ) / 1000 );
 
				// Record tab click event
				recordEvent( 'jetpack_myjetpack_tab_click', {
					tab_name: tabName,
					previous_tab: params.section || MY_JETPACK_SECTION_OVERVIEW,
					session_duration: sessionDuration,
					user_type: isNewUser ? 'new' : 'returning',
				} );
 
				// Reset the timer for the new tab
				tabStartTimeRef.current = Date.now();
 
				navigate( `/${ tabName }` );
			}
		},
		[ navigate, params.section, recordEvent, isNewUser ]
	);
 
	const tabRenderer = useCallback(
		( tab: { name: string } ) => {
			return (
				<>
					<FullWidthSeparator />
					{ beforeContent }
					<TabContent name={ tab.name as MyJetpackSection } />
				</>
			);
		},
		[ beforeContent ]
	);
 
	// Handle external navigation (URL changes not from tab clicks)
	useEffect( () => {
		// If this was an external navigation (browser back/forward, direct URL access)
		if ( lastNavigationSourceRef.current === 'external' ) {
			// Force remount to sync with URL
			setTabKey( prev => prev + 1 );
		}
		// Reset navigation source for next change
		lastNavigationSourceRef.current = 'external';
 
		// Reset timer when tab changes
		tabStartTimeRef.current = Date.now();
	}, [ currentTab ] );
 
	useEffect( () => {
		// Track tab view event
		recordEvent( 'jetpack_myjetpack_tab_view', {
			tab_name: currentTab,
			user_type: isNewUser ? 'new' : 'returning',
			navigation_source: lastNavigationSourceRef.current,
		} );
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [] ); // track this only on page load
 
	const tabs = useMemo( () => {
		// Build the Products tab directly in products-only mode: getMyJetpackSections() strips
		// it for non-admins, and the tab strip is hidden anyway.
		if ( productsOnly ) {
			return [
				{ name: MY_JETPACK_SECTION_PRODUCTS, title: __( 'Products', 'jetpack-my-jetpack' ) },
			];
		}
		return getMyJetpackSections();
	}, [ productsOnly ] );
 
	return (
		<TabPanel
			key={ tabKey }
			className={ clsx(
				styles[ 'tab-panel' ],
				styles[ 'my-jetpack-tab-panel--full-width' ],
				productsOnly && styles[ 'my-jetpack-tab-panel--products-only' ],
				'jetpack-my-jetpack-tab-panel'
			) }
			initialTabName={ currentTab }
			onSelect={ onTabSelect }
			children={ tabRenderer }
			tabs={ tabs }
		/>
	);
}