FileMaster
Search
Toggle Dark Mode
Home
/
.
/
wp-content
/
plugins
/
surerank
/
src
/
global
/
components
/
fix-it-for-me
Edit File: generate-content.js
import { Button, Text, Skeleton, Loader } from '@bsf/force-ui'; import { __, sprintf } from '@wordpress/i18n'; import { RefreshCw } from 'lucide-react'; import { useState, useRef, useEffect } from '@wordpress/element'; import EmptyState from '@GlobalComponents/empty-state'; import { cn } from '@Functions/utils'; import { getMaxLengthForField } from '@Global/constants'; const SAVE_TEXT = __( 'Save', 'surerank' ); const EDIT_TEXT = __( 'Edit', 'surerank' ); const USE_THIS_TEXT = __( 'Use This', 'surerank' ); const FIXING_TEXT = __( 'Fixing…', 'surerank' ); const GENERATE_TEXT = __( 'Generate', 'surerank' ); const RETRY_TEXT = __( 'Retry', 'surerank' ); const GENERATING_TEXT = __( 'Generating…', 'surerank' ); const SHOW_MORE_TEXT = __( 'Regenerate', 'surerank' ); // Header text mapping for different field types const HEADER_TEXT_MAP = { home_page_title: __( 'Here are generated home page titles. Review and apply the one you like.', 'surerank' ), home_page_description: __( 'Here are generated home page descriptions. Review and apply the one you like.', 'surerank' ), home_page_social_title: __( 'Here are generated social titles for your home page. Review and apply the one you like.', 'surerank' ), home_page_social_description: __( 'Here are generated social descriptions for your home page. Review and apply the one you like.', 'surerank' ), page_title: __( 'Here are generated page titles. Review and apply the one you like.', 'surerank' ), page_description: __( 'Here are generated page descriptions. Review and apply the one you like.', 'surerank' ), social_title: __( 'Here are generated social titles. Review and apply the one you like.', 'surerank' ), social_description: __( 'Here are generated social descriptions. Review and apply the one you like.', 'surerank' ), site_tag_line: __( 'Here are generated site taglines. Review and apply the one you like.', 'surerank' ), }; // Default header text for SEO checks const DEFAULT_HEADER_TEXT = __( "Here are the fixes generated by SureRank AI based on your request. Review and apply the ones you'd like to use.", 'surerank' ); // Separate component for individual generated content items const GeneratedContentItem = ( { item, onUseThis, globalFixing, fieldType, } ) => { const maxLength = getMaxLengthForField( fieldType ); const [ isFixing, setIsFixing ] = useState( false ); const [ editedText, setEditedText ] = useState( item.text ); const [ isEditing, setIsEditing ] = useState( false ); const textareaRef = useRef( null ); const handleUseThis = async ( content ) => { if ( isFixing || globalFixing ) { return; } setIsFixing( true ); try { if ( typeof onUseThis === 'function' ) { await onUseThis( content ); } } catch ( error ) { // Silently handle errors to prevent UI breaks } finally { setIsFixing( false ); } }; const handleTextChange = ( e ) => { setEditedText( e.target.value ); }; const handleEditClick = () => { if ( isEditing ) { // Save - just toggle off editing mode setIsEditing( false ); if ( textareaRef?.current ) { textareaRef.current.blur(); // Reset scroll position to top textareaRef.current.scrollTop = 0; } } else { // Edit - enable editing mode setIsEditing( true ); // Focus the textarea after state update setTimeout( () => { if ( textareaRef?.current ) { textareaRef.current.focus(); const length = textareaRef.current.value.length; textareaRef.current.setSelectionRange( length, length ); } }, 0 ); } }; // Auto-adjust textarea height on mount and when text changes useEffect( () => { if ( textareaRef?.current ) { const textarea = textareaRef.current; // Reset height to auto to get proper scrollHeight calculation textarea.style.height = 'auto'; const scrollHeight = textarea.scrollHeight; // Set new height with max of 120px const newHeight = Math.min( scrollHeight, 120 ); textarea.style.height = newHeight + 'px'; } }, [ editedText ] ); const charCount = editedText.length; const editButtonText = isEditing ? SAVE_TEXT : EDIT_TEXT; // Hide character count for site tagline const showCharCount = fieldType !== 'site_tag_line'; const buttonText = isFixing ? FIXING_TEXT : USE_THIS_TEXT; return ( <div className="flex flex-col self-stretch gap-1.5 p-2 bg-white rounded-md shadow-sm"> { /* Text Content */ } <div className="flex flex-row items-start gap-2"> <textarea ref={ textareaRef } value={ editedText } onChange={ handleTextChange } rows={ 1 } readOnly={ ! isEditing } className={ cn( 'flex-1 bg-transparent border-none outline-none resize-none', 'text-sm font-medium text-text-secondary', 'py-[2px] px-[4px] rounded', 'overflow-y-auto', 'transition-[height] duration-150', 'min-h-[20px] leading-[1.4]', isEditing ? 'cursor-text' : 'cursor-default' ) } disabled={ isFixing || globalFixing } onInput={ ( e ) => { e.target.style.height = '0px'; const scrollHeight = e.target.scrollHeight; e.target.style.height = Math.min( scrollHeight, 120 ) + 'px'; } } /> </div> { /* Action Buttons */ } <div className={ cn( 'flex flex-row items-center self-stretch gap-2 p-1', showCharCount ? 'justify-between' : 'justify-end' ) } > { /* Character Count - Left Side */ } { showCharCount && ( <Text size={ 12 } weight={ 400 } color="tertiary" className="whitespace-nowrap" > { sprintf( /* translators: 1: current character count, 2: maximum character count */ __( '%1$d/%2$d', 'surerank' ), charCount, maxLength ) } </Text> ) } { /* Right Side Buttons */ } <div className="flex flex-row items-center gap-2"> { /* Edit/Save Button */ } <Button variant="link" size="xs" tag="button" onClick={ handleEditClick } disabled={ isFixing || globalFixing } > { editButtonText } </Button> { /* Use This Button */ } <Button variant="link" size="xs" tag="button" onClick={ () => handleUseThis( editedText ) } disabled={ isFixing || globalFixing } icon={ isFixing && <Loader size="sm" /> } > { buttonText } </Button> </div> </div> </div> ); }; const GenerateContent = ( { onUseThis, onRegenerate, contents = [], generating = false, fixing = false, error = null, fieldType = null, headerText: customHeaderText = null, } ) => { // Get header text based on field type, or use custom text, or default const headerText = customHeaderText || HEADER_TEXT_MAP[ fieldType ] || DEFAULT_HEADER_TEXT; const handleRegenerate = () => { if ( generating ) { return; } if ( typeof onRegenerate !== 'function' ) { return; } onRegenerate(); }; const hasAnyContent = contents && contents?.length > 0; const hasError = error !== null; // Error handling moved to parent component // Determine button text based on state const getButtonText = () => { if ( hasAnyContent ) { return SHOW_MORE_TEXT; } if ( generating ) { return GENERATING_TEXT; } if ( ! hasAnyContent && hasError ) { return RETRY_TEXT; } return GENERATE_TEXT; }; // Determine empty state message const getEmptyStateMessage = () => { if ( ! hasAnyContent ) { return sprintf( /* translators: %s: "Retry" button text */ __( 'No content generated. Click %s to retry creating AI-powered content suggestions.', 'surerank' ), `"${ getButtonText() }"` ); } return __( 'No content generated yet. Click "Generate" to create AI-powered content suggestions.', 'surerank' ); }; // Determine main content to render let mainContent; if ( generating ) { // Loading skeleton for content items const skeletonItems = Array.from( { length: 5 } ).map( ( _, index ) => ( <div key={ index } className="flex flex-row self-stretch gap-1 p-2 bg-white rounded-md shadow-sm" > { /* Text Content Skeleton */ } <div className="flex flex-row items-center gap-1 p-1 flex-1"> <div className="flex flex-row justify-stretch items-stretch gap-2 flex-1"> <div className="flex-1 space-y-2"> <Skeleton variant="rectangular" className="w-full h-4" /> <Skeleton variant="rectangular" className="w-3/4 h-4" /> </div> </div> </div> { /* Button Skeleton */ } <div className="flex flex-row items-center gap-2"> <Skeleton variant="rectangular" className="w-16 h-8 rounded" /> </div> </div> ) ); mainContent = skeletonItems; } else if ( hasAnyContent ) { // Generated content items using the separate component const contentItems = contents.map( ( item ) => ( <GeneratedContentItem key={ item.id } item={ item } onUseThis={ onUseThis } globalFixing={ fixing } fieldType={ fieldType } /> ) ); mainContent = contentItems; } else { // Empty state mainContent = ( <div className="flex flex-col self-stretch gap-2"> <EmptyState message={ getEmptyStateMessage() } className="m-2 text-center" /> </div> ); } return ( <div className="flex flex-col self-stretch gap-3"> { /* Header Text */ } <Text size={ 14 } weight={ 400 } color="primary" className="self-stretch" > { headerText } </Text> { /* Content Wrapper */ } <div className="flex flex-col items-end self-stretch gap-2 p-2 bg-background-secondary rounded-lg"> { /* Main Content Area */ } { mainContent } { /* Single Action Button */ } <div className="flex flex-row items-center gap-2 p-1"> <Button variant="ghost" size="xs" onClick={ handleRegenerate } disabled={ generating } icon={ <RefreshCw className={ cn( 'w-4 h-4', generating && 'animate-spin' ) } /> } iconPosition="left" className="px-1" > { getButtonText() } </Button> </div> </div> </div> ); }; export default GenerateContent;
Save
Back