import apiFetch from '@wordpress/api-fetch'; import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import Button from '../../components/Button'; type RankMathProgress = { total: number; migrated: number; remaining: number; percent: number; completed: boolean; batchSize: number; }; type MigrationResponse = { progress?: RankMathProgress; processed?: number; settings?: { available?: boolean; }; }; type RankMathMigrationPanelProps = { restBase: string; isActive: boolean; }; const DEFAULT_PROGRESS: RankMathProgress = { total: 0, migrated: 0, remaining: 0, percent: 0, completed: false, batchSize: 10, }; const normalizeBase = ( base: string ): string => base.replace( /\/+$/, '' ); const RankMathMigrationPanel = ( { restBase, isActive }: RankMathMigrationPanelProps ) => { const [ progress, setProgress ] = useState( DEFAULT_PROGRESS ); const [ redirectsProgress, setRedirectsProgress ] = useState( DEFAULT_PROGRESS ); const [ isLoading, setIsLoading ] = useState( false ); const [ isImporting, setIsImporting ] = useState( false ); const [ isRedirectImporting, setIsRedirectImporting ] = useState( false ); const [ settingsStatus, setSettingsStatus ] = useState( null ); const [ redirectStatus, setRedirectStatus ] = useState( null ); const [ error, setError ] = useState( null ); const [ settingsAvailable, setSettingsAvailable ] = useState( false ); const statusPath = `${ normalizeBase( restBase ) }/migration/rankmath`; const importPath = `${ normalizeBase( restBase ) }/migration/rankmath/import`; const settingsPath = `${ normalizeBase( restBase ) }/migration/rankmath/settings`; const redirectsPath = `${ normalizeBase( restBase ) }/migration/rankmath/redirects`; const percent = useMemo( () => { const value = Number.isFinite( progress.percent ) ? progress.percent : 0; return Math.min( 100, Math.max( 0, value ) ); }, [ progress.percent ] ); const progressLabel = useMemo( () => `${ percent }% — ${ progress.migrated } ${ __( 'posts imported', 'airygen-seo' ) } / ${ progress.total } ${ __( 'total', 'airygen-seo' ) }`, [ percent, progress.migrated, progress.total ], ); const redirectPercent = useMemo( () => { const value = Number.isFinite( redirectsProgress.percent ) ? redirectsProgress.percent : 0; return Math.min( 100, Math.max( 0, value ) ); }, [ redirectsProgress.percent ] ); const redirectsLabel = useMemo( () => `${ redirectPercent }% — ${ redirectsProgress.migrated } ${ __( 'rules imported', 'airygen-seo' ) } / ${ redirectsProgress.total } ${ __( 'total', 'airygen-seo' ) }`, [ redirectPercent, redirectsProgress.migrated, redirectsProgress.total ], ); const loadStatus = useCallback( async () => { setIsLoading( true ); setError( null ); try { const response = ( await apiFetch( { path: statusPath, } ) ) as MigrationResponse; if ( response.progress ) { setProgress( { ...DEFAULT_PROGRESS, ...response.progress } ); } setSettingsAvailable( Boolean( response.settings?.available ) ); const redirectsResponse = ( await apiFetch( { path: redirectsPath, } ) ) as MigrationResponse; if ( redirectsResponse.progress ) { setRedirectsProgress( { ...DEFAULT_PROGRESS, ...redirectsResponse.progress, } ); } } catch ( fetchError ) { setError( fetchError instanceof Error ? fetchError.message : __( 'Unable to load migration status.', 'airygen-seo' ), ); } finally { setIsLoading( false ); } }, [ statusPath, redirectsPath ] ); const runBatch = useCallback( async () => { const response = ( await apiFetch( { path: importPath, method: 'POST', } ) ) as MigrationResponse; if ( response.progress ) { const nextProgress = { ...DEFAULT_PROGRESS, ...response.progress }; setProgress( nextProgress ); return nextProgress; } return progress; }, [ importPath, progress ] ); const runRedirectBatch = useCallback( async () => { const response = ( await apiFetch( { path: redirectsPath, method: 'POST', } ) ) as MigrationResponse; const processed = Number( response.processed ?? 0 ); if ( response.progress ) { const nextProgress = { ...DEFAULT_PROGRESS, ...response.progress }; setRedirectsProgress( nextProgress ); return { progress: nextProgress, processed }; } return { progress: redirectsProgress, processed }; }, [ redirectsPath, redirectsProgress ] ); const startImport = useCallback( async () => { if ( isImporting ) { return; } setIsImporting( true ); setError( null ); try { let nextProgress = await runBatch(); while ( nextProgress && ! nextProgress.completed && nextProgress.remaining > 0 ) { await new Promise( ( resolve ) => setTimeout( resolve, 150 ) ); nextProgress = await runBatch(); } } catch ( fetchError ) { setError( fetchError instanceof Error ? fetchError.message : __( 'Migration failed. Please try again.', 'airygen-seo' ), ); } finally { setIsImporting( false ); } }, [ isImporting, runBatch ] ); const startSettingsImport = useCallback( async () => { setSettingsStatus( null ); setError( null ); try { await apiFetch( { path: settingsPath, method: 'POST', } ); setSettingsStatus( __( 'Settings imported.', 'airygen-seo' ) ); } catch ( fetchError ) { setError( fetchError instanceof Error ? fetchError.message : __( 'Settings migration failed.', 'airygen-seo' ), ); } }, [ settingsPath ] ); const startRedirectImport = useCallback( async () => { if ( isRedirectImporting ) { return; } setRedirectStatus( null ); setError( null ); setIsRedirectImporting( true ); try { let batch = await runRedirectBatch(); while ( batch.progress && batch.processed > 0 && ! batch.progress.completed && batch.progress.remaining > 0 ) { await new Promise( ( resolve ) => setTimeout( resolve, 150 ) ); batch = await runRedirectBatch(); } setRedirectStatus( __( 'Redirects imported.', 'airygen-seo' ) ); } catch ( fetchError ) { setError( fetchError instanceof Error ? fetchError.message : __( 'Redirect migration failed.', 'airygen-seo' ), ); } finally { setIsRedirectImporting( false ); } }, [ isRedirectImporting, runRedirectBatch ] ); useEffect( () => { void loadStatus(); }, [ loadStatus ] ); return (
Rank Math
{ isActive ? __( 'Plugin active', 'airygen-seo' ) : __( 'Plugin inactive', 'airygen-seo' ) }
{ __( 'Prepare to migrate Rank Math metadata, social defaults, and schema settings into Airygen SEO.', 'airygen-seo', ) }
{ __( 'Post meta migration', 'airygen-seo' ) }

{ __( 'Import per-post SEO metadata (titles, descriptions, social overrides, and canonicals).', 'airygen-seo', ) }

{ __( 'Progress', 'airygen-seo' ) } { `${ String( percent ) }%` }
{ progressLabel }
{ error ? (

{ error }

) : null }
{ __( 'Global settings migration', 'airygen-seo' ) }

{ __( 'Import global defaults such as title templates, organization schema, and social defaults.', 'airygen-seo', ) }

{ settingsStatus ? (

{ settingsStatus }

) : null }
{ __( 'Redirect rules migration', 'airygen-seo' ) }

{ __( 'Import redirects from Rank Math and add them to Airygen Redirects.', 'airygen-seo', ) }

{ __( 'Progress', 'airygen-seo' ) } { `${ String( redirectPercent ) }%` }
{ redirectsLabel }
{ redirectStatus ? (

{ redirectStatus }

) : null }
); }; export default RankMathMigrationPanel;