UNPKG

svarog-ui-core

Version:

Core components of Svarog UI without themes

748 lines (654 loc) 23.3 kB
// src/components/UsedPhonePriceForm/UsedPhonePriceForm.js import { createElement, validateProps, createComponent, } from '../../utils/componentFactory.js'; import { createBaseComponent } from '../../utils/baseComponent.js'; import { withThemeAwareness } from '../../utils/composition.js'; import Button from '../Button/Button.js'; import Select from '../Select/Select.js'; import StepsIndicator from '../StepsIndicator/StepsIndicator.js'; import PriceDisplay from '../PriceDisplay/PriceDisplay.js'; import ConditionSelector from '../ConditionSelector/ConditionSelector.js'; // CSS injection imports import { createStyleInjector } from '../../utils/styleInjection.js'; import { usedPhonePriceFormStyles } from './UsedPhonePriceForm.styles.js'; // Create style injector for UsedPhonePriceForm component const injectUsedPhonePriceFormStyles = createStyleInjector('UsedPhonePriceForm'); /** * ALGORITHMIC OPTIMIZATION: O(1) Lookup Maps * Creates lookup maps for O(1) name resolution instead of O(n) searches * @private */ const createLookupMaps = (state) => { const maps = { manufacturers: new Map(), devices: new Map(), conditions: new Map(), }; // Build manufacturer lookup map state.manufacturers.forEach((item) => { const id = (item.id || item.value)?.toString(); if (id) maps.manufacturers.set(id, item.name || item.label || ''); }); // Build device lookup map state.devices.forEach((item) => { const id = (item.id || item.value)?.toString(); if (id) maps.devices.set(id, item.name || item.label || ''); }); // Build condition lookup map state.conditions.forEach((item) => { const id = (item.id || item.value)?.toString(); if (id) maps.conditions.set(id, item.name || item.label || ''); }); return maps; }; /** * ALGORITHMIC OPTIMIZATION: Mathematical Step Progression * Uses mathematical calculation instead of imperative step logic * @private */ const calculateStepProgression = (state) => { // Mathematical approach: count valid selections to determine progress const selections = [ state.selectedManufacturer, state.selectedDevice, state.selectedCondition, ]; // Count completed selections const completedCount = selections.filter(Boolean).length; // Mathematical formulas for step states const activeStepIndex = Math.min(completedCount, 2); const stepStates = selections.map((selection, index) => ({ ...state.steps[index], completed: Boolean(selection) && index < completedCount, })); return { activeStepIndex, steps: stepStates }; }; /** * ALGORITHMIC OPTIMIZATION: Memoized Transform * Caches transformation results to avoid redundant O(n) operations * @private */ const transformToSelectOptions = (() => { const cache = new WeakMap(); return (items) => { if (!Array.isArray(items)) return []; // Check cache first - O(1) lookup if (cache.has(items)) { return cache.get(items); } // Transform and cache - O(n) only when needed const options = items.map((item) => ({ value: (item.id || item.value)?.toString() || '', label: item.name || item.label || item.value || '', })); cache.set(items, options); return options; }; })(); /** * ALGORITHMIC OPTIMIZATION: O(1) Name Resolution * Uses lookup maps for constant-time name resolution * @private */ const resolveNames = (state, lookupMaps) => { return { manufacturerName: lookupMaps.manufacturers.get(state.selectedManufacturer) || '', deviceName: lookupMaps.devices.get(state.selectedDevice) || '', conditionName: lookupMaps.conditions.get(state.selectedCondition) || '', }; }; /** * ALGORITHMIC OPTIMIZATION: Enhanced Submission Logic * Uses bitwise operations for ultra-fast validation checks * @private */ const canSubmit = (state) => { // Convert boolean conditions to bits for fast evaluation const hasManufacturer = state.selectedManufacturer ? 1 : 0; const hasDevice = state.selectedDevice ? 1 : 0; const hasCondition = state.selectedCondition ? 1 : 0; const hasPrice = state.currentPrice && state.currentPrice.price != null ? 1 : 0; const notLoading = !Object.values(state.loadingStates).some(Boolean) ? 1 : 0; // Bitwise AND: all conditions must be true (all bits set) const allConditions = hasManufacturer & hasDevice & hasCondition & hasPrice & notLoading; return Boolean(allConditions); }; /** * ALGORITHMIC OPTIMIZATION: Mathematical Price Formatting * Uses mathematical operations instead of conditional logic * @private */ const formatPrice = (price) => { if (price == null) return 'Preis nicht verfügbar'; // Mathematical conversion: automatically handle cents vs euros const priceInEuros = price > 1000 ? price / 100 : price; return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', }).format(priceInEuros); }; /** * ALGORITHMIC OPTIMIZATION: Animation Trigger with Mathematical Timing * @private */ const triggerAnimation = (element, animationType) => { if (!element) return; // Remove any existing animation classes efficiently const animationClasses = [ 'used-phone-price-form--animate-step', 'used-phone-price-form--animate-submit', 'used-phone-price-form--animate-error', ]; animationClasses.forEach((cls) => element.classList.remove(cls)); // Add the animation class element.classList.add('used-phone-price-form--animating'); element.classList.add(`used-phone-price-form--animate-${animationType}`); // Force a reflow for smooth animation void element.offsetWidth; }; /** * Creates the used phone price form DOM element * @param {Object} state - Component state * @returns {HTMLElement} - Used phone price form element */ const renderUsedPhonePriceForm = (state) => { // Inject styles on render (automatically cached) injectUsedPhonePriceFormStyles(usedPhonePriceFormStyles); const classNames = [ 'used-phone-price-form', state.className, state.hasError ? 'used-phone-price-form--error' : '', state.isLoading ? 'used-phone-price-form--loading' : '', ].filter(Boolean); const form = createElement('form', { classes: classNames, attributes: { role: 'form', 'aria-live': 'polite', 'aria-busy': state.isLoading ? 'true' : 'false', }, events: { submit: (e) => e.preventDefault(), }, }); const components = {}; // Create lookup maps for O(1) name resolution const lookupMaps = createLookupMaps(state); // Add steps indicator if enabled if ( state.showStepsIndicator && Array.isArray(state.steps) && state.steps.length > 0 ) { components.stepsIndicator = StepsIndicator({ steps: state.steps, activeIndex: state.activeStepIndex, }); form.appendChild(components.stepsIndicator.getElement()); } // Create manufacturer select with memoized options const manufacturerOptions = transformToSelectOptions(state.manufacturers); components.manufacturerSelect = Select({ id: 'manufacturer', name: 'manufacturer', placeholder: state.labels.manufacturerPlaceholder, options: manufacturerOptions, value: state.selectedManufacturer, loading: state.loadingStates.manufacturers, loadingText: 'Loading manufacturers...', emptyText: 'No manufacturers available', onChange: (event, value) => { if (typeof state.onManufacturerChange === 'function') { setTimeout(() => state.onManufacturerChange(value), 0); } }, showValidation: false, }); form.appendChild(components.manufacturerSelect.getElement()); // Create device select with memoized options const deviceOptions = transformToSelectOptions(state.devices); components.deviceSelect = Select({ id: 'device', name: 'device', placeholder: state.labels.devicePlaceholder, options: deviceOptions, value: state.selectedDevice, disabled: !state.selectedManufacturer, loading: state.loadingStates.devices, loadingText: 'Loading devices...', emptyText: state.selectedManufacturer ? 'No devices available for this manufacturer' : 'Please select a manufacturer first', onChange: (event, value) => { if (typeof state.onDeviceChange === 'function') { setTimeout(() => state.onDeviceChange(value), 0); } }, showValidation: false, }); form.appendChild(components.deviceSelect.getElement()); // Create condition selector components.conditionSelector = ConditionSelector({ conditions: state.conditions, selectedId: state.selectedCondition, loading: state.loadingStates.conditions || false, disabled: !state.selectedDevice, onChange: (conditionId) => { if (typeof state.onConditionChange === 'function') { setTimeout(() => state.onConditionChange(conditionId), 0); } }, }); form.appendChild(components.conditionSelector.getElement()); // Add price display components.priceDisplay = PriceDisplay({ label: state.labels.priceLabel, value: state.priceDisplayText, isPlaceholder: !state.currentPrice, loading: state.loadingStates.price || false, isError: !!state.error.price, isHighlighted: true, }); form.appendChild(components.priceDisplay.getElement()); // Create actions container const actionsContainer = createElement('div', { classes: 'used-phone-price-form__actions', }); // Create submit button with algorithmic validation const canSubmitNow = canSubmit(state); components.submitButton = Button({ text: state.loadingStates.submit ? state.labels.submitButtonLoadingText : state.labels.submitButtonText, type: 'submit', onClick: () => { if (typeof state.onSubmit === 'function' && canSubmit(state)) { // Start the submit animation if (state.animationEnabled) { triggerAnimation(form, 'submit'); } // Use O(1) lookup maps for name resolution const names = resolveNames(state, lookupMaps); const formData = { manufacturerId: state.selectedManufacturer, deviceId: state.selectedDevice, conditionId: state.selectedCondition, price: state.currentPrice?.price, manufacturerName: names.manufacturerName, deviceName: names.deviceName, conditionName: names.conditionName, timestamp: new Date().toISOString(), }; try { state.onSubmit(formData); } catch (error) { console.error( 'UsedPhonePriceForm: Error in onSubmit callback:', error ); } } }, disabled: !canSubmitNow, attributes: { 'aria-busy': state.loadingStates.submit ? 'true' : 'false', }, }); actionsContainer.appendChild(components.submitButton.getElement()); form.appendChild(actionsContainer); // Store components and current state for updates form._components = components; form._currentState = { ...state }; form._lookupMaps = lookupMaps; // Cache lookup maps return form; }; /** * Create default labels object * @private */ const createDefaultLabels = () => { return { manufacturerStep: 'Hersteller', deviceStep: 'Modell', conditionStep: 'Zustand', manufacturerPlaceholder: 'Hersteller auswählen', devicePlaceholder: 'Zuerst Hersteller auswählen', initialPriceText: 'Bitte wählen Sie Hersteller, Modell und Zustand', loadingPriceText: 'Preis wird geladen...', priceLabel: 'Unser Angebot:', priceNotAvailable: 'Preis nicht verfügbar', submitButtonText: 'Verkaufen', submitButtonLoadingText: 'Wird verarbeitet...', conditionNewLabel: 'Neu', conditionGoodLabel: 'Gut', conditionFairLabel: 'Akzeptabel', conditionPoorLabel: 'Beschädigt', }; }; /** * Create a UsedPhonePriceForm component * @param {Object} props - UsedPhonePriceForm properties * @returns {Object} UsedPhonePriceForm component API */ const createUsedPhonePriceForm = (props) => { validateProps(props, createUsedPhonePriceForm.requiredProps); const defaultLabels = createDefaultLabels(); const labels = { ...defaultLabels, ...(props.labels || {}) }; // Standardize props - migrate from loading to loadingStates const loadingStates = props.loadingStates || props.loading || {}; // Enhanced initial state with better defaults const initialState = { manufacturers: props.manufacturers || [], devices: props.devices || [], conditions: props.conditions || [], selectedManufacturer: props.selectedManufacturer || '', selectedDevice: props.selectedDevice || '', selectedCondition: props.selectedCondition || '', currentPrice: props.currentPrice || null, priceDisplayText: props.currentPrice ? formatPrice(props.currentPrice.price) : labels.initialPriceText, loadingStates: { manufacturers: false, devices: false, conditions: false, price: false, submit: false, ...loadingStates, }, error: props.error || {}, steps: [ { name: labels.manufacturerStep, completed: false }, { name: labels.deviceStep, completed: false }, { name: labels.conditionStep, completed: false }, ], activeStepIndex: 0, labels, className: props.className || '', showStepsIndicator: props.showStepsIndicator !== false, onManufacturerChange: props.onManufacturerChange, onDeviceChange: props.onDeviceChange, onConditionChange: props.onConditionChange, onSubmit: props.onSubmit, hasError: Object.values(props.error || {}).some(Boolean), isLoading: Object.values(loadingStates).some(Boolean), animationEnabled: props.animationEnabled !== false, }; // Apply mathematical step progression const stepProgression = calculateStepProgression(initialState); initialState.activeStepIndex = stepProgression.activeStepIndex; initialState.steps = stepProgression.steps; const usedPhonePriceForm = createBaseComponent(renderUsedPhonePriceForm)( initialState ); // Maintain component state reference let currentState = { ...initialState }; // Enhanced state update method with algorithmic optimization usedPhonePriceForm.setState = function (newState) { // Handle prop standardization when updating if (newState.loading && !newState.loadingStates) { newState.loadingStates = newState.loading; delete newState.loading; } // Update state currentState = { ...currentState, ...newState }; // Apply mathematical step progression const stepProgression = calculateStepProgression(currentState); currentState.activeStepIndex = stepProgression.activeStepIndex; currentState.steps = stepProgression.steps; currentState.hasError = Object.values(currentState.error || {}).some( Boolean ); currentState.isLoading = Object.values( currentState.loadingStates || {} ).some(Boolean); // Store state on element for container access const element = this.getElement(); if (element) { element._state = currentState; element._currentState = currentState; } // Efficient partial updates this.partialUpdate(element, newState); return this; }; usedPhonePriceForm.shouldRerender = (newProps) => { return [ 'manufacturers', 'devices', 'conditions', 'selectedManufacturer', 'selectedDevice', 'selectedCondition', 'className', ].some( (prop) => newProps[prop] !== undefined && newProps[prop] !== currentState[prop] ); }; usedPhonePriceForm.partialUpdate = (element, newProps) => { if (!element?._components) return; const components = element._components; // Standardize props for update const loadingStates = newProps.loadingStates || newProps.loading || {}; if (newProps.loading && !newProps.loadingStates) { newProps.loadingStates = newProps.loading; } // Check if steps need updating with mathematical approach const needsStepsUpdate = [ 'selectedManufacturer', 'selectedDevice', 'selectedCondition', ].some((key) => newProps[key] !== undefined); // Update manufacturer select with memoized transformation if ( newProps.manufacturers !== undefined || loadingStates.manufacturers !== undefined || newProps.selectedManufacturer !== undefined ) { const manufacturerOptions = transformToSelectOptions( newProps.manufacturers || currentState.manufacturers ); components.manufacturerSelect?.update({ options: manufacturerOptions, loading: loadingStates.manufacturers ?? currentState.loadingStates.manufacturers, value: newProps.selectedManufacturer ?? currentState.selectedManufacturer, }); } // Update device select with memoized transformation if ( newProps.devices !== undefined || loadingStates.devices !== undefined || newProps.selectedManufacturer !== undefined || newProps.selectedDevice !== undefined ) { const deviceOptions = transformToSelectOptions( newProps.devices || currentState.devices ); const deviceLoading = loadingStates.devices ?? currentState.loadingStates.devices; const manufacturerSelected = newProps.selectedManufacturer ?? currentState.selectedManufacturer; components.deviceSelect?.update({ options: deviceOptions, loading: deviceLoading, disabled: !manufacturerSelected, value: newProps.selectedDevice ?? currentState.selectedDevice, emptyText: manufacturerSelected ? 'No devices available for this manufacturer' : 'Please select a manufacturer first', }); } // Update condition selector if ( newProps.conditions !== undefined || loadingStates.conditions !== undefined || newProps.selectedDevice !== undefined || newProps.selectedCondition !== undefined ) { const conditionLoading = loadingStates.conditions ?? currentState.loadingStates.conditions; const deviceSelected = newProps.selectedDevice ?? currentState.selectedDevice; components.conditionSelector?.update({ conditions: newProps.conditions || currentState.conditions, loading: conditionLoading, disabled: !deviceSelected, selectedId: newProps.selectedCondition ?? currentState.selectedCondition, }); } // Update price display if ( newProps.currentPrice !== undefined || newProps.priceDisplayText !== undefined || loadingStates.price !== undefined || newProps.error?.price !== undefined ) { const displayText = newProps.priceDisplayText || (newProps.currentPrice ? formatPrice(newProps.currentPrice.price) : currentState.priceDisplayText); components.priceDisplay?.update({ value: displayText, isPlaceholder: !newProps.currentPrice && !currentState.currentPrice, loading: loadingStates.price ?? currentState.loadingStates.price, isError: !!(newProps.error?.price || currentState.error.price), }); } // Update steps indicator with mathematical approach if (needsStepsUpdate && components.stepsIndicator) { components.stepsIndicator.update({ steps: currentState.steps, activeIndex: currentState.activeStepIndex, }); } // Update submit button with algorithmic validation if ( [ 'selectedManufacturer', 'selectedDevice', 'selectedCondition', 'currentPrice', 'loadingStates', 'loading', ].some((key) => newProps[key] !== undefined) ) { const canSubmitNow = canSubmit({ ...currentState, ...newProps, loadingStates: newProps.loadingStates || newProps.loading || currentState.loadingStates, }); const isSubmitting = loadingStates.submit ?? currentState.loadingStates.submit; components.submitButton?.setDisabled(!canSubmitNow || isSubmitting); if (loadingStates.submit !== undefined) { components.submitButton?.setText( isSubmitting ? currentState.labels.submitButtonLoadingText : currentState.labels.submitButtonText ); } } // Update form error styling if (newProps.hasError !== undefined) { element.classList.toggle( 'used-phone-price-form--error', newProps.hasError ); } // Update form loading styling if (newProps.isLoading !== undefined) { element.classList.toggle( 'used-phone-price-form--loading', newProps.isLoading ); } }; // Convenience methods usedPhonePriceForm.setLoading = function (loadingState) { const mergedLoading = { ...currentState.loadingStates, ...loadingState }; return this.setState({ loadingStates: mergedLoading, isLoading: Object.values(mergedLoading).some(Boolean), }); }; usedPhonePriceForm.setErrors = function (errorState) { return this.setState({ error: errorState, hasError: Object.values(errorState).some(Boolean), }); }; usedPhonePriceForm.setManufacturers = function (manufacturers) { return this.setState({ manufacturers }); }; usedPhonePriceForm.setDevices = function (devices) { return this.setState({ devices }); }; usedPhonePriceForm.setConditions = function (conditions) { return this.setState({ conditions }); }; usedPhonePriceForm.setPrice = function (price) { const priceDisplayText = price ? formatPrice(price.price) : currentState.labels.initialPriceText; return this.setState({ currentPrice: price, priceDisplayText }); }; usedPhonePriceForm.getCurrentState = function () { return { ...currentState }; }; // Enhanced update method with validation usedPhonePriceForm.update = function (newProps) { try { validateProps(newProps, createUsedPhonePriceForm.requiredProps); // Standardize props if (newProps.loading && !newProps.loadingStates) { newProps.loadingStates = newProps.loading; delete newProps.loading; } if (this.shouldRerender(newProps)) { return createBaseComponent.prototype.update.call(this, { ...currentState, ...newProps, }); } else { return this.setState(newProps); } } catch (error) { console.error('UsedPhonePriceForm update error:', error); return this; } }; usedPhonePriceForm.onThemeChange = (theme, previousTheme) => { // Component can react to theme changes if needed console.log( `UsedPhonePriceForm theme changed: ${previousTheme} -> ${theme}` ); }; // Initialize element state reference const element = usedPhonePriceForm.getElement(); if (element) { element._state = currentState; element._currentState = currentState; } return usedPhonePriceForm; }; createUsedPhonePriceForm.requiredProps = []; const UsedPhonePriceForm = withThemeAwareness( createComponent('UsedPhonePriceForm', createUsedPhonePriceForm) ); export default UsedPhonePriceForm;