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