File "schema.js"

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

import { useEffect, useState, useMemo } from '@wordpress/element';
import PageContentWrapper from '@AdminComponents/page-content-wrapper';
import { __ } from '@wordpress/i18n';
import { Button, Container, Table, Tooltip } from '@bsf/force-ui';
import withSuspense from '@AdminComponents/hoc/with-suspense';
import { useSuspenseSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from '@AdminStore/constants';
import EditSchema from './edit';
import { Edit, Trash } from 'lucide-react';
import {
	generateUUID,
	isSchemaTypeValid,
} from '@AdminComponents/schema-utils/utils';
import Modal from './modal';
import { SaveSettingsButton } from '@/apps/admin-components/global-save-button';
import { createLazyRoute } from '@tanstack/react-router';

// Schema categories
const SCHEMA_CATEGORIES = {
	global: { value: 'global', label: __( 'Global Level', 'surerank' ) },
	content: { value: 'content', label: __( 'Content Level', 'surerank' ) },
};

// Schema categorization mapping
const SCHEMA_CATEGORY_MAP = {
	WebSite: 'global',
	WebPage: 'global',
	Organization: 'global',
	SearchAction: 'global',
	Person: 'global',
	BreadcrumbList: 'content',
	Article: 'content',
	Product: 'content',
	Dataset: 'content',
	FAQ: 'content',
	'Fact Check': 'content',
	HowTo: 'content',
	Movie: 'content',
	'Podcast Episode': 'content',
	Book: 'content',
	Course: 'content',
	Event: 'content',
	'Job Posting': 'content',
	Recipe: 'content',
	Service: 'content',
	'Software app': 'content',
	Video: 'content',
};

const Schema = () => {
	const { metaSettings } = useSuspenseSelect( ( select ) => {
		const { getMetaSettings } = select( STORE_NAME );
		return {
			metaSettings: getMetaSettings(),
		};
	}, [] );

	const { setMetaSetting, invalidateResolutionForStoreSelector } =
		useDispatch( STORE_NAME );

	const schemaData = metaSettings?.schemas || {};
	const schemaArray = Object.entries( schemaData ).map(
		( [ id, schema ] ) => ( {
			id,
			...schema,
		} )
	);

	const schemaTypeOptions = surerank_globals?.schema_type_options || {};
	const schemaTypeData = surerank_globals?.schema_type_data || {};

	// Categorize schemas using useMemo and reduce, filtering out schemas not in schemaTypeData
	const categorizedSchemas = useMemo( () => {
		return schemaArray
			.filter( ( schema ) => {
				// Only include schemas that exist in schemaTypeData
				return isSchemaTypeValid( schema?.title );
			} )
			.reduce(
				( acc, schema ) => {
					const type = schema?.type;
					const category =
						SCHEMA_CATEGORY_MAP[ type ] ||
						SCHEMA_CATEGORIES.content.value; // Default to content level

					if ( ! acc[ category ] ) {
						acc[ category ] = [];
					}
					acc[ category ].push( schema );
					return acc;
				},
				{
					[ SCHEMA_CATEGORIES.global.value ]: [],
					[ SCHEMA_CATEGORIES.content.value ]: [],
				}
			);
	}, [ schemaArray, schemaTypeData ] );

	// console.log( 'Schema Array:', schemaArray );

	// console.log( 'schemaArray', schemaArray );
	const defaultSchemasObject = surerank_globals?.default_schemas || {};
	const defaultSchemas = Object.entries( defaultSchemasObject ).map(
		( [ id, schema ] ) => ( {
			id,
			...schema,
		} )
	);

	const [ isModalOpen, setIsModalOpen ] = useState( false );
	const [ showEditSchema, setShowEditSchema ] = useState( false );
	const [ selectedSchema, setSelectedSchema ] = useState( '' );
	const [ selectedType, setSelectedType ] = useState( '' );
	const [ uniqueId, setUniqueId ] = useState( '' );
	const [ confirmDelete, setConfirmDelete ] = useState( null );

	const closeModal = () => setIsModalOpen( false );

	const handleBackToSchemas = () => {
		setShowEditSchema( false );
	};

	const handleEditSchema = ( schemaId ) => {
		const schemaToEdit = schemaData[ schemaId ];

		setUniqueId( schemaId );
		setSelectedSchema( schemaToEdit.title );
		setSelectedType( schemaToEdit.type );
		setShowEditSchema( true );
	};

	const handleAddSchema = () => {
		const schemaUniqueId = generateUUID();
		const newSchema = {
			title: selectedSchema || '',
			type: selectedType || '',
			show_on: {
				rules: [],
				specific: [],
				specificText: [],
			},
			fields: {
				'@type': selectedType || '',
			},
		};

		setMetaSetting( 'schemas', {
			...schemaData,
			[ schemaUniqueId ]: newSchema,
		} );
		setUniqueId( schemaUniqueId );
		setSelectedSchema( newSchema.title );
		setShowEditSchema( true );
	};

	const handleDeleteSchema = ( schemaId ) => {
		const updatedSchemas = { ...schemaData };
		delete updatedSchemas[ schemaId ];
		setMetaSetting( 'schemas', updatedSchemas );
		setConfirmDelete( null );
	};

	useEffect( () => {
		// Invalidate the resolver to ensure fresh data on next read
		invalidateResolutionForStoreSelector( 'getMetaSettings', [] );
	}, [] );

	// Render table for a specific category
	const renderSchemaTable = ( schemas, title ) => (
		<div
			key={ title }
			className="[&>div]:border-0 [&>div]:overflow-visible"
		>
			<h3 className="text-base font-semibold mb-4 text-text-primary">
				{ title }
			</h3>
			<Table className="w-full">
				<Table.Head className="border-0 [clip-path:inset(0_0_0_0_round_6px)]">
					<Table.HeadCell>
						{ __( 'Schema Title', 'surerank' ) }
					</Table.HeadCell>
					<Table.HeadCell>
						{ __( 'Schema Type', 'surerank' ) }
					</Table.HeadCell>
					<Table.HeadCell className="text-right">
						{ __( 'Actions', 'surerank' ) }
					</Table.HeadCell>
				</Table.Head>
				<Table.Body>
					{ schemas.length === 0 ? (
						<Table.Row>
							<Table.Cell
								colSpan={ 3 }
								className="text-center text-gray-500 py-4"
							>
								{ __(
									'No schemas found in this category.',
									'surerank'
								) }
							</Table.Cell>
						</Table.Row>
					) : (
						schemas.map( ( schema ) => (
							<Table.Row
								key={ schema.id }
								className="last:!border-b-0.5 last:border-x-0 last:border-t-0 last:border-solid last:border-border-subtle"
							>
								<Table.Cell className="p-3">
									<span className="text-sm">
										{ schema?.fields?.schema_name ||
											schema?.title }
									</span>
								</Table.Cell>
								<Table.Cell className="p-3">
									<span className="text-sm">
										{ schema?.fields?.[ '@type' ] ||
											schema?.type }
									</span>
								</Table.Cell>
								<Table.Cell className="p-3 leading-none">
									<div className="flex items-center justify-end gap-2">
										<Button
											variant="ghost"
											size="xs"
											icon={
												<Edit
													aria-label="icon"
													role="img"
												/>
											}
											className="text-text-secondary hover:text-icon-primary"
											onClick={ () =>
												handleEditSchema( schema.id )
											}
										/>
										<Tooltip
											open={ confirmDelete === schema.id }
											setOpen={ () =>
												setConfirmDelete( schema.id )
											}
											variant="light"
											placement="bottom"
											tooltipPortalId="surerank-root"
											className="p-2 border border-solid border-border-subtle [&>svg>path]:stroke-border-subtle z-[99999]"
											interactive
											arrow
											content={
												<div className="space-x-2">
													<Button
														size="xs"
														variant="ghost"
														className="focus:[box-shadow:none]"
														onClick={ () =>
															setConfirmDelete(
																null
															)
														}
													>
														{ __(
															'Cancel',
															'surerank'
														) }
													</Button>
													<Button
														size="xs"
														className="focus:[box-shadow:none] bg-button-danger hover:bg-button-danger-hover outline-button-danger hover:outline-button-danger-hover"
														onClick={ () =>
															handleDeleteSchema(
																schema.id
															)
														}
													>
														{ __(
															'Remove',
															'surerank'
														) }
													</Button>
												</div>
											}
										>
											<Button
												size="xs"
												variant="ghost"
												className="p-0 text-text-secondary inline-flex rounded-sm focus:[box-shadow:none]"
												icon={ <Trash /> }
												onClick={ () =>
													setConfirmDelete(
														schema.id
													)
												}
											/>
										</Tooltip>
									</div>
								</Table.Cell>
							</Table.Row>
						) )
					) }
				</Table.Body>
			</Table>
		</div>
	);

	return showEditSchema ? (
		<EditSchema
			schema={ selectedSchema }
			type={ selectedType }
			onBack={ handleBackToSchemas }
			setMetaSetting={ setMetaSetting }
			schemaId={ uniqueId }
			metaSettings={ metaSettings }
		/>
	) : (
		<PageContentWrapper
			title={ __( 'Schema', 'surerank' ) }
			description={ __(
				'Adds structured data to your content so search engines can better understand and present it. Most fields are already filled in to make setup easier and help your site show up better in search results.',
				'surerank'
			) }
		>
			<div className="flex flex-col items-start p-4 gap-6 bg-white shadow-sm rounded-xl">
				{ renderSchemaTable(
					categorizedSchemas[ SCHEMA_CATEGORIES.global.value ],
					SCHEMA_CATEGORIES.global.label
				) }
				{ renderSchemaTable(
					categorizedSchemas[ SCHEMA_CATEGORIES.content.value ],
					SCHEMA_CATEGORIES.content.label
				) }
				<Container className="py-2 px-0" gap="sm">
					<SaveSettingsButton />
					<Modal
						selectedSchema={ selectedSchema }
						setSelectedSchema={ setSelectedSchema }
						selectedType={ selectedType }
						setSelectedType={ setSelectedType }
						schemaTypeOptions={ schemaTypeOptions }
						defaultSchemas={ defaultSchemas }
						handleAddSchema={ handleAddSchema }
						isModalOpen={ isModalOpen }
						closeModal={ closeModal }
					/>
				</Container>
			</div>
		</PageContentWrapper>
	);
};

export const LazyRoute = createLazyRoute( '/advanced/schema' )( {
	component: withSuspense( Schema ),
} );

export default withSuspense( Schema );