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
JavaScript
/**
* 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;
}