import React from 'react'; import ReactDOM from 'react-dom'; import { __ } from '@wordpress/i18n'; import Icon from '@/utils/Icon'; import { useTourStore, TourStep, TourStepAction } from '@/store/useTourStore'; import { useTheme } from '@/hooks/useTheme'; /** Actions that block Next and require the user to act */ const BLOCKING_ACTIONS = new Set([ 'click_element', 'open_modal', 'hover_element', 'change_metrics', 'apply_filter', 'toggle_columns', 'generate_share_link' ]); const ACTION_INSTRUCTION: Partial> = { click_element: __( 'Click the highlighted element to continue.', 'burst-statistics' ), open_modal: __( 'Click the highlighted button to open it.', 'burst-statistics' ), hover_element: __( 'Hover over the highlighted element.', 'burst-statistics' ), click_tab: __( 'Click the highlighted tab above to continue.', 'burst-statistics' ), change_date_range: __( 'Select "All time" from the date range dropdown to continue.', 'burst-statistics' ), change_metrics: __( 'Select any metric and click Apply to update the graph.', 'burst-statistics' ), apply_filter: __( 'Click + Add filter and apply a filter to continue.', 'burst-statistics' ), toggle_columns: __( 'Toggle any column and click Apply to customize the table.', 'burst-statistics' ), generate_share_link: __( 'Click "Generate shareable link" to create a live share link.', 'burst-statistics' ) }; export interface TourInteractiveHUDProps { step: TourStep; index: number; totalSteps: number; canSkipFeature?: boolean; onSkipFeature?: () => void; canPrevFeature?: boolean; onPrevFeature?: () => void; } /** * Primary-button label shown while the tour waits for the user to act. Uses a * pulsing dot (waiting for input) rather than a spinner (which reads as the app * being busy) so it's clear the next move is the user's. * * @return {JSX.Element} The label with a pulsing indicator. */ const YourTurnLabel = (): React.ReactElement => { return ( { __( 'Your turn πŸ‘†', 'burst-statistics' ) } ); }; /** * Formats tour text, cleanly breaking down bullet points or paragraph lines. */ const formatTourText = ( text?: string, textSecond?: string, textPrimary?: string ) => { if ( ! text ) { return null; } if ( text.includes( '\n' ) || text.includes( 'β€’' ) ) { const rawLines = text.split( /\n|(?=β€’\s*)/g ).map( ( l ) => l.trim() ).filter( Boolean ); return (
{ rawLines.map( ( line, idx ) => { const isBullet = line.startsWith( 'β€’' ); const cleanLine = isBullet ? line.replace( /^β€’\s*/, '' ) : line; const colonIdx = cleanLine.indexOf( ':' ); if ( isBullet && 0 < colonIdx && 30 > colonIdx ) { const label = cleanLine.substring( 0, colonIdx + 1 ); const rest = cleanLine.substring( colonIdx + 1 ); return (
β€’
{ label } { rest }
); } if ( isBullet ) { return (
β€’ { cleanLine }
); } return (

{ cleanLine }

); }) }
); } return (
{ text }
); }; // fallow-ignore-next-line complexity export const TourInteractiveHUD: React.FC = ({ step, index, totalSteps, canSkipFeature, onSkipFeature, canPrevFeature = 0 < index, onPrevFeature }) => { const nextStep = useTourStore( ( s ) => s.nextStep ); const prevStep = useTourStore( ( s ) => s.prevStep ); const dismissTour = useTourStore( ( s ) => s.dismissTour ); const setIsHotspotActive = useTourStore( ( s ) => s.setIsHotspotActive ); const interactionComplete = useTourStore( ( s ) => s.interactionComplete ); const { isDarkTheme } = useTheme(); const isPro = Boolean( step.pro ); const action = step.action as TourStepAction | undefined; const isDateRange = 'change_date_range' === action; const isTabTransition = 'click_tab' === action; const isBlockingAction = Boolean( action && BLOCKING_ACTIONS.has( action ) ); const isInteractive = Boolean( action ); const isLastStep = index >= totalSteps - 1; const actionDone = ( ! isBlockingAction && ! isTabTransition && ! isDateRange ) || interactionComplete; const progress = Math.round( ( ( index + 1 ) / totalSteps ) * 100 ); const bg = isDarkTheme ? 'rgba(15,23,42,0.97)' : 'rgba(255,255,255,0.98)'; const textPrimary = isDarkTheme ? '#f1f5f9' : '#0f172a'; const textSecond = isDarkTheme ? '#94a3b8' : '#64748b'; const border = isDarkTheme ? 'rgba(51,65,85,0.7)' : 'rgba(226,232,240,0.9)'; const accentBg = isDarkTheme ? 'rgba(56,189,248,0.12)' : 'rgba(42,91,140,0.08)'; const accentText = isDarkTheme ? 'var(--color-primary, #38bdf8)' : 'var(--color-primary, #2A5B8C)'; const accentBorder = isDarkTheme ? 'rgba(56,189,248,0.28)' : 'rgba(42,91,140,0.2)'; const primaryBg = isDarkTheme ? 'var(--color-primary, #38bdf8)' : 'var(--color-primary, #2A5B8C)'; const handleNext = ( event: React.MouseEvent ) => { event.preventDefault(); event.stopPropagation(); if ( isDateRange ) { setIsHotspotActive( true ); return; } if ( isTabTransition ) { return; } nextStep(); }; const handlePrev = ( event: React.MouseEvent ) => { event.preventDefault(); event.stopPropagation(); if ( onPrevFeature ) { onPrevFeature(); } else { prevStep(); } }; const handleSkipFeature = ( event: React.MouseEvent ) => { event.preventDefault(); event.stopPropagation(); onSkipFeature?.(); }; let primaryLabel: React.ReactNode; let primaryDisabled = false; if ( isLastStep && actionDone ) { primaryLabel = __( 'Finish πŸŽ‰', 'burst-statistics' ); } else if ( isTabTransition && ! interactionComplete ) { primaryLabel = ; primaryDisabled = true; } else if ( isDateRange ) { primaryLabel = __( 'Try it πŸ“…', 'burst-statistics' ); } else if ( isBlockingAction && ! interactionComplete ) { primaryLabel = ; primaryDisabled = true; } else { primaryLabel = __( 'Next β†’', 'burst-statistics' ); } return ReactDOM.createPortal(
{/* Header */}
{ index + 1 } / { totalSteps } { isPro && ( { __( 'PRO', 'burst-statistics' ) } )} { isInteractive && ( { __( 'Interactive', 'burst-statistics' ) } )}
{/* Progress bar */}
{/* Title + content */}
{ step.title && (

{ step.title }

)} { formatTourText( step.text, textSecond, textPrimary ) }
{/* Action instruction banner */} { action && ACTION_INSTRUCTION[ action ] && (
{ interactionComplete ? 'βœ… ' + __( 'Done! Moving to next step…', 'burst-statistics' ) : ACTION_INSTRUCTION[ action ] }
)} {/* Footer */}
{ canSkipFeature && ( ) }
, document.body ); };