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 };
};