UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

851 lines (770 loc) 25.4 kB
/** * B-esper Site Page Loader * Modular page loading system that replaces the monolithic BesperSiteRenderer * Each page has its own directory with template.html, styles.css, and script.js */ import PageLifecycleManager from '../../services/PageLifecycleManager.js'; class BesperPageLoader { constructor(options = {}) { this.options = { environment: 'prod', baseApiUrl: process.env.API_ENDPOINT, debug: false, basePath: './src', ...options, }; this.loadedPages = new Map(); this.loadedStyles = new Set(); this.pageInstances = new Map(); // Define valid pages this.validPages = new Set([ 'home', 'home-auth', 'about-us', 'case-studies', 'pricing', 'contact-us', 'demo', 'partners', 'get-started', 'help', 'my-bots', 'profile', 'workspace', 'users', 'notifications', 'subscription', 'account-management', 'technical-insights', 'implementation-guide', 'workbench', 'invite-user', 'manage-user', 'manage-workspace', 'notification-details', 'product-purchasing', 'rc-subscription', 'upcoming', 'support-tickets', 'support-ticket-details', // New UI Design Pages 'home-new', 'bot-management-new', 'cost-pool-management', 'user-management-new', 'workspace-management-new', // Admin Pages (restricted to admin power pages) 'admin_customer_outreach_management', ]); } /** * Load and render a specific page - NEW LIFECYCLE IMPLEMENTATION * Uses the new coordinated lifecycle: token generation + content loading + data coordination */ async loadPage(pageId, data = {}, options = {}) { try { // Validate page exists if (!this.isValidPage(pageId)) { throw new Error(`Invalid page: ${pageId}`); } // Import and use the new PageLifecycleManager const lifecycleManager = new PageLifecycleManager({ debug: this.options.debug, tokenTimeout: 15000, tokenCheckInterval: 1000, }); // Use the new coordinated lifecycle const result = await lifecycleManager.loadPage(pageId, data, options); if (result.success) { console.log( `[BesperPageLoader] [SUCCESS] Page ${pageId} loaded successfully using new lifecycle` ); return true; } else { console.error( `[BesperPageLoader] [ERROR] Page ${pageId} failed to load:`, result.error ); this.renderError(new Error(result.error || 'Page loading failed')); return false; } } catch (error) { console.error( `[BesperPageLoader] [ERROR] Error loading page ${pageId}:`, error ); this.renderError(error); return false; } } /** * Check if page ID is valid */ isValidPage(pageId) { return this.validPages.has(pageId); } /** * Load global shared styles - OPTIMIZED VERSION * Uses bundled CSS or loads in parallel from NPM package */ async loadGlobalStyles() { if (this.loadedStyles.has('global')) return; try { // Check if CSS is already bundled with the package (preferred method) const bundledStylesheet = document.querySelector( 'link[href*="bespersite.css"]' ); if (bundledStylesheet) { this.loadedStyles.add('global'); return; } // Load individual global CSS files in parallel from NPM package await this.loadGlobalStylesFromPackage(); } catch (error) { console.warn( 'Failed to load global styles, using embedded fallback:', error ); this.loadEmbeddedStyles(); } } /** * Load page HTML template from individual page directory - OPTIMIZED VERSION */ async loadPageTemplate(pageId) { if (this.loadedPages.has(pageId)) { return this.loadedPages.get(pageId); } try { const templateUrl = this.getAssetUrl(`pages/${pageId}/template.html`); const response = await fetch(templateUrl); if (!response.ok) { throw new Error(`Failed to load template: ${response.status}`); } const template = await response.text(); this.loadedPages.set(pageId, template); return template; } catch (error) { console.error(`Error loading template for ${pageId}:`, error); return this.getFallbackTemplate(pageId); } } /** * Load global styles from NPM package - OPTIMIZED FOR PARALLEL LOADING */ async loadGlobalStylesFromPackage() { const globalStylesheets = [ 'styles/base/globals.css', 'styles/shared/global.css', 'styles/base/typography.css', 'styles/base/layout.css', 'styles/components/ui.css', ]; // OPTIMIZATION: Load all stylesheets in parallel instead of sequentially const stylePromises = globalStylesheets.map(async stylePath => { try { const styleId = `bsp-global-${stylePath.replace(/[/.]/g, '-')}`; // Skip if already loaded if (document.getElementById(styleId)) return; const styleLink = document.createElement('link'); styleLink.rel = 'stylesheet'; styleLink.href = this.getAssetUrl(stylePath); styleLink.id = styleId; return new Promise(resolve => { styleLink.onload = resolve; styleLink.onerror = () => { console.warn(`Failed to load global stylesheet: ${styleLink.href}`); resolve(); // Continue even if one fails }; document.head.appendChild(styleLink); // Shorter timeout for faster fallback setTimeout(resolve, 1000); }); } catch (error) { console.warn(`Error loading global stylesheet ${stylePath}:`, error); } }); // Wait for all stylesheets to load in parallel await Promise.all(stylePromises); this.loadedStyles.add('global'); } /** * Load embedded styles as fallback */ loadEmbeddedStyles() { if (this.loadedStyles.has('embedded')) return; const style = document.createElement('style'); style.id = 'bsp-embedded-styles'; style.textContent = ` /* B-esper Site Embedded Styles - PowerPages Compatibility with CSS Variables */ :root { /* Essential CSS Variables for contact-us page */ --primary-blue: #2563eb; --primary-blue-dark: #1e40af; --secondary-blue: #1e293b; --text-primary: #1a202c; --text-secondary: #4a5568; --text-muted: #718096; --background-primary: #ffffff; --background-secondary: #f7fafc; --border-color: #e2e8f0; --spacing-1: 0.25rem; --spacing-2: 0.5rem; --spacing-3: 0.75rem; --spacing-4: 1rem; --spacing-5: 1.25rem; --spacing-6: 1.5rem; --spacing-8: 2rem; --spacing-10: 2.5rem; --spacing-12: 3rem; --spacing-16: 4rem; --radius-sm: 0.125rem; --radius: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; --radius-2xl: 1rem; --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --transition-fast: 150ms ease-in-out; --transition-base: 250ms ease-in-out; --success-green: #10b981; --warning-orange: #f59e0b; --error-red: #ef4444; } .bsp-site-container { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; position: relative; width: 100%; clear: both; z-index: 1; } .bsp-page-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .bsp-container { width: 100%; } .bsp-card { background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 20px; } .bsp-heading-1 { font-size: 2rem; font-weight: 600; margin-bottom: 16px; color: #1a202c; } .bsp-heading-2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 12px; color: #2d3748; } .bsp-text-base { font-size: 1rem; margin-bottom: 12px; color: #4a5568; } .bsp-btn { display: inline-block; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all 0.2s; } .bsp-btn-primary { background-color: #3182ce; color: white; } .bsp-btn-primary:hover { background-color: #2c5aa0; } .bsp-error { text-align: center; padding: 40px 20px; } .bsp-loading-indicator { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; text-align: center; } .bsp-spinner { border: 3px solid #f3f3f3; border-top: 3px solid #3182ce; border-radius: 50%; width: 40px; height: 40px; animation: bsp-spin 1s linear infinite; margin-bottom: 16px; } @keyframes bsp-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); this.loadedStyles.add('embedded'); } /** * Load page-specific styles - OPTIMIZED VERSION * Non-blocking with shorter timeouts */ async loadPageStyles(pageId) { const styleId = `bsp-page-${pageId}-styles`; if (this.loadedStyles.has(styleId)) return; try { const styleLink = document.createElement('link'); styleLink.rel = 'stylesheet'; styleLink.href = this.getAssetUrl(`pages/${pageId}/styles.css`); styleLink.id = styleId; await new Promise((resolve, reject) => { styleLink.onload = resolve; styleLink.onerror = reject; document.head.appendChild(styleLink); // Shorter timeout for faster fallback setTimeout(reject, 1500); }); this.loadedStyles.add(styleId); } catch (error) { console.warn( `Failed to load page styles for ${pageId}, using embedded fallback:`, error ); await this.loadEmbeddedPageStyles(pageId); } } /** * Load embedded page styles as fallback (for when NPM package fails) */ async loadEmbeddedPageStyles(pageId) { const styleId = `bsp-page-${pageId}-styles`; if (this.loadedStyles.has(styleId)) return; // Basic embedded styles for critical functionality const embeddedStyles = ` /* Embedded fallback styles for ${pageId} */ .bsp-page-${pageId} { padding: 20px; max-width: 1200px; margin: 0 auto; } `; const style = document.createElement('style'); style.id = styleId; style.textContent = embeddedStyles; document.head.appendChild(style); this.loadedStyles.add(styleId); } /** * Load page-specific script - OPTIMIZED VERSION * Reduced logging and faster error handling */ /** * Load page-specific script - OPTIMIZED VERSION * Uses bundled page classes instead of dynamic loading */ async loadPageScript(pageId) { try { console.log( `[BesperPageLoader] 📜 Loading bundled page class for: ${pageId}` ); // Get the page class (convention: PageNamePage) from bundled classes const className = this.getPageClassName(pageId); const PageClass = window[className]; if (!PageClass) { console.error( `[BesperPageLoader] [ERROR] Page class ${className} not found in bundled package` ); console.log( `[BesperPageLoader] [LOADING] Using fallback page class for ${pageId}` ); return this.getFallbackPageClass(pageId); } console.log( `[BesperPageLoader] [SUCCESS] Page class ${className} loaded from bundle for ${pageId}` ); return PageClass; } catch (error) { console.error( `[BesperPageLoader] [ERROR] Error accessing bundled page class for ${pageId}:`, error ); console.log( `[BesperPageLoader] [LOADING] Using fallback page class for ${pageId}` ); return this.getFallbackPageClass(pageId); } } /** * Render page with template and initialize page class - OPTIMIZED FOR INSTANT UI */ async renderPage(pageId, template, PageClass, data, options) { // Automatically detect the container from current execution context const container = this.autoDetectContainer(); // CRITICAL: If no container found, don't render anything if (!container) { console.error( `[BesperPageLoader] Cannot render page ${pageId}: No suitable container found` ); console.error( '[BesperPageLoader] Please ensure b_esper_site() is called within a proper container element (e.g., <div id="besper-site-container">)' ); throw new Error( `No container found for page ${pageId}. Please call b_esper_site() within a proper container element.` ); } // Apply internationalization to template if i18n context is available let localizedTemplate = template; if (options.i18n) { localizedTemplate = this.localizeTemplate(template, options.i18n); if (options.debug) { console.log( `[BesperPageLoader] Applied ${options.i18n.language} localization to page ${pageId}` ); } } // PRIORITY 1: IMMEDIATE UI rendering - inject template HTML instantly container.innerHTML = localizedTemplate; // Make i18n context globally available for form scripts and other components if (options.i18n && typeof window !== 'undefined') { window.besperSiteI18n = options.i18n; } console.log( `[BesperPageLoader] Successfully rendered page ${pageId} in container: ${container.id || container.tagName}` ); // PRIORITY 2: Background initialization - don't block UI requestAnimationFrame(async () => { try { // Clean up previous page instance if (this.pageInstances.has(pageId)) { const prevInstance = this.pageInstances.get(pageId); if (prevInstance && typeof prevInstance.destroy === 'function') { prevInstance.destroy(); } } // Initialize page class with data and i18n context in background if (PageClass) { const pageInstance = new PageClass({ ...data, i18n: options.i18n, // Pass i18n context to page class }); // Use non-blocking initialization setTimeout(() => pageInstance.initialize(), 0); this.pageInstances.set(pageId, pageInstance); } // Update browser title with localized content this.updatePageTitle(pageId, options.i18n); } catch (error) { console.warn( '[BesperPageLoader] Background initialization error:', error ); } }); } /** * Get page class name from page ID */ getPageClassName(pageId) { // Convert kebab-case to PascalCase + 'Page' // e.g., 'home-auth' -> 'HomeAuthPage' return ( pageId .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join('') + 'Page' ); } /** * Load a script dynamically - OPTIMIZED VERSION * Reduced logging for better performance */ /** * Wait for stylesheet to load */ async waitForStylesheet(link) { return new Promise(resolve => { if (link.sheet) { resolve(); } else { link.onload = resolve; setTimeout(resolve, 2000); // Fallback timeout } }); } /** * Get container element - either directly provided or by ID */ getContainer(options) { // If a direct container element is provided, use it if (options.container && options.container.nodeType === Node.ELEMENT_NODE) { console.log('[BesperPageLoader] Using provided container element'); return options.container; } // Otherwise, get by ID const containerId = options.containerId || 'besper-site-content'; let container = document.getElementById(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; container.className = 'bsp-site-container'; document.body.appendChild(container); console.log( `[BesperPageLoader] Created new container with ID: ${containerId}` ); } else { console.log( `[BesperPageLoader] Using existing container with ID: ${containerId}` ); } return container; } /** * Simple container detection - EXACTLY LIKE NPM_GENERAL * Uses the same direct approach as npm_general functions which always work perfectly */ autoDetectContainer() { try { // Method 1: Look for the PRIMARY PowerPages container ID first const primaryContainer = document.getElementById('besper-site-container'); if (primaryContainer) { console.log( '[BesperPageLoader] Found primary PowerPages container: besper-site-container' ); return primaryContainer; } // Method 2: Look for other common container IDs (simple direct search) const containerIds = [ 'besper-site-content', 'besper-content', 'content', 'main-content', 'page-content', 'main', ]; for (const id of containerIds) { const container = document.getElementById(id); if (container) { console.log(`[BesperPageLoader] Found container: ${id}`); return container; } } // Method 3: If we're executing inside a script tag, look for the closest parent container // This is the CRITICAL difference - find the NEAREST container, not create new ones const scripts = document.getElementsByTagName('script'); for (let i = scripts.length - 1; i >= 0; i--) { const script = scripts[i]; // Look up the DOM tree from this script let parent = script.parentElement; while (parent && parent !== document.body) { if ( parent.tagName === 'DIV' && (parent.id.includes('container') || parent.className.includes('container') || parent.id.includes('besper')) ) { console.log( `[BesperPageLoader] Found parent container: ${parent.id || parent.className}` ); return parent; } parent = parent.parentElement; } } // Method 4: FAIL GRACEFULLY - don't create containers at document.body level console.error( '[BesperPageLoader] CRITICAL: No suitable container found. Content cannot be rendered.' ); console.error( '[BesperPageLoader] Please ensure b_esper_site() is called within a proper container element.' ); // Return null to signal failure - let the calling code handle this return null; } catch (error) { console.error('[BesperPageLoader] Container detection failed:', error); return null; } } /** * Get asset URL based on environment - OPTIMIZED VERSION * Reduced logging for better performance */ getAssetUrl(path) { // ALWAYS use NPM package URLs - no relative paths in production environments const packageUrl = this.getPackageUrl(); return `${packageUrl}/src/${path}`; } /** * Update page title with localization support */ updatePageTitle(pageId, i18n = null) { const title = this.getPageTitle(pageId, i18n); document.title = title; } /** * Get page title with localization support */ getPageTitle(pageId, i18n = null) { // If i18n is available, try to get localized titles if (i18n) { const localizedTitle = i18n.getSiteTranslation(`pageTitles.${pageId}`); if (localizedTitle && localizedTitle !== `pageTitles.${pageId}`) { return localizedTitle; } } // Fallback to English titles const titles = { home: 'B-esper - Enterprise AI Solutions', 'home-auth': 'Dashboard - B-esper', 'about-us': 'About Us - B-esper', 'case-studies': 'Case Studies - B-esper', pricing: 'Pricing - B-esper', 'contact-us': 'Contact Us - B-esper', demo: 'Live Demo - B-esper', partners: 'Partners - B-esper', 'get-started': 'Get Started - B-esper', help: 'Help Center - B-esper', 'my-bots': 'My Bots - B-esper', profile: 'Profile - B-esper', workspace: 'Workspace - B-esper', users: 'Users - B-esper', notifications: 'Notifications - B-esper', subscription: 'Subscription - B-esper', 'account-management': 'Account Management - B-esper', 'technical-insights': 'Technical Insights - B-esper', 'implementation-guide': 'Implementation Guide - B-esper', workbench: 'Workbench - B-esper', 'invite-user': 'Invite User - B-esper', 'manage-user': 'Manage User - B-esper', 'manage-workspace': 'Manage Workspace - B-esper', 'notification-details': 'Notification Details - B-esper', 'product-purchasing': 'Product Purchasing - B-esper', 'rc-subscription': 'Resource Consumption - B-esper', upcoming: 'Upcoming Features - B-esper', }; return titles[pageId] || `${pageId} - B-esper`; } /** * Get fallback template for error cases */ getFallbackTemplate(pageId) { return ` <div class="bsp-page-container bsp-error"> <div class="bsp-container"> <div class="bsp-card"> <h1 class="bsp-heading-1">Page Loading Error</h1> <p class="bsp-text-base">Unable to load page: ${pageId}</p> <button class="bsp-btn bsp-btn-primary" onclick="location.reload()"> Retry </button> </div> </div> </div> `; } /** * Get fallback page class */ getFallbackPageClass(pageId) { return class FallbackPage { constructor(data = {}) { this.data = data; this.pageId = pageId; } initialize() { // Fallback page initialized silently } }; } /** * Render error state */ renderError(error) { const container = this.autoDetectContainer(); // If no container found, log error but don't try to render if (!container) { console.error( '[BesperPageLoader] Cannot render error: No container found' ); console.error('[BesperPageLoader] Original error:', error.message); return; } container.innerHTML = ` <div class="bsp-page-container bsp-error"> <div class="bsp-container"> <div class="bsp-card"> <h1 class="bsp-heading-1">Error</h1> <p class="bsp-text-base">${error.message}</p> <button class="bsp-btn bsp-btn-primary" onclick="location.reload()"> Retry </button> </div> </div> </div> `; } /** * Apply internationalization to template HTML * Replaces placeholders like {{i18n.contactForm.title}} with localized text */ localizeTemplate(template, i18n) { if (!i18n || !template) return template; // Replace i18n placeholders in template return template.replace(/\{\{i18n\.([^}]+)\}\}/g, (match, path) => { const translation = i18n.getSiteTranslation(path); return translation || match; // Return original if translation not found }); } /** * Get the base URL for the published NPM package */ getPackageUrl() { // In PowerPages environment, use the configured package URL if (typeof window !== 'undefined' && window.bSitePackageUrl) { return window.bSitePackageUrl; } // Use build-time package name if available (injected during build) if ( typeof process !== 'undefined' && process.env && process.env.PACKAGE_NAME ) { const packageName = process.env.PACKAGE_NAME; return `https://unpkg.com/${packageName}@latest`; } // Try to detect from script tag that loaded this package if (typeof window !== 'undefined') { const scripts = Array.from( document.querySelectorAll('script[src*="besper-frontend-site"]') ); const packageScript = scripts.find(script => script.src.includes('besper-frontend-site') ); if (packageScript) { // Extract package name from script src URL const srcMatch = packageScript.src.match(/unpkg\.com\/([^@/]+)@/); if (srcMatch) { const packageName = srcMatch[1]; return `https://unpkg.com/${packageName}@latest`; } } } // Fallback for current branch (dev-0926) - this should be updated when branch changes return 'https://unpkg.com/besper-frontend-site-dev-0926@latest'; } } export { BesperPageLoader };