File "content-analysis-table.js"

Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/src/apps/admin-dashboard/content-analysis-table.js
File size: 16.62 KB
MIME-type: text/x-java
Charset: utf-8

import apiFetch from '@wordpress/api-fetch';
import {
	Table,
	Badge,
	Button,
	Container,
	toast,
	Skeleton,
	Text,
	Pagination,
} from '@bsf/force-ui';
import {
	ArrowUpRight,
	ArrowUp,
	ArrowDown,
	AlertTriangle,
} from 'lucide-react';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from '@/admin-store/constants';
import { useEffect, useState, useCallback, useRef } from '@wordpress/element';
import ContentPerformanceEmptyState from './content-performance-empty-state';
import { addQueryArgs } from '@wordpress/url';
import { ADMIN_DASHBOARD_URL } from '@Global/constants/index';
import { formatToISOPreserveDate, getLastNDays } from '@/functions/utils';
import { SortableColumn } from '@GlobalComponents/sortable-column';

const isSameDomain = ( url ) => {
	const cleanUrl = url.includes( 'sc-domain:' )
		? url.replace( 'sc-domain:', '' )
		: url;
	return cleanUrl.includes( window.location.host );
};

const LoadingSkeleton = ( { sameDomain = false, numberOfRows = 10 } ) => {
	return (
		<>
			{ [ ...Array( numberOfRows ) ].map( ( _, index ) => (
				<Table.Row key={ index }>
					<Table.Cell className="w-[35%] space-y-1">
						{ sameDomain && <Skeleton className="h-5 w-3/4" /> }
						<Skeleton className="h-5 w-full" />
					</Table.Cell>
					<Table.Cell className="w-1/10">
						<Skeleton className="h-5 w-16 rounded-full" />
					</Table.Cell>
					<Table.Cell className="w-1/10">
						<Skeleton className="h-5 w-12" />
					</Table.Cell>
					<Table.Cell>
						<Skeleton className="h-5 w-14" />
					</Table.Cell>
					<Table.Cell>
						<Skeleton className="h-5 w-16" />
					</Table.Cell>
					{ /* Uncomment this block if you want to show the additional skeletons */ }
					{ /* <Table.Cell>
						<Container direction="column" className="gap-1.5">
							<Skeleton className="h-5 w-16" />
							<Skeleton className="h-2 w-5/6" />
						</Container>
					</Table.Cell>
					<Table.Cell>
						<Skeleton className="h-5 w-16" />
					</Table.Cell> */ }
				</Table.Row>
			) ) }
		</>
	);
};

const ContentAnalysisTable = ( {
	type = 'short',
	searchQuery = '',
	statusFilter = 'All',
} ) => {
	const { setSearchConsole } = useDispatch( STORE_NAME );
	const {
		contentPerformance = [],
		authenticated,
		hasSiteSelected,
		selectedSite,
		contentPerformanceFetchComplete = false,
	} = useSelect( ( select ) => select( STORE_NAME ).getSearchConsole() );
	const [ loading, setLoading ] = useState(
		authenticated && hasSiteSelected && contentPerformance.length === 0
	);
	const [ exception, setException ] = useState( {} );
	const [ sortConfig, setSortConfig ] = useState( {
		key: null,
		direction: 'asc',
	} );
	const [ currentPage, setCurrentPage ] = useState( 1 );
	const itemsPerPage = 20; // Show 20 results per page

	const previousSite = useRef( null ); // Track previous site, start with null
	// Reset currentPage when searchQuery or statusFilter changes
	useEffect( () => {
		setCurrentPage( 1 );
	}, [ searchQuery, statusFilter ] );

	const handleSort = ( key ) => {
		setSortConfig( ( current ) => {
			if ( current.key === key ) {
				return {
					key,
					direction: current.direction === 'asc' ? 'desc' : 'asc',
				};
			}
			return {
				key,
				direction: 'asc',
			};
		} );
		setCurrentPage( 1 ); // Reset to first page when sorting
	};

	// Filter data based on searchQuery and statusFilter
	const filteredData = contentPerformance.filter( ( item ) => {
		// Search filter: match url or title
		const searchMatch =
			! searchQuery ||
			( item.url &&
				item.url
					.toLowerCase()
					.includes( searchQuery.toLowerCase() ) ) ||
			( item.title &&
				item.title
					.toLowerCase()
					.includes( searchQuery.toLowerCase() ) );

		// Status filter
		const statusMatch =
			statusFilter === 'All' ||
			( statusFilter === 'Top Ranked' &&
				item?.current?.position <= 10 &&
				item?.current?.position > 0 ) ||
			( statusFilter === 'On the Rise' &&
				item?.current?.position <= 20 &&
				item?.current?.position > 10 ) ||
			( statusFilter === 'Low Visibility' &&
				item?.current?.position > 20 );

		return searchMatch && statusMatch;
	} );

	const sortedData = [ ...filteredData ].sort( ( a, b ) => {
		if ( ! sortConfig.key ) {
			return 0;
		}

		const aValue = a?.current[ sortConfig.key ];
		const bValue = b?.current[ sortConfig.key ];

		if ( aValue === bValue ) {
			return 0;
		}
		if ( aValue === null || aValue === undefined ) {
			return 1;
		}
		if ( bValue === null || bValue === undefined ) {
			return -1;
		}

		const comparison = aValue < bValue ? -1 : 1;
		return sortConfig.direction === 'asc' ? comparison : -comparison;
	} );

	// Calculate pagination
	const totalItems = sortedData.length;
	const totalPages = Math.ceil( totalItems / itemsPerPage );
	const startIndex = ( currentPage - 1 ) * itemsPerPage;
	const endIndex = startIndex + itemsPerPage;
	const paginatedData =
		type === 'full'
			? sortedData.slice( startIndex, endIndex )
			: sortedData.slice( 0, 5 );

	// Fetch content performance only when the site changes
	const fetchContentPerformance = useCallback( async () => {
		if ( ! authenticated || ! selectedSite ) {
			return;
		}
		const { from, to } = getLastNDays( 90 );
		const formattedStartDate = from
			? formatToISOPreserveDate( new Date( from ) )
			: null;
		const formattedEndDate = to
			? formatToISOPreserveDate( new Date( to ) )
			: null;

		if ( ! formattedStartDate || ! formattedEndDate ) {
			return;
		}
		try {
			setLoading( true );
			const response = await apiFetch( {
				path: addQueryArgs(
					'/surerank/v1/google-search-console/content-performance',
					{
						rowLimit: 100,
						startDate: formattedStartDate,
						endDate: formattedEndDate,
					}
				),
				method: 'GET',
			} );
			if ( ! response.success ) {
				throw new Error(
					response.message ??
						__( 'Failed to fetch content performance', 'surerank' )
				);
			}
			setSearchConsole( {
				contentPerformance: response.data,
				contentPerformanceFetchComplete: true,
			} );
		} catch ( error ) {
			toast.error( error.message );
			setException( {
				icon: <AlertTriangle className="size-4" />,
				title: __( 'Oops! Something went wrong', 'surerank' ),
				description: __(
					'Failed to get content performance. Please try again later. If the problem persists, please contact support.',
					'surerank'
				),
			} );
		} finally {
			setLoading( false );
		}
	}, [ authenticated, selectedSite ] );
	useEffect( () => {
		// Only fetch data when the site changes or on first load without data
		if (
			authenticated &&
			selectedSite &&
			previousSite.current !== selectedSite
		) {
			// Reset fetchComplete when site changes
			if ( previousSite.current !== null ) {
				setSearchConsole( {
					contentPerformanceFetchComplete: false,
				} );
			}

			// Only fetch if we haven't completed a fetch for this session or if the site actually changed
			if (
				! contentPerformanceFetchComplete ||
				previousSite.current !== null
			) {
				fetchContentPerformance();
			}
			previousSite.current = selectedSite;
		}
	}, [
		authenticated,
		selectedSite,
		fetchContentPerformance,
		contentPerformanceFetchComplete,
		setSearchConsole,
	] );

	const handlePageChange = ( page ) => {
		setCurrentPage( page );
	};

	const handleNext = () => {
		if ( currentPage < totalPages ) {
			setCurrentPage( currentPage + 1 );
		}
	};

	const handlePrevious = () => {
		if ( currentPage > 1 ) {
			setCurrentPage( currentPage - 1 );
		}
	};

	const getPageNumbers = () => {
		const delta = 1; // Number of sibling pages to show on each side
		const range = [];
		const rangeWithDots = [];

		// Calculate the range of pages to display
		let left = Math.max( 2, currentPage - delta );
		let right = Math.min( totalPages - 1, currentPage + delta );

		// Adjust the range to ensure we show enough pages
		const rangeSize = right - left + 1;
		if ( rangeSize < 2 * delta + 1 ) {
			if ( currentPage <= totalPages / 2 ) {
				// Near the start, extend right
				right = Math.min( left + 2 * delta, totalPages - 1 );
			} else {
				// Near the end, extend left
				left = Math.max( right - 2 * delta, 2 );
			}
		}

		// Always include page 1
		range.push( 1 );

		// Add pages in the calculated range
		for ( let i = left; i <= right; i++ ) {
			range.push( i );
		}

		if ( totalPages > 1 && ! range.includes( totalPages ) ) {
			range.push( totalPages );
		}

		let prevPage = 0;
		for ( const page of range ) {
			if ( page - prevPage > 1 ) {
				rangeWithDots.push( 'ellipsis' );
			}
			rangeWithDots.push( page );
			prevPage = page;
		}

		return rangeWithDots;
	};

	const pageNumbers = getPageNumbers();

	const getIcon = ( item, key ) => {
		const value = item?.changes[ key ];

		if ( typeof value !== 'number' ) {
			return null;
		}
		if ( value > 0 ) {
			if ( key === 'position' ) {
				return (
					<ArrowDown className="size-3.5 text-support-error shrink-0" />
				);
			}
			return (
				<ArrowUp className="size-3.5 text-support-success shrink-0" />
			);
		}
		if ( value < 0 ) {
			if ( key === 'position' ) {
				return (
					<ArrowUp className="size-3.5 text-support-success shrink-0" />
				);
			}
			return (
				<ArrowDown className="size-3.5 text-support-error shrink-0" />
			);
		}
		return null;
	};

	// Empty state when there is an error
	if ( exception?.title && ! loading ) {
		return (
			<ContentPerformanceEmptyState
				title={ exception?.title }
				description={ exception?.description }
				icon={ exception?.icon }
			/>
		);
	}

	// Empty state when there is no content performance data
	if ( ! filteredData?.length && ! loading ) {
		let description;

		if ( authenticated && ! hasSiteSelected ) {
			description = __(
				"Once a site is selected, you'll see how your content is performing in search engines here.",
				'surerank'
			);
		} else if ( authenticated && hasSiteSelected ) {
			description = __(
				'No content performance data available. Please check back later.',
				'surerank'
			);
		} else {
			description = __(
				'Once connected to Google Search Console, you’ll see how your content is performing in search engines here.',
				'surerank'
			);
		}

		return <ContentPerformanceEmptyState description={ description } />;
	}

	return (
		<Table>
			<Table.Head>
				<Table.HeadCell className="w-[35%] max-w-120 min-w-80">
					{ __( 'Page', 'surerank' ) }
				</Table.HeadCell>
				<Table.HeadCell className="w-1/10">
					{ __( 'Status', 'surerank' ) }
				</Table.HeadCell>
				<SortableColumn
					className="w-[12%]"
					sortKey="clicks"
					onSort={ handleSort }
					currentSort={ sortConfig }
				>
					{ __( 'Clicks', 'surerank' ) }
				</SortableColumn>
				<SortableColumn
					className="w-[12%] text-nowrap"
					sortKey="position"
					onSort={ handleSort }
					currentSort={ sortConfig }
				>
					{ __( 'Avg. Position', 'surerank' ) }
				</SortableColumn>
				<SortableColumn
					className="w-[12%]"
					sortKey="impressions"
					onSort={ handleSort }
					currentSort={ sortConfig }
				>
					{ __( 'Impressions', 'surerank' ) }
				</SortableColumn>
				{ /* Uncomment this when content score feature is ready. */ }
				{ /* <Table.HeadCell className="min-w-[10rem] text-nowrap">
					<Container align="center" className="gap-1">
						<span className="text-text-tertiary">
							{ __( 'Content Score', 'surerank' ) }
						</span>
						<Badge
							className="w-fit"
							size="xs"
							variant="blue"
							label={ __( 'Pro', 'surerank' ) }
						/>
					</Container>
				</Table.HeadCell> */ }
				{ /* <Table.HeadCell className="min-w-[10%]">
					<span className="sr-only">
						{ __( 'Actions', 'surerank' ) }
					</span>
				</Table.HeadCell> */ }
			</Table.Head>
			<Table.Body>
				{ loading ? (
					<LoadingSkeleton
						sameDomain={ isSameDomain( selectedSite ) }
						numberOfRows={ type === 'full' ? 10 : 5 }
					/>
				) : (
					paginatedData.map( ( item, index ) => (
						<Table.Row key={ index }>
							<Table.Cell className="space-y-1">
								<Text
									as="a"
									href={ item.url }
									color="secondary"
									className="line-clamp-1 no-underline text-xs"
									target="_blank"
								>
									{ item.url }
								</Text>
							</Table.Cell>
							<Table.Cell>
								<Badge
									className="w-fit"
									size="xs"
									variant={ ( () => {
										const pos = item?.current?.position;
										if ( ! pos || pos <= 0 ) {
											return 'neutral';
										}
										if ( pos <= 10 ) {
											return 'green';
										}
										if ( pos <= 20 ) {
											return 'yellow';
										}
										return 'neutral';
									} )() }
									label={ ( () => {
										const pos = item?.current?.position;
										if ( pos <= 10 ) {
											return __(
												'Top Ranked',
												'surerank'
											);
										}
										if ( pos <= 20 ) {
											return __(
												'On the Rise',
												'surerank'
											);
										}
										return __(
											'Low Visibility',
											'surerank'
										);
									} )() }
									disableHover
								/>
							</Table.Cell>
							{ [ 'clicks', 'position', 'impressions' ].map(
								( key ) => (
									<Table.Cell key={ key }>
										<Container
											align="center"
											className="gap-1"
										>
											<span className="text-xs">
												{ key === 'position'
													? item.current[
															key
													  ]?.toFixed( 2 )
													: item.current[
															key
													  ]?.toLocaleString() }
											</span>
											{ getIcon( item, key ) }
										</Container>
									</Table.Cell>
								)
							) }
							{ /* Uncomment this when content score feature is ready. */ }
							{ /* <Table.Cell>
								<Container
									direction="column"
									className="gap-1.5"
								>
									<span className="text-xs">
										{ __( 'Out of 100', 'surerank' ) }
									</span>
									<ProgressBar
										progress={ 50 }
										className={ cn(
											'w-full max-w-32',
											'[&>div]:bg-gray-400'
										) }
									/>
								</Container>
							</Table.Cell> */ }
							{ /* <Table.Cell>
								<Container align="center" justify="start">
									<FixButton
										buttonLabel={ __( 'View', 'surerank' ) }
										icon={ <ArrowRight /> }
										iconPosition="right"
										title={ __(
											'Unlock Content Insights',
											'surerank'
										) }
										description={ __(
											"See what's driving traffic, content score, rankings, and performance trends.",
											'surerank'
										) }
										link={ surerank_globals.pricing_link }
										linkLabel={ __(
											'Upgrade',
											'surerank'
										) }
										size="sm"
										tooltipProps={ {
											className: 'z-999999',
										} }
									/>
								</Container>
							</Table.Cell> */ }
						</Table.Row>
					) )
				) }
			</Table.Body>
			{ type !== 'full' && (
				<Table.Footer className="flex items-center justify-center">
					<Button
						size="md"
						variant="link"
						icon={ <ArrowUpRight /> }
						iconPosition="right"
						className="no-underline hover:no-underline"
						onClick={ () => {
							window.location.href = `${ ADMIN_DASHBOARD_URL }?page=surerank#/content-performance`;
						} }
					>
						{ __( 'View Full Report', 'surerank' ) }
					</Button>
				</Table.Footer>
			) }
			{ type === 'full' && (
				<Table.Footer className="flex items-center justify-between w-full">
					<span className="text-sm font-normal leading-5 text-text-secondary">
						{ totalItems > 0
							? `Page ${ currentPage } out of ${ totalPages }`
							: 'No pages available' }
					</span>
					{ loading ? (
						<Skeleton className="w-32 h-8" />
					) : (
						<Pagination className="w-fit">
							<Pagination.Content className="[&>li]:m-0">
								<Pagination.Previous
									onClick={ handlePrevious }
									disabled={ currentPage === 1 }
									className={
										currentPage === 1
											? 'opacity-50 cursor-not-allowed'
											: ''
									}
								/>
								{ pageNumbers.map( ( item, index ) =>
									item === 'ellipsis' ? (
										<Pagination.Ellipsis
											key={ `ellipsis-${ index }` }
										/>
									) : (
										<Pagination.Item
											key={ item }
											isActive={ currentPage === item }
											onClick={ () =>
												handlePageChange( item )
											}
										>
											{ item }
										</Pagination.Item>
									)
								) }
								<Pagination.Next
									onClick={ handleNext }
									disabled={ currentPage === totalPages }
									className={
										currentPage === totalPages
											? 'opacity-50 cursor-not-allowed'
											: ''
									}
								/>
							</Pagination.Content>
						</Pagination>
					) }
				</Table.Footer>
			) }
		</Table>
	);
};

export default ContentAnalysisTable;