| Current File : //home/abedqjib/krishanfoundation.com/wp-content/plugins/surerank/src/functions/router.js |
import {
createRoute as createRouteTanstack,
createRouter,
createHashHistory,
RouterProvider,
ErrorComponent,
Navigate,
createRootRouteWithContext,
} from '@tanstack/react-router';
// Layout component.
import SidebarLayout from '@AdminComponents/layout/sidebar-layout';
import { SureRankLogo } from '@/global/components/icons';
import currentUserCan from './role-capabilities';
/**
* @typedef {Object} RouteConfig
* @property {string} path - The path of the route
* @property {import('react').ComponentType} component - The component to render for this route
* @property {Function} [loader] - Optional data loader function for the route
* @property {Function} [action] - Optional action function for the route
* @property {import('react').ComponentType} [errorComponent] - Optional error boundary component
* @property {RouteConfig[]} [children] - Optional child routes
* @property {Object} [meta] - Optional metadata for the route
* @property {import('react').ComponentType} [layout] - Optional custom layout component for this route
* @property {Object} [layoutProps] - Optional props to pass to the layout component
*/
/**
* @typedef {Object} NavLink
* @property {string} path - The path of the link
* @property {string} label - The label text to display
* @property {import('react').ComponentType} [icon] - Optional icon component
* @property {NavLink[]} [children] - Optional child navigation links
*/
/**
* @typedef {Object} LayoutConfig
* @property {import('react').ComponentType} component - The layout component to use
* @property {Object} [props] - Props to pass to the layout component
* @property {NavLink[]} [props.navLinks] - Navigation links for the layout
* @property {boolean} [props.navbarOnly] - Whether to show only the navbar
*/
/**
* Loading spinner component for pending states
* @return {JSX.Element} Loading spinner component
*/
const LoadingSpinner = () => (
<div className="flex items-center justify-center p-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-800" />
</div>
);
/**
* Creates nested routes recursively
* @param {RouteConfig} routeConfig - The route configuration
* @param {Object} parentRoute - The parent route object
* @param {string} completePath - The complete path of the route
* @param {Object} defaultLayout - The default layout configuration
* @return {Object} The created route
*/
const createNestedRoutes = (
routeConfig,
parentRoute,
completePath = '',
defaultLayout
) => {
const RouteComponent = routeConfig.layout
? () => {
const Layout = routeConfig.layout;
const Component = routeConfig.component;
const layoutProps = {
...defaultLayout.props,
...routeConfig.layoutProps,
};
return (
<Layout { ...layoutProps }>
<Component />
</Layout>
);
}
: routeConfig.component;
const hasChildren = !! routeConfig.children?.length;
const nestedRoutes = hasChildren ? [ ...routeConfig.children ] : [];
if ( ! RouteComponent && hasChildren ) {
completePath += routeConfig.path;
const hasIndexRoute = nestedRoutes.some(
( child ) => child.path === '/'
);
if ( ! hasIndexRoute ) {
const firstChild = nestedRoutes[ 0 ];
const pathToFirstChild = `${ completePath }${ firstChild.path }`;
nestedRoutes.push( {
path: '/',
component: () => <Navigate to={ pathToFirstChild } replace />,
} );
}
}
let route;
const loader = routeConfig.loader ? () => routeConfig.loader : () => void 0;
if ( routeConfig?.component && 'loader' in routeConfig?.component ) {
route = createRouteTanstack( {
getParentRoute: () => parentRoute,
path: routeConfig.path,
errorComponent: routeConfig.errorComponent,
pendingComponent: () => <PendingComponent />,
loader,
} ).lazy( routeConfig?.component?.loader );
} else {
route = createRouteTanstack( {
getParentRoute: () => parentRoute,
path: routeConfig.path,
component: RouteComponent,
errorComponent: routeConfig.errorComponent,
loader,
action: routeConfig.action,
meta: routeConfig.meta,
validateSearch: routeConfig.validateSearch,
} );
}
if ( hasChildren ) {
const childRoutes = nestedRoutes.map( ( childConfig ) =>
createNestedRoutes(
childConfig,
route,
completePath,
defaultLayout
)
);
route.addChildren( childRoutes );
}
return route;
};
/**
* Creates router configuration
* @param {Object} routeTree - The route tree
* @return {Object} Router configuration
*/
const createRouterConfig = ( routeTree ) => ( {
routeTree,
history: createHashHistory(),
defaultPendingComponent: LoadingSpinner,
defaultPendingMs: 1000,
defaultErrorComponent: ErrorComponent,
} );
/**
* Creates an admin router with enhanced features.
*
* @param {Object} options - The options for creating the admin router
* @param {RouteConfig[]} options.routes - The routes configuration
* @param {NavLink[]} [options.navLinks] - Optional navigation links
* @param {import('react').ComponentType} [options.notFoundComponent] - Optional 404 page component
* @param {import('react').ComponentType} [options.defaultErrorComponent] - Optional default error boundary component
* @param {LayoutConfig} [options.defaultLayout] - Optional default layout configuration
* @return {Function|null} The router component or null if no routes
*/
const createAdminRouter = ( {
routes = [],
navLinks = [],
notFoundComponent,
defaultErrorComponent,
defaultLayout = {
component: SidebarLayout,
props: {},
},
} ) => {
if ( ! routes?.length ) {
return null;
}
// Create Root route with error boundary
const rootRoute = createRootRouteWithContext()( {
component: () => {
const Layout = defaultLayout.component;
return (
<Layout
routes={ routes }
navLinks={ navLinks }
{ ...defaultLayout.props }
/>
);
},
errorComponent: defaultErrorComponent,
notFoundComponent,
loader: () => void 0,
} );
// Create routes with nested structure
const createdRoutes = routes.map( ( route ) =>
createNestedRoutes( route, rootRoute, '', defaultLayout )
);
// Add routes to the root route
const routeTree = rootRoute.addChildren( createdRoutes );
// Create router with configuration
const router = createRouter( createRouterConfig( routeTree ) );
// Router State provider component
return () => <RouterProvider router={ router } />;
};
/**
* Creates a route configuration object
* @param {string} path - Route path
* @param {Component} component - Route component
* @param {Array} children - Optional child routes
* @param {Object} options - Optional options
* @return {Object|undefined} Route configuration object
*/
export const createRoute = (
path,
component,
children = null,
options = {}
) => {
// Handle - if capability is not allowed, return undefined
if ( options?.capability && ! currentUserCan( options?.capability ) ) {
return;
}
// Handle - if third param is array, treat as children example : Advanced Settings
if ( Array.isArray( children ) ) {
return {
path,
component,
children,
...options,
};
}
// Handle new options format
return {
path,
component,
children,
...options,
};
};
/**
* Creates a child route configuration object
* @param {string} path - Route path
* @param {Component} component - Route component
* @param {Array} children - Optional nested child routes
* @param {Object} options - Optional options
* @return {Object|undefined} Child route configuration object
*/
export const createChildRoute = (
path,
component,
children = null,
options = {}
) => {
// Handle - if capability is not allowed, return undefined
if ( options?.capability && ! currentUserCan( options?.capability ) ) {
return;
}
// Handle - if third param is array, treat as children
if ( Array.isArray( children ) ) {
return {
path,
component,
children,
...options,
};
}
// Handle new options format
return {
path,
component,
children,
...options,
};
};
/**
* Recursively filters out falsy values from routes array and children
* @param {RouteConfig[]} routes - Array of route configurations
* @return {RouteConfig[]} Filtered routes array without falsy values
*/
export const filterFalsyRoutes = ( routes ) => {
if ( ! Array.isArray( routes ) ) {
return [];
}
return routes
.filter( ( route ) => !! route ) // Filter out falsy values
.map( ( route ) => {
// If route has children, recursively filter them
if ( route.children?.length ) {
return {
...route,
children: filterFalsyRoutes( route.children ),
};
}
return route;
} );
};
/**
* Builds a Set of all available route paths from the routes array
* @param {RouteConfig[]} routes - Array of route configurations
* @return {Set<string>} Set of all route paths
*/
export const buildRoutePathMap = ( routes ) => {
const paths = new Set();
const extractPaths = ( routeArray, basePath = '' ) => {
routeArray.forEach( ( route ) => {
if ( ! route ) {
return;
}
const fullPath = basePath + route.path;
paths.add( fullPath );
// Recursively extract child route paths
if ( route.children?.length ) {
extractPaths( route.children, fullPath );
}
} );
};
extractPaths( routes );
return paths;
};
/**
* Filters navigation links to only include links that have corresponding routes
* @param {Array} navLinks - Array of navigation link sections
* @param {Set<string>} routePaths - Set of available route paths
* @return {Array} Filtered navigation links
*/
export const filterNavLinksByRoutes = ( navLinks, routePaths ) => {
const filteredSections = [];
navLinks.forEach( ( section ) => {
if ( ! section.links ) {
return;
}
const filteredLinks = [];
section.links.forEach( ( link ) => {
// Check if the link's path exists in available routes
if ( routePaths.has( link.path ) ) {
const filteredLink = { ...link };
// Recursively filter submenu items
if ( link.submenu?.length ) {
const filteredSubmenu = link.submenu.filter(
( submenuItem ) => routePaths.has( submenuItem.path )
);
// Only include the link if it has submenu items after filtering
if ( filteredSubmenu.length > 0 ) {
filteredLink.submenu = filteredSubmenu;
filteredLinks.push( filteredLink );
}
} else {
filteredLinks.push( filteredLink );
}
}
} );
// Only include the section if it has links after filtering
if ( filteredLinks.length > 0 ) {
filteredSections.push( {
...section,
links: filteredLinks,
} );
}
} );
return filteredSections;
};
export default createAdminRouter;
const PendingComponent = () => {
return (
<div className="w-full h-full flex items-center justify-center p-4">
<div className="h-96 flex-grow flex items-center justify-center w-full">
<SureRankLogo className="w-10 h-10 animate-ping" />
</div>
</div>
);
};