File "index.js"
Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/src/global/components/check-card/index.js
File size: 10.01 KB
MIME-type: text/x-java
Charset: utf-8
import { Badge, Label, Button } from '@bsf/force-ui';
import { cn, isURL } from '@/functions/utils';
import FixButton from '@GlobalComponents/fix-button';
import { __ } from '@wordpress/i18n';
import { CircleAlert, CircleCheck, Info, TriangleAlert, X } from 'lucide-react';
import {
SeoPopupInfoTooltip,
SeoPopupTooltip,
} from '@/apps/admin-components/tooltip';
import { ConfirmationDialog } from '@GlobalComponents/confirmation-dialog';
import { Fragment, useState } from '@wordpress/element';
import { fetchImageDataByUrl } from '@/functions/api';
import DOMPurify from 'dompurify';
const IMAGE_ID_CACHE = new Map();
const getIcon = ( type ) => {
const commonClassName = 'size-4';
switch ( type ) {
case 'blue':
return (
<Info
className={ cn( commonClassName, 'text-badge-color-sky' ) }
/>
);
case 'red':
return (
<CircleAlert
className={ cn( commonClassName, 'text-badge-color-red' ) }
/>
);
case 'yellow':
return (
<TriangleAlert
className={ cn(
commonClassName,
'text-badge-color-yellow'
) }
/>
);
case 'green':
return (
<CircleCheck
className={ cn(
commonClassName,
'text-badge-color-green'
) }
/>
);
default:
return null;
}
};
const formatBrokenLinkTooltip = ( item ) => {
if ( ! item || typeof item !== 'object' ) {
return null;
}
const { status, details } = item;
// Create a more descriptive tooltip based on the error type
let tooltipContent = '';
if ( details ) {
tooltipContent += details;
}
// Add status-specific helpful information
if ( status === 404 ) {
tooltipContent +=
' ' + __( '(The page or resource was not found)', 'surerank' );
} else if ( status === 'http_request_failed' ) {
tooltipContent +=
' ' + __( '(Unable to connect to the URL)', 'surerank' );
} else if ( status === 403 ) {
tooltipContent +=
' ' + __( '(Access to this resource is forbidden)', 'surerank' );
} else if ( status === 500 ) {
tooltipContent += ' ' + __( '(Server error occurred)', 'surerank' );
} else if ( typeof status === 'number' && status >= 400 ) {
tooltipContent += ` ${ __( '(HTTP error', 'surerank' ) } ${ status })`;
}
// Sanitize the content to prevent XSS attacks
const purifiedContent = DOMPurify.sanitize( tooltipContent );
return (
<div className="space-y-1">
<p className="m-0">
<b>{ __( 'Why is this link broken?', 'surerank' ) }</b>
</p>
<p
className="m-0"
dangerouslySetInnerHTML={ { __html: purifiedContent } }
/>
{ status && (
<p className="text-xs m-0">
<b>{ __( 'Status:', 'surerank' ) }</b> { status }
</p>
) }
</div>
);
};
const renderItem = ( item ) => {
const commonLinkProps = {
tag: 'a',
variant: 'link',
className:
'font-medium focus:outline-none focus:[box-shadow:none] [&>span]:px-0 break-all',
target: '_blank',
rel: 'noopener noreferrer',
};
if ( isURL( item ) ) {
return (
<li className="m-0 text-sm">
<Button { ...commonLinkProps } href={ item }>
{ item }
</Button>
</li>
);
} else if ( typeof item === 'object' && item?.url ) {
// For broken links or similar objects
return (
<li className="my-1 p-2 flex items-center justify-between gap-1.5 text-sm border border-dashed border-border-subtle rounded-md bg-background-secondary">
<Button { ...commonLinkProps } href={ item.url }>
{ item.url }
</Button>
<SeoPopupInfoTooltip
content={ formatBrokenLinkTooltip( item ) }
interactive
placement="top-start"
offset={ {
alignmentAxis: -10,
mainAxis: 8,
} }
/>
</li>
);
}
return (
<li className="m-0 text-sm font-medium text-text-secondary list-none">
{ item }
</li>
);
};
export const CheckCard = ( {
variant,
label,
title,
data,
showImages,
showFixButton = true,
onIgnore,
showRestoreButton = false,
onRestore,
showIgnoreButton = false,
} ) => {
const [ showIgnoreDialog, setShowIgnoreDialog ] = useState( false );
const { data: descriptionData, listStyleClassName } = getData( data );
const handleIgnoreClick = () => {
setShowIgnoreDialog( true );
};
const handleIgnoreConfirm = async () => {
await onIgnore();
setShowIgnoreDialog( false );
};
return (
<>
<div className="relative flex flex-col gap-3 p-3 bg-background-primary rounded-lg shadow-sm border-0.5 border-solid border-border-subtle">
{ showIgnoreButton && (
<Button
variant="outline"
type="button"
onClick={ handleIgnoreClick }
aria-label={ __( 'Ignore this check', 'surerank' ) }
className="absolute -top-2 -right-2 rounded-full *:focus:outline-none [&>svg]:size-3 focus:ring-0 focus:[box-shadow:none] p-0.5"
icon={ <X className="text-text-primary" /> }
size="xs"
/>
) }
<div className="w-full flex items-start gap-2">
{ showRestoreButton ? (
<Badge
label={ label }
size="sm"
type="pill"
variant={ variant }
disableHover
className={ cn(
showRestoreButton
? 'text-badge-color-disabled'
: ''
) }
/>
) : (
<>
<div>
<p className="sr-only">{ label }</p>
<span className="p-1 flex [&>svg]:size-4">
{ getIcon( variant ) }
</span>
</div>
</>
) }
<div className="flex items-center mt-px">
<Label
size="xs"
className="space-x-1 text-sm text-text-secondary inline"
>
{ title }
<SeoPopupTooltip
content={ __(
'Click here to discover more details about this check.',
'surerank'
) }
arrow
>
<a
href={ surerank_globals?.help_link }
className="shrink-0 align-sub ml-2 focus:outline-none focus:ring-0"
target="_blank"
rel="noopener noreferrer"
>
<Info className="size-4 text-icon-secondary hidden" />
</a>
</SeoPopupTooltip>
</Label>
</div>
{ showRestoreButton && (
<Button
variant="outline"
type="button"
onClick={ onRestore }
aria-label={ __(
'Restore this check',
'surerank'
) }
size="xs"
className="ml-auto min-w-fit shrink-0"
>
{ __( 'Restore', 'surerank' ) }
</Button>
) }
</div>
{ showImages && <ImageGrid images={ descriptionData } /> }
{ ! showImages &&
descriptionData &&
descriptionData.length > 0 && (
<ul
className={ cn(
'list-disc list-inside ml-3 mr-0 mt-0 mb-0.5',
listStyleClassName
) }
>
{ descriptionData.map( ( item, index ) => (
<Fragment key={ `${ item }-${ index }` }>
{ renderItem( item ) }
</Fragment>
) ) }
</ul>
) }
{ showFixButton && (
<FixButton
variant="link"
size="xs"
className="mr-auto min-w-fit shrink-0 underline"
tooltipProps={ { className: 'z-999999' } }
>
{ __( 'Help Me Fix', 'surerank' ) }
</FixButton>
) }
</div>
<ConfirmationDialog
open={ showIgnoreDialog }
setOpen={ setShowIgnoreDialog }
title={ __( 'Ignore Page Checks', 'surerank' ) }
description={ __(
"We'll stop flagging this check in future scans. If it's not relevant, feel free to ignore it, you can always bring it back later if needed.",
'surerank'
) }
confirmLabel={ __( 'Ignore', 'surerank' ) }
cancelLabel={ __( 'Cancel', 'surerank' ) }
onConfirm={ handleIgnoreConfirm }
confirmVariant="primary"
confirmDestructive={ true }
/>
</>
);
};
export const ImageGrid = ( { images } ) => {
if ( ! images || ! images.length ) {
return null;
}
const handleImageClick = async ( event, image ) => {
event?.preventDefault();
if ( IMAGE_ID_CACHE.has( image ) ) {
window.open(
`/wp-admin/upload.php?item=${ IMAGE_ID_CACHE.get( image ) }`,
'_blank',
'noopener noreferrer'
);
return;
}
try {
const results = await fetchImageDataByUrl( image );
if ( ! results ) {
throw new Error( 'No image found' );
}
const imageId = results?.id;
IMAGE_ID_CACHE.set( image, imageId );
window.open(
`/wp-admin/upload.php?item=${ imageId }`,
'_blank',
'noopener noreferrer'
);
} catch ( error ) {
// If we can't find the image, open the media library
window.open(
'/wp-admin/upload.php',
'_blank',
'noopener noreferrer'
);
}
};
return (
<div className="grid grid-cols-3 gap-2 mb-0.5">
{ images.map( ( image, index ) =>
isURL( image ) ? (
<Button
variant="link"
className="inline-flex focus:outline-none focus:[box-shadow:none] p-0 relative"
onClick={ ( event ) =>
handleImageClick( event, image )
}
key={ `${ image }-${ index }` }
>
<div className="absolute inset-0 bg-black bg-opacity-20 pointer-events-none"></div>
<div className="relative w-full h-36 rounded overflow-hidden">
<img
src={ image }
alt={ image }
className="w-full h-36 object-cover rounded"
/>
</div>
</Button>
) : null
) }
</div>
);
};
export const getData = ( descriptions ) => {
if ( ! Array.isArray( descriptions ) || ! descriptions.length ) {
return { data: [] };
}
// Handle text or list descriptions only
const data = [];
let listStyleClassName = '';
descriptions.forEach( ( item ) => {
if ( typeof item === 'string' ) {
data.push( item );
} else if (
item &&
typeof item === 'object' &&
Array.isArray( item.list )
) {
data.push( ...item.list );
// For SEO-Bar broken links or similar objects
if ( item.list?.some( ( value ) => value?.url && value?.status ) ) {
listStyleClassName = 'list-none mx-0';
}
} else if (
item &&
typeof item === 'object' &&
! Array.isArray( item?.list ) &&
typeof item?.list === 'object'
) {
data.push( ...Object.values( item.list ) );
} else {
// For any other object, just push it as is
// This could be a broken link object or similar
data.push( item );
listStyleClassName = 'list-none mx-0'; // Reset to none for non-list items
}
} );
return { data, listStyleClassName };
};