import React, { useEffect, useRef, useState } from 'react'; import { __, _n, sprintf } from '@wordpress/i18n'; import { toast } from 'sonner'; import { ArrowRight, Check, X } from 'lucide-react'; import { Checkbox } from '../ui/checkbox'; import ProPill from './ProPill'; import { getPluginSettings, savePluginSettings } from '../../api'; import { formatBytes, numberFormat } from '../../lib/i18n'; import { COMPRESSION_CHECK_PRO_LINK } from './compression-check'; import { gradeFor, gradeStyle, proProjection, type DashboardModelStats } from './model'; type FixKey = 'webp_on_upload' | 'lazy_load' | 'hotlink_protection' | 'right_click_disable'; type FixState = Partial< Record< FixKey, boolean > >; interface FreeFix { key: FixKey; label: string; note: string; /** Pre-checked: changes nothing a visitor would notice. The others change what visitors can do. */ recommended: boolean; } const FREE_FIXES: FreeFix[] = [ { key: 'webp_on_upload', label: __( 'Convert new uploads to WebP', 'image-sizes' ), note: __( 'Every new image is converted as it arrives, and its original files are deleted. This can\'t be undone, so back up your uploads first.', 'image-sizes' ), recommended: false, }, { key: 'lazy_load', label: __( 'Turn on lazy loading', 'image-sizes' ), note: __( 'Images load as visitors scroll to them.', 'image-sizes' ), recommended: true, }, { key: 'hotlink_protection', label: __( 'Stop other sites embedding your images', 'image-sizes' ), note: __( 'Adds rules to your server config. Sites that embed your images will stop showing them.', 'image-sizes' ), recommended: false, }, { key: 'right_click_disable', label: __( 'Block right-click on images', 'image-sizes' ), note: __( 'A light deterrent only. Some visitors find it annoying.', 'image-sizes' ), recommended: false, }, ]; interface ProRow { key: string; num: string; label: string; note: string; to: string; /** A fix made by hand in the Media Library: the link leaves the app and nothing is sold. */ manual?: boolean; } function proRows( s: DashboardModelStats ): ProRow[] { const rows: ProRow[] = []; if ( s.large_images > 0 ) { rows.push( { key: 'large', num: numberFormat( s.large_images ), label: _n( 'heavy image to compress', 'heavy images to compress', s.large_images, 'image-sizes' ), note: __( 'Over 1 MB each. The main thing slowing your pages.', 'image-sizes' ), to: '/large-images' } ); } if ( s.not_compressed > 0 ) { rows.push( { key: 'compress', num: numberFormat( s.not_compressed ), label: _n( 'image not compressed', 'images not compressed', s.not_compressed, 'image-sizes' ), note: __( 'Compress them all in one run.', 'image-sizes' ), to: '/compress-images' } ); } if ( s.missing_alt > 0 ) { rows.push( { key: 'alt', num: numberFormat( s.missing_alt ), label: _n( 'image with no alt text', 'images with no alt text', s.missing_alt, 'image-sizes' ), note: __( 'Google can’t read them.', 'image-sizes' ), to: 'upload.php?mode=list', manual: true } ); } if ( s.duplicate_images > 0 ) { rows.push( { key: 'duplicate', num: numberFormat( s.duplicate_images ), label: _n( 'duplicate image to merge', 'duplicate images to merge', s.duplicate_images, 'image-sizes' ), note: sprintf( /* translators: %s: size of the repeat files. */ __( '%s of repeat files.', 'image-sizes' ), formatBytes( s.duplicate_bytes ), ), to: '/duplicate-images', } ); } if ( s.unused_images > 0 || ( s.likely_unused && s.likely_unused.count > 0 ) ) { rows.push( { key: 'unused', num: s.unused_images > 0 ? numberFormat( s.unused_images ) : '~' + numberFormat( s.likely_unused?.count ?? 0 ), label: s.unused_images > 0 ? _n( 'unused image to review', 'unused images to review', s.unused_images, 'image-sizes' ) : _n( 'likely unused image to review', 'likely unused images to review', s.likely_unused?.count ?? 0, 'image-sizes' ), note: __( 'Nothing is removed until you confirm.', 'image-sizes' ), to: '/unused-images', } ); } return rows; } /** What the server says is on right now, for the four free fixes. */ async function readSettings(): Promise< FixState > { const res = await getPluginSettings(); const data: any = res?.data ?? {}; const state: FixState = {}; for ( const fix of FREE_FIXES ) { state[ fix.key ] = !! data[ fix.key ]; } return state; } /** * Everything the dashboard found, in one place: the free fixes that apply right here, and what Pro * would do with this site's own numbers. */ export default function FixEverythingModal( { stats, pro, onClose, onApplied }: { stats: DashboardModelStats; pro: boolean; onClose: () => void; onApplied: () => void; } ) { const [ current, setCurrent ] = useState< FixState | null >( null ); const [ picked, setPicked ] = useState< FixState >( {} ); const [ busy, setBusy ] = useState( false ); const [ applied, setApplied ] = useState< FixKey[] >( [] ); const dialog = useRef< HTMLDivElement >( null ); const opener = useRef< Element | null >( document.activeElement ); useEffect( () => { readSettings() .then( ( state ) => { const start: FixState = {}; for ( const fix of FREE_FIXES ) { start[ fix.key ] = ! state[ fix.key ] && fix.recommended; } setCurrent( state ); setPicked( start ); } ) .catch( () => setCurrent( {} ) ); }, [] ); useEffect( () => { dialog.current?.focus(); const onKey = ( event: KeyboardEvent ) => { if ( event.key === 'Escape' ) { onClose(); } }; document.addEventListener( 'keydown', onKey ); const back = opener.current; return () => { document.removeEventListener( 'keydown', onKey ); ( back as HTMLElement | null )?.focus?.(); }; }, [] ); const open = FREE_FIXES.filter( ( fix ) => current && current[ fix.key ] === false ); const chosen = open.filter( ( fix ) => picked[ fix.key ] ); const rows = proRows( stats ); const projected = proProjection( stats ); const projectedGrade = gradeFor( projected ); const nowStyle = gradeStyle( stats.grade ); const nextStyle = gradeStyle( projectedGrade ); const showProjection = ! pro && rows.length > 0 && projected > stats.health_score; const apply = async () => { setBusy( true ); try { const payload: FixState = {}; for ( const fix of chosen ) { payload[ fix.key ] = true; } await savePluginSettings( payload ); // Ask the server what is on now, so a row is never marked on because the request merely returned. let state: FixState; try { state = { ...( await readSettings() ) }; } catch { state = { ...current, ...payload }; } const done = chosen.filter( ( fix ) => state[ fix.key ] ).map( ( fix ) => fix.key ); setCurrent( state ); setApplied( done ); if ( done.length < chosen.length ) { toast.error( __( 'Some settings could not be turned on.', 'image-sizes' ) ); } onApplied(); } catch { toast.error( __( 'Could not save. Nothing was changed.', 'image-sizes' ) ); } finally { setBusy( false ); } }; return (

{ __( 'Fix everything', 'image-sizes' ) }

{ __( 'Nothing changes until you apply it. Your images are not touched by the settings below.', 'image-sizes' ) }

{ showProjection && (
{ stats.grade } { sprintf( /* translators: 1: score now, 2: score with Pro. */ __( 'From %1$s to about %2$s with Pro', 'image-sizes' ), numberFormat( stats.health_score ), numberFormat( projected ), ) } { __( 'Estimate from the health score’s own weights, if every open item below were fixed.', 'image-sizes' ) }
) }

{ __( 'Fix now, free', 'image-sizes' ) }

{ current === null ? (

{ __( 'Reading your settings…', 'image-sizes' ) }

) : ( ) } { applied.length > 0 && (
{ sprintf( /* translators: %s: number of settings applied. */ _n( 'Done. %s setting is on.', 'Done. %s settings are on.', applied.length, 'image-sizes' ), numberFormat( applied.length ), ) }
) }
{ rows.length > 0 && (

{ pro ? __( 'Also on this site', 'image-sizes' ) : __( 'Pro would also', 'image-sizes' ) }

) }
{ ! pro ? (
{ __( 'See what Pro does', 'image-sizes' ) } { __( 'Try Pro risk-free for 30 days', 'image-sizes' ) }
) : }
); }