UNPKG

svarog-ui-core

Version:

Core components of Svarog UI without themes

552 lines (477 loc) 16.3 kB
// src/components/MuchandyHero/MuchandyHeroContainer.js import MuchandyHero from './MuchandyHero.js'; import { PhoneRepairFormContainer } from '../PhoneRepairForm/index.js'; import { UsedPhonePriceFormContainer } from '../UsedPhonePriceForm/index.js'; import { createElement } from '../../utils/componentFactory.js'; /** * Container for MuchandyHero that handles async form initialization * @param {Object} props - Container properties * @returns {Object} Container component API */ const MuchandyHeroContainer = (props) => { const { repairService, buybackService, onRepairPriceChange, onScheduleClick, onBuybackPriceChange, onSubmit, backgroundImageUrl, title, subtitle, defaultTab, className, blurIntensity, overlayOpacity, usedPhoneHref, repairFormLabels, buybackFormLabels, ...otherProps } = props; // Validate required services if (!repairService) { throw new Error('MuchandyHeroContainer: repairService is required'); } if (!buybackService) { throw new Error('MuchandyHeroContainer: buybackService is required'); } // Container state let hero = null; let repairForm = null; let buybackForm = null; let containerElement = null; let isInitialized = false; let initializationError = null; let destroyed = false; let initializationPromise = null; // Create placeholder/loading forms immediately for better UX const createLoadingForm = (formType) => { const loadingElement = createElement('div', { classes: [`${formType}-form-loading`, 'form-loading-placeholder'], attributes: { 'aria-busy': 'true', 'aria-label': `Loading ${formType} form`, }, }); loadingElement.innerHTML = ` <div style="padding: 20px; text-align: center; background: rgba(255,255,255,0.1); border-radius: 8px; min-height: 200px; display: flex; flex-direction: column; justify-content: center;"> <div style="margin-bottom: 12px;"> <div style="width: 24px; height: 24px; border: 2px solid #ffffff40; border-top: 2px solid #fff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div> </div> <div style="color: #fff; font-size: 14px; opacity: 0.9;">Loading ${formType === 'repair' ? 'repair' : 'buyback'} options...</div> </div> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> `; return { getElement: () => loadingElement, destroy: () => loadingElement.remove(), update: () => {}, setState: () => {}, getCurrentState: () => ({ loading: true }), }; }; // Create error form for when services fail const createErrorForm = (formType, error) => { console.log(`Creating error form for ${formType}:`, error.message); const errorElement = createElement('div', { classes: [`${formType}-form-error`, 'form-error-placeholder'], attributes: { role: 'alert' }, }); errorElement.innerHTML = ` <div style="padding: 20px; text-align: center; background: rgba(220,53,69,0.1); border: 1px solid rgba(220,53,69,0.3); border-radius: 8px; min-height: 200px; display: flex; flex-direction: column; justify-content: center;"> <div style="margin-bottom: 12px; font-size: 24px;">⚠️</div> <div style="color: #fff; font-size: 14px; margin-bottom: 12px;">Failed to load ${formType === 'repair' ? 'repair' : 'buyback'} options</div> <div style="color: #fff; font-size: 12px; opacity: 0.8; margin-bottom: 16px;">${error.message}</div> <button onclick="window.location.reload()" style="padding: 8px 16px; background: rgba(220,53,69,0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;"> Retry </button> </div> `; const errorForm = { getElement: () => { console.log(`Error form getElement called for ${formType}`); return errorElement; }, destroy: () => errorElement.remove(), update: () => {}, setState: () => {}, getCurrentState: () => ({ error }), }; console.log( `Error form created for ${formType}, element classes:`, errorElement.className ); return errorForm; }; // Update container element content - simplified since we show hero immediately const updateContainerContent = () => { if (!containerElement) return; containerElement.innerHTML = ''; if (hero) { // Always show the hero - it handles its own loading/error states via forms containerElement.appendChild(hero.getElement()); } else { // Fallback loading if hero hasn't been created yet const fallbackLoading = createElement('div', { classes: ['muchandy-hero-container__loading'], attributes: { 'aria-busy': 'true' }, }); fallbackLoading.innerHTML = ` <div style="text-align: center; padding: 40px;"> <div style="font-size: 18px; color: #666;">Initializing...</div> </div> `; containerElement.appendChild(fallbackLoading); } }; // Initialize forms asynchronously with progressive loading const initializeForms = async () => { try { // Create MuchandyHero immediately with loading forms for better UX createInitialHeroWithLoadingForms(); // Simple parallel loading with individual error handling const repairPromise = repairService .fetchManufacturers() .then((data) => ({ success: true, data })) .catch((error) => ({ success: false, error })); const buybackPromise = buybackService .fetchManufacturers() .then((data) => ({ success: true, data })) .catch((error) => ({ success: false, error })); const [repairResult, buybackResult] = await Promise.all([ repairPromise, buybackPromise, ]); if (destroyed) return; // Handle repair service result if (repairResult.success) { console.log('Repair service succeeded, creating real form'); const cachedRepairService = createCachedService( repairService, repairResult.data ); repairForm = PhoneRepairFormContainer({ service: cachedRepairService, onPriceChange: onRepairPriceChange, onScheduleClick, usedPhoneHref, labels: repairFormLabels, }); // Wait for real form to be ready await waitForFormReady(repairForm, 'repair'); } else { console.log( 'Repair service failed, creating error form:', repairResult.error.message ); repairForm = createErrorForm('repair', repairResult.error); } // Handle buyback service result if (buybackResult.success) { console.log('Buyback service succeeded, creating real form'); const cachedBuybackService = createCachedService( buybackService, buybackResult.data ); buybackForm = UsedPhonePriceFormContainer({ service: cachedBuybackService, onPriceChange: onBuybackPriceChange, onSubmit, labels: buybackFormLabels, }); // Wait for real form to be ready await waitForFormReady(buybackForm, 'buyback'); } else { console.log( 'Buyback service failed, creating error form:', buybackResult.error.message ); buybackForm = createErrorForm('buyback', buybackResult.error); } if (destroyed) return; // Update hero with final forms (real or error) updateHeroForms(); // Container is initialized even if some services failed isInitialized = true; // Only set global error if ALL services failed if (!repairResult.success && !buybackResult.success) { initializationError = repairResult.error || buybackResult.error; } } catch (error) { if (destroyed) return; console.error('MuchandyHeroContainer initialization error:', error); initializationError = error; // Even with errors, we might still have a hero with error forms if (hero) { isInitialized = true; } } }; // Create initial hero with loading forms for immediate display const createInitialHeroWithLoadingForms = () => { if (destroyed) return; // Create loading forms repairForm = createLoadingForm('repair'); buybackForm = createLoadingForm('buyback'); // Create MuchandyHero immediately hero = MuchandyHero({ repairForm, buybackForm, backgroundImageUrl, title, subtitle, defaultTab, className, blurIntensity, overlayOpacity, ...otherProps, }); updateContainerContent(); }; // Update hero component with new forms (loading -> real -> error) const updateHeroForms = () => { if (!hero || destroyed) return; console.log('Updating hero forms:', { repairForm: repairForm?.getElement?.()?.className || 'no repair form', buybackForm: buybackForm?.getElement?.()?.className || 'no buyback form', }); // Use the hero's update method to replace forms hero.update({ repairForm, buybackForm, }); }; // Create a service wrapper that caches the first fetchManufacturers call const createCachedService = (originalService, cachedManufacturers) => { let firstCall = true; return { ...originalService, fetchManufacturers: () => { if (firstCall) { firstCall = false; return Promise.resolve(cachedManufacturers); } return originalService.fetchManufacturers(); }, }; }; // Wait for form to have data or error (simplified since we pre-test services) const waitForFormReady = async (formContainer, formType, timeout = 2000) => { const startTime = Date.now(); return new Promise((resolve, reject) => { let checkInterval; const cleanup = () => { if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } }; const checkReady = () => { if (destroyed) { cleanup(); reject(new Error('Container destroyed')); return; } try { const state = formContainer.getFormState(); // Check if manufacturers are loaded (minimum requirement) // Since we pre-tested the service and use cached data, this should be quick if (state.manufacturers && state.manufacturers.length > 0) { cleanup(); resolve(); return; } // Check if form has error (fallback error handling) if (state.error && Object.keys(state.error).length > 0) { cleanup(); // Extract any available error message, but this is secondary // since service errors are handled at the service level now let errorMessage = `${formType} form initialization failed`; if (state.error instanceof Error) { errorMessage = state.error.message; } else if (state.error.message) { errorMessage = state.error.message; } else if (state.error.manufacturers instanceof Error) { errorMessage = state.error.manufacturers.message; } else if (typeof state.error === 'string') { errorMessage = state.error; } reject(new Error(errorMessage)); return; } // Check for timeout if (Date.now() - startTime > timeout) { cleanup(); reject(new Error(`${formType} form initialization timeout`)); return; } } catch (error) { cleanup(); reject(error); } }; // Check immediately checkReady(); // Then check every 50ms checkInterval = setInterval(checkReady, 50); }); }; // Start initialization immediately with progressive loading initializationPromise = initializeForms().catch((error) => { // Error is already handled in initializeForms with individual form error states initializationError = error; }); // Container API return { /** * Get the container element * @returns {HTMLElement} */ getElement() { if (!containerElement) { containerElement = createElement('div', { classes: ['muchandy-hero-container'], }); updateContainerContent(); } return containerElement; }, /** * Update hero props (works immediately since hero is created right away) * @param {Object} newProps */ update(newProps) { if (!hero) { console.warn('MuchandyHeroContainer: Hero not yet created'); return this; } // Filter out container-specific props const { repairService: _rs, buybackService: _bs, onRepairPriceChange: _rpc, onScheduleClick: _sc, onBuybackPriceChange: _bpc, onSubmit: _os, repairFormLabels: _rfl, buybackFormLabels: _bfl, loadingComponent: _lc, errorComponent: _ec, ...heroProps } = newProps; hero.update(heroProps); return this; }, /** * Get initialization state * @returns {Object} */ getState() { // Check if forms are real forms (not loading placeholders) const hasRealRepairForm = repairForm && repairForm.getElement && !repairForm .getElement() .classList?.contains('form-loading-placeholder'); const hasRealBuybackForm = buybackForm && buybackForm.getElement && !buybackForm .getElement() .classList?.contains('form-loading-placeholder'); // Forms are ready if we have moved past the loading state (even if some are errors) const formsReady = isInitialized && hasRealRepairForm && hasRealBuybackForm; return { isInitialized, hasError: !!initializationError, error: initializationError, hasHero: !!hero, formsReady, }; }, /** * Get the hero instance (if initialized) * @returns {Object|null} */ getHero() { return hero; }, /** * Get form containers * @returns {Object} */ getForms() { return { repairForm, buybackForm }; }, /** * Retry initialization after error */ async retry() { if (destroyed) return; // Reset error state initializationError = null; isInitialized = false; // If hero doesn't exist, recreate with loading forms if (!hero) { createInitialHeroWithLoadingForms(); } else { // Replace current forms with loading forms repairForm = createLoadingForm('repair'); buybackForm = createLoadingForm('buyback'); updateHeroForms(); } // Restart initialization initializationPromise = initializeForms().catch((error) => { // Error handling is now done inside initializeForms console.warn('Retry completed with some errors:', error); }); await initializationPromise; }, /** * Wait for initialization to complete (for testing) */ async waitForInitialization() { if (initializationPromise) { try { await initializationPromise; } catch (_error) { // Error is already handled } } }, /** * Clean up resources */ destroy() { destroyed = true; if (hero) { hero.destroy(); hero = null; } if (repairForm) { repairForm.destroy(); repairForm = null; } if (buybackForm) { buybackForm.destroy(); buybackForm = null; } if (containerElement) { containerElement.remove(); containerElement = null; } isInitialized = false; initializationError = null; initializationPromise = null; }, }; }; // Export as default export default MuchandyHeroContainer;