UNPKG

besper-frontend-site-dev-main

Version:

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

550 lines (486 loc) 16.8 kB
/** * Template Loader Service for Besper Site * Loads HTML templates and CSS styles from Azure Storage via Azure Functions */ class TemplateLoaderService { constructor(options = {}) { // Auto-detect environment and branch from the npm package configuration const packageConfig = this.detectPackageEnvironment(); this.options = { environment: packageConfig.environment, branch: packageConfig.branch, functionAppUrl: packageConfig.apiEndpoint, useDirectMapping: false, // Can be enabled to skip API calls ...options, // Allow manual override if needed }; // Debug logging for API URL resolution console.log('[TemplateLoader] 🔧 Auto-detected package configuration:', { packageName: packageConfig.packageName, environment: this.options.environment, branch: this.options.branch, functionAppUrl: this.options.functionAppUrl, useDirectMapping: this.options.useDirectMapping, }); this.templateCache = new Map(); this.styleCache = new Map(); } /** * Auto-detect environment and branch from npm package build configuration * Uses the built-in package configuration instead of URL detection */ detectPackageEnvironment() { // Try to get configuration from build-time environment variables if (typeof process !== 'undefined' && process.env) { const packageName = process.env.PACKAGE_NAME; const envName = process.env.ENVIRONMENT_NAME; const apiEndpoint = process.env.API_ENDPOINT; if (packageName && envName && apiEndpoint) { // Extract branch from package name if it follows the pattern const branchMatch = packageName.match( /besper-frontend-site-([^-]+)-(.+)/ ); const branch = branchMatch ? branchMatch[2] : 'main'; const environment = branchMatch ? branchMatch[1] : 'dev'; return { packageName, environment, branch, apiEndpoint, }; } } // Fallback: Try to detect from global window object (when loaded as script) if (typeof window !== 'undefined') { // Look for script tag that loaded this package const scripts = Array.from( document.querySelectorAll('script[src*="besper-frontend-site"]') ); const packageScript = scripts.find(script => script.src.includes('besper-frontend-site') ); if (packageScript) { const srcMatch = packageScript.src.match( /besper-frontend-site-([^-@]+)-([^@]+)@/ ); if (srcMatch) { const environment = srcMatch[1]; const branch = srcMatch[2]; // Generate API endpoint using environment variable - will be replaced during build const apiEndpoint = process.env.API_ENDPOINT; return { packageName: `besper-frontend-site-${environment}-${branch}`, environment, branch, apiEndpoint, }; } } } // Ultimate fallback - use process.env.API_ENDPOINT if available if ( typeof process !== 'undefined' && process.env && process.env.API_ENDPOINT ) { console.warn( '[TemplateLoader] [WARN] Using fallback API endpoint from environment' ); return { packageName: 'besper-frontend-site-dev-main', environment: 'dev', branch: 'main', apiEndpoint: process.env.API_ENDPOINT, }; } // Final fallback console.warn( '[TemplateLoader] [WARN] Could not auto-detect package environment, using placeholder' ); return { packageName: 'besper-frontend-site-dev-main', environment: 'dev', branch: 'main', apiEndpoint: process.env.API_ENDPOINT, }; } /** * Load page template from server-side storage via the unified endpoint or direct mapping */ async loadTemplate(pageId, language = 'en') { const cacheKey = `${pageId}-${language}-${this.options.environment}-${this.options.branch}`; if (this.templateCache.has(cacheKey)) { return this.templateCache.get(cacheKey); } // If using direct mapping, try to load from local storage structure first if (this.options.useDirectMapping) { try { const directTemplate = await this.loadDirectTemplate(pageId, language); if (directTemplate) { this.templateCache.set(cacheKey, directTemplate); console.log( `[TemplateLoader] [SUCCESS] Loaded template directly for page: ${pageId} (language: ${language})` ); return directTemplate; } } catch (error) { console.warn( `[TemplateLoader] [WARN] Direct template loading failed for ${pageId}, falling back to API:`, error.message ); } } try { // Use GET with query parameters to match APIM internal swagger specification const queryParams = new URLSearchParams({ url: pageId, content_type: 'template', language, }); const response = await fetch( `${this.options.functionAppUrl}/besper-site/page?${queryParams}`, { method: 'GET', headers: { Accept: 'application/json', 'Cache-Control': 'public, max-age=3600', }, } ); if (!response.ok) { throw new Error( `Failed to load template for ${pageId}: ${response.status}` ); } const data = await response.json(); if (data.success && data.template) { this.templateCache.set(cacheKey, data.template); console.log( `[TemplateLoader] [SUCCESS] Loaded template for page: ${pageId} (${data.url_mapping}, language: ${language})` ); return data.template; } else { throw new Error( `Template not found: ${data.template_error || 'Unknown error'}` ); } } catch (error) { console.error( `[TemplateLoader] [ERROR] Error loading template for ${pageId} (language: ${language}):`, error ); return this.getFallbackTemplate(pageId); } } /** * Load template directly from storage structure (no API call) */ async loadDirectTemplate(pageId, language = 'en') { // Check if we have the storage structure available if (typeof window !== 'undefined' && window.besperStorageAssets) { const languageKey = language === 'en' ? 'template' : `template_${language}`; const template = window.besperStorageAssets[pageId]?.[languageKey]; if (template) { return template; } // Fallback to default language if specific language not found if (language !== 'en') { const fallbackTemplate = window.besperStorageAssets[pageId]?.template; if (fallbackTemplate) { console.log( `[TemplateLoader] 📄 Using fallback template for ${pageId} (requested: ${language}, using: en)` ); return fallbackTemplate; } } } // If no direct storage access, return null to trigger API fallback return null; } /** * Load page styles from server-side storage via the unified endpoint or direct mapping */ async loadStyles(pageId, language = 'en') { const cacheKey = `${pageId}-${language}-${this.options.environment}-${this.options.branch}`; if (this.styleCache.has(cacheKey)) { return this.styleCache.get(cacheKey); } // If using direct mapping, try to load from local storage structure first if (this.options.useDirectMapping) { try { const directStyles = await this.loadDirectStyles(pageId, language); if (directStyles) { this.styleCache.set(cacheKey, directStyles); console.log( `[TemplateLoader] [SUCCESS] Loaded styles directly for page: ${pageId} (language: ${language})` ); return directStyles; } } catch (error) { console.warn( `[TemplateLoader] [WARN] Direct styles loading failed for ${pageId}, falling back to API:`, error.message ); } } try { // Use GET with query parameters to match APIM internal swagger specification const queryParams = new URLSearchParams({ url: pageId, content_type: 'styles', language, }); const response = await fetch( `${this.options.functionAppUrl}/besper-site/page?${queryParams}`, { method: 'GET', headers: { Accept: 'application/json', 'Cache-Control': 'public, max-age=86400', }, } ); if (!response.ok) { throw new Error( `Failed to load styles for ${pageId}: ${response.status}` ); } const data = await response.json(); if (data.success && data.styles) { const styles = data.styles || ''; this.styleCache.set(cacheKey, styles); console.log( `[TemplateLoader] [SUCCESS] Loaded styles for page: ${pageId} (language: ${language}, sources: ${data.style_sources?.join(', ') || 'none'})` ); return styles; } else { console.warn( `[TemplateLoader] [WARN] No styles found for page: ${pageId} (language: ${language})` ); return ''; } } catch (error) { console.error( `[TemplateLoader] [ERROR] Error loading styles for ${pageId} (language: ${language}):`, error ); return this.getFallbackStyles(pageId); } } /** * Load styles directly from storage structure (no API call) */ async loadDirectStyles(pageId, language = 'en') { // Check if we have the storage structure available if (typeof window !== 'undefined' && window.besperStorageAssets) { const languageKey = language === 'en' ? 'styles' : `styles_${language}`; const styles = window.besperStorageAssets[pageId]?.[languageKey]; if (styles) { return styles; } // Fallback to default language if specific language not found if (language !== 'en') { const fallbackStyles = window.besperStorageAssets[pageId]?.styles; if (fallbackStyles) { console.log( `[TemplateLoader] 🎨 Using fallback styles for ${pageId} (requested: ${language}, using: en)` ); return fallbackStyles; } } } // If no direct storage access, return null to trigger API fallback return null; } /** * Inject styles into the page */ injectStyles(pageId, styles) { // Remove existing styles for this page const existingStyle = document.getElementById(`besper-styles-${pageId}`); if (existingStyle) { existingStyle.remove(); } // Create and inject new style element const styleElement = document.createElement('style'); styleElement.id = `besper-styles-${pageId}`; styleElement.textContent = styles; document.head.appendChild(styleElement); } /** * Load both template and styles for a page */ async loadPageAssets(pageId, language = 'en') { const startTime = performance.now(); try { // Load template and styles in parallel for optimal performance const [template, styles] = await Promise.all([ this.loadTemplate(pageId, language), this.loadStyles(pageId, language), ]); // Inject styles immediately this.injectStyles(pageId, styles); const loadTime = Math.round(performance.now() - startTime); console.log( `[TemplateLoader] [INIT] Page assets loaded for ${pageId} (language: ${language}) in ${loadTime}ms` ); return { template, styles }; } catch (error) { console.error( `[TemplateLoader] [ERROR] Error loading page assets for ${pageId} (language: ${language}):`, error ); throw error; } } /** * Fallback template for when server-side template is unavailable */ /** * Enhanced fallback template for better user experience when APIM fails */ getFallbackTemplate(pageId) { return ` <div class="bsp-page-container bsp-${pageId}"> <div class="container"> <div class="card"> <header class="bsp-page-header"> <h1 class="skeleton-text skeleton-loading" data-skeleton-width="60%">${this.getPageTitle(pageId)}</h1> <p class="bsp-text-secondary skeleton-text skeleton-loading" data-skeleton-width="40%"></p> </header> <main class="bsp-page-content"> <div id="page-content-area"> <div class="loading-message"> <div class="loading-spinner"></div> </div> <!-- Skeleton loading structure for smooth transitions --> <div class="skeleton-loading" style="height: 20px; margin: 1rem 0; width: 80%;"></div> <div class="skeleton-loading" style="height: 20px; margin: 1rem 0; width: 60%;"></div> <div class="skeleton-loading" style="height: 100px; margin: 2rem 0; width: 100%;"></div> <div class="skeleton-loading" style="height: 40px; margin: 1rem 0; width: 40%;"></div> </div> </main> </div> </div> </div> `; } /** * Fallback styles for when server-side styles are unavailable */ /** * Enhanced fallback styles for better user experience when APIM fails */ getFallbackStyles(pageId) { return ` /* B-esper Fallback Styles - Ensure functionality even when APIM fails */ .bsp-${pageId} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; line-height: 1.6; color: #1a202c; background: #f7fafc; } .bsp-${pageId} .container { max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; } .bsp-${pageId} .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 2rem; margin-bottom: 1.5rem; } .bsp-${pageId} h1, .bsp-${pageId} h2 { color: #022d54; margin-bottom: 1rem; } .bsp-${pageId} .btn { background: #2563eb; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: background-color 0.2s; } .bsp-${pageId} .btn:hover { background: #1e40af; } .bsp-${pageId} .btn:disabled { background: #94a3b8; cursor: not-allowed; } .bsp-${pageId} .loading-message { text-align: center; padding: 3rem 2rem; color: #6c757d; } .bsp-${pageId} .loading-message h2 { color: #022d54; margin-bottom: 1rem; } .bsp-${pageId} .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #e0e0e0; border-top: 2px solid #022d54; border-radius: 50%; animation: spin 1s linear infinite; margin-top: 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Skeleton loading styles included in fallback */ .bsp-${pageId} .skeleton-loading { background: linear-gradient(90deg, #e2e8f0 25%, #f5f5f5 50%, #e2e8f0 75%); background-size: 200% 100%; animation: skeleton-pulse 1.5s ease-in-out infinite; border-radius: 4px; } @keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .bsp-${pageId} .fallback-loading { text-align: center; padding: 2rem; color: #666; } `; } /** * Get human-readable page title */ getPageTitle(pageId) { const titles = { 'manage-workspace': 'Workspace Management', 'contact-us': 'Contact Us', home: 'Home', 'about-us': 'About Us', }; return ( titles[pageId] || pageId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) ); } /** * Clear template and style caches */ clearCache() { this.templateCache.clear(); this.styleCache.clear(); console.log('[TemplateLoader] [CLEANUP] Cache cleared'); } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = TemplateLoaderService; } else if (typeof window !== 'undefined') { window.TemplateLoaderService = TemplateLoaderService; }