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