UNPKG

svarog-ui-core

Version:

Core components of Svarog UI without themes

713 lines (623 loc) 18.7 kB
// src/components/ContactSection/ContactSection.js import { createBaseComponent } from '../../utils/baseComponent.js'; import { createStyleInjector } from '../../utils/styleInjection.js'; import { createElement } from '../../utils/componentFactory.js'; import Section from '../Section/index.js'; import Map from '../Map/index.js'; import Grid from '../Grid/index.js'; import { Form, FormGroup, FormActions } from '../Form/index.js'; import Input from '../Input/index.js'; import Textarea from '../Textarea/index.js'; import Button from '../Button/index.js'; import Checkbox from '../Checkbox/index.js'; import Typography from '../Typography/index.js'; import Link from '../Link/index.js'; import { contactSectionStyles } from './ContactSection.styles.js'; const injectStyles = createStyleInjector('ContactSection'); /** * Creates contact info display element using Link components */ const createContactInfo = (contactInfo) => { const items = []; // Company name if (contactInfo.companyName) { items.push( createElement('div', { classes: 'contact-info__item', children: [ createElement('span', { text: contactInfo.companyName, classes: 'contact-info__value', }), ], }) ); } // Address (street, zipcode, city) if (contactInfo.street || contactInfo.zipcode || contactInfo.city) { const addressParts = []; if (contactInfo.street) addressParts.push(contactInfo.street); if (contactInfo.zipcode && contactInfo.city) { addressParts.push(`${contactInfo.zipcode} ${contactInfo.city}`); } else if (contactInfo.zipcode) { addressParts.push(contactInfo.zipcode); } else if (contactInfo.city) { addressParts.push(contactInfo.city); } if (addressParts.length > 0) { items.push( createElement('div', { classes: 'contact-info__item', children: [ createElement('span', { text: 'Adresse:', classes: 'contact-info__label', }), createElement('div', { classes: 'contact-info__value contact-info__address', children: addressParts.map((part) => createElement('div', { text: part }) ), }), ], }) ); } } // Phone with Link component if (contactInfo.phone) { const phoneLink = Link({ children: contactInfo.phone, href: `tel:${contactInfo.phone.replace(/\s/g, '')}`, }); items.push( createElement('div', { classes: 'contact-info__item', children: [ createElement('span', { text: 'Telefon und E-Mail:', classes: 'contact-info__label', }), phoneLink.getElement(), ], }) ); } // Email with Link component if (contactInfo.email) { const emailLink = Link({ children: contactInfo.email, href: `mailto:${contactInfo.email}`, }); items.push( createElement('div', { classes: 'contact-info__item', children: [emailLink.getElement()], }) ); } // Hours - Enhanced to handle multiple formats if (contactInfo.hours) { const hoursElement = createHoursElement(contactInfo.hours); items.push( createElement('div', { classes: 'contact-info__item contact-info__item--hours', children: [ createElement('span', { text: 'Öffnungszeiten:', classes: 'contact-info__label', }), hoursElement, ], }) ); } return createElement('div', { classes: 'contact-info', children: items, }); }; /** * Creates a flexible hours display element * @param {string|Array|Object} hours - Hours in various formats * @returns {HTMLElement} Hours display element */ const createHoursElement = (hours) => { // Case 1: Array of strings if (Array.isArray(hours)) { return createElement('ul', { classes: 'contact-info__hours-list', children: hours.map((hour) => createElement('li', { text: hour, classes: 'contact-info__hours-item', }) ), }); } // Case 2: Object with day/time pairs if (typeof hours === 'object' && !Array.isArray(hours)) { const hoursList = []; Object.entries(hours).forEach(([day, time]) => { hoursList.push( createElement('li', { classes: 'contact-info__hours-item', children: [ createElement('span', { text: `${day}:`, classes: 'contact-info__hours-day', }), createElement('span', { text: time, classes: 'contact-info__hours-time', }), ], }) ); }); return createElement('ul', { classes: 'contact-info__hours-list', children: hoursList, }); } // Case 3: String with line breaks if (typeof hours === 'string' && hours.includes('\n')) { const lines = hours.split('\n').filter((line) => line.trim()); return createHoursElement(lines); // Recursively handle as array } // Case 4: String with <br> tags if (typeof hours === 'string' && hours.includes('<br')) { const lines = hours.split(/<br\s*\/?>/i).filter((line) => line.trim()); return createHoursElement(lines); // Recursively handle as array } // Case 5: Simple string (fallback) return createElement('span', { text: hours, classes: 'contact-info__value', }); }; /** * Creates a ContactSection component using Svarog components */ const createContactSection = (props = {}) => { // Normalize props with updated contact info structure const normalizedProps = { // Section props title: 'Contact Us', description: 'Get in touch with us', variant: null, backgroundColor: null, className: '', // Map props apiKey: null, latitude: 48.1417262, longitude: 11.5609816, locationName: 'Our Location', placeId: null, googleMapsUrl: null, // Updated contact info structure contactInfo: { companyName: '', street: '', zipcode: '', city: '', phone: '', email: '', hours: '', }, // Form configuration formTitle: 'Send us a message', submitButtonText: 'Send Message', showNameField: true, showEmailField: true, showPhoneField: false, showSubjectField: true, showMessageField: true, showPrivacyCheckbox: true, showNewsletterCheckbox: true, privacyPolicyUrl: '/datenschutz', privacyText: 'Ich stimme der Datenschutzerklärung zu', newsletterText: 'Ich möchte den Newsletter erhalten', // Event handlers onSubmit: null, onChange: null, // Layout options mapPosition: 'left', // 'left' or 'right' mobileLayout: 'stack', // 'stack' or 'reverse' ...props, }; // Keep our own state reference let currentState = { ...normalizedProps }; // Component instances storage let mapComponent = null; let formComponent = null; let formFields = {}; let sectionComponent = null; let gridComponent = null; let linkComponents = []; // Track Link components for cleanup // Create base component with render function const baseComponent = createBaseComponent((state) => { // Update our state reference currentState = { ...state }; // Clear previous link components linkComponents.forEach((link) => link.destroy()); linkComponents = []; // Inject styles injectStyles(contactSectionStyles); // Create map component mapComponent = Map({ apiKey: state.apiKey, latitude: state.latitude, longitude: state.longitude, title: state.locationName, placeId: state.placeId, googleMapsUrl: state.googleMapsUrl, shopInfo: state.contactInfo, }); // Create form elements using Svarog components properly const formElements = []; formFields = {}; // Form title using Typography if (state.formTitle) { formElements.push( Typography({ as: 'h3', children: state.formTitle, }).getElement() ); } // Name field if (state.showNameField) { formFields.name = Input({ id: 'contact-name', name: 'name', placeholder: 'Ihr Name', required: true, }); formElements.push( FormGroup({ label: 'Name', field: formFields.name, id: 'contact-name', required: true, }).getElement() ); } // Email field if (state.showEmailField) { formFields.email = Input({ id: 'contact-email', name: 'email', type: 'email', placeholder: 'ihre@email.de', required: true, }); formElements.push( FormGroup({ label: 'E-Mail', field: formFields.email, id: 'contact-email', required: true, }).getElement() ); } // Phone field if (state.showPhoneField) { formFields.phone = Input({ id: 'contact-phone', name: 'phone', type: 'tel', placeholder: 'Ihre Telefonnummer', }); formElements.push( FormGroup({ label: 'Telefon', field: formFields.phone, id: 'contact-phone', }).getElement() ); } // Subject field if (state.showSubjectField) { formFields.subject = Input({ id: 'contact-subject', name: 'subject', placeholder: 'Betreff', required: true, }); formElements.push( FormGroup({ label: 'Betreff', field: formFields.subject, id: 'contact-subject', required: true, }).getElement() ); } // Message field if (state.showMessageField) { formFields.message = Textarea({ id: 'contact-message', name: 'message', placeholder: 'Ihre Nachricht...', required: true, rows: 5, autoResize: true, showCharCount: true, maxLength: 1000, }); formElements.push( FormGroup({ label: 'Nachricht', field: formFields.message, id: 'contact-message', required: true, }).getElement() ); } // Privacy policy checkbox if (state.showPrivacyCheckbox) { // Create the privacy policy link const privacyLink = Link({ children: 'Datenschutzerklärung', href: state.privacyPolicyUrl, target: '_blank', underline: true, }); linkComponents.push(privacyLink); // Create label element with link const privacyLabel = document.createElement('span'); privacyLabel.appendChild(document.createTextNode('Ich stimme der ')); privacyLabel.appendChild(privacyLink.getElement()); privacyLabel.appendChild(document.createTextNode(' zu')); // Create checkbox with HTML label formFields.privacy = Checkbox({ id: 'contact-privacy', name: 'privacy', label: privacyLabel, // Now we can pass the HTML element directly! required: true, validationMessage: 'Sie müssen der Datenschutzerklärung zustimmen', }); formElements.push(formFields.privacy.getElement()); } // Newsletter checkbox if (state.showNewsletterCheckbox) { formFields.newsletter = Checkbox({ id: 'contact-newsletter', name: 'newsletter', label: state.newsletterText, required: false, }); formElements.push(formFields.newsletter.getElement()); } // Add required fields notice const hasRequiredFields = state.showNameField || state.showEmailField || state.showSubjectField || state.showMessageField || state.showPrivacyCheckbox; if (hasRequiredFields) { formElements.push( createElement('div', { classes: 'contact-form__required-notice', children: [ createElement('span', { text: '* Pflichtfelder', classes: 'contact-form__required-text', }), ], }) ); } // Submit button const submitButton = Button({ text: state.submitButtonText, type: 'submit', variant: 'primary', }); formElements.push( FormActions({ children: submitButton, align: 'left', }).getElement() ); // Create form using Svarog Form component formComponent = Form({ children: formElements, onSubmit: async (event, data, _isValid) => { // Manually validate all required fields let allValid = true; const errors = []; // Validate each field Object.entries(formFields).forEach(([fieldName, field]) => { if (field.validate && !field.validate()) { allValid = false; errors.push(fieldName); } }); // Pass validation result to handler if (state.onSubmit) { state.onSubmit(event, data, allValid, formFields); } }, onChange: (event, data) => { if (state.onChange) { state.onChange(event, data, formFields); } }, }); // Register all form fields for validation Object.values(formFields).forEach((field) => { if (formComponent.registerField) { formComponent.registerField(field); } }); // Create form content container const formContentElements = []; // Contact info - check if any contact info values are provided const hasContactInfo = state.contactInfo && Object.values(state.contactInfo).some( (value) => value && value.toString().trim() !== '' ); if (hasContactInfo) { formContentElements.push(createContactInfo(state.contactInfo)); } // Form formContentElements.push(formComponent.getElement()); const formContent = createElement('div', { classes: 'contact-section__content', children: formContentElements, }); // Create layout using Grid component gridComponent = Grid({ gap: '2rem', alignItems: 'start', }); // Create columns based on layout preference const mapColumn = Grid.Column({ width: 6, mobileWidth: 12, children: mapComponent.getElement(), }); const formColumn = Grid.Column({ width: 6, mobileWidth: 12, children: formContent, }); // Add columns to grid based on layout preference if (state.mapPosition === 'left') { gridComponent.appendChild(mapColumn.getElement()); gridComponent.appendChild(formColumn.getElement()); } else { gridComponent.appendChild(formColumn.getElement()); gridComponent.appendChild(mapColumn.getElement()); } // Handle mobile layout ordering with CSS classes const gridElement = gridComponent.getElement(); if (state.mobileLayout === 'stack') { gridElement.classList.add('contact-section__grid--stack'); } if (state.mobileLayout === 'reverse') { gridElement.classList.add('contact-section__grid--reverse'); } // Create main container using Section component sectionComponent = Section({ title: state.title, description: state.description, variant: state.variant, backgroundColor: state.backgroundColor, className: `contact-section ${state.className}`, children: gridElement, }); return sectionComponent.getElement(); })(normalizedProps); // Add extended API methods return { ...baseComponent, // State access (required for tests) getState() { return { ...currentState }; }, // Override update to keep our state in sync update(newProps) { const result = baseComponent.update(newProps); currentState = { ...currentState, ...newProps }; return result; }, // Contact-specific methods updateContactInfo(newContactInfo) { const updatedContactInfo = { ...currentState.contactInfo, ...newContactInfo, }; this.update({ contactInfo: updatedContactInfo }); if (mapComponent) { mapComponent.update({ shopInfo: updatedContactInfo }); } }, getFormData() { const data = {}; Object.entries(formFields).forEach(([key, field]) => { if (field && field.getValue) { data[key] = field.getValue(); } else if (field && field.isChecked) { data[key] = field.isChecked(); } }); return data; }, validateForm() { let isValid = true; Object.values(formFields).forEach((field) => { if (field && field.validate && !field.validate()) { isValid = false; } }); return isValid; }, resetForm() { if (formComponent && formComponent.reset) { formComponent.reset(); } // Also reset individual fields Object.values(formFields).forEach((field) => { if (field && field.setValue) { field.setValue(''); } else if (field && field.setChecked) { field.setChecked(false); } }); }, // Component access getMapComponent() { return mapComponent; }, getFormComponent() { return formComponent; }, getFormFields() { return { ...formFields }; }, // Enhanced destroy method destroy() { // Get the element before destroying const element = baseComponent.getElement(); // Remove from DOM if it has a parent if (element && element.parentNode) { element.parentNode.removeChild(element); } // Destroy link components linkComponents.forEach((link) => { if (link.destroy && typeof link.destroy === 'function') { link.destroy(); } }); linkComponents = []; // Destroy child components [ mapComponent, formComponent, sectionComponent, gridComponent, ...Object.values(formFields), ] .filter(Boolean) .forEach((component) => { if (component.destroy && typeof component.destroy === 'function') { component.destroy(); } }); // Clear references mapComponent = null; formComponent = null; sectionComponent = null; gridComponent = null; formFields = {}; // Call base destroy baseComponent.destroy(); }, }; }; export default createContactSection;