UNPKG

besper-frontend-site-dev-main

Version:

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

616 lines (534 loc) 17.7 kB
/** * Page Lifecycle Manager * Coordinates token generation, content loading, and data loading * Implements the new npm site lifecycle as requested by user */ import tokenAuthService from './tokenAuth.js'; import TemplateLoaderService from './TemplateLoaderService.js'; class PageLifecycleManager { constructor(options = {}) { this.options = { tokenTimeout: 15000, tokenCheckInterval: 1000, debug: false, ...options, }; this.templateLoader = new TemplateLoaderService(); this.currentPageData = null; } /** * Main lifecycle method - coordinates everything as requested * 1. Generate token as promise * 2. Load HTML/CSS in parallel * 3. Coordinate data loading when token is ready * 4. Handle skeleton loading */ async loadPage(pageId, data = {}, options = {}) { const lifecycleStart = Date.now(); console.log(`[Lifecycle] [INIT] Starting page lifecycle for: ${pageId}`); try { // Phase 1: Start token generation promise and content loading in parallel const tokenPromise = this.startTokenGeneration(); const contentPromise = this.loadPageContent( pageId, options.i18n?.language || 'en' ); console.log( `[Lifecycle] [LOADING] Token generation and content loading started in parallel` ); // Phase 2: Wait for content loading (HTML/CSS) to complete const contentResult = await contentPromise; console.log( `[Lifecycle] 📄 Content loaded after ${Date.now() - lifecycleStart}ms` ); // Phase 3: Show skeleton loading immediately this.showSkeletonLoading(pageId, contentResult.template); // Phase 4: Wait for token generation to complete const tokenResult = await tokenPromise; console.log( `[Lifecycle] 🔑 Token promise resolved after ${Date.now() - lifecycleStart}ms` ); // Phase 5: Load data if authenticated, or show final content if (tokenResult.success) { console.log(`[Lifecycle] [SUCCESS] User authenticated - loading data`); await this.loadPageData(pageId, data, tokenResult.token); } else { console.log( `[Lifecycle] [WARN] User not authenticated - showing static content` ); this.showStaticContent(pageId); } // Phase 6: Replace skeleton with actual content this.replaceSkeletonWithContent(pageId, data, tokenResult); const totalTime = Date.now() - lifecycleStart; console.log( `[Lifecycle] [SUCCESS] Page lifecycle completed in ${totalTime}ms` ); return { success: true, pageId, authenticated: tokenResult.success, loadTime: totalTime, template: contentResult.template, styles: contentResult.styles, }; } catch (error) { console.error( `[Lifecycle] [ERROR] Page lifecycle failed for ${pageId}:`, error ); return { success: false, pageId, error: error.message, }; } } /** * Start token generation as a promise */ async startTokenGeneration() { console.log(`[Lifecycle] [LOADING] Starting token generation promise...`); return tokenAuthService.generateTokenPromise( this.options.tokenTimeout, this.options.tokenCheckInterval ); } /** * Load page content (HTML and CSS) from APIM */ async loadPageContent(pageId, language = 'en') { console.log(`[Lifecycle] 📄 Loading content for ${pageId} (${language})`); try { // Load template and styles in parallel const [template, styles] = await Promise.all([ this.templateLoader.loadTemplate(pageId, language), this.templateLoader.loadStyles(pageId, language), ]); // Also load global styles await this.loadGlobalStyles(); return { template, styles, success: true, }; } catch (error) { console.error( `[Lifecycle] [ERROR] Content loading failed for ${pageId}:`, error ); return { template: this.getFallbackTemplate(pageId), styles: this.getFallbackStyles(pageId), success: false, error: error.message, }; } } /** * Load global styles including skeleton loading styles */ async loadGlobalStyles() { const globalStyleId = 'besper-global-styles'; if (document.getElementById(globalStyleId)) { return; // Already loaded } const globalStyles = ` /* B-esper Global Styles with Skeleton Loading */ :root { --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; --skeleton-base: #e2e8f0; --skeleton-highlight: #f5f5f5; } /* Skeleton Loading Animations - Centrally Styled */ .besper-skeleton { background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-highlight) 50%, var(--skeleton-base) 75%); background-size: 200% 100%; animation: besper-skeleton-pulse 1.5s ease-in-out infinite; } @keyframes besper-skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .besper-skeleton-text { height: 20px; border-radius: 4px; margin-bottom: 1rem; } .besper-skeleton-text.short { width: 60%; } .besper-skeleton-text.medium { width: 80%; } .besper-skeleton-text.long { width: 100%; } .besper-skeleton-block { height: 100px; border-radius: 8px; margin-bottom: 1.5rem; } .besper-skeleton-card { background: white; border-radius: 8px; padding: 1.5rem; border: 1px solid var(--border-color); margin-bottom: 1rem; } /* Page-specific skeleton containers */ .besper-page-skeleton { padding: 2rem 1rem; max-width: 1200px; margin: 0 auto; background: var(--background-secondary); } .besper-loading-message { text-align: center; color: var(--text-secondary); margin-bottom: 2rem; font-size: 14px; } /* Authentication status indicator */ .besper-auth-status { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; border-radius: 20px; font-size: 12px; font-weight: 500; } .besper-auth-status.checking { background: #fef3c7; color: #92400e; } .besper-auth-status.authenticated { background: #d1fae5; color: #065f46; } .besper-auth-status.unauthenticated { background: #fee2e2; color: #991b1b; } .besper-auth-spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top: 2px solid currentColor; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; const styleElement = document.createElement('style'); styleElement.id = globalStyleId; styleElement.textContent = globalStyles; document.head.appendChild(styleElement); console.log(`[Lifecycle] 🎨 Global styles loaded with skeleton animations`); } /** * Show skeleton loading for specific page */ showSkeletonLoading(pageId, _template) { console.log(`[Lifecycle] 🎭 Applying skeleton loading for ${pageId}`); const container = this.getPageContainer(); if (!container) return; // Set skeleton widths from data attributes this.applySkeletonWidths(); // All elements with skeleton-loading class are already in skeleton state console.log( `[Lifecycle] ✨ Skeleton loading applied - elements ready for smooth transition` ); } /** * Generate page-specific skeleton loading */ generatePageSkeleton(pageId, template) { // Try to extract structure from template for intelligent skeleton const hasForm = template && template.includes('<form'); const hasTable = template && template.includes('<table'); const hasCards = template && template.includes('card'); // Base skeleton structure let skeleton = ` <div class="besper-skeleton-card"> <div class="besper-skeleton besper-skeleton-text medium"></div> <div class="besper-skeleton besper-skeleton-text short"></div> </div> `; // Add page-specific skeleton elements if (hasForm) { skeleton += ` <div class="besper-skeleton-card"> <div class="besper-skeleton besper-skeleton-text long"></div> <div class="besper-skeleton besper-skeleton-text medium"></div> <div class="besper-skeleton besper-skeleton-text short"></div> <div class="besper-skeleton besper-skeleton-block"></div> </div> `; } if (hasTable) { skeleton += ` <div class="besper-skeleton-card"> <div class="besper-skeleton besper-skeleton-text medium"></div> <div class="besper-skeleton besper-skeleton-block"></div> <div class="besper-skeleton besper-skeleton-block"></div> </div> `; } if (hasCards || pageId.includes('management')) { skeleton += ` <div class="besper-skeleton-card"> <div class="besper-skeleton besper-skeleton-text long"></div> <div class="besper-skeleton besper-skeleton-text medium"></div> </div> <div class="besper-skeleton-card"> <div class="besper-skeleton besper-skeleton-text medium"></div> <div class="besper-skeleton besper-skeleton-text short"></div> </div> `; } return skeleton; } /** * Load page data after token is ready */ async loadPageData(_pageId, data, token) { console.log(`[Lifecycle] 📊 Loading data for ${_pageId} with token`); try { // Here you would make API calls to get page-specific data // Using the token for authenticated requests // For now, simulate data loading await new Promise(resolve => setTimeout(resolve, 500)); this.currentPageData = { pageId: _pageId, token, timestamp: Date.now(), data, }; console.log(`[Lifecycle] [SUCCESS] Data loaded for ${_pageId}`); } catch (error) { console.error( `[Lifecycle] [ERROR] Data loading failed for ${_pageId}:`, error ); this.currentPageData = null; } } /** * Show static content for unauthenticated users */ showStaticContent(pageId) { console.log(`[Lifecycle] 📄 Showing static content for ${pageId}`); // Update authentication status this.updateAuthStatus( 'unauthenticated', 'Please log in to access full functionality' ); } /** * Replace skeleton loading with actual content - enhanced error handling */ replaceSkeletonWithContent(pageId, data, tokenResult) { console.log( `[Lifecycle] [LOADING] Transitioning skeleton to loaded state for ${pageId}` ); const container = this.getPageContainer(); if (!container) return; // Enhanced authentication status handling (logs only - no user-facing messages) if (tokenResult.success) { if (tokenResult.source === 'guest') { console.log( '[Lifecycle] 🎫 Guest access granted - limited functionality available' ); } else if (tokenResult.source === 'fallback') { console.log( '[Lifecycle] [LOADING] Authentication recovered via fallback method' ); } else { console.log('[Lifecycle] [SUCCESS] Full authentication successful'); } } else { console.log( '[Lifecycle] [WARN] Authentication failed - operating in limited mode' ); // Create a fallback token for basic functionality const fallbackToken = { success: true, token: 'public_access_' + Date.now(), source: 'public', limited: true, }; // Override the token result to ensure page still loads tokenResult = fallbackToken; console.log( '[Lifecycle] [LOADING] Fallback token created to ensure page functionality' ); } // Initialize the page class from bundled JavaScript this.initializePageClass(pageId, data, tokenResult); // Transition skeleton elements to loaded state this.transitionSkeletonToLoaded(); } /** * Enhanced authentication status handling - logs only, no confusing UI messages */ updateAuthStatus(status, message) { // Log authentication status for debugging - never show to users console.log(`[Lifecycle] 🔐 Auth status: ${status} - ${message}`); // Remove any existing authentication UI elements to prevent user confusion const authElements = document.querySelectorAll( '.besper-auth-status, .authentication-message, [class*="auth-"]' ); authElements.forEach(element => { if ( element.textContent && element.textContent.toLowerCase().includes('authentication') ) { element.remove(); } }); // Never show authentication timeout or "checking authentication" messages to users // These are confusing when users are clearly logged in } /** * Apply skeleton widths from data attributes */ applySkeletonWidths() { const elementsWithWidth = document.querySelectorAll( '[data-skeleton-width]' ); elementsWithWidth.forEach(element => { const width = element.getAttribute('data-skeleton-width'); element.style.setProperty('--skeleton-width', width); }); } /** * Transition skeleton elements to loaded state with smooth animation */ transitionSkeletonToLoaded() { // Add a small delay to ensure content is ready setTimeout(() => { const skeletonElements = document.querySelectorAll('.skeleton-loading'); skeletonElements.forEach((element, index) => { // Stagger the transition for a nice effect setTimeout(() => { element.classList.add('loaded'); // Remove skeleton-loading class after transition completes setTimeout(() => { element.classList.remove('skeleton-loading'); }, 300); }, index * 50); // 50ms stagger between elements }); console.log( `[Lifecycle] [SUCCESS] Skeleton to loaded transition completed` ); }, 100); } /** * Initialize the page class from bundled JavaScript (npm package) */ initializePageClass(pageId, data, tokenResult) { try { // Get the page class name (same logic as BesperPageLoader) const className = this.getPageClassName(pageId); const PageClass = window[className]; if (PageClass) { console.log(`[Lifecycle] [INIT] Initializing page class: ${className}`); const pageInstance = new PageClass({ ...data, authenticated: tokenResult.success, token: tokenResult.token, authData: tokenResult, }); // Initialize the page if (typeof pageInstance.initialize === 'function') { pageInstance.initialize(); } console.log( `[Lifecycle] [SUCCESS] Page class ${className} initialized successfully` ); } else { console.warn( `[Lifecycle] [WARN] Page class ${className} not found in bundled package` ); this.showFallbackContent(pageId); } } catch (error) { console.error( `[Lifecycle] [ERROR] Page class initialization failed:`, error ); this.showFallbackContent(pageId); } } /** * Get page class name from page ID */ getPageClassName(pageId) { return ( pageId .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join('') + 'Page' ); } /** * Show fallback content when page class is not available */ showFallbackContent(_pageId) { const container = this.getPageContainer(); if (!container) return; container.innerHTML = ` <div class="besper-page-skeleton"> <div class="besper-skeleton-card"> <h1>Page: ${_pageId}</h1> <p>Content is loading...</p> <p><small>Page class not available in npm package.</small></p> </div> </div> `; } /** * Get page container element */ getPageContainer() { // Try to find the container in the same way as BesperPageLoader return ( document.getElementById('besper-site-container') || document.getElementById('besper-site-content') || document.getElementById('besper-content') || document.querySelector('[id*="besper"]') || document.querySelector('.bsp-site-container') ); } /** * Get fallback template */ getFallbackTemplate(pageId) { return ` <div class="besper-page-container"> <h1>Loading ${pageId}...</h1> <p>Please wait while content is loading.</p> </div> `; } /** * Get fallback styles */ getFallbackStyles(_pageId) { return ` .besper-page-container { padding: 2rem; text-align: center; color: var(--text-secondary); } `; } } export default PageLifecycleManager;