File "sidebar-layout.js"

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

import { __ } from '@wordpress/i18n';
import {
	Outlet,
	Link,
	useMatchRoute,
	useLocation,
	useNavigate,
} from '@tanstack/react-router';
import {
	Badge,
	Topbar,
	Sidebar,
	Accordion,
	HamburgerMenu,
	Button,
	Skeleton,
} from '@bsf/force-ui';
import {
	BookOpenText,
	Megaphone,
	ChartNoAxesColumnIncreasing,
} from 'lucide-react';
import '@Global/style.scss';
import withSuspense from '@AdminComponents/hoc/with-suspense';
import SidebarSkeleton from '../sidebar-skeleton';
import { cn, getSeoCheckLabel } from '@Functions/utils';
import useWhatsNewRSS from '../../../../lib/useWhatsNewRSS';
import {
	renderToString,
	Suspense,
	useLayoutEffect,
	Fragment,
	useMemo,
	useEffect,
} from '@wordpress/element';
import GlobalSearch from '@AdminComponents/global-search';
import ConfirmationDialog from '@AdminComponents/confirmation-dialog';
import { useSuspenseSiteSeoAnalysis } from '@/apps/admin-dashboard/site-seo-checks/site-seo-checks-main';
import { getSeverityColor } from '@GlobalComponents/seo-checks';
import Logo from '@AdminComponents/logo';
import { Tooltip } from '@AdminComponents/tooltip';
import TanStackRouterDevtools from '@AdminComponents/tanstack-router-dev-tools';
import '@AdminStore/store';

const { version } = surerank_globals;

const NavLink = ( { path, children } ) => {
	const matchRoute = useMatchRoute();
	const isActive = matchRoute( { to: path } );

	return (
		<Link
			to={ path }
			className={ cn(
				'flex items-center justify-start gap-2.5 py-2 pl-2.5 pr-2 text-text-secondary [&_svg]:text-icon-secondary hover:bg-background-secondary rounded-md text-base font-normal no-underline cursor-pointer focus:outline-none focus:shadow-none transition ease-in-out duration-150 [&_svg]:size-5',
				isActive &&
					'bg-background-secondary text-text-primary [&_svg]:text-brand-800'
			) }
			role="menuitem"
			tabIndex={ 0 }
		>
			{ children }
		</Link>
	);
};

const SiteSeoAnalysisBadge = () => {
	const [ { report } ] = useSuspenseSiteSeoAnalysis();

	// Check counts of error, warning and success
	const counts = useMemo(
		() =>
			Object.values( report ).reduce(
				( acc, curr ) => {
					//if ignore is true, then it is ignored
					if ( curr.ignore ) {
						acc.ignored++;
					} else {
						acc[ curr.status ]++;
					}
					return acc;
				},
				{ error: 0, warning: 0, success: 0, ignored: 0 }
			),
		[ report ]
	);

	const selectedType =
		( counts.error && 'error' ) ||
		( counts.warning && 'warning' ) ||
		'success';

	const isDashboard = () => {
		const url = new URL( window.location.href );
		const page = url.searchParams.get( 'page' );
		return page === 'surerank';
	};

	// Add/update the badge in the WP sidebar.
	useEffect( () => {
		// WP sidebar element.
		const sidebarMenu = document.querySelector(
			'#toplevel_page_surerank > a > div.wp-menu-name'
		);
		if ( ! sidebarMenu ) {
			return;
		}

		// Check if the badge is already added.
		const notificationBadge = sidebarMenu.querySelector( '.awaiting-mod' );
		if ( notificationBadge ) {
			notificationBadge.className =
				counts.error > 0 ? 'awaiting-mod' : '';
			notificationBadge.textContent =
				counts.error > 0 ? counts.error : '';
			return;
		}

		// Add space after the menu name if not already present.
		if ( ! sidebarMenu.textContent.endsWith( ' ' ) ) {
			sidebarMenu.textContent += ' ';
		}

		// Create and add the badge.
		const badge = document.createElement( 'span' );
		badge.className = counts.error > 0 ? 'awaiting-mod' : '';
		badge.textContent = counts.error > 0 ? counts.error : '';
		sidebarMenu.appendChild( badge );
	}, [ counts ] );

	return (
		<Link
			className="no-underline hover:no-underline focus:no-underline focus:[box-shadow:none]"
			to={
				isDashboard()
					? '/site-seo-analysis'
					: `${ surerank_globals.wp_dashboard_url }?page=surerank#/site-seo-analysis`
			}
		>
			<Badge
				icon={ <ChartNoAxesColumnIncreasing /> }
				label={ getSeoCheckLabel(
					selectedType,
					counts.error || counts.warning || counts.success
				) }
				variant={ getSeverityColor( selectedType ) }
			/>
		</Link>
	);
};

const SubmenuAccordion = ( { label, icon: Icon, submenu } ) => {
	const navigate = useNavigate();

	return (
		<Accordion defaultValue="item1" iconType="arrow" type="simple">
			<Accordion.Item value="item1">
				<Accordion.Trigger
					iconType="arrow"
					collapsible={ false }
					className="p-2 pl-2.5 text-base font-normal [&_svg]:text-icon-secondary hover:bg-background-primary rounded-md no-underline cursor-pointer focus:outline-none focus:shadow-none transition ease-in-out duration-150 [&_svg]:size-5 [&_div]:font-normal [&_div]:text-text-primary"
					aria-label={ `${ label } submenu` }
					onClick={ ( event ) => {
						event.preventDefault();
						event.stopPropagation();

						if ( submenu?.length <= 0 || ! submenu[ 0 ]?.path ) {
							return;
						}

						// Redirect to the first item in the submenu.
						navigate( {
							to: submenu[ 0 ].path,
						} );
					} }
				>
					{ Icon && <Icon className="size-4" /> }
					{ label }
				</Accordion.Trigger>
				<Accordion.Content className="p-2 [&>div]:pb-0">
					<div
						className="border-l border-solid border-r-0 border-t-0 border-b-0 border-border-subtle pl-2 ml-1 space-y-0.5"
						role="menu"
					>
						{ submenu.map(
							( { path, label: subLabel, icon: SubIcon } ) => (
								<NavLink key={ path } path={ path }>
									{ SubIcon && (
										<SubIcon className="size-4" />
									) }
									{ subLabel }
								</NavLink>
							)
						) }
					</div>
				</Accordion.Content>
			</Accordion.Item>
		</Accordion>
	);
};

const SidebarSection = ( { section, links } ) => {
	if ( ! links?.length ) {
		return null;
	}

	return (
		<Sidebar.Item
			key={ section }
			arrow
			heading={ section }
			open={ true }
			className="space-y-0.5"
		>
			{ links.map( ( { path, label, icon: Icon, submenu } ) =>
				submenu ? (
					<SubmenuAccordion
						key={ path || label }
						label={ label }
						icon={ Icon }
						submenu={ submenu }
					/>
				) : (
					<NavLink key={ path } path={ path }>
						{ Icon && <Icon className="size-4" /> }
						{ label }
					</NavLink>
				)
			) }
		</Sidebar.Item>
	);
};

const SidebarNavigation = ( { navLinks = [] } ) => {
	return (
		<div className="h-full w-full">
			<Sidebar borderOn className="!h-full w-full p-4">
				<Sidebar.Body>
					<Sidebar.Item
						role="navigation"
						aria-label="Main Navigation"
					>
						{ navLinks.map(
							( { section, links, path } ) =>
								! path &&
								links?.length > 0 && (
									<SidebarSection
										key={ section }
										section={ section }
										links={ links }
									/>
								)
						) }
					</Sidebar.Item>
				</Sidebar.Body>
			</Sidebar>
		</div>
	);
};

const SuspenseNavbar = withSuspense( SidebarNavigation, SidebarSkeleton );

const useNavbarLinks = ( navLinks ) => {
	const matchRoute = useMatchRoute();

	const activeSection = navLinks.find( ( { links = [] } ) =>
		links.some( ( { path, submenu = null } ) => {
			if ( submenu ) {
				return submenu.some( ( { path: subPath } ) =>
					matchRoute( { to: subPath } )
				);
			}
			return matchRoute( { to: path } );
		} )
	);

	const reConstructedNavLinks = navLinks.reduce( ( acc, curr ) => {
		acc.push( {
			label: curr.section,
			path: curr.links[ 0 ].path,
			active: curr.sectionId === activeSection?.sectionId,
		} );
		return acc;
	}, [] );

	return {
		activeSection,
		navbarLinks: reConstructedNavLinks,
	};
};

const useRouteConfig = ( routes ) => {
	const location = useLocation();
	const currentPath = location.pathname;

	// Function to recursively search for route configuration
	const findRouteConfig = ( routesList, path, parentPath = '' ) => {
		for ( const route of routesList ) {
			// Build the full path by combining parent path with current route path
			const fullPath = parentPath + route.path;
			// Check if this route matches the current path
			if ( fullPath === path ) {
				return route;
			}

			// Check child routes recursively
			if ( route.children ) {
				const childResult = findRouteConfig(
					route.children,
					path,
					fullPath
				);
				if ( childResult ) {
					return childResult;
				}
			}
		}
		return null;
	};

	const currentRoute = findRouteConfig( routes, currentPath );
	return {
		isNavbarOnly: currentRoute?.navbarOnly || false,
		isFullWidth: currentRoute?.fullWidth || false,
	};
};

const SidebarLayout = ( {
	navLinks = [],
	routes = [],
	navbarOnly = false,
} ) => {
	const { activeSection, navbarLinks: topNavbarLinks } =
		useNavbarLinks( navLinks );
	const location = useLocation();
	// Get route configuration using the unified hook
	const { isNavbarOnly: routeNavbarOnly, isFullWidth } =
		useRouteConfig( routes );
	const isNavbarOnly = routeNavbarOnly || navbarOnly;

	// Use only the links of the active section
	const filteredNavLinks = activeSection ? [ activeSection ] : [];

	useWhatsNewRSS( {
		uniqueKey: 'surerank',
		rssFeedURL: 'https://surerank.com/whats-new/feed/', // TODO: domain name change to surerank.
		selector: '#surerank_whats_new',
		flyout: {
			title: __( "What's New?", 'surerank' ),
		},
		triggerButton: {
			icon: renderToString(
				<Megaphone
					className="size-4 m-1 text-icon-primary"
					strokeWidth={ 1.5 }
				/>
			),
		},
	} );

	// Manipulate the WP sidebar menu to make active menu item.
	useLayoutEffect( () => {
		const sidebarMenu = document.getElementById( 'toplevel_page_surerank' );
		if ( ! sidebarMenu ) {
			return;
		}

		const menuItems = sidebarMenu.querySelectorAll( 'a' );
		const currentPath = location.pathname.split( '/' )[ 1 ];

		// Remove current class from the currently active item
		const currentActiveItem = sidebarMenu.querySelector( '.current' );
		if ( currentActiveItem ) {
			currentActiveItem.classList.remove( 'current' );
		}
		Array.from( menuItems ).forEach( ( item ) => {
			const itemPath = item.href.split( '#' )[ 1 ]?.split( '/' )[ 1 ];

			if ( currentPath === 'dashboard' ) {
				// If path is 'dashboard', add current to the item with no path (undefined)
				if ( itemPath === undefined ) {
					item.parentElement.classList.add( 'current' );
				}
			}
			// Otherwise, add current to the matching path item
			if ( currentPath !== 'dashboard' && itemPath === currentPath ) {
				item.parentElement.classList.add( 'current' );
			}
		} );

		return () => {
			Array.from( menuItems ).forEach( ( item ) => {
				item.parentElement.classList.remove( 'current' );
			} );
		};
	}, [ location.pathname ] );

	return (
		<Fragment>
			<div className="grid max-[782px]:grid-rows-[64px_calc(100dvh_-_110px)] grid-rows-[64px_calc(100dvh_-_96px)] min-h-full bg-background-secondary">
				{ /* Header */ }
				<Topbar
					className="w-auto min-h-[unset] h-16 shadow-sm p-0 relative"
					gap={ 0 }
				>
					<Topbar.Left className="p-5">
						<Topbar.Item className="flex md:hidden">
							<HamburgerMenu className="lg:hidden">
								<HamburgerMenu.Toggle className="size-6" />
								<HamburgerMenu.Options>
									{ topNavbarLinks.map( ( option ) => (
										<HamburgerMenu.Option
											key={ option.label }
											to={ option.path }
											tag={ Link }
											active={ option.active }
										>
											{ option.label }
										</HamburgerMenu.Option>
									) ) }
								</HamburgerMenu.Options>
							</HamburgerMenu>
						</Topbar.Item>
						<Topbar.Item>
							<Logo />
						</Topbar.Item>
					</Topbar.Left>
					<Topbar.Middle align="left" className="h-full">
						<Topbar.Item className="h-full gap-4 hidden md:flex">
							{ topNavbarLinks.map(
								( { path, label, active } ) => (
									<Link
										key={ path }
										to={ path }
										className={ cn(
											'relative content-center no-underline h-full py-0 px-3 m-0 bg-transparent outline-none shadow-none border-0 focus:outline-none text-text-secondary text-sm font-medium cursor-pointer',
											active && 'text-text-primary'
										) }
									>
										{ label }
										{ active && (
											<span className="absolute bottom-0 left-0 w-full h-px bg-brand-800" />
										) }
									</Link>
								)
							) }
						</Topbar.Item>
					</Topbar.Middle>
					<Topbar.Right className="p-5">
						<Topbar.Item>
							<GlobalSearch navLinks={ navLinks } />
						</Topbar.Item>
						<Topbar.Item>
							<Badge
								label={ `V ${ version }` }
								size="xs"
								variant="neutral"
							/>
						</Topbar.Item>
						<Topbar.Item>
							<Suspense
								fallback={ <Skeleton className="w-36 h-6" /> }
							>
								<SiteSeoAnalysisBadge />
							</Suspense>
						</Topbar.Item>
						<Topbar.Item>
							<Tooltip
								content={ __( 'Knowledge Base', 'surerank' ) }
								placement="bottom"
								arrow
								className="z-[99999]"
							>
								<Button
									size="sm"
									tag="a"
									variant="link"
									className="text-text-primary focus:[box-shadow:none]"
									href={ surerank_globals?.help_link ?? '#' }
									target="_blank"
									rel="noreferrer noopener"
									aria-label={ __(
										'Knowledge Base',
										'surerank'
									) }
									icon={
										<BookOpenText
											className="size-4 m-1"
											strokeWidth="1.5"
										/>
									}
								/>
							</Tooltip>
						</Topbar.Item>
						<Topbar.Item>
							<div
								id="surerank_whats_new"
								className="[&>a]:p-0.5 [&>a]:pl-0"
							></div>
						</Topbar.Item>
					</Topbar.Right>
				</Topbar>
				{
					// Sidebar Navigation
					! isNavbarOnly && (
						<div className="w-full h-full grid grid-cols-[290px_1fr]">
							{ ! isNavbarOnly && (
								<SuspenseNavbar navLinks={ filteredNavLinks } />
							) }

							{ /* Main content */ }
							<div className="bg-background-secondary p-5">
								<main
									className={ cn(
										'mx-auto',
										isFullWidth ? 'w-full' : 'max-w-[768px]'
									) }
								>
									<Outlet />
								</main>
							</div>
						</div>
					)
				}
				{
					// Without sidebar navigation
					isNavbarOnly && (
						<div className="w-full h-fit bg-background-secondary">
							<main className="w-full h-full mx-auto relative">
								<Outlet />
							</main>
						</div>
					)
				}
				{ /* TanStack Router Devtools */ }
				{ process.env.NODE_ENV !== 'production' && (
					<Suspense>
						<TanStackRouterDevtools />
					</Suspense>
				) }
			</div>
			<ConfirmationDialog />
		</Fragment>
	);
};

export default SidebarLayout;