File "site-search-traffic.js"

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

import { useSelect, useDispatch } from '@wordpress/data';
import {
	Container,
	Title,
	Button,
	Label,
	toast,
	Skeleton,
	LineChart,
	Badge,
	Text,
} from '@bsf/force-ui';
import { __ } from '@wordpress/i18n';
import {
	useEffect,
	useState,
	useMemo,
	useCallback,
	useRef,
} from '@wordpress/element';
import { ArrowUp, ArrowDown, LogOut } from 'lucide-react';
import {
	cn,
	formatNumber,
	formatToISOPreserveDate,
	getLastNDays,
	formatDateRange,
} from '@/functions/utils';
import Section from './section';
import { STORE_NAME } from '@/admin-store/constants';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { handleDisconnectConfirm } from '../admin-components/user-dropdown';
const { auth_url: authURL } = surerank_admin_common;

const DEFAULT_DATE_RANGE = 90;

const ClicksAndImpressions = ( { item, isLoading } ) => {
	const renderValue =
		item.value === null && item.previous === null
			? 'N/A'
			: formatNumber( item.value );
	const renderDifference =
		item.value === null && item.previous === null
			? 'N/A'
			: formatNumber( Math.abs( item?.value - item?.previous ) );
	let renderIcon =
		item.percentageType === 'success' ? (
			<ArrowUp className="size-5" />
		) : (
			<ArrowDown className="size-5" />
		);

	let differenceClassName = '';
	switch ( item.percentageType ) {
		case 'danger':
			differenceClassName = 'text-support-error [&>*]:text-support-error';
			break;
		case 'success':
			differenceClassName =
				'text-support-success [&>*]:text-support-success';
			break;
		default:
			differenceClassName = '';
	}

	let fallbackClassName = '';
	// Render N/A and null for difference and icon when both value and previous are null.
	if ( item.value === null && item.previous === null ) {
		fallbackClassName = 'text-text-tertiary [&>*]:text-text-tertiary';
		renderIcon = null;
	}

	if ( item.value === 0 && item.previous === 0 ) {
		renderIcon = null;
	}

	return (
		<Container.Item
			key={ item.label }
			className="px-3 py-5 space-y-4 w-full h-full bg-background-primary rounded-md shadow-sm"
		>
			<Container
				align="center"
				justify="between"
				gap="sm"
				className="p-1"
			>
				<Label tag="p" size="md" className="font-medium">
					{ item.label }
				</Label>
				<span className={ cn( 'size-2 rounded-sm', item.color ) } />
			</Container>
			<Container
				align="center"
				justify="between"
				gap="sm"
				className="p-1"
			>
				{ isLoading ? (
					<Skeleton variant="rectangular" className="w-24 h-10" />
				) : (
					<Label
						tag="p"
						size="md"
						className={ cn(
							'font-semibold text-4xl',
							fallbackClassName
						) }
					>
						{ renderValue }
					</Label>
				) }
				{ isLoading ? (
					<Skeleton variant="rectangular" className="w-16 h-6" />
				) : (
					<Label
						tag="p"
						size="sm"
						className={ cn(
							'font-medium',
							differenceClassName,
							fallbackClassName
						) }
					>
						{ renderIcon }
						<span className="text-inherit">
							{ renderDifference }
						</span>
					</Label>
				) }
			</Container>
		</Container.Item>
	);
};

const EmptyState = ( {
	onClickActionBtn,
	imageSrc = `${ surerank_globals.admin_assets_url }/images/search-console.svg`,
	title,
	description,
	actionButtonText,
} ) => {
	return (
		<Container
			gap="lg"
			direction="column"
			align="center"
			justify="center"
			className="p-[3.125rem]"
		>
			<img src={ imageSrc } alt="Site Search Traffic" />
			<Container.Item className="mx-auto text-center max-w-[39.875rem] space-y-1">
				<Label
					tag="h6"
					className="text-lg font-semibold text-center block"
				>
					{ title }
				</Label>
				<Label
					tag="p"
					size="md"
					className="font-normal text-text-secondary"
				>
					{ description }
				</Label>
			</Container.Item>
			<Button
				variant="primary"
				size="md"
				className="focus:[box-shadow:none]"
				onClick={ onClickActionBtn }
			>
				{ actionButtonText }
			</Button>
		</Container>
	);
};

const SiteSearchTraffic = () => {
	const { setSearchConsole, toggleSiteSelectorModal, setConfirmationModal } =
		useDispatch( STORE_NAME );
	const {
		clicksData = [
			{
				label: __( 'Clicks', 'surerank' ),
				value: null,
				previous: null,
				percentage: null,
				percentageType: 'success',
				color: 'bg-sky-500',
			},
			{
				label: __( 'Impressions', 'surerank' ),
				value: null,
				previous: null,
				percentage: null,
				percentageType: 'success',
				color: 'bg-background-brand',
			},
		],
		authenticated,
		hasSiteSelected,
		selectedSite,
		siteTraffic = [],
		siteTrafficFetchComplete = false,
	} = useSelect( ( select ) => select( STORE_NAME ).getSearchConsole(), [] );

	const [ isLoading, setIsLoading ] = useState(
		authenticated &&
			hasSiteSelected &&
			clicksData[ 0 ].value === null &&
			siteTraffic.length === 0
	);

	const previousSite = useRef( null ); // Track previous site, start with null

	const fetchClicksAndImpressions = useCallback( async ( from, to ) => {
		const formattedStartDate = formatToISOPreserveDate( from );
		const formattedEndDate = formatToISOPreserveDate( to );

		try {
			const response = await apiFetch( {
				path: '/surerank/v1/google-search-console/clicks-and-impressions',
				method: 'POST',
				data: {
					startDate: formattedStartDate,
					endDate: formattedEndDate,
				},
			} );
			if ( ! response.success ) {
				throw new Error(
					response.message ??
						__( 'Failed to fetch matched site', 'surerank' )
				);
			}
			const clicks = response?.data?.clicks;
			const impressions = response?.data?.impressions;

			const getPercentageType = ( percentage ) => {
				if ( percentage === 0 ) {
					return 'neutral';
				}
				return percentage > 0 ? 'success' : 'danger';
			};

			setSearchConsole( {
				clicksData: [
					{
						label: __( 'Clicks', 'surerank' ),
						value: clicks?.current,
						previous: clicks?.previous,
						percentage: clicks?.percentage,
						percentageType: getPercentageType( clicks?.percentage ),
						color: 'bg-sky-500',
					},
					{
						label: __( 'Impressions', 'surerank' ),
						value: impressions?.current,
						percentage: impressions?.percentage,
						previous: impressions?.previous,
						percentageType: getPercentageType(
							impressions?.percentage
						),
						color: 'bg-background-brand',
					},
				],
			} );
		} catch ( error ) {
			toast.error( error.message );
		}
	}, [] );

	const fetchSiteTraffic = useCallback( async ( from, to ) => {
		const formattedStartDate = formatToISOPreserveDate( from );
		const formattedEndDate = formatToISOPreserveDate( to );

		try {
			const response = await apiFetch( {
				path: addQueryArgs(
					'/surerank/v1/google-search-console/site-traffic',
					{
						startDate: formattedStartDate,
						endDate: formattedEndDate,
					}
				),
				method: 'GET',
			} );
			if ( ! response.success ) {
				throw new Error(
					response.message ??
						__( 'Failed to fetch site traffic', 'surerank' )
				);
			}
			setSearchConsole( {
				siteTraffic: response?.data?.length
					? response?.data?.map( ( item ) => ( {
							...item,
							readableDate: formatDateRange(
								item.date,
								from,
								to
							),
					  } ) )
					: [],
			} );
		} catch ( error ) {
			toast.error( error.message );
		}
	}, [] );

	const initiateAPICalls = useCallback( async () => {
		if ( ! authenticated || ! selectedSite ) {
			return;
		}
		setIsLoading( true );
		const { from, to } = getLastNDays( DEFAULT_DATE_RANGE );
		try {
			await fetchClicksAndImpressions( from, to );
			await fetchSiteTraffic( from, to );
			setSearchConsole( {
				siteTrafficFetchComplete: true,
			} );
		} catch ( error ) {
			// do nothing
		} finally {
			setIsLoading( false );
		}
	}, [
		authenticated,
		selectedSite,
		fetchClicksAndImpressions,
		fetchSiteTraffic,
	] );

	const handleChangeSite = () => {
		toggleSiteSelectorModal();
	};

	const handleDisconnect = () => {
		setConfirmationModal( {
			open: true,
			title: __( 'Disconnect Search Console Account', 'surerank' ),
			description: __(
				'Are you sure you want to disconnect your Search Console account from SureRank?',
				'surerank'
			),
			onConfirm: handleDisconnectConfirm,
			confirmButtonText: __( 'Disconnect', 'surerank' ),
		} );
	};
	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( {
					siteTrafficFetchComplete: false,
				} );
			}

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

	const handleOpenAuthURL = () => {
		window.open( authURL, '_self', 'noopener,noreferrer' );
	};

	const emptyStateProps = useMemo(
		() =>
			! authenticated
				? {
						imageSrc: `${ surerank_globals.admin_assets_url }/images/search-console.svg`,
						title: __(
							'Let’s connect to Search Console to Optimize further!',
							'surerank'
						),
						description: __(
							'Link your website to Google Search Console to access detailed search analytics, track performance, and optimize your site for better search rankings.',
							'surerank'
						),
						actionButtonText: __(
							'Connect to Search Console - It’s Free',
							'surerank'
						),
						onClickActionBtn: handleOpenAuthURL,
				  }
				: {
						imageSrc: `${ surerank_globals.admin_assets_url }/images/search-console.svg`,
						title: __(
							'Select a Site to View Analytics',
							'surerank'
						),
						description: __(
							'Select a site to access detailed search analytics, track performance metrics, and boost your visibility in search results effectively.',
							'surerank'
						),
						actionButtonText: __( 'Select a Site', 'surerank' ),
						onClickActionBtn: toggleSiteSelectorModal,
				  },
		[ authenticated ]
	);

	let renderContent = null;
	if ( ! authenticated || ! hasSiteSelected ) {
		renderContent = (
			<EmptyState
				imageSrc={ emptyStateProps.imageSrc }
				title={ emptyStateProps.title }
				description={ emptyStateProps.description }
				actionButtonText={ emptyStateProps.actionButtonText }
				onClickActionBtn={ emptyStateProps.onClickActionBtn }
			/>
		);
	} else {
		renderContent = (
			<Container className="p-1 rounded-lg bg-background-secondary gap-1 flex-wrap md:flex-nowrap">
				<div className="w-full rounded-md bg-background-primary shadow-sm">
					{ isLoading && (
						<Skeleton
							variant="rectangular"
							className="w-full h-[288px]"
						/>
					) }
					{ ! isLoading && siteTraffic.length === 0 && (
						<Container
							gap="md"
							direction="column"
							align="center"
							justify="center"
							className="h-[288px] p-8 gap-2"
						>
							<Text
								size={ 14 }
								weight={ 600 }
								className="text-center"
								color="primary"
							>
								{ __( 'No data available', 'surerank' ) }
							</Text>
							<Text
								size={ 14 }
								weight={ 400 }
								color="tertiary"
								className="text-center max-w-md"
							>
								{ __(
									'Search Console data might take up to 30 days to appear for newly added sites. Please check back later.',
									'surerank'
								) }
							</Text>
						</Container>
					) }
					{ ! isLoading && siteTraffic.length > 0 && (
						<LineChart
							colors={ [
								{
									stroke: '#4B3BED',
								},
								{
									stroke: '#38BDF8',
								},
							] }
							yAxisFontColor={ [ '#4B3BED', '#38BDF8' ] }
							data={ siteTraffic }
							dataKeys={ [ 'impressions', 'clicks' ] }
							showTooltip
							showXAxis={ true }
							showYAxis={ true }
							biaxial
							tooltipIndicator="dot"
							variant="gradient"
							xAxisDataKey="readableDate"
							yAxisTickFormatter={ ( value ) =>
								formatNumber( value )
							}
							showLegend={ false }
							chartHeight={ 288 }
							chartWidth="100%"
							lineChartWrapperProps={ {
								margin: {
									top: 25,
									right: 10,
									bottom: 25,
									left: 10,
								},
							} }
						/>
					) }
				</div>
				<Container
					className="w-full md:w-[30%] gap-1 flex-row md:flex-col"
					align="stretch"
				>
					{ clicksData.map( ( item ) => (
						<ClicksAndImpressions
							key={ item.label }
							item={ item }
							isLoading={ isLoading }
						/>
					) ) }
				</Container>
			</Container>
		);
	}

	const updateURL = ( site ) => {
		let url = site ?? '';
		if ( url.includes( 'sc-domain:' ) ) {
			url = url.replace( /sc-domain:/, '' );
		}
		if ( ! url.includes( 'https://' ) && ! url.includes( 'http://' ) ) {
			url = `https://${ url }`;
		}
		return 'Site: ' + url;
	};

	return (
		<Section>
			<Container
				gap="none"
				justify="between"
				align="center"
				className="p-1"
			>
				<div className="flex items-center gap-3">
					<Title
						title={ __( 'Site Search Traffic', 'surerank' ) }
						tag="h4"
						size="md"
					/>
					{ selectedSite && (
						<Text size={ 16 } weight={ 400 } color="secondary">
							{ __( '(Last 90 days)', 'surerank' ) }
						</Text>
					) }
				</div>
				<Container
					gap="xs"
					justify="between"
					align="center"
					className="p-1"
				>
					{ selectedSite && (
						<span
							role="button"
							tabIndex={ 0 }
							onClick={ handleChangeSite }
							onKeyDown={ ( event ) => {
								if (
									event.key === 'Enter' ||
									event.key === ' '
								) {
									handleChangeSite();
								}
							} }
							className="focus:outline-none"
						>
							<Badge
								size="md"
								label={ updateURL( selectedSite ) }
								className="cursor-pointer"
							/>
						</span>
					) }

					{ authenticated && (
						<span
							role="button"
							tabIndex={ 0 }
							onClick={ handleDisconnect }
							onKeyDown={ ( event ) => {
								if (
									event.key === 'Enter' ||
									event.key === ' '
								) {
									handleDisconnect();
								}
							} }
							className="focus:outline-none"
						>
							<Badge
								size="md"
								label={ __( 'Disconnect', 'surerank' ) }
								icon={ <LogOut /> }
								iconPosition="left"
								className="cursor-pointer pl-2"
							/>
						</span>
					) }
				</Container>
			</Container>
			{ renderContent }
		</Section>
	);
};

export default SiteSearchTraffic;