UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

375 lines (374 loc) 14.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.withCmsRouting = withCmsRouting; exports.createCmsRoutingFactory = createCmsRoutingFactory; exports.useCmsRoutingContext = useCmsRoutingContext; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = __importStar(require("react")); const router_1 = require("next/router"); const route_config_1 = require("../utils/route-config"); const cms_content_1 = require("../utils/cms-content"); const CmsRouteHandler_1 = require("./CmsRouteHandler"); // ============================================================================ // HOC IMPLEMENTATION // ============================================================================ /** * Higher-Order Component that wraps page components with CMS routing capabilities * * This HOC provides automatic CMS content handling for pages by injecting CMS-related * props into the wrapped component. The wrapped component maintains full control over * its rendering while receiving CMS state as props. * * @param WrappedComponent - The page component to wrap with CMS functionality * @param options - Configuration options for CMS behavior * @returns A new component with CMS routing capabilities * * @example * ```tsx * // Basic usage - component receives CMS props * const BlogPage = ({ title, cmsContent, isCmsRoute, cmsLoading }) => { * if (isCmsRoute && cmsLoading) { * return <div>Loading CMS content...</div>; * } * * if (isCmsRoute && cmsContent) { * return ( * <div> * <h1>{cmsContent.title}</h1> * <div>{cmsContent.content}</div> * </div> * ); * } * * return <div>Regular page: {title}</div>; * }; * * export default withCmsRouting(BlogPage); * * // With options * export default withCmsRouting(BlogPage, { * contentOptions: { includeSEO: true, includeAuthor: true }, * onContentLoaded: (content) => console.log('Loaded:', content.title) * }); * * // TypeScript usage * interface BlogPageProps { * title: string; * } * * const BlogPage: React.FC<BlogPageProps & CmsInjectedProps> = ({ * title, * cmsContent, * isCmsRoute, * cmsLoading, * cmsError * }) => { * // Component implementation with full control over rendering * }; * * export default withCmsRouting<BlogPageProps>(BlogPage, options); * ``` */ function withCmsRouting(WrappedComponent, options = {}) { const { contentOptions = {}, displayName, autoFetch = true, customTemplate, onContentLoaded, onContentError, onRouteChange, } = options; // Create the wrapped component const WithCmsRoutingComponent = (0, react_1.forwardRef)((props, ref) => { const router = (0, router_1.useRouter)(); const [cmsState, setCmsState] = (0, react_1.useState)({ content: null, isLoading: false, error: null, isCmsRoute: false, currentPath: '', }); // ============================================================================ // CONTENT FETCHING // ============================================================================ const fetchContent = async (path) => { try { setCmsState(prev => ({ ...prev, isLoading: true, error: null, })); // Validate the route and fetch content const validationResult = await (0, cms_content_1.validateContentRoute)(path, contentOptions); if (validationResult.isValid && validationResult.content) { setCmsState(prev => ({ ...prev, isLoading: false, content: validationResult.content, error: null, })); // Notify about successful content load onContentLoaded?.(validationResult.content); } else { const error = validationResult.error || 'Content not found'; setCmsState(prev => ({ ...prev, isLoading: false, content: null, error, })); // Notify about content error onContentError?.(error); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to fetch content'; setCmsState(prev => ({ ...prev, isLoading: false, content: null, error: errorMessage, })); // Notify about content error onContentError?.(errorMessage); } }; const handleFetchCmsContent = async () => { if (cmsState.currentPath && cmsState.isCmsRoute) { await fetchContent(cmsState.currentPath); } }; const handleClearCmsContent = () => { setCmsState(prev => ({ ...prev, content: null, error: null, isLoading: false, })); }; // ============================================================================ // ROUTER INTEGRATION // ============================================================================ (0, react_1.useEffect)(() => { // Skip if router is not ready if (!router.isReady) { return; } const currentPath = router.asPath; // Skip if path hasn't changed if (currentPath === cmsState.currentPath) { return; } try { // Check if this is a valid CMS route const isCmsRoute = (0, route_config_1.isValidCmsRoute)(currentPath); // Update state with new path and CMS route status setCmsState(prev => ({ ...prev, currentPath, isCmsRoute, content: null, error: null, isLoading: false, })); // Notify about route change onRouteChange?.(currentPath, isCmsRoute); // Fetch content if this is a CMS route and autoFetch is enabled if (isCmsRoute && autoFetch) { fetchContent(currentPath); } } catch { // If route validation fails, treat as non-CMS route setCmsState(prev => ({ ...prev, currentPath, isCmsRoute: false, content: null, error: null, isLoading: false, })); onRouteChange?.(currentPath, false); } }, [router.isReady, router.asPath, cmsState.currentPath, contentOptions, onRouteChange, autoFetch]); // ============================================================================ // RENDERING // ============================================================================ // Create enhanced props with CMS data const enhancedProps = react_1.default.useMemo(() => ({ ...props, cmsContent: cmsState.content, isCmsRoute: cmsState.isCmsRoute, cmsLoading: cmsState.isLoading, cmsError: cmsState.error, fetchCmsContent: handleFetchCmsContent, clearCmsContent: handleClearCmsContent, }), [props, cmsState]); // If we have a custom template and this is a CMS route, use CmsRouteHandler if (customTemplate && cmsState.isCmsRoute) { const cmsHandlerProps = { customTemplate, contentOptions, }; if (onContentLoaded) cmsHandlerProps.onContentLoaded = onContentLoaded; if (onContentError) cmsHandlerProps.onContentError = onContentError; if (onRouteChange) cmsHandlerProps.onRouteChange = onRouteChange; return ((0, jsx_runtime_1.jsx)(CmsRouteHandler_1.CmsRouteHandler, { ...cmsHandlerProps, children: (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props }) })); } // Always render the wrapped component with enhanced props return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...enhancedProps, ref: ref }); }); // Set display name for debugging const componentName = displayName || WrappedComponent.displayName || WrappedComponent.name || 'Component'; WithCmsRoutingComponent.displayName = `withCmsRouting(${componentName})`; // Copy static properties from the wrapped component (improved version) const hoistStatics = (target, source) => { // Get all property names from the source component const sourceProperties = Object.getOwnPropertyNames(source); // List of properties that should not be copied const skipProperties = [ 'prototype', 'length', 'name', 'caller', 'callee', 'arguments', 'constructor' ]; sourceProperties.forEach(propName => { if (!skipProperties.includes(propName)) { try { const descriptor = Object.getOwnPropertyDescriptor(source, propName); if (descriptor && (descriptor.value !== undefined || descriptor.get)) { Object.defineProperty(target, propName, descriptor); } } catch { // Ignore copy failures for read-only properties } } }); return target; }; return hoistStatics(WithCmsRoutingComponent, WrappedComponent); } // ============================================================================ // CONVENIENCE FUNCTIONS // ============================================================================ /** * Creates a pre-configured version of withCmsRouting with default options * * @param defaultOptions - Default options to apply to all wrapped components * @returns A withCmsRouting function with pre-configured options * * @example * ```tsx * // Create a factory with common options * const withBlogCms = createCmsRoutingFactory({ * contentOptions: { includeSEO: true, includeAuthor: true }, * autoFetch: true, * }); * * // Use the factory * const BlogPage = ({ title, cmsContent, isCmsRoute }) => { * if (isCmsRoute && cmsContent) { * return <div>CMS: {cmsContent.title}</div>; * } * return <div>Regular: {title}</div>; * }; * export default withBlogCms(BlogPage); * * // Override specific options when needed * const SpecialBlogPage = ({ title }) => <div>{title}</div>; * export default withBlogCms(SpecialBlogPage, { * autoFetch: false * }); * ``` */ function createCmsRoutingFactory(defaultOptions) { return function (WrappedComponent, overrideOptions = {}) { const mergedOptions = { ...defaultOptions, ...overrideOptions, // Merge nested objects contentOptions: { ...defaultOptions.contentOptions, ...overrideOptions.contentOptions, }, }; return withCmsRouting(WrappedComponent, mergedOptions); }; } /** * Hook to access CMS routing state from within a component wrapped by withCmsRouting * * Note: When using withCmsRouting, CMS state is available directly as props. * This hook is provided for consistency but will warn and return default values. * * @returns CMS routing state including content, loading, and error information * * @example * ```tsx * function MyPageComponent({ cmsContent, isCmsRoute, cmsLoading, cmsError }) { * // Access CMS state directly from props when using withCmsRouting * * if (cmsLoading) { * return <div>Loading CMS content...</div>; * } * * if (cmsError) { * return <div>Error: {cmsError}</div>; * } * * if (isCmsRoute && cmsContent) { * return <div>CMS: {cmsContent.title}</div>; * } * * return <div>Regular page content</div>; * } * ``` */ function useCmsRoutingContext() { // This hook would typically use a context provider, but since we're injecting // props directly into components, we'll provide a warning for now console.warn('useCmsRoutingContext: CMS state is available as props when using withCmsRouting. ' + 'Access cmsContent, isCmsRoute, cmsLoading, and cmsError from component props instead.'); return { isCmsRoute: false, cmsLoading: false, cmsError: null, }; } exports.default = withCmsRouting;