diff --git a/src/Component/Application/DTO/.gitkeep b/Admin/.gitkeep similarity index 100% rename from src/Component/Application/DTO/.gitkeep rename to Admin/.gitkeep diff --git a/Admin/Application/UseCases/RenderDashboardUseCase.php b/Admin/Application/UseCases/RenderDashboardUseCase.php new file mode 100644 index 00000000..fec459c3 --- /dev/null +++ b/Admin/Application/UseCases/RenderDashboardUseCase.php @@ -0,0 +1,41 @@ +renderer->supports($viewType)) { + throw new \RuntimeException( + sprintf('Renderer does not support view type: %s', $viewType) + ); + } + + return $this->renderer->render(); + } +} diff --git a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php new file mode 100644 index 00000000..f0c3cc5b --- /dev/null +++ b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php @@ -0,0 +1,601 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildContactInfoGroup($componentId); + $html .= $this->buildFormLabelsGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildIntegrationGroup($componentId); + $html .= $this->buildMessagesGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('contactFormEnabled', 'Activar componente', 'bi-power', $enabled); + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('contactFormShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + $sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', '¿Tienes alguna pregunta?'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sectionDescription = $this->renderer->getFieldValue($componentId, 'content', 'section_description', 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $submitButtonText = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_text', 'Enviar Mensaje'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $submitButtonIcon = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_icon', 'bi-send-fill'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildContactInfoGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Info de Contacto'; + $html .= '
'; + + $showContactInfo = $this->renderer->getFieldValue($componentId, 'contact_info', 'show_contact_info', true); + $html .= $this->buildSwitch('contactFormShowContactInfo', 'Mostrar info contacto', 'bi-eye', $showContactInfo); + + $html .= '
'; + $html .= '

Telefono

'; + + $phoneLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_label', 'Teléfono'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $phoneValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_value', '+52 55 1234 5678'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $html .= '

Email

'; + + $emailLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_label', 'Email'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $emailValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_value', 'contacto@apumexico.com'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $html .= '

Ubicacion

'; + + $locationLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_label', 'Ubicación'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $locationValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_value', 'Ciudad de México, México'); + $html .= '
'; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildFormLabelsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Labels del Formulario'; + $html .= '
'; + + $fullnamePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'fullname_placeholder', 'Nombre completo *'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $companyPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'company_placeholder', 'Empresa'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $whatsappPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'whatsapp_placeholder', 'WhatsApp *'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $emailPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'email_placeholder', 'Correo electrónico *'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $messagePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'message_placeholder', '¿En qué podemos ayudarte?'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildIntegrationGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Integracion Webhook'; + $html .= ' Privado'; + $html .= '
'; + + $html .= '
'; + $html .= ' '; + $html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.'; + $html .= '
'; + + $webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', ''); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Seccion + $html .= '

Seccion

'; + $html .= '
'; + + $sectionBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_bg_color', 'rgba(108, 117, 125, 0.25)'); + $html .= $this->buildColorPicker('contactFormSectionBgColor', 'Fondo seccion', $sectionBgColor); + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#212529'); + $html .= $this->buildColorPicker('contactFormTitleColor', 'Color titulo', $titleColor); + + $html .= '
'; + + // Boton + $html .= '

Boton

'; + $html .= '
'; + + $buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#FF8600'); + $html .= $this->buildColorPicker('contactFormButtonBgColor', 'Fondo boton', $buttonBgColor); + + $buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff'); + $html .= $this->buildColorPicker('contactFormButtonTextColor', 'Texto boton', $buttonTextColor); + + $html .= '
'; + + $html .= '
'; + + $buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#e67a00'); + $html .= $this->buildColorPicker('contactFormButtonHoverBg', 'Hover boton', $buttonHoverBg); + + $iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600'); + $html .= $this->buildColorPicker('contactFormIconColor', 'Iconos', $iconColor); + + $html .= '
'; + + // Mensajes + $html .= '

Mensajes

'; + $html .= '
'; + + $successBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'success_bg_color', '#d1e7dd'); + $html .= $this->buildColorPicker('contactFormSuccessBgColor', 'Fondo exito', $successBgColor); + + $errorBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'error_bg_color', '#f8d7da'); + $html .= $this->buildColorPicker('contactFormErrorBgColor', 'Fondo error', $errorBgColor); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $sectionPaddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'section_padding_y', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $formGap = $this->renderer->getFieldValue($componentId, 'spacing', 'form_gap', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + $inputBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.75rem 2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + // Manejar colores rgba + $colorValue = $value; + if (strpos($value, 'rgba') === 0 || strpos($value, 'rgb') === 0) { + // Para rgba usamos un color aproximado en el picker + $colorValue = '#6c757d'; + } + + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($colorValue) + ); + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php new file mode 100644 index 00000000..eea6b8cc --- /dev/null +++ b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php @@ -0,0 +1,518 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildBehaviorGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('ctaEnabled', 'Activar CTA box', 'bi-power', $enabled); + + // show_on_desktop + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('ctaShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + // show_on_mobile + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false); + $html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + // show_on_pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // title + $title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // description + $description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_text + $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_icon + $buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Clase de Bootstrap Icons'; + $html .= '
'; + + // button_action + $buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_link + $buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Para modal usa #nombreModal, para scroll usa #idSeccion'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildBehaviorGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Comportamiento'; + $html .= '
'; + + // text_align + $textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + $html .= '
'; + + // title_font_size + $titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.25rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // title_font_weight + $titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // description_font_size + $descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '0.9rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_font_size + $buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // button_font_weight + $buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Colores principales + $html .= '

Contenedor

'; + $html .= '
'; + + $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600'); + $html .= $this->buildColorPicker('ctaBackgroundColor', 'Fondo', $bgColor); + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaTitleColor', 'Titulo', $titleColor); + + $html .= '
'; + + $html .= '
'; + + $descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)'); + $html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor); + + $html .= '
'; + + // Colores del boton + $html .= '

Boton

'; + $html .= '
'; + + $buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_background_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaButtonBgColor', 'Fondo', $buttonBgColor); + + $buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#FF8600'); + $html .= $this->buildColorPicker('ctaButtonTextColor', 'Texto', $buttonTextColor); + + $html .= '
'; + + // Colores hover + $html .= '

Boton Hover

'; + $html .= '
'; + + $buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_background', '#0E2337'); + $html .= $this->buildColorPicker('ctaButtonHoverBg', 'Fondo hover', $buttonHoverBg); + + $buttonHoverText = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_text_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaButtonHoverText', 'Texto hover', $buttonHoverText); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + // container_padding + $containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '24px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // title_margin_bottom + $titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // description_margin_bottom + $descMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'description_margin_bottom', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_padding + $buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.75rem 1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // icon_margin_right + $iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + // border_radius + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_border_radius + $buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '8px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // box_shadow + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // transition_duration + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, bool $checked): string + { + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php new file mode 100644 index 00000000..a1e76b00 --- /dev/null +++ b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php @@ -0,0 +1,450 @@ +buildHeader($componentId); + + // Layout 2 columnas + $html .= '
'; + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildBehaviorGroup($componentId); + $html .= '
'; + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildVisualEffectsGroup($componentId); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Desktop + $showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Mobile + $showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Select: Show on Pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // Text: Button Text + $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk"); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Switch: Show Icon + $showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Text: Icon Class + $iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Usa clases de Bootstrap Icons (ej: bi-chat-dots)'; + $html .= '
'; + + // Text: Modal Target + $modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: ARIA Label + $ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildBehaviorGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Comportamiento'; + $html .= '
'; + + // Switch: Enable Modal + $enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= ' Si está desactivado, usará la URL personalizada'; + $html .= '
'; + + // URL: Custom URL + $customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', ''); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Solo se usa si "Abrir modal" está desactivado'; + $html .= '
'; + + // Switch: Open in New Tab + $openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografía'; + $html .= '
'; + + // Text: Font Size + $fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Select: Font Weight + $fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Select: Text Transform + $textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Color: Background + $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Color: Background Hover + $bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Color: Text + $textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Color: Text Hover + $textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: Border Color (permite transparent) + $borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Usa "transparent" para sin borde visible'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + // Text: Padding Top/Bottom + $paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: Padding Left/Right + $paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: Margin Left + $marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Separación del menú en pantallas ≥992px'; + $html .= '
'; + + // Text: Icon Spacing + $iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildVisualEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + // Text: Border Radius + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: Border Width + $borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Text: Box Shadow + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Ej: 0 2px 4px rgba(0,0,0,0.1)'; + $html .= '
'; + + // Text: Transition Duration + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php new file mode 100644 index 00000000..ee1e14f4 --- /dev/null +++ b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php @@ -0,0 +1,440 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildTypographyGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('ctaPostEnabled', 'Activar componente', 'bi-power', $enabled); + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('ctaPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // Title + $title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Description + $description = $this->renderer->getFieldValue($componentId, 'content', 'description', ''); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Button Text + $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Button URL + $buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Button Icon + $buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Clase de Bootstrap Icons'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + $html .= '
'; + + $titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1.125rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Gradiente + $html .= '

Gradiente de fondo

'; + $html .= '
'; + + $gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#FF8600'); + $html .= $this->buildColorPicker('ctaPostGradientStart', 'Inicio', $gradientStart); + + $gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#FFB800'); + $html .= $this->buildColorPicker('ctaPostGradientEnd', 'Fin', $gradientEnd); + + $html .= '
'; + + // Textos + $html .= '

Textos

'; + $html .= '
'; + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaPostTitleColor', 'Titulo', $titleColor); + + $descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaPostDescColor', 'Descripcion', $descColor); + + $html .= '
'; + + // Boton + $html .= '

Boton

'; + $html .= '
'; + + $buttonBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#ffffff'); + $html .= $this->buildColorPicker('ctaPostButtonBg', 'Fondo', $buttonBg); + + $buttonText = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529'); + $html .= $this->buildColorPicker('ctaPostButtonText', 'Texto', $buttonText); + + $html .= '
'; + + $html .= '
'; + + $buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa'); + $html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $padding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleMargin = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '0.375rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $gradientAngle = $this->renderer->getFieldValue($componentId, 'visual_effects', 'gradient_angle', '135deg'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.5rem 1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/Domain/Contracts/ComponentTabInterface.php b/Admin/Domain/Contracts/ComponentTabInterface.php new file mode 100644 index 00000000..dd65cb27 --- /dev/null +++ b/Admin/Domain/Contracts/ComponentTabInterface.php @@ -0,0 +1,48 @@ +validate(); + } + + private function validate(): void + { + if (empty($this->pageTitle)) { + throw new \InvalidArgumentException('Page title cannot be empty'); + } + + if (empty($this->menuTitle)) { + throw new \InvalidArgumentException('Menu title cannot be empty'); + } + + if (empty($this->capability)) { + throw new \InvalidArgumentException('Capability cannot be empty'); + } + + if (empty($this->menuSlug)) { + throw new \InvalidArgumentException('Menu slug cannot be empty'); + } + + if ($this->position < 0) { + throw new \InvalidArgumentException('Position must be >= 0'); + } + } + + public function getPageTitle(): string + { + return $this->pageTitle; + } + + public function getMenuTitle(): string + { + return $this->menuTitle; + } + + public function getCapability(): string + { + return $this->capability; + } + + public function getMenuSlug(): string + { + return $this->menuSlug; + } + + public function getIcon(): string + { + return $this->icon; + } + + public function getPosition(): int + { + return $this->position; + } +} diff --git a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php new file mode 100644 index 00000000..fbdf247e --- /dev/null +++ b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php @@ -0,0 +1,280 @@ +buildHeader($componentId); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= '
'; + $html .= '
'; + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + $imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= ' Mejora rendimiento cargando imagen cuando es visible'; + $html .= '
'; + + $linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= ' Abre la imagen en tamano completo al hacer clic'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= ' Aplica efecto de escala sutil al pasar el mouse'; + $html .= '
'; + + $html .= '
'; + + $hoverScale = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_scale', '1.02'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php new file mode 100644 index 00000000..fe9f3418 --- /dev/null +++ b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php @@ -0,0 +1,413 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildWidget1Group($componentId); + $html .= $this->buildWidget2Group($componentId); + $html .= $this->buildWidget3Group($componentId); + $html .= $this->buildNewsletterGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildFooterBottomGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('footerEnabled', 'Activar componente', 'bi-power', $enabled); + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('footerShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildWidget1Group(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Widget 1 (Menu)'; + $html .= '
'; + + $visible = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_visible', true); + $html .= $this->buildSwitch('footerWidget1Visible', 'Mostrar Widget 1', 'bi-eye', $visible); + + $title = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_title', 'Recursos'); + $html .= $this->buildTextInput('footerWidget1Title', 'Titulo', 'bi-type', $title); + + $html .= '
'; + $html .= ' '; + $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 1'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildWidget2Group(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Widget 2 (Menu)'; + $html .= '
'; + + $visible = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_visible', true); + $html .= $this->buildSwitch('footerWidget2Visible', 'Mostrar Widget 2', 'bi-eye', $visible); + + $title = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_title', 'Soporte'); + $html .= $this->buildTextInput('footerWidget2Title', 'Titulo', 'bi-type', $title); + + $html .= '
'; + $html .= ' '; + $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 2'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildWidget3Group(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Widget 3 (Menu)'; + $html .= '
'; + + $visible = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_visible', true); + $html .= $this->buildSwitch('footerWidget3Visible', 'Mostrar Widget 3', 'bi-eye', $visible); + + $title = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_title', 'Empresa'); + $html .= $this->buildTextInput('footerWidget3Title', 'Titulo', 'bi-type', $title); + + $html .= '
'; + $html .= ' '; + $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 3'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildNewsletterGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Newsletter'; + $html .= '
'; + + $visible = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_visible', true); + $html .= $this->buildSwitch('footerNewsletterVisible', 'Mostrar Newsletter', 'bi-eye', $visible); + + $title = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_title', 'Suscribete al Newsletter'); + $html .= $this->buildTextInput('footerNewsletterTitle', 'Titulo', 'bi-type', $title); + + $description = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_description', 'Recibe las ultimas actualizaciones.'); + $html .= $this->buildTextarea('footerNewsletterDescription', 'Descripcion', 'bi-text-paragraph', $description); + + $placeholder = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_placeholder', 'Email'); + $html .= $this->buildTextInput('footerNewsletterPlaceholder', 'Placeholder email', 'bi-input-cursor', $placeholder); + + $buttonText = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_button_text', 'Suscribirse'); + $html .= $this->buildTextInput('footerNewsletterButtonText', 'Texto boton', 'bi-cursor', $buttonText); + + $webhookUrl = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_webhook_url', ''); + $html .= $this->buildPasswordInput('footerNewsletterWebhookUrl', 'URL del Webhook', 'bi-link-45deg', $webhookUrl); + + $successMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_success_message', 'Gracias por suscribirte!'); + $html .= $this->buildTextInput('footerNewsletterSuccessMessage', 'Mensaje exito', 'bi-check-circle', $successMsg); + + $errorMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_error_message', 'Error al suscribirse.'); + $html .= $this->buildTextInput('footerNewsletterErrorMessage', 'Mensaje error', 'bi-x-circle', $errorMsg); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildFooterBottomGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Pie de Footer'; + $html .= '
'; + + $copyright = $this->renderer->getFieldValue($componentId, 'footer_bottom', 'copyright_text', date('Y') . ' Todos los derechos reservados.'); + $html .= $this->buildTextInput('footerCopyrightText', 'Texto copyright', 'bi-c-circle', $copyright); + + $html .= '
'; + $html .= ' '; + $html .= ' El simbolo © se agrega automaticamente'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'bg_color', '#212529'); + $html .= $this->buildColorInput('footerBgColor', 'Fondo footer', $bgColor); + + $textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#ffffff'); + $html .= $this->buildColorInput('footerTextColor', 'Color texto', $textColor); + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff'); + $html .= $this->buildColorInput('footerTitleColor', 'Color titulos', $titleColor); + + $linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#ffffff'); + $html .= $this->buildColorInput('footerLinkColor', 'Color links', $linkColor); + + $linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600'); + $html .= $this->buildColorInput('footerLinkHoverColor', 'Color links hover', $linkHoverColor); + + $buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#0d6efd'); + $html .= $this->buildColorInput('footerButtonBgColor', 'Fondo boton', $buttonBgColor); + + $buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff'); + $html .= $this->buildColorInput('footerButtonTextColor', 'Texto boton', $buttonTextColor); + + $buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#0b5ed7'); + $html .= $this->buildColorInput('footerButtonHoverBg', 'Fondo boton hover', $buttonHoverBg); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $paddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_y', '3rem'); + $html .= $this->buildTextInput('footerPaddingY', 'Padding vertical', 'bi-arrows-vertical', $paddingY); + + $marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '0'); + $html .= $this->buildTextInput('footerMarginTop', 'Margen superior', 'bi-arrow-up', $marginTop); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $inputRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px'); + $html .= $this->buildTextInput('footerInputBorderRadius', 'Radio input', 'bi-square', $inputRadius); + + $buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px'); + $html .= $this->buildTextInput('footerButtonBorderRadius', 'Radio boton', 'bi-square', $buttonRadius); + + $transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= $this->buildTextInput('footerTransitionDuration', 'Duracion transicion', 'bi-hourglass', $transition); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + // Helper methods + private function buildSwitch(string $id, string $label, string $icon, $value): string + { + $checked = $value === true || $value === '1' || $value === 1 ? 'checked' : ''; + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + return $html; + } + + private function buildTextInput(string $id, string $label, string $icon, mixed $value): string + { + $value = $this->normalizeStringValue($value); + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + return $html; + } + + private function buildPasswordInput(string $id, string $label, string $icon, mixed $value): string + { + $value = $this->normalizeStringValue($value); + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
URL oculta por seguridad
'; + $html .= '
'; + + return $html; + } + + private function buildTextarea(string $id, string $label, string $icon, mixed $value): string + { + $value = $this->normalizeStringValue($value); + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + return $html; + } + + private function buildColorInput(string $id, string $label, mixed $value): string + { + $value = $this->normalizeStringValue($value); + + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + return $html; + } + + /** + * Normaliza un valor a string para inputs de formulario + * + * El repositorio convierte '0' a false y '1' a true automáticamente, + * pero para campos de texto necesitamos el valor original como string. + */ + private function normalizeStringValue(mixed $value): string + { + if ($value === false) { + return '0'; + } + if ($value === true) { + return '1'; + } + return (string) $value; + } +} diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php new file mode 100644 index 00000000..160dce70 --- /dev/null +++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php @@ -0,0 +1,416 @@ +buildHeader($componentId); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + $html .= '
'; + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + $showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Usa clases de Bootstrap Icons'; + $html .= '
'; + + $titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + $html .= '
'; + + $gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#1e3a5f'); + $html .= $this->buildColorPicker('heroGradientStart', 'Degradado (inicio)', 'circle-half', $gradientStart); + + $gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#2c5282'); + $html .= $this->buildColorPicker('heroGradientEnd', 'Degradado (fin)', 'circle-fill', $gradientEnd); + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#FFFFFF'); + $html .= $this->buildColorPicker('heroTitleColor', 'Color título', 'fonts', $titleColor); + + $badgeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_bg_color', '#FFFFFF'); + $html .= $this->buildColorPicker('heroBadgeBgColor', 'Fondo badges', 'badge', $badgeBgColor); + + $html .= '
'; + $html .= '
'; + + $badgeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_text_color', '#FFFFFF'); + $html .= $this->buildColorPicker('heroBadgeTextColor', 'Texto badges', 'card-text', $badgeTextColor); + + $badgeIconColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_icon_color', '#FFB800'); + $html .= $this->buildColorPicker('heroBadgeIconColor', 'Ícono badges', 'star-fill', $badgeIconColor); + + $badgeHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'badge_hover_bg', '#FF8600'); + $html .= $this->buildColorPicker('heroBadgeHoverBg', 'Badges (hover)', 'hand-index', $badgeHoverBg); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografía'; + $html .= '
'; + + $html .= '
'; + + $titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '2.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleFontSizeMobile = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size_mobile', '1.75rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + $titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $paddingVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_vertical', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + $badgePadding = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_padding', '0.375rem 0.875rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $badgeBorderRadius = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_border_radius', '20px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos'; + $html .= '
'; + + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $icon, string $value): string + { + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= ' ' . esc_html(strtoupper($value)) . ''; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php b/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php new file mode 100644 index 00000000..96003ccf --- /dev/null +++ b/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php @@ -0,0 +1,596 @@ + 'No tienes permisos para realizar esta acción.' + ]); + } + + // Obtener datos + $component = sanitize_text_field($_POST['component'] ?? ''); + $settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true); + + if (empty($component) || empty($settings)) { + wp_send_json_error([ + 'message' => 'Datos incompletos.' + ]); + } + + // Mapear IDs de campos a nombres de atributos del schema + $fieldMapping = $this->getFieldMapping(); + $mappedSettings = []; + + foreach ($settings as $fieldId => $value) { + // Convertir ID del campo a nombre del atributo + if (!isset($fieldMapping[$fieldId])) { + continue; // Campo no mapeado, ignorar + } + + $mapping = $fieldMapping[$fieldId]; + $groupName = $mapping['group']; + $attributeName = $mapping['attribute']; + + if (!isset($mappedSettings[$groupName])) { + $mappedSettings[$groupName] = []; + } + + $mappedSettings[$groupName][$attributeName] = $value; + } + + // Usar Use Case para guardar + if ($this->saveComponentSettingsUseCase !== null) { + $updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings); + + wp_send_json_success([ + 'message' => sprintf('Se guardaron %d campos correctamente.', $updated) + ]); + } else { + wp_send_json_error([ + 'message' => 'Error: Use Case no disponible.' + ]); + } + } + + /** + * Restaura los valores por defecto de un componente + */ + public function resetComponentDefaults(): void + { + // Verificar nonce + check_ajax_referer('roi_admin_dashboard', 'nonce'); + + // Verificar permisos + if (!current_user_can('manage_options')) { + wp_send_json_error([ + 'message' => 'No tienes permisos para realizar esta acción.' + ]); + } + + // Obtener componente + $component = sanitize_text_field($_POST['component'] ?? ''); + + if (empty($component)) { + wp_send_json_error([ + 'message' => 'Componente no especificado.' + ]); + } + + // Ruta al schema JSON + $schemaPath = get_template_directory() . '/Schemas/' . $component . '.json'; + + if (!file_exists($schemaPath)) { + wp_send_json_error([ + 'message' => 'Schema del componente no encontrado.' + ]); + } + + // Usar repositorio para restaurar valores + if ($this->saveComponentSettingsUseCase !== null) { + global $wpdb; + $repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb); + $updated = $repository->resetToDefaults($component, $schemaPath); + + wp_send_json_success([ + 'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated) + ]); + } else { + wp_send_json_error([ + 'message' => 'Error: Repositorio no disponible.' + ]); + } + } + + /** + * Mapeo de IDs de campos HTML a nombres de atributos del schema + * + * @return array + */ + private function getFieldMapping(): array + { + return [ + // ===================================================== + // TOP NOTIFICATION BAR + // ===================================================== + + // Activación y Visibilidad + 'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Contenido + 'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'], + 'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'], + 'topBarMessageText' => ['group' => 'content', 'attribute' => 'message_text'], + 'topBarLinkText' => ['group' => 'content', 'attribute' => 'link_text'], + 'topBarLinkUrl' => ['group' => 'content', 'attribute' => 'link_url'], + + // Colores + 'topBarBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'], + 'topBarTextColor' => ['group' => 'colors', 'attribute' => 'text_color'], + 'topBarLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'], + 'topBarIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'], + 'topBarLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'], + 'topBarLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'], + + // Espaciado + 'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'], + 'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'], + + // ===================================================== + // NAVBAR + // ===================================================== + + // Visibility + 'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + 'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'], + + // Layout + 'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'], + 'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'], + 'navbarZIndex' => ['group' => 'layout', 'attribute' => 'z_index'], + + // Behavior + 'navbarMenuLocation' => ['group' => 'behavior', 'attribute' => 'menu_location'], + 'navbarCustomMenuId' => ['group' => 'behavior', 'attribute' => 'custom_menu_id'], + 'navbarEnableDropdowns' => ['group' => 'behavior', 'attribute' => 'enable_dropdowns'], + 'navbarMobileBreakpoint' => ['group' => 'behavior', 'attribute' => 'mobile_breakpoint'], + + // Media (Logo/Marca) + 'navbarShowBrand' => ['group' => 'media', 'attribute' => 'show_brand'], + 'navbarUseLogo' => ['group' => 'media', 'attribute' => 'use_logo'], + 'navbarLogoUrl' => ['group' => 'media', 'attribute' => 'logo_url'], + 'navbarLogoHeight' => ['group' => 'media', 'attribute' => 'logo_height'], + 'navbarBrandText' => ['group' => 'media', 'attribute' => 'brand_text'], + 'navbarBrandFontSize' => ['group' => 'media', 'attribute' => 'brand_font_size'], + 'navbarBrandColor' => ['group' => 'media', 'attribute' => 'brand_color'], + 'navbarBrandHoverColor' => ['group' => 'media', 'attribute' => 'brand_hover_color'], + + // Links + 'linksTextColor' => ['group' => 'links', 'attribute' => 'text_color'], + 'linksHoverColor' => ['group' => 'links', 'attribute' => 'hover_color'], + 'linksActiveColor' => ['group' => 'links', 'attribute' => 'active_color'], + 'linksFontSize' => ['group' => 'links', 'attribute' => 'font_size'], + 'linksFontWeight' => ['group' => 'links', 'attribute' => 'font_weight'], + 'linksPadding' => ['group' => 'links', 'attribute' => 'padding'], + 'linksBorderRadius' => ['group' => 'links', 'attribute' => 'border_radius'], + 'linksShowUnderline' => ['group' => 'links', 'attribute' => 'show_underline_effect'], + 'linksUnderlineColor' => ['group' => 'links', 'attribute' => 'underline_color'], + + // Visual Effects (Dropdown) + 'dropdownBgColor' => ['group' => 'visual_effects', 'attribute' => 'background_color'], + 'dropdownBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'], + 'dropdownShadow' => ['group' => 'visual_effects', 'attribute' => 'shadow'], + 'dropdownItemColor' => ['group' => 'visual_effects', 'attribute' => 'item_color'], + 'dropdownItemHoverBg' => ['group' => 'visual_effects', 'attribute' => 'item_hover_background'], + 'dropdownItemPadding' => ['group' => 'visual_effects', 'attribute' => 'item_padding'], + 'dropdownMaxHeight' => ['group' => 'visual_effects', 'attribute' => 'dropdown_max_height'], + + // Colors (Navbar styles) + 'navbarBgColor' => ['group' => 'colors', 'attribute' => 'background_color'], + 'navbarBoxShadow' => ['group' => 'colors', 'attribute' => 'box_shadow'], + + // ===================================================== + // CTA LETS TALK + // ===================================================== + + // Visibility + 'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'], + 'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'], + 'ctaLetsTalkIconClass' => ['group' => 'content', 'attribute' => 'icon_class'], + 'ctaLetsTalkModalTarget' => ['group' => 'content', 'attribute' => 'modal_target'], + 'ctaLetsTalkAriaLabel' => ['group' => 'content', 'attribute' => 'aria_label'], + + // Behavior + 'ctaLetsTalkEnableModal' => ['group' => 'behavior', 'attribute' => 'enable_modal'], + 'ctaLetsTalkCustomUrl' => ['group' => 'behavior', 'attribute' => 'custom_url'], + 'ctaLetsTalkOpenNewTab' => ['group' => 'behavior', 'attribute' => 'open_in_new_tab'], + + // Typography + 'ctaLetsTalkFontSize' => ['group' => 'typography', 'attribute' => 'font_size'], + 'ctaLetsTalkFontWeight' => ['group' => 'typography', 'attribute' => 'font_weight'], + 'ctaLetsTalkTextTransform' => ['group' => 'typography', 'attribute' => 'text_transform'], + + // Colors + 'ctaLetsTalkBgColor' => ['group' => 'colors', 'attribute' => 'background_color'], + 'ctaLetsTalkBgHoverColor' => ['group' => 'colors', 'attribute' => 'background_hover_color'], + 'ctaLetsTalkTextColor' => ['group' => 'colors', 'attribute' => 'text_color'], + 'ctaLetsTalkTextHoverColor' => ['group' => 'colors', 'attribute' => 'text_hover_color'], + 'ctaLetsTalkBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'], + + // Spacing + 'ctaLetsTalkPaddingTB' => ['group' => 'spacing', 'attribute' => 'padding_top_bottom'], + 'ctaLetsTalkPaddingLR' => ['group' => 'spacing', 'attribute' => 'padding_left_right'], + 'ctaLetsTalkMarginLeft' => ['group' => 'spacing', 'attribute' => 'margin_left'], + 'ctaLetsTalkIconSpacing' => ['group' => 'spacing', 'attribute' => 'icon_spacing'], + + // Visual Effects + 'ctaLetsTalkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'], + 'ctaLetsTalkBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'], + 'ctaLetsTalkBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'], + 'ctaLetsTalkTransition' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + + // ===================================================== + // HERO SECTION + // ===================================================== + + // Visibility + 'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'], + 'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'], + 'heroBadgeIconClass' => ['group' => 'content', 'attribute' => 'badge_icon_class'], + 'heroTitleTag' => ['group' => 'content', 'attribute' => 'title_tag'], + + // Colors + 'heroGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'], + 'heroGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'], + 'heroTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'], + 'heroBadgeBgColor' => ['group' => 'colors', 'attribute' => 'badge_bg_color'], + 'heroBadgeTextColor' => ['group' => 'colors', 'attribute' => 'badge_text_color'], + 'heroBadgeIconColor' => ['group' => 'colors', 'attribute' => 'badge_icon_color'], + 'heroBadgeHoverBg' => ['group' => 'colors', 'attribute' => 'badge_hover_bg'], + + // Typography + 'heroTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'], + 'heroTitleFontSizeMobile' => ['group' => 'typography', 'attribute' => 'title_font_size_mobile'], + 'heroTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'], + 'heroTitleLineHeight' => ['group' => 'typography', 'attribute' => 'title_line_height'], + 'heroBadgeFontSize' => ['group' => 'typography', 'attribute' => 'badge_font_size'], + + // Spacing + 'heroPaddingVertical' => ['group' => 'spacing', 'attribute' => 'padding_vertical'], + 'heroMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'], + 'heroBadgePadding' => ['group' => 'spacing', 'attribute' => 'badge_padding'], + 'heroBadgeBorderRadius' => ['group' => 'spacing', 'attribute' => 'badge_border_radius'], + + // Visual Effects + 'heroBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'], + 'heroTitleTextShadow' => ['group' => 'visual_effects', 'attribute' => 'title_text_shadow'], + 'heroBadgeBackdropBlur' => ['group' => 'visual_effects', 'attribute' => 'badge_backdrop_blur'], + + // ===================================================== + // FEATURED IMAGE + // ===================================================== + + // Visibility + 'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'], + 'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'], + 'featuredImageLinkToMedia' => ['group' => 'content', 'attribute' => 'link_to_media'], + + // Spacing + 'featuredImageMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'], + 'featuredImageMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'], + + // Visual Effects + 'featuredImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'], + 'featuredImageBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'], + 'featuredImageHoverEffect' => ['group' => 'visual_effects', 'attribute' => 'hover_effect'], + 'featuredImageHoverScale' => ['group' => 'visual_effects', 'attribute' => 'hover_scale'], + 'featuredImageTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + + // ===================================================== + // TABLE OF CONTENTS + // ===================================================== + + // Visibility + 'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'tocTitle' => ['group' => 'content', 'attribute' => 'title'], + 'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'], + 'tocHeadingLevels' => ['group' => 'content', 'attribute' => 'heading_levels'], + 'tocSmoothScroll' => ['group' => 'content', 'attribute' => 'smooth_scroll'], + + // Typography + 'tocTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'], + 'tocTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'], + 'tocLinkFontSize' => ['group' => 'typography', 'attribute' => 'link_font_size'], + 'tocLinkLineHeight' => ['group' => 'typography', 'attribute' => 'link_line_height'], + 'tocLevelThreeFontSize' => ['group' => 'typography', 'attribute' => 'level_three_font_size'], + 'tocLevelFourFontSize' => ['group' => 'typography', 'attribute' => 'level_four_font_size'], + + // Colors + 'tocBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'], + 'tocBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'], + 'tocTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'], + 'tocTitleBorderColor' => ['group' => 'colors', 'attribute' => 'title_border_color'], + 'tocLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'], + 'tocLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'], + 'tocLinkHoverBackground' => ['group' => 'colors', 'attribute' => 'link_hover_background'], + 'tocActiveBorderColor' => ['group' => 'colors', 'attribute' => 'active_border_color'], + 'tocActiveBackgroundColor' => ['group' => 'colors', 'attribute' => 'active_background_color'], + 'tocActiveTextColor' => ['group' => 'colors', 'attribute' => 'active_text_color'], + 'tocScrollbarTrackColor' => ['group' => 'colors', 'attribute' => 'scrollbar_track_color'], + 'tocScrollbarThumbColor' => ['group' => 'colors', 'attribute' => 'scrollbar_thumb_color'], + + // Spacing + 'tocContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'], + 'tocMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'], + 'tocTitlePaddingBottom' => ['group' => 'spacing', 'attribute' => 'title_padding_bottom'], + 'tocTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'], + 'tocItemMarginBottom' => ['group' => 'spacing', 'attribute' => 'item_margin_bottom'], + 'tocLinkPadding' => ['group' => 'spacing', 'attribute' => 'link_padding'], + 'tocLevelThreePaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_three_padding_left'], + 'tocLevelFourPaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_four_padding_left'], + 'tocScrollbarWidth' => ['group' => 'spacing', 'attribute' => 'scrollbar_width'], + + // Visual Effects + 'tocBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'], + 'tocBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'], + 'tocBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'], + 'tocLinkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'link_border_radius'], + 'tocActiveBorderLeftWidth' => ['group' => 'visual_effects', 'attribute' => 'active_border_left_width'], + 'tocTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + 'tocScrollbarBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'scrollbar_border_radius'], + + // Behavior + 'tocIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'], + 'tocScrollOffset' => ['group' => 'behavior', 'attribute' => 'scroll_offset'], + 'tocMaxHeight' => ['group' => 'behavior', 'attribute' => 'max_height'], + + // ===================================================== + // SOCIAL SHARE + // ===================================================== + + // Visibility + 'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'], + 'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'], + + // Networks + 'socialShareFacebook' => ['group' => 'networks', 'attribute' => 'show_facebook'], + 'socialShareFacebookUrl' => ['group' => 'networks', 'attribute' => 'facebook_url'], + 'socialShareInstagram' => ['group' => 'networks', 'attribute' => 'show_instagram'], + 'socialShareInstagramUrl' => ['group' => 'networks', 'attribute' => 'instagram_url'], + 'socialShareLinkedin' => ['group' => 'networks', 'attribute' => 'show_linkedin'], + 'socialShareLinkedinUrl' => ['group' => 'networks', 'attribute' => 'linkedin_url'], + 'socialShareWhatsapp' => ['group' => 'networks', 'attribute' => 'show_whatsapp'], + 'socialShareWhatsappNumber' => ['group' => 'networks', 'attribute' => 'whatsapp_number'], + 'socialShareTwitter' => ['group' => 'networks', 'attribute' => 'show_twitter'], + 'socialShareTwitterUrl' => ['group' => 'networks', 'attribute' => 'twitter_url'], + 'socialShareEmail' => ['group' => 'networks', 'attribute' => 'show_email'], + 'socialShareEmailAddress' => ['group' => 'networks', 'attribute' => 'email_address'], + + // Colors + 'socialShareLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'], + 'socialShareBorderTopColor' => ['group' => 'colors', 'attribute' => 'border_top_color'], + 'socialShareButtonBg' => ['group' => 'colors', 'attribute' => 'button_background'], + 'socialShareFacebookColor' => ['group' => 'colors', 'attribute' => 'facebook_color'], + 'socialShareInstagramColor' => ['group' => 'colors', 'attribute' => 'instagram_color'], + 'socialShareLinkedinColor' => ['group' => 'colors', 'attribute' => 'linkedin_color'], + 'socialShareWhatsappColor' => ['group' => 'colors', 'attribute' => 'whatsapp_color'], + 'socialShareTwitterColor' => ['group' => 'colors', 'attribute' => 'twitter_color'], + 'socialShareEmailColor' => ['group' => 'colors', 'attribute' => 'email_color'], + + // Typography + 'socialShareLabelFontSize' => ['group' => 'typography', 'attribute' => 'label_font_size'], + 'socialShareIconFontSize' => ['group' => 'typography', 'attribute' => 'icon_font_size'], + + // Spacing + 'socialShareMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'], + 'socialShareMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'], + 'socialSharePaddingTop' => ['group' => 'spacing', 'attribute' => 'container_padding_top'], + 'socialSharePaddingBottom' => ['group' => 'spacing', 'attribute' => 'container_padding_bottom'], + 'socialShareLabelMarginBottom' => ['group' => 'spacing', 'attribute' => 'label_margin_bottom'], + 'socialShareButtonsGap' => ['group' => 'spacing', 'attribute' => 'buttons_gap'], + 'socialShareButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'], + + // Visual Effects + 'socialShareBorderTopWidth' => ['group' => 'visual_effects', 'attribute' => 'border_top_width'], + 'socialShareButtonBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'button_border_width'], + 'socialShareButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'], + 'socialShareTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + 'socialShareHoverBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'hover_box_shadow'], + + // ===================================================== + // CTA POST + // ===================================================== + + // Visibility + 'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'], + 'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'], + 'ctaPostButtonText' => ['group' => 'content', 'attribute' => 'button_text'], + 'ctaPostButtonUrl' => ['group' => 'content', 'attribute' => 'button_url'], + 'ctaPostButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'], + + // Typography + 'ctaPostTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'], + 'ctaPostTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'], + 'ctaPostDescriptionFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'], + 'ctaPostButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'], + + // Colors + 'ctaPostGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'], + 'ctaPostGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'], + 'ctaPostTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'], + 'ctaPostDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'], + 'ctaPostButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'], + 'ctaPostButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'], + 'ctaPostButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'], + + // Spacing + 'ctaPostContainerMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'], + 'ctaPostContainerMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'], + 'ctaPostContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'], + 'ctaPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'], + 'ctaPostButtonIconMargin' => ['group' => 'spacing', 'attribute' => 'button_icon_margin'], + + // Visual Effects + 'ctaPostBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'], + 'ctaPostGradientAngle' => ['group' => 'visual_effects', 'attribute' => 'gradient_angle'], + 'ctaPostButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'], + 'ctaPostButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'], + 'ctaPostTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + 'ctaPostBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'], + + // ===================================================== + // CONTACT FORM + // ===================================================== + + // Visibility + 'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], + 'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], + 'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], + 'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Content + 'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'], + 'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'], + 'contactFormSubmitButtonText' => ['group' => 'content', 'attribute' => 'submit_button_text'], + 'contactFormSubmitButtonIcon' => ['group' => 'content', 'attribute' => 'submit_button_icon'], + + // Contact Info + 'contactFormShowContactInfo' => ['group' => 'contact_info', 'attribute' => 'show_contact_info'], + 'contactFormPhoneLabel' => ['group' => 'contact_info', 'attribute' => 'phone_label'], + 'contactFormPhoneValue' => ['group' => 'contact_info', 'attribute' => 'phone_value'], + 'contactFormEmailLabel' => ['group' => 'contact_info', 'attribute' => 'email_label'], + 'contactFormEmailValue' => ['group' => 'contact_info', 'attribute' => 'email_value'], + 'contactFormLocationLabel' => ['group' => 'contact_info', 'attribute' => 'location_label'], + 'contactFormLocationValue' => ['group' => 'contact_info', 'attribute' => 'location_value'], + + // Form Labels + 'contactFormFullnamePlaceholder' => ['group' => 'form_labels', 'attribute' => 'fullname_placeholder'], + 'contactFormCompanyPlaceholder' => ['group' => 'form_labels', 'attribute' => 'company_placeholder'], + 'contactFormWhatsappPlaceholder' => ['group' => 'form_labels', 'attribute' => 'whatsapp_placeholder'], + 'contactFormEmailPlaceholder' => ['group' => 'form_labels', 'attribute' => 'email_placeholder'], + 'contactFormMessagePlaceholder' => ['group' => 'form_labels', 'attribute' => 'message_placeholder'], + + // Integration + 'contactFormWebhookUrl' => ['group' => 'integration', 'attribute' => 'webhook_url'], + 'contactFormWebhookMethod' => ['group' => 'integration', 'attribute' => 'webhook_method'], + 'contactFormIncludePageUrl' => ['group' => 'integration', 'attribute' => 'include_page_url'], + 'contactFormIncludeTimestamp' => ['group' => 'integration', 'attribute' => 'include_timestamp'], + + // Messages + 'contactFormSuccessMessage' => ['group' => 'messages', 'attribute' => 'success_message'], + 'contactFormErrorMessage' => ['group' => 'messages', 'attribute' => 'error_message'], + 'contactFormSendingMessage' => ['group' => 'messages', 'attribute' => 'sending_message'], + 'contactFormValidationRequired' => ['group' => 'messages', 'attribute' => 'validation_required'], + 'contactFormValidationEmail' => ['group' => 'messages', 'attribute' => 'validation_email'], + + // Colors + 'contactFormSectionBgColor' => ['group' => 'colors', 'attribute' => 'section_bg_color'], + 'contactFormTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'], + 'contactFormDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'], + 'contactFormIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'], + 'contactFormInfoLabelColor' => ['group' => 'colors', 'attribute' => 'info_label_color'], + 'contactFormInfoValueColor' => ['group' => 'colors', 'attribute' => 'info_value_color'], + 'contactFormInputBorderColor' => ['group' => 'colors', 'attribute' => 'input_border_color'], + 'contactFormInputFocusBorder' => ['group' => 'colors', 'attribute' => 'input_focus_border'], + 'contactFormButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'], + 'contactFormButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'], + 'contactFormButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'], + 'contactFormSuccessBgColor' => ['group' => 'colors', 'attribute' => 'success_bg_color'], + 'contactFormSuccessTextColor' => ['group' => 'colors', 'attribute' => 'success_text_color'], + 'contactFormErrorBgColor' => ['group' => 'colors', 'attribute' => 'error_bg_color'], + 'contactFormErrorTextColor' => ['group' => 'colors', 'attribute' => 'error_text_color'], + + // Spacing + 'contactFormSectionPaddingY' => ['group' => 'spacing', 'attribute' => 'section_padding_y'], + 'contactFormSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'], + 'contactFormTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'], + 'contactFormDescriptionMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'], + 'contactFormFormGap' => ['group' => 'spacing', 'attribute' => 'form_gap'], + + // Visual Effects + 'contactFormInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'], + 'contactFormButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'], + 'contactFormButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'], + 'contactFormTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'], + 'contactFormTextareaRows' => ['group' => 'visual_effects', 'attribute' => 'textarea_rows'], + ]; + } +} diff --git a/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php b/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php new file mode 100644 index 00000000..50ba6cb9 --- /dev/null +++ b/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php @@ -0,0 +1,76 @@ +menuItem = $menuItem; + } + + /** + * Registra el menú en WordPress + */ + public function register(): void + { + add_action('admin_menu', [$this, 'addMenuPage']); + } + + /** + * Callback para agregar la página al menú de WordPress + */ + public function addMenuPage(): void + { + add_menu_page( + $this->menuItem->getPageTitle(), + $this->menuItem->getMenuTitle(), + $this->menuItem->getCapability(), + $this->menuItem->getMenuSlug(), + [$this, 'renderPage'], + $this->menuItem->getIcon(), + $this->menuItem->getPosition() + ); + } + + /** + * Callback para renderizar la página + */ + public function renderPage(): void + { + try { + echo $this->renderUseCase->execute('dashboard'); + } catch (\Exception $e) { + echo '

Error rendering dashboard: ' . esc_html($e->getMessage()) . '

'; + } + } + + public function getCapability(): string + { + return $this->menuItem->getCapability(); + } + + public function getSlug(): string + { + return $this->menuItem->getMenuSlug(); + } +} diff --git a/Admin/Infrastructure/Services/AdminAssetEnqueuer.php b/Admin/Infrastructure/Services/AdminAssetEnqueuer.php new file mode 100644 index 00000000..936c1aad --- /dev/null +++ b/Admin/Infrastructure/Services/AdminAssetEnqueuer.php @@ -0,0 +1,120 @@ +isAdminPage($hook)) { + return; + } + + $this->enqueueStyles(); + $this->enqueueScripts(); + } + + /** + * Verifica si estamos en la página del dashboard + * + * @param string $hook Hook name + * @return bool + */ + private function isAdminPage(string $hook): bool + { + return strpos($hook, self::ADMIN_PAGE_SLUG) !== false; + } + + /** + * Enqueue de estilos CSS + */ + private function enqueueStyles(): void + { + // Bootstrap 5 CSS + wp_enqueue_style( + 'bootstrap', + 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css', + [], + '5.3.2' + ); + + // Bootstrap Icons + wp_enqueue_style( + 'bootstrap-icons', + 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css', + [], + '1.11.3' + ); + + // Estilos del dashboard + wp_enqueue_style( + 'roi-admin-dashboard', + $this->themeUri . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css', + ['bootstrap', 'bootstrap-icons'], + filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css') + ); + } + + /** + * Enqueue de scripts JavaScript + */ + private function enqueueScripts(): void + { + // Bootstrap 5 JS Bundle (incluye Popper) + // IMPORTANTE: Cargar en header (false) para que esté disponible antes del contenido + wp_enqueue_script( + 'bootstrap', + 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js', + [], + '5.3.2', + false // Load in header, not footer - required for Bootstrap tabs to work + ); + + // Script del dashboard + wp_enqueue_script( + 'roi-admin-dashboard', + $this->themeUri . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js', + ['bootstrap'], + filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js'), + true + ); + + // Pasar variables al JavaScript + wp_localize_script( + 'roi-admin-dashboard', + 'roiAdminDashboard', + [ + 'nonce' => wp_create_nonce('roi_admin_dashboard'), + 'ajaxurl' => admin_url('admin-ajax.php') + ] + ); + } +} diff --git a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php new file mode 100644 index 00000000..600b9a32 --- /dev/null +++ b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php @@ -0,0 +1,160 @@ + $components Componentes disponibles + */ + public function __construct( + private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null, + private readonly array $components = [] + ) { + } + + public function render(): string + { + ob_start(); + require __DIR__ . '/Views/dashboard.php'; + return ob_get_clean(); + } + + public function supports(string $viewType): bool + { + return in_array($viewType, self::SUPPORTED_VIEWS, true); + } + + /** + * Obtiene los componentes disponibles + * + * @return array> + */ + public function getComponents(): array + { + return [ + 'top-notification-bar' => [ + 'id' => 'top-notification-bar', + 'label' => 'TopBar', + 'icon' => 'bi-megaphone-fill', + ], + 'navbar' => [ + 'id' => 'navbar', + 'label' => 'Navbar', + 'icon' => 'bi-list', + ], + 'cta-lets-talk' => [ + 'id' => 'cta-lets-talk', + 'label' => "Let's Talk", + 'icon' => 'bi-lightning-charge-fill', + ], + 'hero' => [ + 'id' => 'hero', + 'label' => 'Hero Section', + 'icon' => 'bi-image', + ], + 'featured-image' => [ + 'id' => 'featured-image', + 'label' => 'Featured Image', + 'icon' => 'bi-card-image', + ], + 'table-of-contents' => [ + 'id' => 'table-of-contents', + 'label' => 'Table of Contents', + 'icon' => 'bi-list-nested', + ], + 'cta-box-sidebar' => [ + 'id' => 'cta-box-sidebar', + 'label' => 'CTA Sidebar', + 'icon' => 'bi-megaphone', + ], + 'social-share' => [ + 'id' => 'social-share', + 'label' => 'Social Share', + 'icon' => 'bi-share', + ], + 'cta-post' => [ + 'id' => 'cta-post', + 'label' => 'CTA Post', + 'icon' => 'bi-megaphone-fill', + ], + 'related-post' => [ + 'id' => 'related-post', + 'label' => 'Related Posts', + 'icon' => 'bi-grid-3x3-gap', + ], + 'contact-form' => [ + 'id' => 'contact-form', + 'label' => 'Contact Form', + 'icon' => 'bi-envelope-paper', + ], + 'footer' => [ + 'id' => 'footer', + 'label' => 'Footer', + 'icon' => 'bi-layout-text-window-reverse', + ], + ]; + } + + /** + * Obtiene las configuraciones de un componente + * + * @param string $componentName Nombre del componente + * @return array> Configuraciones agrupadas por grupo + */ + public function getComponentSettings(string $componentName): array + { + if ($this->getComponentSettingsUseCase === null) { + return []; + } + + return $this->getComponentSettingsUseCase->execute($componentName); + } + + /** + * Obtiene el valor de un campo de configuración + * + * @param string $componentName Nombre del componente + * @param string $groupName Nombre del grupo + * @param string $attributeName Nombre del atributo + * @param mixed $default Valor por defecto si no existe + * @return mixed + */ + public function getFieldValue(string $componentName, string $groupName, string $attributeName, mixed $default = null): mixed + { + $settings = $this->getComponentSettings($componentName); + + return $settings[$groupName][$attributeName] ?? $default; + } + + /** + * Obtiene la clase del FormBuilder para un componente + * + * @param string $componentId ID del componente en kebab-case (ej: 'top-notification-bar') + * @return string Namespace completo del FormBuilder + */ + public function getFormBuilderClass(string $componentId): string + { + // Convertir kebab-case a PascalCase + // 'top-notification-bar' → 'TopNotificationBar' + $className = str_replace('-', '', ucwords($componentId, '-')); + + // Construir namespace completo + // ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder + return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder"; + } + +} diff --git a/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css b/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css new file mode 100644 index 00000000..22a3d474 --- /dev/null +++ b/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css @@ -0,0 +1,137 @@ +/** + * Estilos para el Dashboard del Panel de Administración ROI Theme + * Siguiendo especificaciones del Design System + */ + +/* Sobrescribir max-width de .card de WordPress */ +.wrap.roi-admin-panel .card { + max-width: none !important; +} + +/* Fix para switches de Bootstrap - resetear completamente estilos de WordPress */ +.wrap.roi-admin-panel .form-switch .form-check-input { + all: unset !important; + /* Restaurar estilos necesarios de Bootstrap */ + width: 2em !important; + height: 1em !important; + margin-left: -2.5em !important; + margin-right: 0.5em !important; + background-color: #dee2e6 !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important; + background-position: left center !important; + background-repeat: no-repeat !important; + background-size: contain !important; + border: 1px solid rgba(0, 0, 0, 0.25) !important; + border-radius: 2em !important; + transition: background-position 0.15s ease-in-out !important; + cursor: pointer !important; + flex-shrink: 0 !important; + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; +} + +.wrap.roi-admin-panel .form-switch .form-check-input:checked { + background-color: #0d6efd !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important; + background-position: right center !important; + border-color: #0d6efd !important; +} + +.wrap.roi-admin-panel .form-switch .form-check-input::before, +.wrap.roi-admin-panel .form-switch .form-check-input::after { + display: none !important; + content: none !important; +} + +.wrap.roi-admin-panel .form-switch .form-check-input:focus { + outline: 0 !important; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important; +} + +/* Alinear verticalmente los labels con los switches */ +.wrap.roi-admin-panel .form-check { + display: flex !important; + align-items: center !important; +} + +.wrap.roi-admin-panel .form-check-label { + display: inline-flex !important; + align-items: center !important; + margin-bottom: 0 !important; + padding-top: 0 !important; +} + +/* Tabs Navigation */ +.nav-tabs-admin { + border-bottom: 2px solid #e9ecef; +} + +.nav-tabs-admin .nav-item { + margin-right: 0.1rem; +} + +.nav-tabs-admin .nav-link { + color: #6c757d; + border: none; + border-bottom: 3px solid transparent; + padding: 0.3rem 0.4rem; + font-weight: 600; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.nav-tabs-admin .nav-link i.bi { + margin-right: 0.2rem !important; + font-size: 0.7rem; +} + +.nav-tabs-admin .nav-link:hover { + color: #FF8600; + border-bottom-color: #FFB800; +} + +.nav-tabs-admin .nav-link.active { + color: #FF8600; + border-bottom-color: #FF8600; + background-color: transparent; +} + +/* Tab Content */ +.tab-content { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive */ +@media (max-width: 991px) { + .nav-tabs-admin { + flex-wrap: wrap; + } + + .nav-tabs-admin .nav-link { + font-size: 0.8rem; + padding: 0.35rem 0.5rem; + } +} + +@media (max-width: 767px) { + .nav-tabs-admin { + overflow-x: auto; + flex-wrap: nowrap; + } + + .nav-tabs-admin .nav-item { + white-space: nowrap; + } +} diff --git a/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js new file mode 100644 index 00000000..425167ae --- /dev/null +++ b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js @@ -0,0 +1,407 @@ +/** + * JavaScript para el Dashboard del Panel de Administración ROI Theme + * Vanilla JavaScript - No frameworks + */ + +(function() { + 'use strict'; + + /** + * Inicializa el dashboard cuando el DOM está listo + */ + document.addEventListener('DOMContentLoaded', function() { + initializeTabs(); + initializeFormValidation(); + initializeButtons(); + initializeColorPickers(); + }); + + /** + * Inicializa el sistema de tabs + */ + function initializeTabs() { + const tabs = document.querySelectorAll('.nav-tab'); + + tabs.forEach(function(tab) { + tab.addEventListener('click', function(e) { + // Prevenir comportamiento por defecto si es necesario + // (En este caso dejamos que funcione la navegación normal) + }); + }); + } + + /** + * Inicializa validación de formularios + */ + function initializeFormValidation() { + const forms = document.querySelectorAll('.roi-component-config form'); + + forms.forEach(function(form) { + form.addEventListener('submit', function(e) { + if (!validateForm(form)) { + e.preventDefault(); + showError('Por favor, corrige los errores en el formulario.'); + } + }); + }); + } + + /** + * Valida un formulario + * + * @param {HTMLFormElement} form Formulario a validar + * @returns {boolean} True si es válido + */ + function validateForm(form) { + let isValid = true; + const requiredFields = form.querySelectorAll('[required]'); + + requiredFields.forEach(function(field) { + if (!field.value.trim()) { + field.classList.add('error'); + isValid = false; + } else { + field.classList.remove('error'); + } + }); + + return isValid; + } + + /** + * Muestra un mensaje de error + * + * @param {string} message Mensaje a mostrar + */ + function showError(message) { + const notice = document.createElement('div'); + notice.className = 'notice notice-error is-dismissible'; + notice.innerHTML = '

' + escapeHtml(message) + '

'; + + const h1 = document.querySelector('.roi-admin-dashboard h1'); + if (h1 && h1.nextElementSibling) { + h1.nextElementSibling.after(notice); + } + } + + /** + * Escapa HTML para prevenir XSS + * + * @param {string} text Texto a escapar + * @returns {string} Texto escapado + */ + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Inicializa los botones del panel + */ + function initializeButtons() { + // Botón Guardar Cambios + const saveButton = document.getElementById('saveSettings'); + if (saveButton) { + saveButton.addEventListener('click', handleSaveSettings); + } + + // Botón Cancelar + const cancelButton = document.getElementById('cancelChanges'); + if (cancelButton) { + cancelButton.addEventListener('click', handleCancelChanges); + } + + // Botones Restaurar valores por defecto (dinámico para todos los componentes) + const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]'); + resetButtons.forEach(function(button) { + button.addEventListener('click', function(e) { + const componentId = this.getAttribute('data-component'); + handleResetDefaults(e, componentId, this); + }); + }); + } + + /** + * Guarda los cambios del formulario + */ + function handleSaveSettings(e) { + e.preventDefault(); + + // Obtener el tab activo + const activeTab = document.querySelector('.tab-pane.active'); + if (!activeTab) { + showNotice('error', 'No hay ningún componente seleccionado.'); + return; + } + + // Obtener el ID del componente desde el tab + const componentId = activeTab.id.replace('Tab', ''); + + // Recopilar todos los campos del formulario activo + const formData = collectFormData(activeTab); + + // Mostrar loading en el botón + const saveButton = document.getElementById('saveSettings'); + const originalText = saveButton.innerHTML; + saveButton.disabled = true; + saveButton.innerHTML = ' Guardando...'; + + // Enviar por AJAX + fetch(ajaxurl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + action: 'roi_save_component_settings', + nonce: roiAdminDashboard.nonce, + component: componentId, + settings: JSON.stringify(formData) + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotice('success', data.data.message || 'Cambios guardados correctamente.'); + } else { + showNotice('error', data.data.message || 'Error al guardar los cambios.'); + } + }) + .catch(error => { + console.error('Error:', error); + showNotice('error', 'Error de conexión al guardar los cambios.'); + }) + .finally(() => { + saveButton.disabled = false; + saveButton.innerHTML = originalText; + }); + } + + /** + * Cancela los cambios y recarga la página + */ + function handleCancelChanges(e) { + e.preventDefault(); + showConfirmModal( + 'Cancelar cambios', + '¿Descartar todos los cambios no guardados?', + function() { + location.reload(); + } + ); + } + + /** + * Restaura los valores por defecto de un componente + * + * @param {Event} e Evento del click + * @param {string} componentId ID del componente a resetear + * @param {HTMLElement} resetButton Elemento del botón que disparó el evento + */ + function handleResetDefaults(e, componentId, resetButton) { + e.preventDefault(); + + showConfirmModal( + 'Restaurar valores por defecto', + '¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.', + function() { + // Mostrar loading en el botón + const originalText = resetButton.innerHTML; + resetButton.disabled = true; + resetButton.innerHTML = ' Restaurando...'; + + // Enviar por AJAX + fetch(ajaxurl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + action: 'roi_reset_component_defaults', + nonce: roiAdminDashboard.nonce, + component: componentId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotice('success', data.data.message || 'Valores restaurados correctamente.'); + setTimeout(() => location.reload(), 1500); + } else { + showNotice('error', data.data.message || 'Error al restaurar los valores.'); + } + }) + .catch(error => { + console.error('Error:', error); + showNotice('error', 'Error de conexión al restaurar los valores.'); + }) + .finally(() => { + resetButton.disabled = false; + resetButton.innerHTML = originalText; + }); + } + ); + } + + /** + * Recopila los datos del formulario del tab activo + */ + function collectFormData(container) { + const formData = {}; + + // Inputs de texto, textarea, select, color, number, email, password + const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select'); + textInputs.forEach(input => { + if (input.id) { + formData[input.id] = input.value; + } + }); + + // Checkboxes (switches) + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + if (checkbox.id) { + formData[checkbox.id] = checkbox.checked; + } + }); + + return formData; + } + + /** + * Muestra un toast de Bootstrap + */ + function showNotice(type, message) { + // Mapear tipos + const typeMap = { + 'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' }, + 'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' }, + 'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' }, + 'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' } + }; + + const config = typeMap[type] || typeMap['info']; + + // Crear container de toasts si no existe + let toastContainer = document.getElementById('roiToastContainer'); + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.id = 'roiToastContainer'; + toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x'; + toastContainer.style.top = '60px'; + toastContainer.style.zIndex = '999999'; + document.body.appendChild(toastContainer); + } + + // Crear toast + const toastId = 'toast-' + Date.now(); + const toastHTML = ` + + `; + + toastContainer.insertAdjacentHTML('beforeend', toastHTML); + + // Mostrar toast + const toastElement = document.getElementById(toastId); + const toast = new bootstrap.Toast(toastElement, { + autohide: true, + delay: 5000 + }); + + toast.show(); + + // Eliminar del DOM después de ocultarse + toastElement.addEventListener('hidden.bs.toast', function() { + toastElement.remove(); + }); + } + + /** + * Muestra un modal de confirmación de Bootstrap + */ + function showConfirmModal(title, message, onConfirm) { + // Crear modal si no existe + let modal = document.getElementById('roiConfirmModal'); + if (!modal) { + const modalHTML = ` + + `; + document.body.insertAdjacentHTML('beforeend', modalHTML); + modal = document.getElementById('roiConfirmModal'); + } + + // Actualizar contenido + document.getElementById('roiConfirmModalTitle').textContent = title; + document.getElementById('roiConfirmModalBody').textContent = message; + + // Configurar callback + const confirmButton = document.getElementById('roiConfirmModalConfirm'); + const newConfirmButton = confirmButton.cloneNode(true); + confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton); + + newConfirmButton.addEventListener('click', function() { + const bsModal = bootstrap.Modal.getInstance(modal); + bsModal.hide(); + if (typeof onConfirm === 'function') { + onConfirm(); + } + }); + + // Mostrar modal + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + } + + /** + * Inicializa los color pickers para mostrar el valor HEX + */ + function initializeColorPickers() { + const colorPickers = document.querySelectorAll('input[type="color"]'); + + colorPickers.forEach(picker => { + // Elemento donde se muestra el valor HEX + const valueDisplay = document.getElementById(picker.id + 'Value'); + + if (valueDisplay) { + picker.addEventListener('input', function() { + valueDisplay.textContent = this.value.toUpperCase(); + }); + } + }); + } + +})(); diff --git a/Admin/Infrastructure/Ui/Views/dashboard.php b/Admin/Infrastructure/Ui/Views/dashboard.php new file mode 100644 index 00000000..e8d8f08d --- /dev/null +++ b/Admin/Infrastructure/Ui/Views/dashboard.php @@ -0,0 +1,76 @@ +getComponents(); +$firstComponentId = array_key_first($components); +?> + +
+ + + + +
+ $component): + $isFirst = ($componentId === $firstComponentId); + $componentSettings = $this->getComponentSettings($componentId); + ?> + +
+ + getFormBuilderClass($componentId); + if (class_exists($formBuilderClass)) { + $formBuilder = new $formBuilderClass($this); + echo $formBuilder->buildForm($componentId); + } else { + echo '

FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '

'; + } + ?> + +
+ +
+ + +
+ + +
+ +
diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php new file mode 100644 index 00000000..1a568dc0 --- /dev/null +++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php @@ -0,0 +1,517 @@ +buildHeader($componentId); + + // Layout 2 columnas + $html .= '
'; + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildLayoutGroup($componentId); + $html .= $this->buildBehaviorGroup($componentId); + $html .= $this->buildMediaGroup($componentId); + $html .= '
'; + $html .= '
'; + $html .= $this->buildLinksGroup($componentId); + $html .= $this->buildVisualEffectsGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Mobile + $showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Desktop + $showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Select: Show on Pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Switch: Sticky + $sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildLayoutGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Layout y Estructura'; + $html .= '
'; + + // Container Type + $containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Padding Vertical + $paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Z-index + $zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildBehaviorGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Configuración del Menú'; + $html .= '
'; + + // Menu Location + $menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Custom Menu ID + $customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Enable Dropdowns + $enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Mobile Breakpoint + $mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildMediaGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Logo/Marca'; + $html .= '
'; + + // Show Brand + $showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Use Logo + $useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Logo URL + $logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', ''); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Logo Height + $logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Brand Text + $brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Brand Font Size + $brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Brand Color + $brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Brand Hover Color + $brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildLinksGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Estilos de Enlaces'; + $html .= '
'; + + // Text Color + $textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Hover Color + $hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Active Color + $activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Font Size + $fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Font Weight + $fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Padding + $padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Border Radius + $borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Show Underline Effect + $showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Underline Color + $underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildVisualEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Estilos de Dropdown'; + $html .= '
'; + + // Background Color + $bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Border Radius + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Shadow + $shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Item Color + $itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Item Hover Background + $itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Item Padding + $itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Dropdown Max Height + $dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Si se excede, aparece scroll vertical'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Estilos del Navbar'; + $html .= '
'; + + // Background Color + $navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Box Shadow + $boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html b/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html new file mode 100644 index 00000000..403af7f5 --- /dev/null +++ b/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html @@ -0,0 +1,544 @@ + + + + + + Navbar - Preview de Diseño + + + + + + + + + + + + + + + + + + + + diff --git a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php new file mode 100644 index 00000000..98024fc5 --- /dev/null +++ b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php @@ -0,0 +1,501 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildLayoutGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('relatedPostEnabled', 'Activar componente', 'bi-power', $enabled); + + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('relatedPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // Section Title + $sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Posts per page + $postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Order by + $orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Order direction + $order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Show pagination + $showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true); + $html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildLayoutGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Disposicion'; + $html .= '
'; + + // Columns desktop + $colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Columns tablet + $colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Columns mobile + $colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + $html .= '
'; + + $sectionTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_size', '1.75rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sectionTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_weight', '500'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '500'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Seccion + $html .= '

Seccion

'; + $html .= '
'; + + $sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529'); + $html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor); + + $html .= '
'; + + // Cards + $html .= '

Cards

'; + $html .= '
'; + + $cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff'); + $html .= $this->buildColorPicker('relatedPostCardBgColor', 'Fondo card', $cardBgColor); + + $cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#212529'); + $html .= $this->buildColorPicker('relatedPostCardTitleColor', 'Titulo card', $cardTitleColor); + + $html .= '
'; + + $html .= '
'; + + $cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa'); + $html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor); + + $html .= '
'; + + // Paginacion + $html .= '

Paginacion

'; + $html .= '
'; + + $paginationBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_bg_color', '#ffffff'); + $html .= $this->buildColorPicker('relatedPostPaginationBgColor', 'Fondo', $paginationBgColor); + + $paginationTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_text_color', '#0d6efd'); + $html .= $this->buildColorPicker('relatedPostPaginationTextColor', 'Texto', $paginationTextColor); + + $html .= '
'; + + $html .= '
'; + + $paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#0d6efd'); + $html .= $this->buildColorPicker('relatedPostPaginationActiveBg', 'Activo fondo', $paginationActiveBg); + + $paginationActiveText = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_text', '#ffffff'); + $html .= $this->buildColorPicker('relatedPostPaginationActiveText', 'Activo texto', $paginationActiveText); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + $cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.375rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', '0.3s ease'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php new file mode 100644 index 00000000..38bd27b5 --- /dev/null +++ b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php @@ -0,0 +1,529 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildNetworksGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('socialShareEnabled', 'Activar componente', 'bi-power', $enabled); + + // show_on_desktop + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('socialShareShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + // show_on_mobile + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + // show_on_pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // show_label + $showLabel = $this->renderer->getFieldValue($componentId, 'content', 'show_label', true); + $html .= $this->buildSwitch('socialShareShowLabel', 'Mostrar etiqueta', 'bi-tag', $showLabel); + + // label_text + $labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Compartir:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildNetworksGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Redes Sociales'; + $html .= '
'; + $html .= '

Configura las redes sociales y sus URLs

'; + + // Facebook + $showFacebook = $this->renderer->getFieldValue($componentId, 'networks', 'show_facebook', true); + $facebookUrl = $this->renderer->getFieldValue($componentId, 'networks', 'facebook_url', ''); + $html .= $this->buildNetworkField('socialShareFacebook', 'socialShareFacebookUrl', 'Facebook', 'bi-facebook', $showFacebook, $facebookUrl, 'https://facebook.com/tu-pagina'); + + // Instagram + $showInstagram = $this->renderer->getFieldValue($componentId, 'networks', 'show_instagram', true); + $instagramUrl = $this->renderer->getFieldValue($componentId, 'networks', 'instagram_url', ''); + $html .= $this->buildNetworkField('socialShareInstagram', 'socialShareInstagramUrl', 'Instagram', 'bi-instagram', $showInstagram, $instagramUrl, 'https://instagram.com/tu-perfil'); + + // LinkedIn + $showLinkedin = $this->renderer->getFieldValue($componentId, 'networks', 'show_linkedin', true); + $linkedinUrl = $this->renderer->getFieldValue($componentId, 'networks', 'linkedin_url', ''); + $html .= $this->buildNetworkField('socialShareLinkedin', 'socialShareLinkedinUrl', 'LinkedIn', 'bi-linkedin', $showLinkedin, $linkedinUrl, 'https://linkedin.com/in/tu-perfil'); + + // WhatsApp + $showWhatsapp = $this->renderer->getFieldValue($componentId, 'networks', 'show_whatsapp', true); + $whatsappNumber = $this->renderer->getFieldValue($componentId, 'networks', 'whatsapp_number', ''); + $html .= $this->buildNetworkField('socialShareWhatsapp', 'socialShareWhatsappNumber', 'WhatsApp', 'bi-whatsapp', $showWhatsapp, $whatsappNumber, '521234567890'); + + // X (Twitter) + $showTwitter = $this->renderer->getFieldValue($componentId, 'networks', 'show_twitter', true); + $twitterUrl = $this->renderer->getFieldValue($componentId, 'networks', 'twitter_url', ''); + $html .= $this->buildNetworkField('socialShareTwitter', 'socialShareTwitterUrl', 'X (Twitter)', 'bi-twitter-x', $showTwitter, $twitterUrl, 'https://x.com/tu-perfil'); + + // Email + $showEmail = $this->renderer->getFieldValue($componentId, 'networks', 'show_email', true); + $emailAddress = $this->renderer->getFieldValue($componentId, 'networks', 'email_address', ''); + $html .= $this->buildNetworkField('socialShareEmail', 'socialShareEmailAddress', 'Email', 'bi-envelope', $showEmail, $emailAddress, 'contacto@tudominio.com'); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildNetworkField(string $switchId, string $urlId, string $label, string $icon, mixed $checked, string $urlValue, string $placeholder): string + { + // Normalizar valor booleano desde BD + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + + // Switch + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($switchId), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + + // URL Input + $html .= sprintf( + ' ', + esc_attr($urlId), + esc_attr($urlValue), + esc_attr($placeholder) + ); + + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Colores generales + $html .= '

General

'; + $html .= '
'; + + $labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#6c757d'); + $html .= $this->buildColorPicker('socialShareLabelColor', 'Etiqueta', $labelColor); + + $borderTopColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_top_color', '#dee2e6'); + $html .= $this->buildColorPicker('socialShareBorderTopColor', 'Borde superior', $borderTopColor); + + $html .= '
'; + + $html .= '
'; + + $buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff'); + $html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground); + + $html .= '
'; + + // Colores por red social + $html .= '

Redes Sociales

'; + $html .= '
'; + + $facebookColor = $this->renderer->getFieldValue($componentId, 'colors', 'facebook_color', '#0d6efd'); + $html .= $this->buildColorPicker('socialShareFacebookColor', 'Facebook', $facebookColor); + + $instagramColor = $this->renderer->getFieldValue($componentId, 'colors', 'instagram_color', '#dc3545'); + $html .= $this->buildColorPicker('socialShareInstagramColor', 'Instagram', $instagramColor); + + $html .= '
'; + + $html .= '
'; + + $linkedinColor = $this->renderer->getFieldValue($componentId, 'colors', 'linkedin_color', '#0dcaf0'); + $html .= $this->buildColorPicker('socialShareLinkedinColor', 'LinkedIn', $linkedinColor); + + $whatsappColor = $this->renderer->getFieldValue($componentId, 'colors', 'whatsapp_color', '#198754'); + $html .= $this->buildColorPicker('socialShareWhatsappColor', 'WhatsApp', $whatsappColor); + + $html .= '
'; + + $html .= '
'; + + $twitterColor = $this->renderer->getFieldValue($componentId, 'colors', 'twitter_color', '#212529'); + $html .= $this->buildColorPicker('socialShareTwitterColor', 'X (Twitter)', $twitterColor); + + $emailColor = $this->renderer->getFieldValue($componentId, 'colors', 'email_color', '#6c757d'); + $html .= $this->buildColorPicker('socialShareEmailColor', 'Email', $emailColor); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + $html .= '
'; + + // label_font_size + $labelFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'label_font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // icon_font_size + $iconFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'icon_font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + // container_margin_top + $containerMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // container_margin_bottom + $containerMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // container_padding_top + $containerPaddingTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_top', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // container_padding_bottom + $containerPaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_bottom', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // label_margin_bottom + $labelMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'label_margin_bottom', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // buttons_gap + $buttonsGap = $this->renderer->getFieldValue($componentId, 'spacing', 'buttons_gap', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // button_padding + $buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + // border_top_width + $borderTopWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_top_width', '1px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // button_border_width + $buttonBorderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_width', '2px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // button_border_radius + $buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // transition_duration + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // hover_box_shadow + $hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + // Normalizar valor booleano desde BD + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php new file mode 100644 index 00000000..cb4238ce --- /dev/null +++ b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php @@ -0,0 +1,588 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildBehaviorGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= $this->buildSwitch('tocEnabled', 'Activar tabla de contenido', 'bi-power', $enabled); + + // show_on_desktop + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= $this->buildSwitch('tocShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop); + + // show_on_mobile + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false); + $html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); + + // show_on_pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // title + $title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // auto_generate + $autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true); + $html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate); + $html .= ' Genera TOC desde los encabezados del contenido'; + + // heading_levels + $headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Separados por coma: h2,h3,h4'; + $html .= '
'; + + // smooth_scroll + $smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true); + $html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildBehaviorGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Comportamiento'; + $html .= '
'; + + // is_sticky + $isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', true); + $html .= $this->buildSwitch('tocIsSticky', 'Sticky (fijo al scroll)', 'bi-pin', $isSticky); + + // scroll_offset + $scrollOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'scroll_offset', '100'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // max_height + $maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + $html .= '
'; + + // title_font_size + $titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // title_font_weight + $titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // link_font_size + $linkFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'link_font_size', '0.9rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // link_line_height + $linkLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'link_line_height', '1.3'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // level_three_font_size + $level3FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_three_font_size', '0.85rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // level_four_font_size + $level4FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_four_font_size', '0.8rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Colores principales + $html .= '

Contenedor

'; + $html .= '
'; + + $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#ffffff'); + $html .= $this->buildColorPicker('tocBackgroundColor', 'Fondo', $bgColor); + + $borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', '#E6E9ED'); + $html .= $this->buildColorPicker('tocBorderColor', 'Borde', $borderColor); + + $html .= '
'; + + // Colores del titulo + $html .= '

Titulo

'; + $html .= '
'; + + $titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337'); + $html .= $this->buildColorPicker('tocTitleColor', 'Color', $titleColor); + + $titleBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_border_color', '#E6E9ED'); + $html .= $this->buildColorPicker('tocTitleBorderColor', 'Borde', $titleBorderColor); + + $html .= '
'; + + // Colores de enlaces + $html .= '

Enlaces

'; + $html .= '
'; + + $linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#6B7280'); + $html .= $this->buildColorPicker('tocLinkColor', 'Normal', $linkColor); + + $linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#0E2337'); + $html .= $this->buildColorPicker('tocLinkHoverColor', 'Hover', $linkHoverColor); + + $html .= '
'; + + $html .= '
'; + + $linkHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_background', '#F9FAFB'); + $html .= $this->buildColorPicker('tocLinkHoverBackground', 'Fondo hover', $linkHoverBg); + + $activeBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_border_color', '#0E2337'); + $html .= $this->buildColorPicker('tocActiveBorderColor', 'Borde activo', $activeBorderColor); + + $html .= '
'; + + // Colores de activo + $html .= '

Estado Activo

'; + $html .= '
'; + + $activeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_background_color', '#F9FAFB'); + $html .= $this->buildColorPicker('tocActiveBackgroundColor', 'Fondo', $activeBgColor); + + $activeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_text_color', '#0E2337'); + $html .= $this->buildColorPicker('tocActiveTextColor', 'Texto', $activeTextColor); + + $html .= '
'; + + // Colores de scrollbar + $html .= '

Scrollbar

'; + $html .= '
'; + + $scrollbarTrack = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_track_color', '#F9FAFB'); + $html .= $this->buildColorPicker('tocScrollbarTrackColor', 'Pista', $scrollbarTrack); + + $scrollbarThumb = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_thumb_color', '#6B7280'); + $html .= $this->buildColorPicker('tocScrollbarThumbColor', 'Thumb', $scrollbarThumb); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + // container_padding + $containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '12px 16px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // margin_bottom + $marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '13px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // title_padding_bottom + $titlePaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_padding_bottom', '8px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // title_margin_bottom + $titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // item_margin_bottom + $itemMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'item_margin_bottom', '0.15rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // link_padding + $linkPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'link_padding', '0.3rem 0.85rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // level_three_padding_left + $level3PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_three_padding_left', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // level_four_padding_left + $level4PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_four_padding_left', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + // border_radius + $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // border_width + $borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '1px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + // box_shadow + $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + // link_border_radius + $linkBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'link_border_radius', '4px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // active_border_left_width + $activeBorderLeftWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'active_border_left_width', '3px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + // transition_duration + $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // scrollbar_border_radius + $scrollbarBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'scrollbar_border_radius', '3px'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, bool $checked): string + { + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php new file mode 100644 index 00000000..b5087dc6 --- /dev/null +++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php @@ -0,0 +1,308 @@ +buildHeader($componentId); + + // Layout 2 columnas + $html .= '
'; + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= '
'; + $html .= '
'; + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildTypographyAndSpacingGroup($componentId); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Mobile + $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Switch: Show on Desktop + $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true); + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // Select: Show on Pages + $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // icon_class + label_text (row) + $html .= '
'; + $html .= '
'; + $html .= ' '; + $iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-megaphone-fill'); + $html .= ' '; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Nuevo:'); + $html .= ' '; + $html .= '
'; + $html .= '
'; + + // message_text (textarea) + $messageText = $this->renderer->getFieldValue($componentId, 'content', 'message_text', + 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Máximo 200 caracteres'; + $html .= '
'; + + // link_text + link_url (row) + $html .= '
'; + $html .= '
'; + $html .= ' '; + $linkText = $this->renderer->getFieldValue($componentId, 'content', 'link_text', 'Ver Catálogo'); + $html .= ' '; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $linkUrl = $this->renderer->getFieldValue($componentId, 'content', 'link_url', '#'); + $html .= ' '; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Grid 2x3 de color pickers + $html .= '
'; + + // Background Color + $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#0E2337'); + $html .= $this->buildColorPicker('topBarBackgroundColor', 'Color de fondo', 'paint-bucket', $bgColor); + + // Text Color + $textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF'); + $html .= $this->buildColorPicker('topBarTextColor', 'Color de texto', 'fonts', $textColor); + + // Label Color + $labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#FF8600'); + $html .= $this->buildColorPicker('topBarLabelColor', 'Color etiqueta', 'tag-fill', $labelColor); + + // Icon Color + $iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600'); + $html .= $this->buildColorPicker('topBarIconColor', 'Color ícono', 'star', $iconColor); + + $html .= '
'; + + // Row 2 de color pickers + $html .= '
'; + + // Link Color + $linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#FFFFFF'); + $html .= $this->buildColorPicker('topBarLinkColor', 'Color enlace', 'link', $linkColor); + + // Link Hover Color + $linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600'); + $html .= $this->buildColorPicker('topBarLinkHoverColor', 'Color enlace (hover)', 'hand-index', $linkHoverColor); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyAndSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografía y Espaciado'; + $html .= '
'; + + $html .= '
'; + + // Font Size + $html .= '
'; + $html .= ' '; + $fontSize = $this->renderer->getFieldValue($componentId, 'spacing', 'font_size', '0.9rem'); + $html .= ' '; + $html .= ' Ej: 0.9rem, 14px'; + $html .= '
'; + + // Padding + $html .= '
'; + $html .= ' '; + $padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '0.5rem 0'); + $html .= ' '; + $html .= ' Ej: 0.5rem 0'; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $icon, string $value): string + { + $html = '
'; + $html .= ' '; + $html .= ' '; + $html .= ' ' . esc_html(strtoupper($value)) . ''; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html b/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html new file mode 100644 index 00000000..3b536771 --- /dev/null +++ b/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html @@ -0,0 +1,283 @@ + + + + + + TopBar - Preview de Diseño + + + + + + + + + + + + + +
+ + +
+
+
+

+ + Configuración de TopBar +

+

+ Personaliza la barra de notificación superior del sitio +

+
+ +
+
+ + +
+
+ +
+
+
+ + Activación y Visibilidad +
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+
+ + Contenido +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + Máximo 200 caracteres +
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ +
+
+
+ + Estilos - Colores +
+ + +
+
+ + + #0E2337 +
+
+ + + #FFFFFF +
+
+ + + #FF8600 +
+
+ + + #FF8600 +
+
+ +
+
+ + + #FFFFFF +
+
+ + + #FF8600 +
+
+
+
+ + +
+
+
+ + Estilos - Tamaños +
+ +
+
+ + + Ej: 0.9rem, 14px +
+
+ + + Ej: 0.5rem 0 +
+
+
+
+
+
+ +
+ + + + diff --git a/src/Component/Application/UseCases/SyncSchema/.gitkeep b/Public/.gitkeep similarity index 100% rename from src/Component/Application/UseCases/SyncSchema/.gitkeep rename to Public/.gitkeep diff --git a/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php b/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php new file mode 100644 index 00000000..23c69ba3 --- /dev/null +++ b/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php @@ -0,0 +1,303 @@ + __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme') + ], 403); + return; + } + + // 2. Rate limiting basico (1 envio por IP cada 30 segundos) + if (!$this->checkRateLimit()) { + wp_send_json_error([ + 'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme') + ], 429); + return; + } + + // 3. Sanitizar y validar inputs + $formData = $this->sanitizeFormData($_POST); + $validation = $this->validateFormData($formData); + + if (!$validation['valid']) { + wp_send_json_error([ + 'message' => $validation['message'], + 'errors' => $validation['errors'] + ], 422); + return; + } + + // 4. Obtener configuracion del componente (incluye webhook URL) + $settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME); + + if (empty($settings)) { + wp_send_json_error([ + 'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme') + ], 500); + return; + } + + $integration = $settings['integration'] ?? []; + $webhookUrl = $integration['webhook_url'] ?? ''; + $webhookMethod = $integration['webhook_method'] ?? 'POST'; + $includePageUrl = $this->toBool($integration['include_page_url'] ?? true); + $includeTimestamp = $this->toBool($integration['include_timestamp'] ?? true); + + if (empty($webhookUrl)) { + // Si no hay webhook configurado, simular exito para UX + // pero loguear warning para admin + error_log('ROI Theme Contact Form: No webhook URL configured'); + wp_send_json_success([ + 'message' => $this->getSuccessMessage($settings) + ]); + return; + } + + // 5. Preparar payload para webhook + $payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp); + + // 6. Enviar a webhook + $result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload); + + if ($result['success']) { + wp_send_json_success([ + 'message' => $this->getSuccessMessage($settings) + ]); + } else { + error_log('ROI Theme Contact Form webhook error: ' . $result['error']); + wp_send_json_error([ + 'message' => $this->getErrorMessage($settings) + ], 500); + } + } + + /** + * Sanitizar datos del formulario + */ + private function sanitizeFormData(array $post): array + { + return [ + 'fullName' => sanitize_text_field($post['fullName'] ?? ''), + 'company' => sanitize_text_field($post['company'] ?? ''), + 'whatsapp' => sanitize_text_field($post['whatsapp'] ?? ''), + 'email' => sanitize_email($post['email'] ?? ''), + 'message' => sanitize_textarea_field($post['message'] ?? ''), + ]; + } + + /** + * Validar datos del formulario + */ + private function validateFormData(array $data): array + { + $errors = []; + + // Nombre requerido + if (empty($data['fullName'])) { + $errors['fullName'] = __('El nombre es obligatorio', 'roi-theme'); + } + + // WhatsApp requerido + if (empty($data['whatsapp'])) { + $errors['whatsapp'] = __('El WhatsApp es obligatorio', 'roi-theme'); + } + + // Email requerido y valido + if (empty($data['email'])) { + $errors['email'] = __('El email es obligatorio', 'roi-theme'); + } elseif (!is_email($data['email'])) { + $errors['email'] = __('Por favor ingresa un email valido', 'roi-theme'); + } + + if (!empty($errors)) { + return [ + 'valid' => false, + 'message' => __('Por favor corrige los errores del formulario', 'roi-theme'), + 'errors' => $errors + ]; + } + + return ['valid' => true, 'message' => '', 'errors' => []]; + } + + /** + * Preparar payload para webhook + */ + private function preparePayload(array $formData, bool $includePageUrl, bool $includeTimestamp): array + { + $payload = [ + 'fullName' => $formData['fullName'], + 'company' => $formData['company'], + 'whatsapp' => $formData['whatsapp'], + 'email' => $formData['email'], + 'message' => $formData['message'], + ]; + + if ($includePageUrl) { + $payload['pageUrl'] = sanitize_url($_POST['pageUrl'] ?? ''); + $payload['pageTitle'] = sanitize_text_field($_POST['pageTitle'] ?? ''); + } + + if ($includeTimestamp) { + $payload['timestamp'] = current_time('c'); + $payload['timezone'] = wp_timezone_string(); + } + + // Metadata adicional util para el webhook + $payload['source'] = 'contact-form'; + $payload['siteName'] = get_bloginfo('name'); + $payload['siteUrl'] = home_url(); + + return $payload; + } + + /** + * Enviar datos al webhook + */ + private function sendToWebhook(string $url, string $method, array $payload): array + { + $args = [ + 'method' => strtoupper($method), + 'timeout' => 30, + 'redirection' => 5, + 'httpversion' => '1.1', + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + ]; + + if ($method === 'POST') { + $args['body'] = wp_json_encode($payload); + } else { + $url = add_query_arg($payload, $url); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + return [ + 'success' => false, + 'error' => $response->get_error_message() + ]; + } + + $statusCode = wp_remote_retrieve_response_code($response); + + // Considerar 2xx como exito + if ($statusCode >= 200 && $statusCode < 300) { + return ['success' => true, 'error' => '']; + } + + return [ + 'success' => false, + 'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response)) + ]; + } + + /** + * Rate limiting basico por IP + */ + private function checkRateLimit(): bool + { + $ip = $this->getClientIP(); + $transientKey = 'roi_contact_form_' . md5($ip); + $lastSubmit = get_transient($transientKey); + + if ($lastSubmit !== false) { + return false; + } + + set_transient($transientKey, time(), 30); + return true; + } + + /** + * Obtener IP del cliente + */ + private function getClientIP(): string + { + $ip = ''; + + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']); + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]); + } elseif (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']); + } + + return $ip; + } + + /** + * Obtener mensaje de exito desde configuracion + */ + private function getSuccessMessage(array $data): string + { + $messages = $data['messages'] ?? []; + return $messages['success_message'] ?? __('¡Gracias por contactarnos! Te responderemos pronto.', 'roi-theme'); + } + + /** + * Obtener mensaje de error desde configuracion + */ + private function getErrorMessage(array $data): string + { + $messages = $data['messages'] ?? []; + return $messages['error_message'] ?? __('Hubo un error al enviar el mensaje. Por favor intenta de nuevo.', 'roi-theme'); + } + + /** + * Convertir valor a boolean + */ + private function toBool($value): bool + { + return $value === true || $value === '1' || $value === 1; + } +} diff --git a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php new file mode 100644 index 00000000..dae7b4e7 --- /dev/null +++ b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php @@ -0,0 +1,461 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $visibilityClass = $this->getVisibilityClass($data); + if ($visibilityClass === null) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data, $visibilityClass); + $js = $this->buildJS($data); + + return sprintf("\n%s\n", $css, $html, $js); + } + + public function supports(string $componentType): bool + { + return $componentType === 'contact-form'; + } + + private function isEnabled(array $data): bool + { + $value = $data['visibility']['is_enabled'] ?? false; + return $value === true || $value === '1' || $value === 1; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'all'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function getVisibilityClass(array $data): ?string + { + $showDesktop = $data['visibility']['show_on_desktop'] ?? true; + $showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1; + $showMobile = $data['visibility']['show_on_mobile'] ?? true; + $showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1; + + if (!$showDesktop && !$showMobile) { + return null; + } + if (!$showDesktop && $showMobile) { + return 'd-lg-none'; + } + if ($showDesktop && !$showMobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $effects = $data['visual_effects'] ?? []; + + $cssRules = []; + + // Section background + $sectionBgColor = $colors['section_bg_color'] ?? 'rgba(108, 117, 125, 0.25)'; + $sectionPaddingY = $spacing['section_padding_y'] ?? '3rem'; + $sectionMarginTop = $spacing['section_margin_top'] ?? '3rem'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section', [ + 'background-color' => $sectionBgColor, + 'padding-top' => $sectionPaddingY, + 'padding-bottom' => $sectionPaddingY, + 'margin-top' => $sectionMarginTop, + ]); + + // Title + $titleColor = $colors['title_color'] ?? '#212529'; + $titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.75rem'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-title', [ + 'color' => $titleColor, + 'margin-bottom' => $titleMarginBottom, + ]); + + // Description + $descColor = $colors['description_color'] ?? '#212529'; + $descMarginBottom = $spacing['description_margin_bottom'] ?? '1.5rem'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-description', [ + 'color' => $descColor, + 'margin-bottom' => $descMarginBottom, + ]); + + // Icons + $iconColor = $colors['icon_color'] ?? '#FF8600'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-icon', [ + 'color' => $iconColor, + ]); + + // Info labels and values + $infoLabelColor = $colors['info_label_color'] ?? '#212529'; + $infoValueColor = $colors['info_value_color'] ?? '#6c757d'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-label', [ + 'color' => $infoLabelColor, + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-value', [ + 'color' => $infoValueColor, + ]); + + // Form inputs + $inputBorderColor = $colors['input_border_color'] ?? '#dee2e6'; + $inputFocusBorder = $colors['input_focus_border'] ?? '#FF8600'; + $inputBorderRadius = $effects['input_border_radius'] ?? '6px'; + $transitionDuration = $effects['transition_duration'] ?? '0.3s'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control', [ + 'border-color' => $inputBorderColor, + 'border-radius' => $inputBorderRadius, + 'transition' => "all {$transitionDuration} ease", + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control:focus', [ + 'border-color' => $inputFocusBorder, + 'box-shadow' => "0 0 0 0.2rem rgba(255, 134, 0, 0.25)", + 'outline' => 'none', + ]); + + // Submit button + $buttonBgColor = $colors['button_bg_color'] ?? '#FF8600'; + $buttonTextColor = $colors['button_text_color'] ?? '#ffffff'; + $buttonHoverBg = $colors['button_hover_bg'] ?? '#e67a00'; + $buttonBorderRadius = $effects['button_border_radius'] ?? '6px'; + $buttonPadding = $effects['button_padding'] ?? '0.75rem 2rem'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit', [ + 'background-color' => $buttonBgColor, + 'color' => $buttonTextColor, + 'font-weight' => '600', + 'padding' => $buttonPadding, + 'border' => 'none', + 'border-radius' => $buttonBorderRadius, + 'transition' => "all {$transitionDuration} ease", + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:hover', [ + 'background-color' => $buttonHoverBg, + 'color' => $buttonTextColor, + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:disabled', [ + 'opacity' => '0.7', + 'cursor' => 'not-allowed', + ]); + + // Success/Error messages + $successBgColor = $colors['success_bg_color'] ?? '#d1e7dd'; + $successTextColor = $colors['success_text_color'] ?? '#0f5132'; + $errorBgColor = $colors['error_bg_color'] ?? '#f8d7da'; + $errorTextColor = $colors['error_text_color'] ?? '#842029'; + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-success', [ + 'background-color' => $successBgColor, + 'color' => $successTextColor, + 'border-color' => $successBgColor, + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-danger', [ + 'background-color' => $errorBgColor, + 'color' => $errorTextColor, + 'border-color' => $errorBgColor, + ]); + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data, string $visibilityClass): string + { + $content = $data['content'] ?? []; + $contactInfo = $data['contact_info'] ?? []; + $formLabels = $data['form_labels'] ?? []; + $effects = $data['visual_effects'] ?? []; + + // Content + $sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?'; + $sectionDesc = $content['section_description'] ?? 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.'; + $submitText = $content['submit_button_text'] ?? 'Enviar Mensaje'; + $submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill'; + + // Contact info + $showContactInfo = $contactInfo['show_contact_info'] ?? true; + $showContactInfo = $showContactInfo === true || $showContactInfo === '1' || $showContactInfo === 1; + + // Form labels/placeholders + $fullnamePlaceholder = $formLabels['fullname_placeholder'] ?? 'Nombre completo *'; + $companyPlaceholder = $formLabels['company_placeholder'] ?? 'Empresa'; + $whatsappPlaceholder = $formLabels['whatsapp_placeholder'] ?? 'WhatsApp *'; + $emailPlaceholder = $formLabels['email_placeholder'] ?? 'Correo electrónico *'; + $messagePlaceholder = $formLabels['message_placeholder'] ?? '¿En qué podemos ayudarte?'; + + $textareaRows = $effects['textarea_rows'] ?? '4'; + + // Container class + $containerClass = 'roi-contact-form-section'; + if (!empty($visibilityClass)) { + $containerClass .= ' ' . $visibilityClass; + } + + // Nonce for AJAX security + $nonce = wp_create_nonce('roi_contact_form_nonce'); + + $html = sprintf('
', esc_attr($containerClass)); + $html .= '
'; + $html .= '
'; + $html .= '
'; + $html .= '
'; + + // Left column - Contact info + $html .= '
'; + $html .= sprintf('

%s

', esc_html($sectionTitle)); + $html .= sprintf('

%s

', esc_html($sectionDesc)); + + if ($showContactInfo) { + $html .= $this->buildContactInfoHTML($contactInfo); + } + + $html .= '
'; + + // Right column - Form + $html .= '
'; + $html .= sprintf('
', esc_attr($nonce)); + $html .= '
'; + + // Full name field + $html .= '
'; + $html .= sprintf( + '', + esc_attr($fullnamePlaceholder) + ); + $html .= '
'; + + // Company field + $html .= '
'; + $html .= sprintf( + '', + esc_attr($companyPlaceholder) + ); + $html .= '
'; + + // WhatsApp field + $html .= '
'; + $html .= sprintf( + '', + esc_attr($whatsappPlaceholder) + ); + $html .= '
'; + + // Email field + $html .= '
'; + $html .= sprintf( + '', + esc_attr($emailPlaceholder) + ); + $html .= '
'; + + // Message field + $html .= '
'; + $html .= sprintf( + '', + esc_attr($textareaRows), + esc_attr($messagePlaceholder) + ); + $html .= '
'; + + // Submit button + $html .= '
'; + $html .= ''; + $html .= '
'; + + // Message container + $html .= ''; + + $html .= '
'; // .row g-3 + $html .= '
'; + $html .= '
'; // .col-lg-7 + + $html .= '
'; // .row + $html .= '
'; // .col-lg-10 + $html .= '
'; // .row justify-content-center + $html .= '
'; // .container + $html .= '
'; + + return $html; + } + + private function buildContactInfoHTML(array $contactInfo): string + { + $phoneLabel = $contactInfo['phone_label'] ?? 'Teléfono'; + $phoneValue = $contactInfo['phone_value'] ?? '+52 55 1234 5678'; + $emailLabel = $contactInfo['email_label'] ?? 'Email'; + $emailValue = $contactInfo['email_value'] ?? 'contacto@apumexico.com'; + $locationLabel = $contactInfo['location_label'] ?? 'Ubicación'; + $locationValue = $contactInfo['location_value'] ?? 'Ciudad de México, México'; + + $html = '
'; + + // Phone + $html .= '
'; + $html .= ''; + $html .= '
'; + $html .= sprintf('
%s
', esc_html($phoneLabel)); + $html .= sprintf('

%s

', esc_html($phoneValue)); + $html .= '
'; + $html .= '
'; + + // Email + $html .= '
'; + $html .= ''; + $html .= '
'; + $html .= sprintf('
%s
', esc_html($emailLabel)); + $html .= sprintf('

%s

', esc_html($emailValue)); + $html .= '
'; + $html .= '
'; + + // Location + $html .= '
'; + $html .= ''; + $html .= '
'; + $html .= sprintf('
%s
', esc_html($locationLabel)); + $html .= sprintf('

%s

', esc_html($locationValue)); + $html .= '
'; + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildJS(array $data): string + { + $messages = $data['messages'] ?? []; + $content = $data['content'] ?? []; + + $successMessage = $messages['success_message'] ?? '¡Gracias por contactarnos! Te responderemos pronto.'; + $errorMessage = $messages['error_message'] ?? 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.'; + $sendingMessage = $messages['sending_message'] ?? 'Enviando...'; + $submitText = $content['submit_button_text'] ?? 'Enviar Mensaje'; + $submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill'; + + // AJAX URL for WordPress + $ajaxUrl = admin_url('admin-ajax.php'); + + $js = <<getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data); + $script = $this->buildScript(); + + return sprintf("\n%s\n%s", $css, $html, $script); + } + + public function supports(string $componentType): bool + { + return $componentType === 'cta-box-sidebar'; + } + + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $typography = $data['typography'] ?? []; + $effects = $data['visual_effects'] ?? []; + $behavior = $data['behavior'] ?? []; + $visibility = $data['visibility'] ?? []; + + $cssRules = []; + $transitionDuration = $effects['transition_duration'] ?? '0.3s'; + + // Container styles - Match template exactly (height: 250px, flexbox centering) + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar', [ + 'background' => $colors['background_color'] ?? '#FF8600', + 'border-radius' => $effects['border_radius'] ?? '8px', + 'padding' => $spacing['container_padding'] ?? '24px', + 'text-align' => $behavior['text_align'] ?? 'center', + 'box-shadow' => $effects['box_shadow'] ?? '0 4px 12px rgba(255, 133, 0, 0.2)', + 'margin-top' => '0', + 'margin-bottom' => '15px', + 'height' => '250px', + 'display' => 'flex', + 'flex-direction' => 'column', + 'justify-content' => 'center', + ]); + + // Title styles + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-title', [ + 'color' => $colors['title_color'] ?? '#ffffff', + 'font-weight' => $typography['title_font_weight'] ?? '700', + 'font-size' => $typography['title_font_size'] ?? '1.25rem', + 'margin-bottom' => $spacing['title_margin_bottom'] ?? '1rem', + 'margin-top' => '0', + ]); + + // Description styles + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-text', [ + 'color' => $colors['description_color'] ?? 'rgba(255, 255, 255, 0.95)', + 'font-size' => $typography['description_font_size'] ?? '0.9rem', + 'margin-bottom' => $spacing['description_margin_bottom'] ?? '1rem', + ]); + + // Button styles + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box', [ + 'background-color' => $colors['button_background_color'] ?? '#ffffff', + 'color' => $colors['button_text_color'] ?? '#FF8600', + 'font-weight' => $typography['button_font_weight'] ?? '700', + 'font-size' => $typography['button_font_size'] ?? '1rem', + 'border' => 'none', + 'padding' => $spacing['button_padding'] ?? '0.75rem 1.5rem', + 'border-radius' => $effects['button_border_radius'] ?? '8px', + 'transition' => "all {$transitionDuration} ease", + 'cursor' => 'pointer', + 'display' => 'inline-flex', + 'align-items' => 'center', + 'justify-content' => 'center', + 'width' => '100%', + ]); + + // Button hover styles (template uses --color-navy-primary = #1e3a5f) + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box:hover', [ + 'background-color' => $colors['button_hover_background'] ?? '#1e3a5f', + 'color' => $colors['button_hover_text_color'] ?? '#ffffff', + ]); + + // Button icon spacing + $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box i', [ + 'margin-right' => $spacing['icon_margin_right'] ?? '0.5rem', + ]); + + // Responsive visibility + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnMobile = $visibility['show_on_mobile'] ?? false; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 991.98px) { + .cta-box-sidebar { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 992px) { + .cta-box-sidebar { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data): string + { + $content = $data['content'] ?? []; + + $title = $content['title'] ?? '¿Listo para potenciar tus proyectos?'; + $description = $content['description'] ?? 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.'; + $buttonText = $content['button_text'] ?? 'Solicitar Demo'; + $buttonIcon = $content['button_icon'] ?? 'bi bi-calendar-check'; + $buttonAction = $content['button_action'] ?? 'modal'; + $buttonLink = $content['button_link'] ?? '#contactModal'; + + // Build button attributes based on action type + $buttonAttributes = $this->getButtonAttributes($buttonAction, $buttonLink); + + $html = '
'; + + // Title + $html .= sprintf( + '
%s
', + esc_html($title) + ); + + // Description + $html .= sprintf( + '

%s

', + esc_html($description) + ); + + // Button + $iconHtml = !empty($buttonIcon) + ? sprintf('', esc_attr($buttonIcon)) + : ''; + + $html .= sprintf( + '', + $buttonAttributes, + $iconHtml, + esc_html($buttonText) + ); + + $html .= '
'; + + return $html; + } + + private function getButtonAttributes(string $action, string $link): string + { + switch ($action) { + case 'modal': + // Extract modal ID from link (e.g., #contactModal -> contactModal) + $modalId = ltrim($link, '#'); + return sprintf( + 'type="button" data-bs-toggle="modal" data-bs-target="#%s"', + esc_attr($modalId) + ); + + case 'link': + return sprintf( + 'type="button" data-cta-action="link" data-cta-href="%s"', + esc_url($link) + ); + + case 'scroll': + $targetId = ltrim($link, '#'); + return sprintf( + 'type="button" data-cta-action="scroll" data-cta-target="%s"', + esc_attr($targetId) + ); + + default: + return 'type="button"'; + } + } + + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if (!$desktop && !$mobile) { + return null; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + private function buildScript(): string + { + return << +document.addEventListener('DOMContentLoaded', function() { + var ctaButtons = document.querySelectorAll('.btn-cta-box[data-cta-action]'); + ctaButtons.forEach(function(btn) { + btn.addEventListener('click', function() { + var action = this.getAttribute('data-cta-action'); + if (action === 'link') { + var href = this.getAttribute('data-cta-href'); + if (href) window.location.href = href; + } else if (action === 'scroll') { + var target = this.getAttribute('data-cta-target'); + var el = document.getElementById(target); + if (el) el.scrollIntoView({behavior: 'smooth'}); + } + }); + }); +}); + +JS; + } +} diff --git a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php new file mode 100644 index 00000000..34382d7e --- /dev/null +++ b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php @@ -0,0 +1,360 @@ +getData(); + + // Validar visibilidad general + if (!$this->isEnabled($data)) { + return ''; + } + + // Validar visibilidad por página + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + // Generar CSS usando CSSGeneratorService + $css = $this->generateCSS($data); + + // Generar HTML + $html = $this->buildHTML($data); + + // Combinar todo + return sprintf( + "\n%s", + $css, + $html + ); + } + + /** + * {@inheritDoc} + */ + public function supports(string $componentType): bool + { + return $componentType === 'cta-lets-talk'; + } + + /** + * Verificar si el componente está habilitado + * + * @param array $data Datos del componente + * @return bool + */ + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + /** + * Verificar si debe mostrarse en la página actual + * + * @param array $data Datos del componente + * @return bool + */ + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'all'; + + return match ($showOn) { + 'all' => true, + 'home' => is_front_page(), + 'posts' => is_single(), + 'pages' => is_page(), + default => true, + }; + } + + /** + * Calcular clases de visibilidad responsive + * + * @param bool $desktop Mostrar en desktop + * @param bool $mobile Mostrar en mobile + * @return string|null Clases CSS o null si no debe mostrarse + */ + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if (!$desktop && !$mobile) { + return null; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + /** + * Generar CSS usando CSSGeneratorService + * + * @param array $data Datos del componente + * @return string CSS generado + */ + private function generateCSS(array $data): string + { + $css = ''; + + // Estilos base del botón + $baseStyles = [ + 'background_color' => $data['colors']['background_color'] ?? '#FF8600', + 'color' => $data['colors']['text_color'] ?? '#FFFFFF', + 'font_size' => $data['typography']['font_size'] ?? '1rem', + 'font_weight' => $data['typography']['font_weight'] ?? '600', + 'text_transform' => $data['typography']['text_transform'] ?? 'none', + 'padding' => sprintf( + '%s %s', + $data['spacing']['padding_top_bottom'] ?? '0.5rem', + $data['spacing']['padding_left_right'] ?? '1.5rem' + ), + 'border' => sprintf( + '%s solid %s', + $data['visual_effects']['border_width'] ?? '0', + $data['colors']['border_color'] ?? 'transparent' + ), + 'border_radius' => $data['visual_effects']['border_radius'] ?? '6px', + 'box_shadow' => $data['visual_effects']['box_shadow'] ?? 'none', + 'transition' => sprintf( + 'all %s ease', + $data['visual_effects']['transition_duration'] ?? '0.3s' + ), + 'cursor' => 'pointer', + ]; + $css .= $this->cssGenerator->generate('.btn-lets-talk', $baseStyles); + + // Estilos hover del botón + $hoverStyles = [ + 'background_color' => $data['colors']['background_hover_color'] ?? '#FF6B35', + 'color' => $data['colors']['text_hover_color'] ?? '#FFFFFF', + ]; + $css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk:hover', $hoverStyles); + + // Estilos del ícono dentro del botón + $iconStyles = [ + 'color' => $data['colors']['text_color'] ?? '#FFFFFF', + 'margin_right' => $data['spacing']['icon_spacing'] ?? '0.5rem', + ]; + $css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk i', $iconStyles); + + // Estilos responsive - ocultar en móvil si show_on_mobile = false + $showOnMobile = ($data['visibility']['show_on_mobile'] ?? false) === true; + if (!$showOnMobile) { + $responsiveStyles = [ + 'display' => 'none !important', + ]; + $css .= "\n@media (max-width: 991px) {\n"; + $css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles); + $css .= "\n}"; + } + + // Estilos responsive - ocultar en desktop si show_on_desktop = false + $showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true; + if (!$showOnDesktop) { + $responsiveStyles = [ + 'display' => 'none !important', + ]; + $css .= "\n@media (min-width: 992px) {\n"; + $css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles); + $css .= "\n}"; + } + + // Margen izquierdo para separar del menú (solo desktop) + $marginLeft = $data['spacing']['margin_left'] ?? '1rem'; + if (!empty($marginLeft) && $marginLeft !== '0') { + $css .= "\n@media (min-width: 992px) {\n"; + $css .= $this->cssGenerator->generate('.btn-lets-talk', ['margin_left' => $marginLeft]); + $css .= "\n}"; + } + + return $css; + } + + /** + * Generar HTML del componente + * + * @param array $data Datos del componente + * @return string HTML generado + */ + private function buildHTML(array $data): string + { + $classes = $this->buildClasses($data); + $attributes = $this->buildAttributes($data); + $content = $this->buildContent($data); + + $tag = $this->useModal($data) ? 'button' : 'a'; + + return sprintf( + '<%s class="%s"%s>%s', + $tag, + esc_attr($classes), + $attributes, + $content, + $tag + ); + } + + /** + * Construir clases CSS del componente + * + * @param array $data Datos del componente + * @return string Clases CSS + */ + private function buildClasses(array $data): string + { + $classes = ['btn', 'btn-lets-talk']; + + // Agregar clase ms-lg-3 para margen en desktop (Bootstrap) + // Esto solo aplica en pantallas >= lg (992px) + $classes[] = 'ms-lg-3'; + + return implode(' ', $classes); + } + + /** + * Determinar si debe usar modal o URL + * + * @param array $data Datos del componente + * @return bool + */ + private function useModal(array $data): bool + { + return ($data['behavior']['enable_modal'] ?? true) === true; + } + + /** + * Construir atributos HTML del componente + * + * @param array $data Datos del componente + * @return string Atributos HTML + */ + private function buildAttributes(array $data): string + { + $attributes = []; + + if ($this->useModal($data)) { + // Atributos para modal de Bootstrap + $attributes[] = 'type="button"'; + $attributes[] = 'data-bs-toggle="modal"'; + + $modalTarget = $data['content']['modal_target'] ?? '#contactModal'; + $attributes[] = sprintf('data-bs-target="%s"', esc_attr($modalTarget)); + } else { + // Atributos para enlace + $customUrl = $data['behavior']['custom_url'] ?? ''; + $attributes[] = sprintf('href="%s"', esc_url($customUrl ?: '#')); + + if (($data['behavior']['open_in_new_tab'] ?? false) === true) { + $attributes[] = 'target="_blank"'; + $attributes[] = 'rel="noopener noreferrer"'; + } + } + + // Atributo ARIA para accesibilidad + $ariaLabel = $data['content']['aria_label'] ?? 'Abrir formulario de contacto'; + if (!empty($ariaLabel)) { + $attributes[] = sprintf('aria-label="%s"', esc_attr($ariaLabel)); + } + + return !empty($attributes) ? ' ' . implode(' ', $attributes) : ''; + } + + /** + * Construir contenido del botón + * + * @param array $data Datos del componente + * @return string HTML del contenido + */ + private function buildContent(array $data): string + { + $html = ''; + + // Ícono (si está habilitado) + if ($this->shouldShowIcon($data)) { + $html .= $this->buildIcon($data); + } + + // Texto del botón + $buttonText = $data['content']['button_text'] ?? "Let's Talk"; + $html .= esc_html($buttonText); + + return $html; + } + + /** + * Verificar si debe mostrar el ícono + * + * @param array $data Datos del componente + * @return bool + */ + private function shouldShowIcon(array $data): bool + { + return ($data['content']['show_icon'] ?? true) === true; + } + + /** + * Construir ícono del componente + * + * @param array $data Datos del componente + * @return string HTML del ícono + */ + private function buildIcon(array $data): string + { + $iconClass = $data['content']['icon_class'] ?? 'bi-lightning-charge-fill'; + + // Asegurar prefijo 'bi-' + if (strpos($iconClass, 'bi-') !== 0) { + $iconClass = 'bi-' . $iconClass; + } + + return sprintf( + '', + esc_attr($iconClass) + ); + } +} diff --git a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php new file mode 100644 index 00000000..831cfb27 --- /dev/null +++ b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php @@ -0,0 +1,187 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data); + + return sprintf("\n%s", $css, $html); + } + + public function supports(string $componentType): bool + { + return $componentType === 'cta-post'; + } + + private function isEnabled(array $data): bool + { + $value = $data['visibility']['is_enabled'] ?? false; + return $value === true || $value === '1' || $value === 1; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $effects = $data['visual_effects'] ?? []; + $visibility = $data['visibility'] ?? []; + + $cssRules = []; + + $gradientStart = $colors['gradient_start'] ?? '#FF8600'; + $gradientEnd = $colors['gradient_end'] ?? '#FFB800'; + $gradientAngle = $effects['gradient_angle'] ?? '135deg'; + $buttonBgColor = $colors['button_bg_color'] ?? '#FF8600'; + $buttonTextColor = $colors['button_text_color'] ?? '#ffffff'; + $buttonHoverBgColor = $colors['button_hover_bg_color'] ?? '#e67a00'; + + // Container - gradient background + $cssRules[] = $this->cssGenerator->generate('.cta-post-container', [ + 'background' => "linear-gradient({$gradientAngle}, {$gradientStart} 0%, {$gradientEnd} 100%)", + ]); + + // Button styles (matching template .cta-button) - Using !important to override Bootstrap btn-light + $cssRules[] = ".cta-post-container .cta-button { + background-color: {$buttonBgColor} !important; + color: {$buttonTextColor} !important; + font-weight: 600; + padding: 0.75rem 2rem; + border: none !important; + border-radius: 8px; + transition: 0.3s; + text-decoration: none; + display: inline-block; + }"; + + // Button hover state + $cssRules[] = ".cta-post-container .cta-button:hover { + background-color: {$buttonHoverBgColor}; + color: {$buttonTextColor}; + }"; + + // Responsive: button full width on mobile + $cssRules[] = "@media (max-width: 768px) { + .cta-post-container .cta-button { + width: 100%; + margin-top: 1rem; + } + }"; + + // Responsive visibility + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1; + $showOnMobile = $visibility['show_on_mobile'] ?? true; + $showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 991.98px) { + .cta-post-container { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 992px) { + .cta-post-container { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data): string + { + $content = $data['content'] ?? []; + + $title = $content['title'] ?? 'Accede a 200,000+ Análisis de Precios Unitarios'; + $description = $content['description'] ?? ''; + $buttonText = $content['button_text'] ?? 'Ver Catálogo Completo'; + $buttonUrl = $content['button_url'] ?? '#'; + $buttonIcon = $content['button_icon'] ?? 'bi-arrow-right'; + + $html = '
'; + $html .= '
'; + + // Left column - Content + $html .= '
'; + $html .= sprintf( + '

%s

', + esc_html($title) + ); + if (!empty($description)) { + $html .= sprintf( + '

%s

', + esc_html($description) + ); + } + $html .= '
'; + + // Right column - Button + $html .= ' '; + + $html .= '
'; + $html .= '
'; + + return $html; + } +} diff --git a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php new file mode 100644 index 00000000..023fe981 --- /dev/null +++ b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php @@ -0,0 +1,202 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + if (!$this->hasPostThumbnail()) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data); + + return sprintf("\n%s", $css, $html); + } + + public function supports(string $componentType): bool + { + return $componentType === 'featured-image'; + } + + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function hasPostThumbnail(): bool + { + return is_singular() && has_post_thumbnail(); + } + + private function generateCSS(array $data): string + { + $spacing = $data['spacing'] ?? []; + $effects = $data['visual_effects'] ?? []; + $visibility = $data['visibility'] ?? []; + + $marginTop = $spacing['margin_top'] ?? '1rem'; + $marginBottom = $spacing['margin_bottom'] ?? '2rem'; + + $borderRadius = $effects['border_radius'] ?? '12px'; + $boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)'; + $hoverEffect = $effects['hover_effect'] ?? true; + $hoverScale = $effects['hover_scale'] ?? '1.02'; + $transitionDuration = $effects['transition_duration'] ?? '0.3s'; + + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnMobile = $visibility['show_on_mobile'] ?? true; + + $cssRules = []; + + // Container styles + $cssRules[] = $this->cssGenerator->generate('.featured-image-container', [ + 'border-radius' => $borderRadius, + 'overflow' => 'hidden', + 'box-shadow' => $boxShadow, + 'margin-top' => $marginTop, + 'margin-bottom' => $marginBottom, + 'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease", + ]); + + // Image styles + $cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [ + 'width' => '100%', + 'height' => 'auto', + 'display' => 'block', + 'transition' => "transform {$transitionDuration} ease", + ]); + + // Hover effect + if ($hoverEffect) { + $cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover', [ + 'box-shadow' => '0 12px 32px rgba(0, 0, 0, 0.15)', + ]); + + $cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover img', [ + 'transform' => "scale({$hoverScale})", + ]); + } + + // Link styles (remove default link styling) + $cssRules[] = $this->cssGenerator->generate('.featured-image-container a', [ + 'display' => 'block', + 'line-height' => '0', + ]); + + // Responsive visibility + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 767.98px) { + .featured-image-container { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 768px) { + .featured-image-container { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data): string + { + $content = $data['content'] ?? []; + + $imageSize = $content['image_size'] ?? 'roi-featured-large'; + $lazyLoading = $content['lazy_loading'] ?? true; + $linkToMedia = $content['link_to_media'] ?? false; + + $imgAttr = [ + 'class' => 'img-fluid featured-image', + 'alt' => get_the_title(), + ]; + + if ($lazyLoading) { + $imgAttr['loading'] = 'lazy'; + } + + $thumbnail = get_the_post_thumbnail(null, $imageSize, $imgAttr); + + if (empty($thumbnail)) { + return ''; + } + + $html = ''; + + return $html; + } +} diff --git a/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php b/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php new file mode 100644 index 00000000..24a92bb1 --- /dev/null +++ b/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php @@ -0,0 +1,185 @@ + __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme') + ], 403); + return; + } + + // 2. Rate limiting (1 suscripcion por IP cada 60 segundos) + if (!$this->checkRateLimit()) { + wp_send_json_error([ + 'message' => __('Por favor espera un momento antes de intentar de nuevo.', 'roi-theme') + ], 429); + return; + } + + // 3. Validar email + $email = sanitize_email($_POST['email'] ?? ''); + if (empty($email) || !is_email($email)) { + wp_send_json_error([ + 'message' => __('Por favor ingresa un email valido.', 'roi-theme') + ], 422); + return; + } + + // 4. Obtener configuracion del componente + $settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME); + + if (empty($settings)) { + wp_send_json_error([ + 'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme') + ], 500); + return; + } + + $newsletter = $settings['newsletter'] ?? []; + $webhookUrl = $newsletter['newsletter_webhook_url'] ?? ''; + $successMsg = $newsletter['newsletter_success_message'] ?? __('Gracias por suscribirte!', 'roi-theme'); + $errorMsg = $newsletter['newsletter_error_message'] ?? __('Error al suscribirse. Intenta de nuevo.', 'roi-theme'); + + if (empty($webhookUrl)) { + // Si no hay webhook, simular exito para UX pero loguear warning + error_log('ROI Theme Newsletter: No webhook URL configured'); + wp_send_json_success([ + 'message' => $successMsg + ]); + return; + } + + // 5. Preparar payload + $payload = [ + 'email' => $email, + 'source' => 'newsletter-footer', + 'timestamp' => current_time('c'), + 'siteName' => get_bloginfo('name'), + 'siteUrl' => home_url(), + ]; + + // 6. Enviar a webhook + $result = $this->sendToWebhook($webhookUrl, $payload); + + if ($result['success']) { + wp_send_json_success([ + 'message' => $successMsg + ]); + } else { + error_log('ROI Theme Newsletter webhook error: ' . $result['error']); + wp_send_json_error([ + 'message' => $errorMsg + ], 500); + } + } + + /** + * Enviar datos al webhook + */ + private function sendToWebhook(string $url, array $payload): array + { + $response = wp_remote_post($url, [ + 'timeout' => 30, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + 'body' => wp_json_encode($payload), + ]); + + if (is_wp_error($response)) { + return [ + 'success' => false, + 'error' => $response->get_error_message() + ]; + } + + $statusCode = wp_remote_retrieve_response_code($response); + + if ($statusCode >= 200 && $statusCode < 300) { + return ['success' => true, 'error' => '']; + } + + return [ + 'success' => false, + 'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response)) + ]; + } + + /** + * Rate limiting por IP + */ + private function checkRateLimit(): bool + { + $ip = $this->getClientIP(); + $transientKey = 'roi_newsletter_' . md5($ip); + $lastSubmit = get_transient($transientKey); + + if ($lastSubmit !== false) { + return false; + } + + set_transient($transientKey, time(), 60); + return true; + } + + /** + * Obtener IP del cliente + */ + private function getClientIP(): string + { + $ip = ''; + + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']); + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]); + } elseif (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']); + } + + return $ip; + } +} diff --git a/Public/Footer/Infrastructure/Ui/FooterRenderer.php b/Public/Footer/Infrastructure/Ui/FooterRenderer.php new file mode 100644 index 00000000..056b0990 --- /dev/null +++ b/Public/Footer/Infrastructure/Ui/FooterRenderer.php @@ -0,0 +1,423 @@ +getData(); + + // Validar visibilidad + $visibility = $data['visibility'] ?? []; + if (!($visibility['is_enabled'] ?? true)) { + return ''; + } + + // Verificar visibilidad responsive + $showDesktop = $visibility['show_on_desktop'] ?? true; + $showMobile = $visibility['show_on_mobile'] ?? true; + + if (!$showDesktop && !$showMobile) { + return ''; + } + + // Generar CSS + $css = $this->generateCSS($data, $showDesktop, $showMobile); + + // Generar HTML + $html = $this->generateHTML($data); + + // Generar JavaScript + $js = $this->generateJS($data); + + return $css . $html . $js; + } + + private function generateCSS(array $data, bool $showDesktop, bool $showMobile): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $effects = $data['visual_effects'] ?? []; + + // Valores con fallbacks + $bgColor = $colors['bg_color'] ?? '#212529'; + $textColor = $colors['text_color'] ?? '#ffffff'; + $titleColor = $colors['title_color'] ?? '#ffffff'; + $linkColor = $colors['link_color'] ?? '#ffffff'; + $linkHoverColor = $colors['link_hover_color'] ?? '#FF8600'; + $inputBgColor = $colors['input_bg_color'] ?? '#ffffff'; + $inputTextColor = $colors['input_text_color'] ?? '#212529'; + $inputBorderColor = $colors['input_border_color'] ?? '#dee2e6'; + $buttonBgColor = $colors['button_bg_color'] ?? '#0d6efd'; + $buttonTextColor = $colors['button_text_color'] ?? '#ffffff'; + $buttonHoverBg = $colors['button_hover_bg'] ?? '#0b5ed7'; + $borderTopColor = $colors['border_top_color'] ?? 'rgba(255, 255, 255, 0.2)'; + + $paddingY = $spacing['padding_y'] ?? '3rem'; + $marginTop = $spacing['margin_top'] ?? '0'; + $widgetTitleMb = $spacing['widget_title_margin_bottom'] ?? '1rem'; + $linkMb = $spacing['link_margin_bottom'] ?? '0.5rem'; + $copyrightPy = $spacing['copyright_padding_y'] ?? '1.5rem'; + + $inputRadius = $effects['input_border_radius'] ?? '6px'; + $buttonRadius = $effects['button_border_radius'] ?? '6px'; + $transition = $effects['transition_duration'] ?? '0.3s'; + + $cssRules = []; + + // Footer principal + $cssRules[] = $this->cssGenerator->generate('.roi-footer', [ + 'background-color' => $bgColor, + 'color' => $textColor, + 'padding-top' => $paddingY, + 'padding-bottom' => $paddingY, + 'margin-top' => $marginTop, + ]); + + // Grid custom para 3+3+3+4 = 13 columnas + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-grid', [ + 'display' => 'grid', + 'grid-template-columns' => 'repeat(4, 1fr)', + 'gap' => '2rem', + ]); + + // En desktop: distribucion 3+3+3+4 + $cssRules[] = "@media (min-width: 768px) { + .roi-footer .footer-grid { + grid-template-columns: 23% 23% 23% 31%; + } + }"; + + // En mobile: 2 columnas + $cssRules[] = "@media (max-width: 767px) { + .roi-footer .footer-grid { + grid-template-columns: 1fr 1fr; + } + .roi-footer .footer-widget-newsletter { + grid-column: span 2; + } + }"; + + // Titulos de widgets + $cssRules[] = $this->cssGenerator->generate('.roi-footer .widget-title', [ + 'color' => $titleColor, + 'font-size' => '1.25rem', + 'font-weight' => '500', + 'margin-bottom' => $widgetTitleMb, + ]); + + // Links de navegacion + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav', [ + 'list-style' => 'none', + 'padding' => '0', + 'margin' => '0', + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav li', [ + 'margin-bottom' => $linkMb, + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a', [ + 'color' => $linkColor, + 'text-decoration' => 'none', + 'transition' => "color {$transition}", + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a:hover', [ + 'color' => $linkHoverColor, + ]); + + // Newsletter description + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-description', [ + 'color' => $textColor, + 'margin-bottom' => '1rem', + 'opacity' => '0.9', + ]); + + // Input newsletter + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input', [ + 'width' => '100%', + 'padding' => '0.75rem 1rem', + 'background-color' => $inputBgColor, + 'color' => $inputTextColor, + 'border' => "1px solid {$inputBorderColor}", + 'border-radius' => $inputRadius, + 'margin-bottom' => '0.75rem', + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input:focus', [ + 'outline' => 'none', + 'border-color' => $buttonBgColor, + 'box-shadow' => "0 0 0 0.2rem rgba(13, 110, 253, 0.25)", + ]); + + // Boton newsletter + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn', [ + 'width' => '100%', + 'padding' => '0.75rem 1.5rem', + 'background-color' => $buttonBgColor, + 'color' => $buttonTextColor, + 'border' => 'none', + 'border-radius' => $buttonRadius, + 'font-weight' => '500', + 'cursor' => 'pointer', + 'transition' => "background-color {$transition}", + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:hover', [ + 'background-color' => $buttonHoverBg, + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:disabled', [ + 'opacity' => '0.7', + 'cursor' => 'not-allowed', + ]); + + // Mensaje newsletter + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message', [ + 'margin-top' => '0.75rem', + 'padding' => '0.5rem', + 'border-radius' => '4px', + 'font-size' => '0.875rem', + 'display' => 'none', + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.success', [ + 'background-color' => '#d1e7dd', + 'color' => '#0f5132', + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.error', [ + 'background-color' => '#f8d7da', + 'color' => '#842029', + ]); + + // Footer bottom (copyright) + $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-bottom', [ + 'border-top' => "1px solid {$borderTopColor}", + 'padding-top' => $copyrightPy, + 'margin-top' => '2rem', + 'text-align' => 'center', + ]); + + $cssRules[] = $this->cssGenerator->generate('.roi-footer .copyright-text', [ + 'margin' => '0', + 'opacity' => '0.9', + ]); + + // Responsive visibility + if (!$showDesktop) { + $cssRules[] = "@media (min-width: 992px) { .roi-footer { display: none !important; } }"; + } + if (!$showMobile) { + $cssRules[] = "@media (max-width: 991px) { .roi-footer { display: none !important; } }"; + } + + return ''; + } + + private function generateHTML(array $data): string + { + $widget1 = $data['widget_1'] ?? []; + $widget2 = $data['widget_2'] ?? []; + $widget3 = $data['widget_3'] ?? []; + $newsletter = $data['newsletter'] ?? []; + $footerBottom = $data['footer_bottom'] ?? []; + + $widget1Visible = $this->toBool($widget1['widget_1_visible'] ?? true); + $widget2Visible = $this->toBool($widget2['widget_2_visible'] ?? true); + $widget3Visible = $this->toBool($widget3['widget_3_visible'] ?? true); + $newsletterVisible = $this->toBool($newsletter['newsletter_visible'] ?? true); + + $widget1Title = esc_html($widget1['widget_1_title'] ?? 'Recursos'); + $widget2Title = esc_html($widget2['widget_2_title'] ?? 'Soporte'); + $widget3Title = esc_html($widget3['widget_3_title'] ?? 'Empresa'); + + $newsletterTitle = esc_html($newsletter['newsletter_title'] ?? 'Suscribete al Newsletter'); + $newsletterDesc = esc_html($newsletter['newsletter_description'] ?? 'Recibe las ultimas actualizaciones.'); + $newsletterPlaceholder = esc_attr($newsletter['newsletter_placeholder'] ?? 'Email'); + $newsletterBtnText = esc_html($newsletter['newsletter_button_text'] ?? 'Suscribirse'); + + $copyrightText = esc_html($footerBottom['copyright_text'] ?? date('Y') . ' Todos los derechos reservados.'); + + $nonce = wp_create_nonce(self::NONCE_ACTION); + $ajaxUrl = admin_url('admin-ajax.php'); + + $html = '
'; + $html .= '
'; + $html .= ''; // .footer-grid + + // Footer bottom + $html .= ''; + + $html .= '
'; // .container + $html .= '
'; + + return $html; + } + + private function renderMenu(string $menuLocation): string + { + if (!has_nav_menu($menuLocation)) { + return '

Menu no asignado

'; + } + + return wp_nav_menu([ + 'theme_location' => $menuLocation, + 'container' => false, + 'menu_class' => 'footer-nav', + 'fallback_cb' => false, + 'echo' => false, + 'depth' => 1, + ]) ?: ''; + } + + private function generateJS(array $data): string + { + $newsletter = $data['newsletter'] ?? []; + $successMsg = esc_js($newsletter['newsletter_success_message'] ?? 'Gracias por suscribirte!'); + $errorMsg = esc_js($newsletter['newsletter_error_message'] ?? 'Error al suscribirse. Intenta de nuevo.'); + + $ajaxUrl = admin_url('admin-ajax.php'); + + $js = << +(function() { + const form = document.getElementById('roi-newsletter-form'); + if (!form) return; + + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + const btn = form.querySelector('.newsletter-btn'); + const msgDiv = form.querySelector('.newsletter-message'); + const emailInput = form.querySelector('input[name="email"]'); + const originalText = btn.textContent; + + // Reset message + msgDiv.style.display = 'none'; + msgDiv.className = 'newsletter-message'; + + // Validate email + if (!emailInput.value || !emailInput.validity.valid) { + msgDiv.textContent = 'Por favor ingresa un email valido'; + msgDiv.classList.add('error'); + msgDiv.style.display = 'block'; + return; + } + + // Disable button + btn.disabled = true; + btn.textContent = 'Enviando...'; + + try { + const formData = new FormData(form); + + const response = await fetch('{$ajaxUrl}', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.success) { + msgDiv.textContent = '{$successMsg}'; + msgDiv.classList.add('success'); + emailInput.value = ''; + } else { + msgDiv.textContent = result.data?.message || '{$errorMsg}'; + msgDiv.classList.add('error'); + } + } catch (error) { + msgDiv.textContent = '{$errorMsg}'; + msgDiv.classList.add('error'); + } + + msgDiv.style.display = 'block'; + btn.disabled = false; + btn.textContent = originalText; + }); +})(); + +JS; + + return $js; + } + + private function toBool($value): bool + { + return $value === true || $value === '1' || $value === 1; + } +} diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php new file mode 100644 index 00000000..db8416e1 --- /dev/null +++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php @@ -0,0 +1,278 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data); + + return sprintf("\n%s", $css, $html); + } + + public function supports(string $componentType): bool + { + return $componentType === 'hero'; + } + + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'home': + return is_front_page() || is_home(); + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $typography = $data['typography'] ?? []; + $spacing = $data['spacing'] ?? []; + $effects = $data['visual_effects'] ?? []; + $visibility = $data['visibility'] ?? []; + + $gradientStart = $colors['gradient_start'] ?? '#1e3a5f'; + $gradientEnd = $colors['gradient_end'] ?? '#2c5282'; + $titleColor = $colors['title_color'] ?? '#FFFFFF'; + $badgeBgColor = $colors['badge_bg_color'] ?? '#FFFFFF'; + $badgeTextColor = $colors['badge_text_color'] ?? '#FFFFFF'; + $badgeIconColor = $colors['badge_icon_color'] ?? '#FFB800'; + $badgeHoverBg = $colors['badge_hover_bg'] ?? '#FF8600'; + + $titleFontSize = $typography['title_font_size'] ?? '2.5rem'; + $titleFontSizeMobile = $typography['title_font_size_mobile'] ?? '1.75rem'; + $titleFontWeight = $typography['title_font_weight'] ?? '700'; + $titleLineHeight = $typography['title_line_height'] ?? '1.4'; + $badgeFontSize = $typography['badge_font_size'] ?? '0.813rem'; + + $paddingVertical = $spacing['padding_vertical'] ?? '3rem'; + $marginBottom = $spacing['margin_bottom'] ?? '1.5rem'; + $badgePadding = $spacing['badge_padding'] ?? '0.375rem 0.875rem'; + $badgeBorderRadius = $spacing['badge_border_radius'] ?? '20px'; + + $boxShadow = $effects['box_shadow'] ?? '0 4px 16px rgba(30, 58, 95, 0.25)'; + $titleTextShadow = $effects['title_text_shadow'] ?? '1px 1px 2px rgba(0, 0, 0, 0.2)'; + $badgeBackdropBlur = $effects['badge_backdrop_blur'] ?? '10px'; + + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnMobile = $visibility['show_on_mobile'] ?? true; + + $cssRules = []; + + $cssRules[] = $this->cssGenerator->generate('.hero-section', [ + 'background' => "linear-gradient(135deg, {$gradientStart} 0%, {$gradientEnd} 100%)", + 'box-shadow' => $boxShadow, + 'padding' => "{$paddingVertical} 0", + 'margin-bottom' => $marginBottom, + ]); + + $cssRules[] = $this->cssGenerator->generate('.hero-section__title', [ + 'color' => "{$titleColor} !important", + 'font-weight' => $titleFontWeight, + 'font-size' => $titleFontSize, + 'line-height' => $titleLineHeight, + 'text-shadow' => $titleTextShadow, + 'margin-bottom' => '0', + 'text-align' => 'center', + ]); + + $cssRules[] = $this->cssGenerator->generate('.hero-section__badge', [ + 'background' => $this->hexToRgba($badgeBgColor, 0.15), + 'backdrop-filter' => "blur({$badgeBackdropBlur})", + '-webkit-backdrop-filter' => "blur({$badgeBackdropBlur})", + 'border' => '1px solid ' . $this->hexToRgba($badgeBgColor, 0.2), + 'color' => $this->hexToRgba($badgeTextColor, 0.95), + 'padding' => $badgePadding, + 'border-radius' => $badgeBorderRadius, + 'font-size' => $badgeFontSize, + 'font-weight' => '500', + 'text-decoration' => 'none', + 'display' => 'inline-block', + 'transition' => 'all 0.3s ease', + ]); + + $cssRules[] = $this->cssGenerator->generate('.hero-section__badge:hover', [ + 'background' => $this->hexToRgba($badgeHoverBg, 0.2), + 'border-color' => $this->hexToRgba($badgeHoverBg, 0.4), + 'color' => '#ffffff', + ]); + + $cssRules[] = $this->cssGenerator->generate('.hero-section__badge i', [ + 'color' => $badgeIconColor, + ]); + + $cssRules[] = "@media (max-width: 767.98px) { + .hero-section__title { + font-size: {$titleFontSizeMobile}; + } + }"; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 767.98px) { + .hero-section { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 768px) { + .hero-section { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data): string + { + $content = $data['content'] ?? []; + $showCategories = $content['show_categories'] ?? true; + $showBadgeIcon = $content['show_badge_icon'] ?? true; + $badgeIconClass = $content['badge_icon_class'] ?? 'bi-folder-fill'; + $titleTag = $content['title_tag'] ?? 'h1'; + + $allowedTags = ['h1', 'h2', 'div']; + if (!in_array($titleTag, $allowedTags, true)) { + $titleTag = 'h1'; + } + + $title = is_singular() ? get_the_title() : ''; + if (empty($title)) { + $title = wp_title('', false); + } + + $html = '
'; + $html .= '
'; + + if ($showCategories && is_single()) { + $categories = get_the_category(); + if (!empty($categories)) { + $html .= '
'; + $html .= '
'; + + foreach ($categories as $category) { + $categoryLink = esc_url(get_category_link($category->term_id)); + $categoryName = esc_html($category->name); + $iconHtml = $showBadgeIcon + ? '' + : ''; + + $html .= sprintf( + '%s%s', + $categoryLink, + $iconHtml, + $categoryName + ); + } + + $html .= '
'; + $html .= '
'; + } + } + + $html .= sprintf( + '<%s class="hero-section__title">%s', + $titleTag, + esc_html($title), + $titleTag + ); + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function hexToRgba(string $hex, float $alpha): string + { + $hex = ltrim($hex, '#'); + + if (strlen($hex) === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + + return "rgba({$r}, {$g}, {$b}, {$alpha})"; + } + + /** + * Genera clases Bootstrap de visibilidad responsive + * + * @param bool $desktop Si debe mostrarse en desktop + * @param bool $mobile Si debe mostrarse en mobile + * @return string|null Clases Bootstrap o null si visible en todos + */ + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if ($desktop && $mobile) { + return null; + } + + if ($desktop && !$mobile) { + return 'd-none d-md-block'; + } + + if (!$desktop && $mobile) { + return 'd-block d-md-none'; + } + + return 'd-none'; + } +} diff --git a/src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php similarity index 95% rename from src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php rename to Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php index a8a79258..17960a89 100644 --- a/src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php +++ b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php @@ -1,11 +1,24 @@ getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildMenu($data); + + return sprintf( + "\n%s", + $css, + $html + ); + } + + private function isEnabled(array $data): bool + { + return isset($data['visibility']['is_enabled']) && + $data['visibility']['is_enabled'] === true; + } + + private function shouldShowOnMobile(array $data): bool + { + return isset($data['visibility']['show_on_mobile']) && + $data['visibility']['show_on_mobile'] === true; + } + + /** + * Generar CSS usando CSSGeneratorService + * + * @param array $data Datos del componente + * @return string CSS generado + */ + private function generateCSS(array $data): string + { + $css = ''; + + // Obtener valores de configuración + $stickyEnabled = $data['visibility']['sticky_enabled'] ?? true; + $paddingVertical = $data['layout']['padding_vertical'] ?? '0.75rem 0'; + $zIndex = $data['layout']['z_index'] ?? '1030'; + + $bgColor = $data['colors']['background_color'] ?? '#1e3a5f'; + $boxShadow = $data['colors']['box_shadow'] ?? '0 4px 12px rgba(30, 58, 95, 0.15)'; + + $linkTextColor = $data['links']['text_color'] ?? '#FFFFFF'; + $linkHoverColor = $data['links']['hover_color'] ?? '#FF8600'; + $linkActiveColor = $data['links']['active_color'] ?? '#FF8600'; + $linkFontSize = $data['links']['font_size'] ?? '0.9rem'; + $linkFontWeight = $data['links']['font_weight'] ?? '500'; + $linkPadding = $data['links']['padding'] ?? '0.5rem 0.65rem'; + $linkBorderRadius = $data['links']['border_radius'] ?? '4px'; + $showUnderlineEffect = $data['links']['show_underline_effect'] ?? true; + $underlineColor = $data['links']['underline_color'] ?? '#FF8600'; + + // Estilos del navbar container + $navbarStyles = [ + 'background-color' => $bgColor . ' !important', + 'box-shadow' => $boxShadow, + 'padding' => $paddingVertical, + 'transition' => 'all 0.3s ease', + ]; + + if ($stickyEnabled) { + $navbarStyles['position'] = 'sticky'; + $navbarStyles['top'] = '0'; + $navbarStyles['z-index'] = $zIndex; + } + + $css .= $this->cssGenerator->generate('.navbar', $navbarStyles); + + // Efecto scrolled del navbar + $css .= "\n" . $this->cssGenerator->generate('.navbar.scrolled', [ + 'box-shadow' => '0 6px 20px rgba(30, 58, 95, 0.25)', + ]); + + // Estilos de los enlaces del navbar + $navLinkStyles = [ + 'color' => 'rgba(255, 255, 255, 0.9) !important', + 'font-weight' => $linkFontWeight, + 'position' => 'relative', + 'padding' => $linkPadding . ' !important', + 'transition' => 'all 0.3s ease', + 'font-size' => $linkFontSize, + 'white-space' => 'nowrap', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link', $navLinkStyles); + + // Efecto de subrayado (::after pseudo-element) + if ($showUnderlineEffect) { + $css .= "\n.navbar .nav-link::after {"; + $css .= "\n content: '';"; + $css .= "\n position: absolute;"; + $css .= "\n bottom: 0;"; + $css .= "\n left: 50%;"; + $css .= "\n transform: translateX(-50%) scaleX(0);"; + $css .= "\n width: 80%;"; + $css .= "\n height: 2px;"; + $css .= "\n background: {$underlineColor};"; + $css .= "\n transition: transform 0.3s ease;"; + $css .= "\n}"; + + $css .= "\n.navbar .nav-link:hover::after {"; + $css .= "\n transform: translateX(-50%) scaleX(1);"; + $css .= "\n}"; + } + + // Estilos hover y focus de los enlaces + $navLinkHoverStyles = [ + 'color' => $linkHoverColor . ' !important', + 'background-color' => 'rgba(255, 133, 0, 0.1)', + 'border-radius' => $linkBorderRadius, + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link:hover, .navbar .nav-link:focus', $navLinkHoverStyles); + + // Estilos de enlaces activos + $navLinkActiveStyles = [ + 'color' => $linkActiveColor . ' !important', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link.active, .navbar .nav-item.current-menu-item > .nav-link', $navLinkActiveStyles); + + // Estilos del dropdown menu + $dropdownMaxHeight = $data['visual_effects']['dropdown_max_height'] ?? '300px'; + $dropdownStyles = [ + 'background' => $data['visual_effects']['background_color'] ?? '#ffffff', + 'border' => 'none', + 'box-shadow' => $data['visual_effects']['shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.12)', + 'border-radius' => $data['visual_effects']['border_radius'] ?? '8px', + 'padding' => '0.5rem 0', + 'max-height' => $dropdownMaxHeight, + 'overflow-y' => 'auto', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-menu', $dropdownStyles); + + // Hover en desktop para mostrar dropdown (sin necesidad de clic) + $css .= "\n@media (min-width: 992px) {"; + $css .= "\n .navbar .dropdown:hover > .dropdown-menu {"; + $css .= "\n display: block;"; + $css .= "\n margin-top: 0;"; + $css .= "\n }"; + $css .= "\n .navbar .dropdown > .dropdown-toggle:active {"; + $css .= "\n pointer-events: none;"; + $css .= "\n }"; + $css .= "\n}"; + + // Estilos de items del dropdown + $dropdownItemStyles = [ + 'color' => $data['visual_effects']['item_color'] ?? '#495057', + 'padding' => $data['visual_effects']['item_padding'] ?? '0.625rem 1.25rem', + 'transition' => 'all 0.3s ease', + 'font-weight' => '500', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item', $dropdownItemStyles); + + // Estilos hover de items del dropdown + $dropdownItemHoverStyles = [ + 'background-color' => $data['visual_effects']['item_hover_background'] ?? 'rgba(255, 133, 0, 0.1)', + 'color' => $linkHoverColor, + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item:hover, .navbar .dropdown-item:focus', $dropdownItemHoverStyles); + + // Estilos del brand (texto) + $brandStyles = [ + 'color' => ($data['media']['brand_color'] ?? '#FFFFFF') . ' !important', + 'font-weight' => '700', + 'font-size' => $data['media']['brand_font_size'] ?? '1.5rem', + 'transition' => 'color 0.3s ease', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand, .navbar .roi-navbar-brand', $brandStyles); + + // Estilos hover del brand + $brandHoverStyles = [ + 'color' => ($data['media']['brand_hover_color'] ?? '#FF8600') . ' !important', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand:hover, .navbar .roi-navbar-brand:hover', $brandHoverStyles); + + // Estilos del logo (imagen) + $logoStyles = [ + 'height' => $data['media']['logo_height'] ?? '40px', + 'width' => 'auto', + ]; + $css .= "\n" . $this->cssGenerator->generate('.navbar .roi-navbar-logo', $logoStyles); + + return $css; + } + + private function buildMenu(array $data): string + { + $menuLocation = $data['behavior']['menu_location'] ?? 'primary'; + $enableDropdowns = $data['behavior']['enable_dropdowns'] ?? true; + $mobileBreakpoint = $data['behavior']['mobile_breakpoint'] ?? 'lg'; + + $ulClass = 'navbar-nav mb-2 mb-lg-0'; + + $args = [ + 'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation, + 'menu' => $menuLocation === 'custom' ? ($data['behavior']['custom_menu_id'] ?? 0) : '', + 'container' => false, + 'menu_class' => $ulClass, + 'fallback_cb' => '__return_false', + 'items_wrap' => '
    %3$s
', + 'depth' => $enableDropdowns ? 2 : 1, + 'walker' => new ROI_Bootstrap_Nav_Walker() + ]; + + ob_start(); + wp_nav_menu($args); + return ob_get_clean(); + } + + /** + * Obtiene las clases CSS de Bootstrap para visibilidad responsive + * + * Implementa tabla de decisión según especificación: + * - Desktop Y Mobile = null (visible en ambos) + * - Solo Desktop = 'd-none d-lg-block' + * - Solo Mobile = 'd-lg-none' + * - Ninguno = 'd-none' (oculto) + * + * @param bool $desktop Mostrar en desktop + * @param bool $mobile Mostrar en mobile + * @return string|null Clases CSS o null si visible en ambos + */ + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if ($desktop && $mobile) { + return null; // Sin clases = visible siempre + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + return 'd-none'; + } + + public function supports(string $componentType): bool + { + return $componentType === 'navbar'; + } +} + +/** + * Custom Walker for Bootstrap 5 Navigation + * + * RESPONSABILIDAD: Adaptar wp_nav_menu() a Bootstrap 5 + * + * CARACTERÍSTICAS: + * - Clases Bootstrap 5 (.nav-item, .nav-link, .dropdown) + * - Atributos data-bs-toggle para dropdowns + * - Soporte para current-menu-item + */ +class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu +{ + public function start_lvl(&$output, $depth = 0, $args = null) + { + $indent = str_repeat("\t", $depth); + $output .= "\n$indent
    \n"; + } + + public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) + { + $indent = ($depth) ? str_repeat("\t", $depth) : ''; + + $classes = empty($item->classes) ? [] : (array) $item->classes; + $classes[] = 'nav-item'; + + if ($args->walker->has_children) { + $classes[] = 'dropdown'; + } + + $class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth)); + $class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : ''; + + $id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth); + $id = $id ? ' id="' . esc_attr($id) . '"' : ''; + + $output .= $indent . ''; + + $atts = []; + $atts['title'] = !empty($item->attr_title) ? $item->attr_title : ''; + $atts['target'] = !empty($item->target) ? $item->target : ''; + $atts['rel'] = !empty($item->xfn) ? $item->xfn : ''; + $atts['href'] = !empty($item->url) ? $item->url : ''; + + if ($depth === 0) { + $atts['class'] = 'nav-link'; + if ($args->walker->has_children) { + $atts['class'] .= ' dropdown-toggle'; + $atts['data-bs-toggle'] = 'dropdown'; + $atts['role'] = 'button'; + $atts['aria-expanded'] = 'false'; + } + } else { + $atts['class'] = 'dropdown-item'; + } + + if (in_array('current-menu-item', $classes)) { + $atts['class'] .= ' active'; + } + + $atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth); + + $attributes = ''; + foreach ($atts as $attr => $value) { + if (!empty($value)) { + $value = ('href' === $attr) ? esc_url($value) : esc_attr($value); + $attributes .= ' ' . $attr . '="' . $value . '"'; + } + } + + $title = apply_filters('the_title', $item->title, $item->ID); + $title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth); + + $item_output = $args->before; + $item_output .= ''; + $item_output .= $args->link_before . $title . $args->link_after; + $item_output .= ''; + $item_output .= $args->after; + + $output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args); + } +} diff --git a/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php new file mode 100644 index 00000000..075fd12e --- /dev/null +++ b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php @@ -0,0 +1,380 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $visibilityClass = $this->getVisibilityClass($data); + if ($visibilityClass === null) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data, $visibilityClass); + + return sprintf("\n%s", $css, $html); + } + + public function supports(string $componentType): bool + { + return $componentType === 'related-post'; + } + + private function isEnabled(array $data): bool + { + $value = $data['visibility']['is_enabled'] ?? false; + return $value === true || $value === '1' || $value === 1; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function getVisibilityClass(array $data): ?string + { + $showDesktop = $data['visibility']['show_on_desktop'] ?? true; + $showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1; + $showMobile = $data['visibility']['show_on_mobile'] ?? true; + $showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1; + + if (!$showDesktop && !$showMobile) { + return null; + } + if (!$showDesktop && $showMobile) { + return 'd-lg-none'; + } + if ($showDesktop && !$showMobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $effects = $data['visual_effects'] ?? []; + $typography = $data['typography'] ?? []; + $visibility = $data['visibility'] ?? []; + + $cssRules = []; + + // Variables de colores del tema (defaults del template) + $colorNavyPrimary = $colors['section_title_color'] ?? '#0E2337'; + $colorOrangePrimary = $colors['card_hover_border_color'] ?? '#FF8600'; + $colorNeutral50 = '#f9fafb'; + $colorNeutral100 = '#e5e7eb'; + $colorNeutral600 = $colors['card_border_color'] ?? '#6b7280'; + + // Container - margin 3rem 0 + $cssRules[] = $this->cssGenerator->generate('.related-posts', [ + 'margin' => '3rem 0', + ]); + + // Section title - color navy, font-weight 700, margin-bottom 2rem + $cssRules[] = $this->cssGenerator->generate('.related-posts h2', [ + 'color' => $colorNavyPrimary, + 'font-weight' => '700', + 'margin-bottom' => '2rem', + ]); + + // Card styles - cursor pointer, border, border-left 4px + $cardBgColor = $colors['card_bg_color'] ?? '#ffffff'; + $cardHoverBgColor = $colors['card_hover_bg_color'] ?? $colorNeutral50; + + $cssRules[] = ".related-posts .card { + cursor: pointer; + background: {$cardBgColor} !important; + border: 1px solid {$colorNeutral100} !important; + border-left: 4px solid {$colorNeutral600} !important; + transition: all 0.3s ease; + height: 100%; + }"; + + // Card hover - background change, shadow, border-left orange + $cssRules[] = ".related-posts .card:hover { + background: {$cardHoverBgColor} !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; + border-left-color: {$colorOrangePrimary} !important; + }"; + + // Card body - padding 1.5rem + $cssRules[] = $this->cssGenerator->generate('.related-posts .card-body', [ + 'padding' => '1.5rem !important', + ]); + + // Card title - color navy, font-weight 600, font-size 0.95rem + $cardTitleColor = $colors['card_title_color'] ?? $colorNavyPrimary; + + $cssRules[] = ".related-posts .card-title { + color: {$cardTitleColor} !important; + font-weight: 600; + font-size: 0.95rem; + line-height: 1.4; + }"; + + // Link hover - title changes to orange + $cssRules[] = ".related-posts a:hover .card-title { + color: {$colorOrangePrimary} !important; + }"; + + // Pagination styles - matching template exactly + $cssRules[] = ".related-posts .page-link { + color: {$colorNeutral600}; + border: 1px solid {$colorNeutral100}; + padding: 0.5rem 1rem; + margin: 0 0.25rem; + border-radius: 4px; + font-weight: 500; + transition: all 0.3s ease; + }"; + + $cssRules[] = ".related-posts .page-link:hover { + background-color: rgba(255, 133, 0, 0.1); + border-color: {$colorOrangePrimary}; + color: {$colorOrangePrimary}; + }"; + + $cssRules[] = ".related-posts .page-item.active .page-link { + background-color: {$colorOrangePrimary}; + border-color: {$colorOrangePrimary}; + color: #ffffff; + }"; + + // Responsive visibility + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1; + $showOnMobile = $visibility['show_on_mobile'] ?? true; + $showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 991.98px) { + .related-posts { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 992px) { + .related-posts { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data, string $visibilityClass): string + { + $content = $data['content'] ?? []; + $layout = $data['layout'] ?? []; + + $sectionTitle = $content['section_title'] ?? 'Descubre Mas Contenido'; + $postsPerPage = (int)($content['posts_per_page'] ?? 12); + $orderby = $content['orderby'] ?? 'rand'; + $order = $content['order'] ?? 'DESC'; + $showPagination = $content['show_pagination'] ?? true; + $showPagination = $showPagination === true || $showPagination === '1' || $showPagination === 1; + + // Layout columns (cast to string to handle boolean conversion from DB) + $colsDesktop = (string)($layout['columns_desktop'] ?? '3'); + $colsTablet = (string)($layout['columns_tablet'] ?? '2'); + $colsMobile = (string)($layout['columns_mobile'] ?? '1'); + + // Handle '1' stored as boolean true in DB + if ($colsDesktop === '1' || $colsDesktop === '') $colsDesktop = '3'; + if ($colsTablet === '1' || $colsTablet === '') $colsTablet = '2'; + if ($colsMobile === '1' || $colsMobile === '') $colsMobile = '1'; + + // Bootstrap column classes + $colClass = $this->getColumnClass($colsDesktop, $colsTablet, $colsMobile); + + // Query related posts + $posts = $this->getRelatedPosts($postsPerPage, $orderby, $order); + + if (empty($posts)) { + return ''; + } + + $containerClass = 'my-5 related-posts'; + if (!empty($visibilityClass)) { + $containerClass .= ' ' . $visibilityClass; + } + + $html = sprintf('
    ', esc_attr($containerClass)); + $html .= sprintf( + '

    %s

    ', + esc_html($sectionTitle) + ); + $html .= '
    '; + + foreach ($posts as $post) { + $html .= $this->buildCardHTML($post, $colClass); + } + + $html .= '
    '; + + if ($showPagination) { + $html .= $this->buildPaginationHTML($data); + } + + $html .= '
    '; + + // Reset post data + wp_reset_postdata(); + + return $html; + } + + private function getColumnClass(string $desktop, string $tablet, string $mobile): string + { + $desktopCols = 12 / (int)$desktop; + $tabletCols = 12 / (int)$tablet; + $mobileCols = 12 / (int)$mobile; + + // Template original usa col-md-4 (3 columnas desde tablet) + // col-{mobile} col-md-{tablet/desktop} + return sprintf('col-%d col-md-%d', $mobileCols, $desktopCols); + } + + private function getRelatedPosts(int $perPage, string $orderby, string $order): array + { + $currentPostId = get_the_ID(); + + $args = [ + 'post_type' => 'post', + 'posts_per_page' => $perPage, + 'post__not_in' => $currentPostId ? [$currentPostId] : [], + 'orderby' => $orderby, + 'order' => $order, + 'no_found_rows' => true, + ]; + + $query = new \WP_Query($args); + + return $query->posts; + } + + private function buildCardHTML(\WP_Post $post, string $colClass): string + { + $permalink = get_permalink($post); + $title = get_the_title($post); + + $html = sprintf(''; + + return $html; + } + + private function buildPaginationHTML(array $data): string + { + $content = $data['content'] ?? []; + + $textFirst = $content['pagination_text_first'] ?? 'Inicio'; + $textLast = $content['pagination_text_last'] ?? 'Fin'; + $textMore = $content['pagination_text_more'] ?? 'Ver mas'; + + $html = ''; + + return $html; + } +} diff --git a/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php new file mode 100644 index 00000000..c3a14276 --- /dev/null +++ b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php @@ -0,0 +1,390 @@ + [ + 'field' => 'show_facebook', + 'url_field' => 'facebook_url', + 'icon' => 'bi-facebook', + 'label' => 'Facebook', + 'share_pattern' => 'https://www.facebook.com/sharer/sharer.php?u=%s', + ], + 'instagram' => [ + 'field' => 'show_instagram', + 'url_field' => 'instagram_url', + 'icon' => 'bi-instagram', + 'label' => 'Instagram', + 'share_pattern' => '', // Instagram no tiene share directo - requiere URL configurada + ], + 'linkedin' => [ + 'field' => 'show_linkedin', + 'url_field' => 'linkedin_url', + 'icon' => 'bi-linkedin', + 'label' => 'LinkedIn', + 'share_pattern' => 'https://www.linkedin.com/sharing/share-offsite/?url=%s', + ], + 'whatsapp' => [ + 'field' => 'show_whatsapp', + 'url_field' => 'whatsapp_number', + 'icon' => 'bi-whatsapp', + 'label' => 'WhatsApp', + 'share_pattern' => 'https://wa.me/?text=%s', // Compartir via WhatsApp + ], + 'twitter' => [ + 'field' => 'show_twitter', + 'url_field' => 'twitter_url', + 'icon' => 'bi-twitter-x', + 'label' => 'X', + 'share_pattern' => 'https://twitter.com/intent/tweet?url=%s&text=%s', + ], + 'email' => [ + 'field' => 'show_email', + 'url_field' => 'email_address', + 'icon' => 'bi-envelope', + 'label' => 'Email', + 'share_pattern' => 'mailto:?subject=%s&body=%s', // Compartir via Email + ], + ]; + + public function __construct( + private CSSGeneratorInterface $cssGenerator + ) {} + + public function render(Component $component): string + { + $data = $component->getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data); + + return sprintf("\n%s", $css, $html); + } + + public function supports(string $componentType): bool + { + return $componentType === 'social-share'; + } + + private function isEnabled(array $data): bool + { + $value = $data['visibility']['is_enabled'] ?? false; + return $value === true || $value === '1' || $value === 1; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $typography = $data['typography'] ?? []; + $effects = $data['visual_effects'] ?? []; + $visibility = $data['visibility'] ?? []; + + $cssRules = []; + $transitionDuration = $effects['transition_duration'] ?? '0.3s'; + $buttonBorderWidth = $effects['button_border_width'] ?? '2px'; + + // Container styles + $cssRules[] = $this->cssGenerator->generate('.social-share-container', [ + 'margin-top' => $spacing['container_margin_top'] ?? '3rem', + 'margin-bottom' => $spacing['container_margin_bottom'] ?? '3rem', + 'padding-top' => $spacing['container_padding_top'] ?? '1.5rem', + 'padding-bottom' => $spacing['container_padding_bottom'] ?? '1.5rem', + 'border-top' => sprintf('%s solid %s', + $effects['border_top_width'] ?? '1px', + $colors['border_top_color'] ?? '#dee2e6' + ), + ]); + + // Label styles + $cssRules[] = $this->cssGenerator->generate('.social-share-container .share-label', [ + 'font-size' => $typography['label_font_size'] ?? '1rem', + 'color' => $colors['label_color'] ?? '#6c757d', + 'margin-bottom' => $spacing['label_margin_bottom'] ?? '1rem', + ]); + + // Buttons wrapper + $cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons', [ + 'display' => 'flex', + 'flex-wrap' => 'wrap', + 'gap' => $spacing['buttons_gap'] ?? '0.5rem', + ]); + + // Base button styles + $cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn', [ + 'padding' => $spacing['button_padding'] ?? '0.25rem 0.5rem', + 'font-size' => $typography['icon_font_size'] ?? '1rem', + 'border-width' => $buttonBorderWidth, + 'border-radius' => $effects['button_border_radius'] ?? '0.375rem', + 'transition' => "all {$transitionDuration} ease", + 'background-color' => $colors['button_background'] ?? '#ffffff', + ]); + + // Hover effect + $cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn:hover', [ + 'box-shadow' => $effects['hover_box_shadow'] ?? '0 4px 12px rgba(0, 0, 0, 0.15)', + ]); + + // Network-specific colors + $networkColors = [ + 'facebook' => $colors['facebook_color'] ?? '#0d6efd', + 'instagram' => $colors['instagram_color'] ?? '#dc3545', + 'linkedin' => $colors['linkedin_color'] ?? '#0dcaf0', + 'whatsapp' => $colors['whatsapp_color'] ?? '#198754', + 'twitter' => $colors['twitter_color'] ?? '#212529', + 'email' => $colors['email_color'] ?? '#6c757d', + ]; + + foreach ($networkColors as $network => $color) { + // Outline style + $cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}", [ + 'color' => $color, + 'border-color' => $color, + ]); + // Hover fills the button + $cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}:hover", [ + 'background-color' => $color, + 'color' => '#ffffff', + ]); + } + + // Responsive visibility (normalizar booleanos desde BD) + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1; + $showOnMobile = $visibility['show_on_mobile'] ?? true; + $showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 991.98px) { + .social-share-container { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 992px) { + .social-share-container { display: none !important; } + }"; + } + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data): string + { + $content = $data['content'] ?? []; + $networks = $data['networks'] ?? []; + + $labelText = $content['label_text'] ?? 'Compartir:'; + $showLabel = $content['show_label'] ?? true; + $showLabel = $showLabel === true || $showLabel === '1' || $showLabel === 1; + + $html = ''; // .social-share-container + + return $html; + } + + private function getCurrentUrl(): string + { + if (is_singular()) { + return get_permalink() ?: ''; + } + return home_url(add_query_arg([], $GLOBALS['wp']->request ?? '')); + } + + private function getCurrentTitle(): string + { + if (is_singular()) { + return get_the_title() ?: ''; + } + return wp_title('', false) ?: get_bloginfo('name'); + } + + /** + * Construye la URL para un botón de red social + * + * Prioridad: + * 1. URL configurada por el usuario → enlace directo al perfil + * 2. Sin URL configurada → usar patrón de compartir (si existe) + */ + private function buildNetworkUrl( + string $network, + string $configuredUrl, + string $sharePattern, + string $pageUrl, + string $pageTitle + ): string { + // Si hay URL configurada, usarla directamente + if (!empty($configuredUrl)) { + return $this->formatConfiguredUrl($network, $configuredUrl); + } + + // Si no hay URL configurada pero existe patrón de compartir + if (!empty($sharePattern)) { + return $this->formatShareUrl($network, $sharePattern, $pageUrl, $pageTitle); + } + + return '#'; + } + + /** + * Formatea URL configurada según el tipo de red + */ + private function formatConfiguredUrl(string $network, string $url): string + { + switch ($network) { + case 'whatsapp': + // Para WhatsApp, el número debe ir sin el + + $number = preg_replace('/[^0-9]/', '', $url); + return "https://wa.me/{$number}"; + case 'email': + // Para email, agregar mailto: si no lo tiene + if (!str_starts_with($url, 'mailto:')) { + return "mailto:{$url}"; + } + return $url; + default: + return $url; + } + } + + /** + * Formatea URL de compartir usando el patrón + */ + private function formatShareUrl(string $network, string $pattern, string $url, string $title): string + { + $encodedUrl = rawurlencode($url); + $encodedTitle = rawurlencode($title); + + switch ($network) { + case 'twitter': + return sprintf($pattern, $encodedUrl, $encodedTitle); + case 'whatsapp': + $text = $title . ' - ' . $url; + return sprintf($pattern, rawurlencode($text)); + case 'email': + return sprintf($pattern, $encodedTitle, $encodedUrl); + default: + return sprintf($pattern, $encodedUrl); + } + } + + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if (!$desktop && !$mobile) { + return null; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + return ''; + } +} diff --git a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php new file mode 100644 index 00000000..664fcc7e --- /dev/null +++ b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php @@ -0,0 +1,491 @@ +getData(); + + if (!$this->isEnabled($data)) { + return ''; + } + + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + $tocItems = $this->generateTocItems($data); + + if (empty($tocItems)) { + return ''; + } + + $css = $this->generateCSS($data); + $html = $this->buildHTML($data, $tocItems); + $script = $this->buildScript($data); + + return sprintf("\n%s\n%s", $css, $html, $script); + } + + public function supports(string $componentType): bool + { + return $componentType === 'table-of-contents'; + } + + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; + + switch ($showOn) { + case 'all': + return true; + case 'posts': + return is_single(); + case 'pages': + return is_page(); + default: + return true; + } + } + + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + if (!$desktop && !$mobile) { + return null; + } + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + return ''; + } + + private function generateTocItems(array $data): array + { + $content = $data['content'] ?? []; + $autoGenerate = $content['auto_generate'] ?? true; + + if (!$autoGenerate) { + return []; + } + + $headingLevelsStr = $content['heading_levels'] ?? 'h2,h3'; + $headingLevels = array_map('trim', explode(',', $headingLevelsStr)); + + return $this->generateTocFromContent($headingLevels); + } + + private function generateTocFromContent(array $headingLevels): array + { + global $post; + + if (!$post || empty($post->post_content)) { + return []; + } + + $content = apply_filters('the_content', $post->post_content); + + $dom = new DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML('' . $content); + libxml_clear_errors(); + + $xpath = new DOMXPath($dom); + $tocItems = []; + + $xpathQuery = implode(' | ', array_map(function($level) { + return '//' . $level; + }, $headingLevels)); + + $headings = $xpath->query($xpathQuery); + + if ($headings->length === 0) { + return []; + } + + foreach ($headings as $heading) { + $tagName = strtolower($heading->tagName); + $level = intval(substr($tagName, 1)); + + $text = trim($heading->textContent); + + if (empty($text)) { + continue; + } + + $existingId = $heading->getAttribute('id'); + + if (empty($existingId)) { + $anchor = $this->generateAnchorId($text); + $this->addIdToHeading($text, $anchor); + } else { + $anchor = $existingId; + } + + $tocItems[] = [ + 'text' => $text, + 'anchor' => $anchor, + 'level' => $level + ]; + } + + return $tocItems; + } + + private function generateAnchorId(string $text): string + { + $id = strtolower($text); + $id = remove_accents($id); + $id = preg_replace('/[^a-z0-9]+/', '-', $id); + $id = trim($id, '-'); + + $baseId = $id; + $count = 1; + + while (isset($this->headingCounter[$id])) { + $id = $baseId . '-' . $count; + $count++; + } + + $this->headingCounter[$id] = true; + + return $id; + } + + private function addIdToHeading(string $headingText, string $anchorId): void + { + add_filter('the_content', function($content) use ($headingText, $anchorId) { + $pattern = '/<(h[2-6])([^>]*)>(\s*)' . preg_quote($headingText, '/') . '(\s*)<\/\1>/i'; + $replacement = '<$1 id="' . esc_attr($anchorId) . '"$2>$3' . $headingText . '$4'; + return preg_replace($pattern, $replacement, $content, 1); + }, 20); + } + + private function generateCSS(array $data): string + { + $colors = $data['colors'] ?? []; + $spacing = $data['spacing'] ?? []; + $typography = $data['typography'] ?? []; + $effects = $data['visual_effects'] ?? []; + $behavior = $data['behavior'] ?? []; + $visibility = $data['visibility'] ?? []; + + $cssRules = []; + + // Container styles - Flexbox layout for proper scrolling + $cssRules[] = $this->cssGenerator->generate('.toc-container', [ + 'background-color' => $colors['background_color'] ?? '#ffffff', + 'border' => ($effects['border_width'] ?? '1px') . ' solid ' . ($colors['border_color'] ?? '#E6E9ED'), + 'border-radius' => $effects['border_radius'] ?? '8px', + 'box-shadow' => $effects['box_shadow'] ?? '0 2px 8px rgba(0, 0, 0, 0.08)', + 'padding' => $spacing['container_padding'] ?? '12px 16px', + 'margin-bottom' => $spacing['margin_bottom'] ?? '13px', + 'max-height' => $behavior['max_height'] ?? 'calc(100vh - 71px - 10px - 250px - 15px - 15px)', + 'display' => 'flex', + 'flex-direction' => 'column', + 'overflow' => 'visible', + ]); + + // Sticky behavior - aplica al wrapper .sidebar-sticky de single.php + // NO al .toc-container individual (ver template líneas 817-835) + if (($behavior['is_sticky'] ?? true)) { + $cssRules[] = $this->cssGenerator->generate('.sidebar-sticky', [ + 'position' => 'sticky', + 'top' => '85px', + 'display' => 'flex', + 'flex-direction' => 'column', + ]); + } + + // Custom scrollbar + $cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar', [ + 'width' => $spacing['scrollbar_width'] ?? '6px', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-track', [ + 'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB', + 'border-radius' => $effects['scrollbar_border_radius'] ?? '3px', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-thumb', [ + 'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280', + 'border-radius' => $effects['scrollbar_border_radius'] ?? '3px', + ]); + + // Title styles - Color #1e3a5f = navy-primary del Design System + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-title', [ + 'font-size' => $typography['title_font_size'] ?? '1rem', + 'font-weight' => $typography['title_font_weight'] ?? '600', + 'color' => $colors['title_color'] ?? '#1e3a5f', + 'padding-bottom' => $spacing['title_padding_bottom'] ?? '8px', + 'margin-bottom' => $spacing['title_margin_bottom'] ?? '0.75rem', + 'border-bottom' => '2px solid ' . ($colors['title_border_color'] ?? '#E6E9ED'), + 'margin-top' => '0', + ]); + + // List styles - Scrollable area with flex + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list', [ + 'margin' => '0', + 'padding' => '0', + 'padding-right' => '0.5rem', + 'list-style' => 'none', + 'overflow-y' => 'auto', + 'flex' => '1', + 'min-height' => '0', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list li', [ + 'margin-bottom' => $spacing['item_margin_bottom'] ?? '0.15rem', + ]); + + // Link styles - Color #495057 = neutral-600 del template + $transitionDuration = $effects['transition_duration'] ?? '0.3s'; + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link', [ + 'display' => 'block', + 'font-size' => $typography['link_font_size'] ?? '0.9rem', + 'line-height' => $typography['link_line_height'] ?? '1.3', + 'color' => $colors['link_color'] ?? '#495057', + 'text-decoration' => 'none', + 'padding' => $spacing['link_padding'] ?? '0.3rem 0.85rem', + 'border-radius' => $effects['link_border_radius'] ?? '4px', + 'border-left' => ($effects['active_border_left_width'] ?? '3px') . ' solid transparent', + 'transition' => "all {$transitionDuration} ease", + ]); + + // Link hover - Color #1e3a5f = navy-primary del Design System + // Template: background, border-left-color, color + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link:hover', [ + 'color' => $colors['link_hover_color'] ?? '#1e3a5f', + 'background-color' => $colors['link_hover_background'] ?? '#F9FAFB', + 'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f', + ]); + + // Active link - Color #1e3a5f = navy-primary del Design System + // Template: font-weight: 600 + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link.active', [ + 'color' => $colors['active_text_color'] ?? '#1e3a5f', + 'background-color' => $colors['active_background_color'] ?? '#F9FAFB', + 'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f', + 'font-weight' => '600', + ]); + + // Level indentation + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-3 .toc-link', [ + 'padding-left' => $spacing['level_three_padding_left'] ?? '1.5rem', + 'font-size' => $typography['level_three_font_size'] ?? '0.85rem', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-4 .toc-link', [ + 'padding-left' => $spacing['level_four_padding_left'] ?? '2rem', + 'font-size' => $typography['level_four_font_size'] ?? '0.8rem', + ]); + + // Scrollbar for toc-list + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar', [ + 'width' => $spacing['scrollbar_width'] ?? '6px', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-track', [ + 'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB', + 'border-radius' => $effects['scrollbar_border_radius'] ?? '3px', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb', [ + 'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280', + 'border-radius' => $effects['scrollbar_border_radius'] ?? '3px', + ]); + + $cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb:hover', [ + 'background' => $colors['active_border_color'] ?? '#1e3a5f', + ]); + + // Responsive visibility + $showOnDesktop = $visibility['show_on_desktop'] ?? true; + $showOnMobile = $visibility['show_on_mobile'] ?? false; + + if (!$showOnMobile) { + $cssRules[] = "@media (max-width: 991.98px) { + .toc-container { display: none !important; } + }"; + } + + if (!$showOnDesktop) { + $cssRules[] = "@media (min-width: 992px) { + .toc-container { display: none !important; } + }"; + } + + // Responsive layout adjustments + $cssRules[] = "@media (max-width: 991px) { + .sidebar-sticky { + position: relative !important; + top: 0 !important; + } + .toc-container { + margin-bottom: 2rem; + } + .toc-container .toc-list { + max-height: 300px; + } + }"; + + return implode("\n", $cssRules); + } + + private function buildHTML(array $data, array $tocItems): string + { + $content = $data['content'] ?? []; + + $title = $content['title'] ?? 'Tabla de Contenido'; + + // NOTA: El sticky behavior se maneja en el wrapper .sidebar-sticky de single.php + // El TOC no debe tener la clase sidebar-sticky - está dentro del wrapper + $html = '
    '; + + $html .= sprintf( + '

    %s

    ', + esc_html($title) + ); + + $html .= '
      '; + + foreach ($tocItems as $item) { + $text = $item['text'] ?? ''; + $anchor = $item['anchor'] ?? ''; + $level = $item['level'] ?? 2; + + if (empty($text) || empty($anchor)) { + continue; + } + + $indentClass = $level > 2 ? 'toc-level-' . $level : ''; + + $html .= sprintf( + '
    1. %s
    2. ', + esc_attr($indentClass), + esc_attr($anchor), + intval($level), + esc_html($text) + ); + } + + $html .= '
    '; + $html .= '
    '; + + return $html; + } + + private function buildScript(array $data): string + { + $content = $data['content'] ?? []; + $behavior = $data['behavior'] ?? []; + + $smoothScroll = $content['smooth_scroll'] ?? true; + $scrollOffset = intval($behavior['scroll_offset'] ?? 100); + + if (!$smoothScroll) { + return ''; + } + + $script = << +document.addEventListener('DOMContentLoaded', function() { + var tocLinks = document.querySelectorAll('.toc-link'); + var offsetTop = {$scrollOffset}; + + tocLinks.forEach(function(link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + var targetId = this.getAttribute('href'); + var targetElement = document.querySelector(targetId); + + if (targetElement) { + var elementPosition = targetElement.getBoundingClientRect().top; + var offsetPosition = elementPosition + window.pageYOffset - offsetTop; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + } + }); + }); + + // ScrollSpy + var sections = []; + tocLinks.forEach(function(link) { + var id = link.getAttribute('href').substring(1); + var section = document.getElementById(id); + if (section) { + sections.push({ id: id, element: section }); + } + }); + + function updateActiveLink() { + var scrollPosition = window.pageYOffset + offsetTop + 50; + var currentSection = ''; + + sections.forEach(function(section) { + if (section.element.offsetTop <= scrollPosition) { + currentSection = section.id; + } + }); + + tocLinks.forEach(function(link) { + link.classList.remove('active'); + if (link.getAttribute('href') === '#' + currentSection) { + link.classList.add('active'); + } + }); + } + + window.addEventListener('scroll', updateActiveLink); + updateActiveLink(); +}); + +JS; + + return $script; + } +} diff --git a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php new file mode 100644 index 00000000..825621a2 --- /dev/null +++ b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php @@ -0,0 +1,466 @@ +getData(); + + // Validar visibilidad general + if (!$this->isEnabled($data)) { + return ''; + } + + // Validar visibilidad por página + if (!$this->shouldShowOnCurrentPage($data)) { + return ''; + } + + // Generar CSS usando CSSGeneratorService + $css = $this->generateCSS($data); + + // Generar HTML + $html = $this->buildHTML($data); + + // Combinar todo + return sprintf( + "\n%s", + $css, + $html + ); + } + + /** + * {@inheritDoc} + */ + public function supports(string $componentType): bool + { + return $componentType === 'top-notification-bar'; + } + + /** + * Verificar si el componente está habilitado + * + * @param array $data Datos del componente + * @return bool + */ + private function isEnabled(array $data): bool + { + return ($data['visibility']['is_enabled'] ?? false) === true; + } + + /** + * Verificar si debe mostrarse en la página actual + * + * @param array $data Datos del componente + * @return bool + */ + private function shouldShowOnCurrentPage(array $data): bool + { + $showOn = $data['visibility']['show_on_pages'] ?? 'all'; + + return match ($showOn) { + 'all' => true, + 'home' => is_front_page(), + 'posts' => is_single(), + 'pages' => is_page(), + 'custom' => $this->isInCustomPages($data), + default => true, + }; + } + + /** + * Verificar si está en páginas personalizadas + * + * @param array $data Datos del componente + * @return bool + */ + private function isInCustomPages(array $data): bool + { + $pageIds = $data['visibility']['custom_page_ids'] ?? ''; + + if (empty($pageIds)) { + return false; + } + + $allowedIds = array_map('trim', explode(',', $pageIds)); + $currentId = (string) get_the_ID(); + + return in_array($currentId, $allowedIds, true); + } + + /** + * Verificar si el componente fue dismissed por el usuario + * + * @param array $data Datos del componente + * @return bool + */ + private function isDismissed(array $data): bool + { + if (!$this->isDismissible($data)) { + return false; + } + + $cookieName = 'roi_notification_bar_dismissed'; + return isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === '1'; + } + + /** + * Verificar si el componente es dismissible + * + * @param array $data Datos del componente + * @return bool + */ + private function isDismissible(array $data): bool + { + return ($data['behavior']['is_dismissible'] ?? false) === true; + } + + /** + * Generar CSS usando CSSGeneratorService + * + * @param array $data Datos del componente + * @return string CSS generado + */ + private function generateCSS(array $data): string + { + $css = ''; + + // Estilos base de la barra + $baseStyles = [ + 'background_color' => $data['styles']['background_color'] ?? '#0E2337', + 'color' => $data['styles']['text_color'] ?? '#FFFFFF', + 'font_size' => $data['styles']['font_size'] ?? '0.9rem', + 'padding' => $data['styles']['padding'] ?? '0.5rem 0', + 'width' => '100%', + 'z_index' => '1050', + ]; + $css .= $this->cssGenerator->generate('.top-notification-bar', $baseStyles); + + // Estilos del ícono + $iconStyles = [ + 'color' => $data['styles']['icon_color'] ?? '#FF8600', + ]; + $css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-icon', $iconStyles); + + // Estilos de la etiqueta (label) + $labelStyles = [ + 'color' => $data['styles']['label_color'] ?? '#FF8600', + ]; + $css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-label', $labelStyles); + + // Estilos del enlace + $linkStyles = [ + 'color' => $data['styles']['link_color'] ?? '#FFFFFF', + ]; + $css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link', $linkStyles); + + // Estilos del enlace hover + $linkHoverStyles = [ + 'color' => $data['styles']['link_hover_color'] ?? '#FF8600', + ]; + $css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link:hover', $linkHoverStyles); + + // Estilos del ícono personalizado + $customIconStyles = [ + 'width' => '24px', + 'height' => '24px', + ]; + $css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .custom-icon', $customIconStyles); + + return $css; + } + + /** + * Generar HTML del componente + * + * @param array $data Datos del componente + * @return string HTML generado + */ + private function buildHTML(array $data): string + { + $classes = $this->buildClasses($data); + $content = $this->buildContent($data); + + return sprintf( + '
    %s
    ', + esc_attr($classes), + $content + ); + } + + /** + * Construir clases CSS del componente + * + * @param array $data Datos del componente + * @return string Clases CSS + */ + private function buildClasses(array $data): string + { + return 'top-notification-bar'; + } + + /** + * Construir atributos data para dismissible + * + * @param array $data Datos del componente + * @return string Atributos HTML + */ + private function buildDismissAttributes(array $data): string + { + if (!$this->isDismissible($data)) { + return ''; + } + + $days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7); + return sprintf(' data-dismissible-days="%d"', $days); + } + + /** + * Construir contenido del componente + * + * @param array $data Datos del componente + * @return string HTML del contenido + */ + private function buildContent(array $data): string + { + $html = '
    '; + $html .= '
    '; + + // Ícono + $html .= $this->buildIcon($data); + + // Texto del anuncio + $html .= $this->buildAnnouncementText($data); + + // Enlace + $html .= $this->buildLink($data); + + $html .= '
    '; + $html .= '
    '; + + return $html; + } + + /** + * Construir ícono del componente + * + * @param array $data Datos del componente + * @return string HTML del ícono + */ + private function buildIcon(array $data): string + { + // Siempre usar Bootstrap icon desde content.icon_class + $iconClass = $data['content']['icon_class'] ?? 'bi-megaphone-fill'; + + // Asegurar prefijo 'bi-' + if (strpos($iconClass, 'bi-') !== 0) { + $iconClass = 'bi-' . $iconClass; + } + + return sprintf( + '', + esc_attr($iconClass) + ); + } + + /** + * Construir texto del anuncio + * + * @param array $data Datos del componente + * @return string HTML del texto + */ + private function buildAnnouncementText(array $data): string + { + $label = $data['content']['label_text'] ?? ''; + $text = $data['content']['message_text'] ?? ''; + + if (empty($text)) { + return ''; + } + + $html = ''; + + if (!empty($label)) { + $html .= sprintf('%s ', esc_html($label)); + } + + $html .= esc_html($text); + $html .= ''; + + return $html; + } + + /** + * Construir enlace de acción + * + * @param array $data Datos del componente + * @return string HTML del enlace + */ + private function buildLink(array $data): string + { + $linkText = $data['content']['link_text'] ?? ''; + $linkUrl = $data['content']['link_url'] ?? '#'; + + if (empty($linkText)) { + return ''; + } + + return sprintf( + '%s', + esc_url($linkUrl), + esc_html($linkText) + ); + } + + /** + * Construir botón de cerrar + * + * @return string HTML del botón + */ + private function buildDismissButton(): string + { + return ''; + } + + /** + * Construir estilos CSS de animaciones + * + * @param array $data Datos del componente + * @return string CSS de animaciones + */ + private function buildAnimationStyles(array $data): string + { + $animationType = $data['visual_effects']['animation_type'] ?? 'slide-down'; + + $animations = [ + 'slide-down' => [ + 'keyframes' => '@keyframes roiSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }', + 'animation' => 'roiSlideDown 0.5s ease-out', + ], + 'fade-in' => [ + 'keyframes' => '@keyframes roiFadeIn { from { opacity: 0; } to { opacity: 1; } }', + 'animation' => 'roiFadeIn 0.5s ease-out', + ], + ]; + + $anim = $animations[$animationType] ?? $animations['slide-down']; + + return sprintf( + "%s\n.top-notification-bar.roi-animated.roi-%s { animation: %s; }", + $anim['keyframes'], + $animationType, + $anim['animation'] + ); + } + + /** + * Construir script para funcionalidad dismissible + * + * @param array $data Datos del componente + * @return string JavaScript + */ + private function buildDismissScript(array $data): string + { + $days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7); + + return sprintf( + '', + $days + ); + } + + /** + * Obtiene las clases CSS de Bootstrap para visibilidad responsive + * + * Implementa tabla de decisión según especificación (10.03): + * - Desktop Y Mobile = null (visible en ambos) + * - Solo Desktop = 'd-none d-lg-block' + * - Solo Mobile = 'd-lg-none' + * - Ninguno = 'd-none' (oculto) + * + * @param bool $desktop Mostrar en desktop + * @param bool $mobile Mostrar en mobile + * @return string|null Clases CSS o null si visible en ambos + */ + private function getVisibilityClasses(bool $desktop, bool $mobile): ?string + { + // Desktop Y Mobile = visible en ambos dispositivos + if ($desktop && $mobile) { + return null; // Sin clases = visible siempre + } + + // Solo Desktop + if ($desktop && !$mobile) { + return 'd-none d-lg-block'; + } + + // Solo Mobile + if (!$desktop && $mobile) { + return 'd-lg-none'; + } + + // Ninguno = oculto completamente + return 'd-none'; + } +} diff --git a/template-parts/content-cta-box.php b/TemplateParts/content-cta-box.php similarity index 100% rename from template-parts/content-cta-box.php rename to TemplateParts/content-cta-box.php diff --git a/template-parts/content-cta.php b/TemplateParts/content-cta.php similarity index 100% rename from template-parts/content-cta.php rename to TemplateParts/content-cta.php diff --git a/template-parts/content-hero.php b/TemplateParts/content-hero.php similarity index 100% rename from template-parts/content-hero.php rename to TemplateParts/content-hero.php diff --git a/template-parts/content-none.php b/TemplateParts/content-none.php similarity index 100% rename from template-parts/content-none.php rename to TemplateParts/content-none.php diff --git a/template-parts/content-toc.php b/TemplateParts/content-toc.php similarity index 100% rename from template-parts/content-toc.php rename to TemplateParts/content-toc.php diff --git a/template-parts/content.php b/TemplateParts/content.php similarity index 100% rename from template-parts/content.php rename to TemplateParts/content.php diff --git a/template-parts/cta-box-sidebar.php b/TemplateParts/cta-box-sidebar.php similarity index 100% rename from template-parts/cta-box-sidebar.php rename to TemplateParts/cta-box-sidebar.php diff --git a/template-parts/modal-contact.php b/TemplateParts/modal-contact.php similarity index 100% rename from template-parts/modal-contact.php rename to TemplateParts/modal-contact.php diff --git a/template-parts/top-notification-bar.php b/TemplateParts/top-notification-bar.php similarity index 100% rename from template-parts/top-notification-bar.php rename to TemplateParts/top-notification-bar.php diff --git a/admin/README.md b/admin/README.md deleted file mode 100644 index 027a8218..00000000 --- a/admin/README.md +++ /dev/null @@ -1,257 +0,0 @@ -# Contexto Admin - Administración de Componentes - -## Propósito - -El contexto `admin/` contiene **todo el código relacionado con la administración de componentes** en el panel -de WordPress. Cada componente tiene su propia carpeta con su Clean Architecture completa. - -## Filosofía: Context-First Architecture - -En lugar de separar por capas (Domain, Application, Infrastructure), separamos por **contextos** -(admin, public, shared) y cada contexto tiene sus propias capas internas. - -## Estructura (Fase-00) - -En Fase-00 solo creamos la estructura base. Los componentes se crearán en fases posteriores: - -``` -admin/ -├── README.md (este archivo) -└── .gitkeep (preserva directorio en Git) -``` - -## Estructura Futura (Post Fase-00) - -``` -admin/ -├── Navbar/ # Componente Navbar -│ ├── Domain/ -│ │ ├── NavbarComponent.php # Entidad -│ │ └── NavbarRepositoryInterface.php -│ ├── Application/ -│ │ ├── SaveNavbarUseCase.php # Caso de uso -│ │ └── DTO/ -│ │ └── NavbarSettingsDTO.php -│ ├── Infrastructure/ -│ │ ├── Persistence/ -│ │ │ └── WordPressNavbarRepository.php -│ │ ├── UI/ -│ │ │ ├── NavbarAdminPage.php # Página de admin -│ │ │ ├── NavbarForm.php # Formulario -│ │ │ └── views/ -│ │ │ └── navbar-settings.php -│ │ └── API/ -│ │ └── NavbarAjaxHandler.php # AJAX endpoints -│ └── README.md -├── Footer/ # Otro componente -│ └── (misma estructura) -└── (más componentes...) -``` - -## Principios del Contexto Admin - -1. **Aislamiento**: Cada componente es independiente -2. **Clean Architecture**: Cada componente tiene Domain, Application, Infrastructure -3. **Sin acoplamiento**: Un componente no depende de otro directamente -4. **Código compartido**: Va en `shared/` si es usado por múltiples componentes - -## Responsabilidades - -El contexto `admin/` se encarga de: - -✅ **Formularios de configuración** de componentes -✅ **Validación de entrada** del usuario administrador -✅ **Guardado de configuraciones** en la base de datos -✅ **Páginas de administración** en el panel de WordPress -✅ **AJAX endpoints** para operaciones administrativas -✅ **Permisos y capabilities** de administrador - -❌ **NO** se encarga de: -- Renderizado frontend de componentes (va en `public/`) -- Lógica compartida entre contextos (va en `shared/`) -- Configuraciones globales del tema - -## Ejemplo de Flujo de Admin - -### 1. Usuario administra el Navbar - -``` -Usuario Admin (navegador) - ↓ -NavbarAdminPage.php (muestra formulario) - ↓ -Usuario envía formulario via AJAX - ↓ -NavbarAjaxHandler.php (recibe request) - ↓ -SaveNavbarUseCase.php (orquesta la lógica) - ↓ -WordPressNavbarRepository.php (guarda en DB) - ↓ -Respuesta JSON al navegador -``` - -### 2. Código de ejemplo - -```php -// admin/Navbar/Application/SaveNavbarUseCase.php -namespace ROITheme\Admin\Navbar\Application; - -use ROITheme\Admin\Navbar\Domain\NavbarRepositoryInterface; -use ROITheme\Shared\Application\Contracts\ValidationServiceInterface; - -final class SaveNavbarUseCase -{ - public function __construct( - private readonly NavbarRepositoryInterface $repository, - private readonly ValidationServiceInterface $validator - ) {} - - public function execute(array $data): void - { - $validated = $this->validator->validate($data, [ - 'logo_url' => 'url', - 'menu_items' => 'array', - 'sticky' => 'bool', - ]); - - if ($this->validator->fails()) { - throw new ValidationException($this->validator->errors()); - } - - $this->repository->save($validated); - } -} -``` - -```php -// admin/Navbar/Infrastructure/UI/NavbarAdminPage.php -namespace ROITheme\Admin\Navbar\Infrastructure\UI; - -final class NavbarAdminPage -{ - public function register(): void - { - add_action('admin_menu', [$this, 'addMenuPage']); - } - - public function addMenuPage(): void - { - add_menu_page( - 'Navbar Settings', - 'Navbar', - 'manage_options', - 'roi-navbar-settings', - [$this, 'render'], - 'dashicons-menu', - 30 - ); - } - - public function render(): void - { - require __DIR__ . '/views/navbar-settings.php'; - } -} -``` - -## Relación con Otros Contextos - -``` -admin/ → Administra configuraciones - ↓ guarda -Base de Datos → Almacena settings - ↓ lee -public/ → Renderiza componentes en frontend -``` - -**Clave**: `admin/` y `public/` NO se hablan directamente. Se comunican a través de la base de datos. - -## Reglas de Dependencia - -✅ **PUEDE** depender de: -- `shared/Domain/` (Value Objects, Exceptions) -- `shared/Application/` (Contracts, Services) -- `shared/Infrastructure/` (Implementaciones de servicios) -- WordPress admin functions (`add_menu_page`, `add_settings_section`, etc.) - -❌ **NO PUEDE** depender de: -- `public/` (contexto independiente) -- Código de frontend (JS/CSS va en Infrastructure/UI/) - -## Testing - -### Tests Unitarios -```php -// tests/Unit/Admin/Navbar/Application/SaveNavbarUseCaseTest.php -public function test_saves_valid_navbar_settings() -{ - $repository = $this->createMock(NavbarRepositoryInterface::class); - $validator = $this->createMock(ValidationServiceInterface::class); - - $useCase = new SaveNavbarUseCase($repository, $validator); - $useCase->execute(['logo_url' => 'http://example.com/logo.png']); - - // Assertions... -} -``` - -### Tests de Integración -```php -// tests/Integration/Admin/Navbar/Infrastructure/WordPressNavbarRepositoryTest.php -public function test_saves_and_retrieves_navbar_settings() -{ - $repository = new WordPressNavbarRepository(); - $settings = ['logo_url' => 'http://example.com/logo.png']; - - $repository->save($settings); - $retrieved = $repository->get(); - - $this->assertEquals($settings, $retrieved); -} -``` - -### Tests E2E (Playwright) -```php -// tests/E2E/Admin/NavbarAdminPageTest.php -public function test_admin_can_save_navbar_settings() -{ - $this->loginAsAdmin(); - $this->visit('/wp-admin/admin.php?page=roi-navbar-settings'); - $this->fillField('logo_url', 'http://example.com/logo.png'); - $this->click('Save Settings'); - $this->see('Settings saved successfully'); -} -``` - -## Cuándo Agregar Código Aquí - -Agrega código a `admin/` cuando: -- Creas un nuevo componente administrable -- Necesitas una página de configuración en el panel de WordPress -- Manejas formularios de administrador -- Procesas AJAX desde el admin -- Validas o guardas configuraciones - -No agregues aquí: -- Renderizado frontend (va en `public/`) -- Lógica compartida (va en `shared/`) -- Configuraciones globales del tema - -## Estado Actual (Fase-00) - -En Fase-00, `admin/` solo tiene la estructura base. Los componentes se crearán en las siguientes fases: - -- **Fase-1**: Estructura base e infraestructura -- **Fase-2**: Migración de base de datos -- **Fase-3**: Implementación de componentes admin -- **Fase-4+**: Componentes adicionales - -## Próximos Pasos - -1. Crear primer componente en Fase-3 (ej: Navbar) -2. Implementar Domain layer del componente -3. Implementar Application layer (casos de uso) -4. Implementar Infrastructure layer (WordPress integration) -5. Crear tests unitarios e integración -6. Repetir para cada componente diff --git a/admin/assets/css/admin-panel.css b/admin/assets/css/admin-panel.css deleted file mode 100644 index f323c26e..00000000 --- a/admin/assets/css/admin-panel.css +++ /dev/null @@ -1,511 +0,0 @@ -/** - * Admin Panel Styles - * - * Estilos base para el panel de administración - * - * @package ROI_Theme - * @since 2.0.0 - */ - -/* ======================================== - Container - ======================================== */ - -.roi-admin-panel { - max-width: 1400px; - margin: 20px auto; -} - -/* ======================================== - Header - ======================================== */ - -.roi-admin-panel h1 { - margin-bottom: 10px; -} - -.roi-admin-panel .description { - color: #666; - margin-bottom: 20px; -} - -/* ======================================== - Tabs - ======================================== */ - -.nav-tabs { - border-bottom: 2px solid #dee2e6; -} - -.nav-tabs .nav-link { - color: #666; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -2px; -} - -.nav-tabs .nav-link:hover { - color: #0073aa; - border-bottom-color: #0073aa; -} - -.nav-tabs .nav-link.active { - color: #0073aa; - font-weight: 600; - border-bottom-color: #0073aa; - background-color: transparent; -} - -/* ======================================== - Tab Content - ======================================== */ - -.tab-content { - background: #fff; - padding: 20px; - border: 1px solid #ddd; - border-radius: 4px; -} - -.tab-pane h3 { - margin-top: 0; - margin-bottom: 15px; - font-size: 18px; -} - -.tab-pane h4 { - margin-top: 25px; - margin-bottom: 10px; - font-size: 16px; - color: #333; -} - -/* ======================================== - Form Sections - ======================================== */ - -.form-section { - padding-bottom: 20px; - border-bottom: 1px solid #eee; -} - -.form-section:last-child { - border-bottom: none; -} - -.form-group { - margin-bottom: 15px; -} - -.form-group label { - display: block; - font-weight: 600; - margin-bottom: 5px; - color: #333; -} - -.form-group input[type="text"], -.form-group input[type="url"], -.form-group input[type="email"], -.form-group input[type="number"], -.form-group select, -.form-group textarea { - max-width: 600px; -} - -.form-group .form-text { - margin-top: 5px; - font-size: 13px; -} - -.form-group .form-text code { - background: #f5f5f5; - padding: 2px 5px; - border-radius: 3px; - font-size: 12px; -} - -/* ======================================== - Action Buttons - ======================================== */ - -.admin-actions { - padding: 20px; - background: #f9f9f9; - border-top: 1px solid #ddd; - border-radius: 4px; -} - -.admin-actions .button-primary { - font-size: 14px; - padding: 8px 20px; - height: auto; -} - -.admin-actions .button-primary i { - vertical-align: middle; -} - -/* ======================================== - Responsive - ======================================== */ - -@media (max-width: 782px) { - .roi-admin-panel { - margin: 10px; - } - - .tab-content { - padding: 15px; - } - - .form-group input[type="text"], - .form-group input[type="url"], - .form-group input[type="email"], - .form-group select, - .form-group textarea { - max-width: 100%; - } -} - -/* ============================================ - MEJORAS ESPECÍFICAS PARA ADMIN PANEL - ============================================ */ - -/* Variables CSS */ -:root { - --color-navy-primary: #1E3A5F; - --color-navy-light: #2C5282; - --color-navy-dark: #0E2337; - --color-orange-primary: #FF8600; - --color-orange-hover: #FF6B35; - --color-neutral-50: #F9FAFB; - --color-neutral-100: #F3F4F6; - --color-neutral-600: #6B7280; - --color-neutral-700: #4B5563; -} - -/* Colores de marca como clases de utilidad */ -.text-navy-primary { color: var(--color-navy-primary) !important; } -.text-navy-dark { color: var(--color-navy-dark) !important; } -.text-orange-primary { color: var(--color-orange-primary) !important; } -.text-neutral-600 { color: var(--color-neutral-600) !important; } -.text-neutral-700 { color: var(--color-neutral-700) !important; } - -.bg-neutral-50 { background-color: var(--color-neutral-50) !important; } -.bg-neutral-100 { background-color: var(--color-neutral-100) !important; } - -/* Tab Header mejorado */ -.tab-header { - padding: 1.5rem; - background: linear-gradient(135deg, rgba(30, 58, 95, 0.03) 0%, rgba(255, 134, 0, 0.02) 100%); - border-radius: 8px; - margin-bottom: 2rem; - border-left: 4px solid var(--color-orange-primary); -} - -/* Section Title con icono */ -.section-title { - color: var(--color-navy-primary); - font-size: 1.25rem; - font-weight: 700; - margin-bottom: 1.5rem; - padding-bottom: 0.75rem; - border-bottom: 2px solid var(--color-neutral-100); - display: flex; - align-items: center; - gap: 0.75rem; -} - -.section-title .title-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - background: linear-gradient(135deg, var(--color-orange-primary), var(--color-orange-hover)); - border-radius: 8px; - color: white; - font-size: 1rem; -} - -/* Form Section Cards mejorados */ -.form-section.card { - border: 1px solid var(--color-neutral-100); - transition: all 0.3s ease; -} - -.form-section.card:hover { - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08); - border-color: var(--color-neutral-100); -} - -/* Toggle Container mejorado */ -.toggle-container { - background: var(--color-neutral-50); - padding: 1rem; - border-radius: 8px; - border: 1px solid var(--color-neutral-100); -} - -/* Switch más grande */ -.form-switch-lg .form-check-input { - width: 3rem; - height: 1.5rem; - cursor: pointer; -} - -.form-switch-lg .form-check-input:checked { - background-color: var(--color-orange-primary); - border-color: var(--color-orange-primary); -} - -.form-switch-lg .form-check-input:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 134, 0, 0.25); - border-color: var(--color-orange-primary); -} - -.form-switch-lg .form-check-label { - margin-left: 0.5rem; - font-weight: 500; -} - -/* Input Group merge (sin borde en el medio) */ -.input-group-merge .input-group-text { - border-right: 0; - background-color: var(--color-neutral-50); -} - -.input-group-merge .form-control { - border-left: 0; -} - -.input-group-merge .form-control:focus { - border-color: var(--color-orange-primary); - box-shadow: none; -} - -/* Color Picker mejorado */ -.color-picker-wrapper .form-control-color { - width: 100%; - height: 60px; - border-radius: 8px; - border: 2px solid var(--color-neutral-100); - cursor: pointer; - transition: all 0.3s ease; -} - -.color-picker-wrapper .form-control-color:hover { - border-color: var(--color-orange-primary); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(255, 134, 0, 0.15); -} - -.color-picker-wrapper .form-control-color::-webkit-color-swatch { - border-radius: 4px; - border: none; -} - -.color-picker-wrapper .color-preview-text { - text-align: center; -} - -.color-picker-wrapper .color-preview-text code { - display: block; - font-size: 0.875rem; - font-weight: 600; - margin-bottom: 0.25rem; -} - -/* Alert personalizado */ -.alert-info-custom { - background: linear-gradient(135deg, rgba(255, 134, 0, 0.08), rgba(255, 134, 0, 0.03)); - border: 1px solid rgba(255, 134, 0, 0.2); - border-radius: 8px; - padding: 1rem; -} - -.alert-info-custom .alert-heading { - color: var(--color-navy-primary); - font-weight: 700; -} - -.alert-info-custom p { - color: var(--color-neutral-600); -} - -/* Preview Container */ -.preview-container { - position: relative; - min-height: 150px; -} - -.top-bar-preview { - animation: fadeInUp 0.5s ease; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Botones de marca */ -.btn-navy-primary { - background-color: var(--color-navy-primary); - border-color: var(--color-navy-primary); - color: white; - font-weight: 600; - transition: all 0.3s ease; -} - -.btn-navy-primary:hover { - background-color: var(--color-navy-light); - border-color: var(--color-navy-light); - color: white; - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(30, 58, 95, 0.2); -} - -.btn-orange-primary { - background-color: var(--color-orange-primary); - border-color: var(--color-orange-primary); - color: white; - font-weight: 600; - transition: all 0.3s ease; -} - -.btn-orange-primary:hover { - background-color: var(--color-orange-hover); - border-color: var(--color-orange-hover); - color: white; - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(255, 134, 0, 0.3); -} - -/* Sticky Footer Actions */ -.sticky-bottom { - position: sticky; - bottom: 0; - z-index: 10; - box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05); -} - -/* Progress bar para textarea */ -.progress { - background-color: var(--color-neutral-100); - height: 4px; - border-radius: 2px; - overflow: hidden; -} - -.progress .progress-bar { - transition: width 0.3s ease; -} - -.progress .progress-bar.bg-orange-primary { - background-color: var(--color-orange-primary); -} - -/* Badges personalizados */ -.badge.bg-neutral-100 { - background-color: var(--color-neutral-100) !important; -} - -.badge.text-neutral-600 { - color: var(--color-neutral-600) !important; -} - -/* Form control improvements */ -.form-control-lg { - font-size: 1rem; - padding: 0.75rem 1rem; -} - -.form-select-lg { - font-size: 1rem; - padding: 0.75rem 1rem; -} - -/* Gap utilities */ -.g-4 { - gap: 1.5rem; -} - -/* Border utilities */ -.border-0 { - border: 0 !important; -} - -.border-neutral-100 { - border-color: var(--color-neutral-100) !important; -} - -/* Shadow utilities */ -.shadow-sm { - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06) !important; -} - -/* Rounded utilities */ -.rounded-3 { - border-radius: 0.5rem !important; -} - -.rounded-bottom { - border-bottom-left-radius: 0.5rem !important; - border-bottom-right-radius: 0.5rem !important; -} - -/* Card utilities */ -.card { - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); - border-radius: 0.5rem; -} - -.card-body { - padding: 1.5rem; -} - -/* Typography utilities */ -.fw-bold { - font-weight: 700 !important; -} - -.fw-medium { - font-weight: 500 !important; -} - -.small { - font-size: 0.875rem; -} - -/* Responsive mejoras */ -@media (max-width: 768px) { - .tab-header { - padding: 1rem; - } - - .tab-header .d-flex { - flex-direction: column; - gap: 1rem; - } - - .section-title { - font-size: 1.1rem; - } - - .color-picker-wrapper .form-control-color { - height: 50px; - } - - .form-switch-lg .form-check-input { - width: 2.5rem; - height: 1.25rem; - } -} diff --git a/admin/assets/css/theme-options.css b/admin/assets/css/theme-options.css deleted file mode 100644 index 9d800aa1..00000000 --- a/admin/assets/css/theme-options.css +++ /dev/null @@ -1,471 +0,0 @@ -/** - * Theme Options Admin Styles - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* Main Container */ -.roi-theme-options { - margin: 20px 20px 0 0; -} - -/* Header */ -.roi-options-header { - background: #fff; - border: 1px solid #c3c4c7; - padding: 20px; - margin: 20px 0; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 1px 1px rgba(0,0,0,.04); -} - -.roi-options-logo h2 { - margin: 0; - font-size: 24px; - color: #1d2327; - display: inline-block; -} - -.roi-options-logo .version { - background: #2271b1; - color: #fff; - padding: 3px 8px; - border-radius: 3px; - font-size: 12px; - margin-left: 10px; -} - -.roi-options-actions { - display: flex; - gap: 10px; -} - -.roi-options-actions .button .dashicons { - margin-top: 3px; - margin-right: 3px; -} - -/* Form */ -.roi-options-form { - background: #fff; - border: 1px solid #c3c4c7; - box-shadow: 0 1px 1px rgba(0,0,0,.04); -} - -/* Tabs Container */ -.roi-options-container { - display: flex; - min-height: 600px; -} - -/* Tabs Navigation */ -.roi-tabs-nav { - width: 200px; - background: #f6f7f7; - border-right: 1px solid #c3c4c7; -} - -.roi-tabs-nav ul { - margin: 0; - padding: 0; - list-style: none; -} - -.roi-tabs-nav li { - margin: 0; - padding: 0; - border-bottom: 1px solid #c3c4c7; -} - -.roi-tabs-nav li:first-child { - border-top: 1px solid #c3c4c7; -} - -.roi-tabs-nav a { - display: block; - padding: 15px 20px; - color: #50575e; - text-decoration: none; - transition: all 0.2s; - position: relative; -} - -.roi-tabs-nav a .dashicons { - margin-right: 8px; - color: #787c82; -} - -.roi-tabs-nav a:hover { - background: #fff; - color: #2271b1; -} - -.roi-tabs-nav a:hover .dashicons { - color: #2271b1; -} - -.roi-tabs-nav li.active a { - background: #fff; - color: #2271b1; - font-weight: 600; - border-left: 3px solid #2271b1; - padding-left: 17px; -} - -.roi-tabs-nav li.active a .dashicons { - color: #2271b1; -} - -/* Tabs Content */ -.roi-tabs-content { - flex: 1; - padding: 30px; -} - -.roi-tab-pane { - display: none; -} - -.roi-tab-pane.active { - display: block; -} - -.roi-tab-pane h2 { - margin: 0 0 10px 0; - font-size: 23px; - font-weight: 400; - line-height: 1.3; -} - -.roi-tab-pane > p.description { - margin: 0 0 20px 0; - color: #646970; -} - -.roi-tab-pane h3 { - margin: 30px 0 0 0; - padding: 15px 0 10px 0; - border-top: 1px solid #dcdcde; - font-size: 18px; -} - -/* Form Table */ -.roi-tab-pane .form-table { - margin-top: 20px; -} - -.roi-tab-pane .form-table th { - padding: 20px 10px 20px 0; - width: 200px; -} - -.roi-tab-pane .form-table td { - padding: 15px 10px; -} - -/* Toggle Switch */ -.roi-switch { - position: relative; - display: inline-block; - width: 50px; - height: 24px; -} - -.roi-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.roi-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - transition: .4s; - border-radius: 24px; -} - -.roi-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: white; - transition: .4s; - border-radius: 50%; -} - -input:checked + .roi-slider { - background-color: #2271b1; -} - -input:focus + .roi-slider { - box-shadow: 0 0 1px #2271b1; -} - -input:checked + .roi-slider:before { - transform: translateX(26px); -} - -/* Image Upload */ -.roi-image-upload { - max-width: 600px; -} - -.roi-image-preview { - margin-bottom: 10px; - border: 1px solid #c3c4c7; - background: #f6f7f7; - padding: 10px; - min-height: 100px; - display: flex; - align-items: center; - justify-content: center; -} - -.roi-image-preview:empty { - display: none; -} - -.roi-preview-image { - max-width: 100%; - height: auto; - display: block; -} - -.roi-upload-image, -.roi-remove-image { - margin-right: 10px; -} - -/* Submit Button */ -.roi-options-form .submit { - margin: 0; - padding: 20px 30px; - border-top: 1px solid #c3c4c7; - background: #f6f7f7; -} - -/* Modal */ -.roi-modal { - display: none; - position: fixed; - z-index: 100000; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0,0,0,0.5); -} - -.roi-modal-content { - background-color: #fff; - margin: 10% auto; - padding: 30px; - border: 1px solid #c3c4c7; - width: 80%; - max-width: 600px; - box-shadow: 0 5px 15px rgba(0,0,0,0.3); - border-radius: 4px; -} - -.roi-modal-close { - color: #646970; - float: right; - font-size: 28px; - font-weight: bold; - line-height: 20px; - cursor: pointer; -} - -.roi-modal-close:hover, -.roi-modal-close:focus { - color: #1d2327; -} - -.roi-modal-content h2 { - margin-top: 0; -} - -.roi-modal-content textarea { - font-family: 'Courier New', Courier, monospace; - font-size: 12px; -} - -/* Notices */ -.roi-notice { - padding: 12px; - margin: 20px 0; - border-left: 4px solid; - background: #fff; - box-shadow: 0 1px 1px rgba(0,0,0,.04); -} - -.roi-notice.success { - border-left-color: #00a32a; -} - -.roi-notice.error { - border-left-color: #d63638; -} - -.roi-notice.warning { - border-left-color: #dba617; -} - -.roi-notice.info { - border-left-color: #2271b1; -} - -/* Code Editor */ -textarea.code { - font-family: 'Courier New', Courier, monospace; - font-size: 13px; - line-height: 1.5; -} - -/* Responsive */ -@media screen and (max-width: 782px) { - .roi-options-container { - flex-direction: column; - } - - .roi-tabs-nav { - width: 100%; - border-right: none; - border-bottom: 1px solid #c3c4c7; - } - - .roi-tabs-nav ul { - display: flex; - flex-wrap: wrap; - } - - .roi-tabs-nav li { - flex: 1; - min-width: 50%; - border-right: 1px solid #c3c4c7; - border-bottom: none; - } - - .roi-tabs-nav li:first-child { - border-top: none; - } - - .roi-tabs-nav a { - text-align: center; - padding: 12px 10px; - font-size: 13px; - } - - .roi-tabs-nav a .dashicons { - display: block; - margin: 0 auto 5px; - } - - .roi-tabs-nav li.active a { - border-left: none; - border-bottom: 3px solid #2271b1; - padding-left: 10px; - } - - .roi-tabs-content { - padding: 20px; - } - - .roi-options-header { - flex-direction: column; - gap: 15px; - } - - .roi-options-actions { - width: 100%; - flex-direction: column; - } - - .roi-options-actions .button { - width: 100%; - text-align: center; - } - - .roi-tab-pane .form-table th { - width: auto; - padding: 15px 10px 5px 0; - display: block; - } - - .roi-tab-pane .form-table td { - display: block; - padding: 5px 10px 15px 0; - } -} - -/* Loading Spinner */ -.roi-spinner { - display: inline-block; - width: 20px; - height: 20px; - border: 3px solid rgba(0,0,0,.1); - border-radius: 50%; - border-top-color: #2271b1; - animation: roispin 1s ease-in-out infinite; -} - -@keyframes roispin { - to { transform: rotate(360deg); } -} - -/* Helper Classes */ -.roi-hidden { - display: none !important; -} - -.roi-text-center { - text-align: center; -} - -.roi-mt-20 { - margin-top: 20px; -} - -.roi-mb-20 { - margin-bottom: 20px; -} - -/* Color Picker */ -.wp-picker-container { - display: inline-block; -} - -/* Field Dependencies */ -.roi-field-dependency { - opacity: 0.5; - pointer-events: none; -} - -/* Success Animation */ -@keyframes roisaved { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } - 100% { - transform: scale(1); - } -} - -.roi-saved { - animation: roisaved 0.3s ease-in-out; -} diff --git a/admin/assets/js/admin-app.js b/admin/assets/js/admin-app.js deleted file mode 100644 index 71d23e5e..00000000 --- a/admin/assets/js/admin-app.js +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Admin Panel Application - * - * Gestión de configuraciones de componentes del tema - * - * @package ROI_Theme - * @since 2.0.0 - */ - -const AdminPanel = { - /** - * Estado de la aplicación - */ - STATE: { - settings: {}, - hasChanges: false, - isLoading: false - }, - - /** - * Inicializar aplicación - */ - init() { - this.bindEvents(); - this.loadSettings(); - }, - - /** - * Vincular eventos - */ - bindEvents() { - // Botón guardar - const saveBtn = document.getElementById('saveSettings'); - if (saveBtn) { - saveBtn.addEventListener('click', () => { - this.saveSettings(); - }); - } - - // Detectar cambios en formularios - const enableSaveButton = () => { - this.STATE.hasChanges = true; - const btn = document.getElementById('saveSettings'); - if (btn) btn.disabled = false; - }; - - document.querySelectorAll('input, select, textarea').forEach(input => { - // Evento 'input' se dispara mientras se escribe (tiempo real) - input.addEventListener('input', enableSaveButton); - // Evento 'change' se dispara cuando pierde foco (para select y checkboxes) - input.addEventListener('change', enableSaveButton); - }); - - // Tabs - const tabs = document.querySelectorAll('.nav-tabs .nav-link'); - tabs.forEach(tab => { - tab.addEventListener('click', (e) => { - e.preventDefault(); - this.switchTab(tab); - }); - }); - }, - - /** - * Cambiar tab - */ - switchTab(tab) { - // Remover active de todos - document.querySelectorAll('.nav-tabs .nav-link').forEach(t => { - t.classList.remove('active'); - }); - document.querySelectorAll('.tab-pane').forEach(pane => { - pane.classList.remove('show', 'active'); - }); - - // Activar seleccionado - tab.classList.add('active'); - const targetId = tab.getAttribute('data-bs-target').substring(1); - const targetPane = document.getElementById(targetId); - if (targetPane) { - targetPane.classList.add('show', 'active'); - } - }, - - /** - * Cargar configuraciones desde servidor - */ - async loadSettings() { - this.STATE.isLoading = true; - this.showSpinner(true); - - try { - const response = await axios({ - method: 'POST', - url: roiAdminData.ajaxUrl, - data: new URLSearchParams({ - action: 'roi_get_settings', - nonce: roiAdminData.nonce - }) - }); - - if (response.data.success) { - this.STATE.settings = response.data.data; - this.renderAllComponents(); - } else { - this.showNotice('Error al cargar configuraciones', 'error'); - } - } catch (error) { - console.error('Error loading settings:', error); - this.showNotice('Error de conexión', 'error'); - } finally { - this.STATE.isLoading = false; - this.showSpinner(false); - } - }, - - /** - * Guardar configuraciones al servidor - */ - async saveSettings() { - if (!this.STATE.hasChanges) { - this.showNotice('No hay cambios para guardar', 'info'); - return; - } - - this.showSpinner(true); - - try { - const formData = this.collectFormData(); - - // Crear FormData para WordPress AJAX - const postData = new URLSearchParams(); - postData.append('action', 'roi_save_settings'); - postData.append('nonce', roiAdminData.nonce); - - // Agregar components como JSON string - postData.append('components', JSON.stringify(formData.components)); - - const response = await axios({ - method: 'POST', - url: roiAdminData.ajaxUrl, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: postData - }); - - if (response.data.success) { - this.STATE.hasChanges = false; - const btn = document.getElementById('saveSettings'); - if (btn) btn.disabled = true; - this.showNotice('Configuración guardada correctamente', 'success'); - } else { - this.showNotice(response.data.data.message || 'Error al guardar', 'error'); - } - } catch (error) { - console.error('Error saving settings:', error); - this.showNotice('Error de conexión', 'error'); - } finally { - this.showSpinner(false); - } - }, - - /** - * Recolectar datos del formulario - * Cada componente agregará su sección aquí cuando se implemente - */ - collectFormData() { - return { - components: { - // Los componentes se agregarán aquí cuando se ejecute el algoritmo - } - }; - }, - - /** - * Renderizar todos los componentes - */ - renderAllComponents() { - const components = this.STATE.settings.components || {}; - - // Los métodos render de componentes se llamarán aquí cuando se implementen - }, - - /** - * Utilidad: Mostrar spinner - */ - showSpinner(show) { - const spinner = document.querySelector('.spinner'); - if (spinner) { - spinner.style.display = show ? 'inline-block' : 'none'; - } - }, - - /** - * Utilidad: Mostrar notificación - */ - showNotice(message, type = 'info') { - // WordPress admin notices - const noticeDiv = document.createElement('div'); - noticeDiv.className = `notice notice-${type} is-dismissible`; - noticeDiv.innerHTML = `

    ${message}

    `; - - const container = document.querySelector('.roi-admin-panel'); - if (container) { - container.insertBefore(noticeDiv, container.firstChild); - - // Auto-dismiss después de 5 segundos - setTimeout(() => { - noticeDiv.remove(); - }, 5000); - } - } -}; - -// Inicializar cuando el DOM esté listo -document.addEventListener('DOMContentLoaded', () => { - AdminPanel.init(); -}); diff --git a/admin/assets/js/theme-options.js b/admin/assets/js/theme-options.js deleted file mode 100644 index 21fa1ebb..00000000 --- a/admin/assets/js/theme-options.js +++ /dev/null @@ -1,440 +0,0 @@ -/** - * Theme Options Admin JavaScript - * - * @package ROI_Theme - * @since 1.0.0 - */ - -(function($) { - 'use strict'; - - var ROIThemeOptions = { - - /** - * Initialize - */ - init: function() { - this.tabs(); - this.imageUpload(); - this.resetOptions(); - this.exportOptions(); - this.importOptions(); - this.formValidation(); - this.conditionalFields(); - }, - - /** - * Tab Navigation - */ - tabs: function() { - // Tab click handler - $('.roi-tabs-nav a').on('click', function(e) { - e.preventDefault(); - - var tabId = $(this).attr('href'); - - // Update active states - $('.roi-tabs-nav li').removeClass('active'); - $(this).parent().addClass('active'); - - // Show/hide tab content - $('.roi-tab-pane').removeClass('active'); - $(tabId).addClass('active'); - - // Update URL hash without scrolling - if (history.pushState) { - history.pushState(null, null, tabId); - } else { - window.location.hash = tabId; - } - }); - - // Load tab from URL hash on page load - if (window.location.hash) { - var hash = window.location.hash; - if ($(hash).length) { - $('.roi-tabs-nav a[href="' + hash + '"]').trigger('click'); - } - } - - // Handle browser back/forward buttons - $(window).on('hashchange', function() { - if (window.location.hash) { - $('.roi-tabs-nav a[href="' + window.location.hash + '"]').trigger('click'); - } - }); - }, - - /** - * Image Upload - */ - imageUpload: function() { - var self = this; - var mediaUploader; - - // Upload button click - $(document).on('click', '.roi-upload-image', function(e) { - e.preventDefault(); - - var button = $(this); - var container = button.closest('.roi-image-upload'); - var preview = container.find('.roi-image-preview'); - var input = container.find('.roi-image-id'); - var removeBtn = container.find('.roi-remove-image'); - - // If the media uploader already exists, reopen it - if (mediaUploader) { - mediaUploader.open(); - return; - } - - // Create new media uploader - mediaUploader = wp.media({ - title: roiAdminOptions.strings.selectImage, - button: { - text: roiAdminOptions.strings.useImage - }, - multiple: false - }); - - // When an image is selected - mediaUploader.on('select', function() { - var attachment = mediaUploader.state().get('selection').first().toJSON(); - - // Set image ID - input.val(attachment.id); - - // Show preview - var imgUrl = attachment.sizes && attachment.sizes.medium ? - attachment.sizes.medium.url : attachment.url; - preview.html(''); - - // Show remove button - removeBtn.show(); - }); - - // Open the uploader - mediaUploader.open(); - }); - - // Remove button click - $(document).on('click', '.roi-remove-image', function(e) { - e.preventDefault(); - - var button = $(this); - var container = button.closest('.roi-image-upload'); - var preview = container.find('.roi-image-preview'); - var input = container.find('.roi-image-id'); - - // Clear values - input.val(''); - preview.empty(); - button.hide(); - }); - }, - - /** - * Reset Options - */ - resetOptions: function() { - $('#roi-reset-options').on('click', function(e) { - e.preventDefault(); - - if (!confirm(roiAdminOptions.strings.confirmReset)) { - return; - } - - var button = $(this); - button.prop('disabled', true).addClass('updating-message'); - - $.ajax({ - url: roiAdminOptions.ajaxUrl, - type: 'POST', - data: { - action: 'roi_reset_options', - nonce: roiAdminOptions.nonce - }, - success: function(response) { - if (response.success) { - // Show success message - ROIThemeOptions.showNotice('success', response.data.message); - - // Reload page after 1 second - setTimeout(function() { - window.location.reload(); - }, 1000); - } else { - ROIThemeOptions.showNotice('error', response.data.message); - button.prop('disabled', false).removeClass('updating-message'); - } - }, - error: function() { - ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error); - button.prop('disabled', false).removeClass('updating-message'); - } - }); - }); - }, - - /** - * Export Options - */ - exportOptions: function() { - $('#roi-export-options').on('click', function(e) { - e.preventDefault(); - - var button = $(this); - button.prop('disabled', true).addClass('updating-message'); - - $.ajax({ - url: roiAdminOptions.ajaxUrl, - type: 'POST', - data: { - action: 'roi_export_options', - nonce: roiAdminOptions.nonce - }, - success: function(response) { - if (response.success) { - // Create download link - var blob = new Blob([response.data.data], { type: 'application/json' }); - var url = window.URL.createObjectURL(blob); - var a = document.createElement('a'); - a.href = url; - a.download = response.data.filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - - ROIThemeOptions.showNotice('success', 'Options exported successfully!'); - } else { - ROIThemeOptions.showNotice('error', response.data.message); - } - button.prop('disabled', false).removeClass('updating-message'); - }, - error: function() { - ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error); - button.prop('disabled', false).removeClass('updating-message'); - } - }); - }); - }, - - /** - * Import Options - */ - importOptions: function() { - var modal = $('#roi-import-modal'); - var importData = $('#roi-import-data'); - - // Show modal - $('#roi-import-options').on('click', function(e) { - e.preventDefault(); - modal.show(); - }); - - // Close modal - $('.roi-modal-close, #roi-import-cancel').on('click', function() { - modal.hide(); - importData.val(''); - }); - - // Close modal on outside click - $(window).on('click', function(e) { - if ($(e.target).is(modal)) { - modal.hide(); - importData.val(''); - } - }); - - // Submit import - $('#roi-import-submit').on('click', function(e) { - e.preventDefault(); - - var data = importData.val().trim(); - - if (!data) { - alert('Please paste your import data.'); - return; - } - - var button = $(this); - button.prop('disabled', true).addClass('updating-message'); - - $.ajax({ - url: roiAdminOptions.ajaxUrl, - type: 'POST', - data: { - action: 'roi_import_options', - nonce: roiAdminOptions.nonce, - import_data: data - }, - success: function(response) { - if (response.success) { - ROIThemeOptions.showNotice('success', response.data.message); - modal.hide(); - importData.val(''); - - // Reload page after 1 second - setTimeout(function() { - window.location.reload(); - }, 1000); - } else { - ROIThemeOptions.showNotice('error', response.data.message); - button.prop('disabled', false).removeClass('updating-message'); - } - }, - error: function() { - ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error); - button.prop('disabled', false).removeClass('updating-message'); - } - }); - }); - }, - - /** - * Form Validation - */ - formValidation: function() { - $('.roi-options-form').on('submit', function(e) { - var valid = true; - var firstError = null; - - // Validate required fields - $(this).find('[required]').each(function() { - if (!$(this).val()) { - valid = false; - $(this).addClass('error'); - - if (!firstError) { - firstError = $(this); - } - } else { - $(this).removeClass('error'); - } - }); - - // Validate number fields - $(this).find('input[type="number"]').each(function() { - var val = $(this).val(); - var min = $(this).attr('min'); - var max = $(this).attr('max'); - - if (val && min && parseInt(val) < parseInt(min)) { - valid = false; - $(this).addClass('error'); - if (!firstError) { - firstError = $(this); - } - } - - if (val && max && parseInt(val) > parseInt(max)) { - valid = false; - $(this).addClass('error'); - if (!firstError) { - firstError = $(this); - } - } - }); - - // Validate URL fields - $(this).find('input[type="url"]').each(function() { - var val = $(this).val(); - if (val && !ROIThemeOptions.isValidUrl(val)) { - valid = false; - $(this).addClass('error'); - if (!firstError) { - firstError = $(this); - } - } - }); - - if (!valid) { - e.preventDefault(); - - if (firstError) { - // Scroll to first error - $('html, body').animate({ - scrollTop: firstError.offset().top - 100 - }, 500); - firstError.focus(); - } - - ROIThemeOptions.showNotice('error', 'Please fix the errors in the form.'); - return false; - } - - // Add saving animation - $(this).find('.submit .button-primary').addClass('updating-message'); - }); - - // Remove error class on input - $('.roi-options-form input, .roi-options-form select, .roi-options-form textarea').on('change input', function() { - $(this).removeClass('error'); - }); - }, - - /** - * Conditional Fields - */ - conditionalFields: function() { - // Enable/disable related posts options based on checkbox - $('#enable_related_posts').on('change', function() { - var checked = $(this).is(':checked'); - var fields = $('#related_posts_count, #related_posts_taxonomy, #related_posts_title, #related_posts_columns'); - - fields.closest('tr').toggleClass('roi-field-dependency', !checked); - fields.prop('disabled', !checked); - }).trigger('change'); - - // Enable/disable breadcrumb separator based on breadcrumbs checkbox - $('#enable_breadcrumbs').on('change', function() { - var checked = $(this).is(':checked'); - var field = $('#breadcrumb_separator'); - - field.closest('tr').toggleClass('roi-field-dependency', !checked); - field.prop('disabled', !checked); - }).trigger('change'); - }, - - /** - * Show Notice - */ - showNotice: function(type, message) { - var notice = $('

    ' + message + '

    '); - - $('.roi-theme-options h1').after(notice); - - // Auto-dismiss after 5 seconds - setTimeout(function() { - notice.fadeOut(function() { - $(this).remove(); - }); - }, 5000); - - // Scroll to top - $('html, body').animate({ scrollTop: 0 }, 300); - }, - - /** - * Validate URL - */ - isValidUrl: function(url) { - try { - new URL(url); - return true; - } catch (e) { - return false; - } - } - }; - - // Initialize on document ready - $(document).ready(function() { - ROIThemeOptions.init(); - }); - - // Make it globally accessible - window.ROIThemeOptions = ROIThemeOptions; - -})(jQuery); diff --git a/admin/includes/class-admin-menu.php b/admin/includes/class-admin-menu.php deleted file mode 100644 index 81bd7d65..00000000 --- a/admin/includes/class-admin-menu.php +++ /dev/null @@ -1,205 +0,0 @@ - admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('roi_admin_nonce'), - 'strings' => array( - 'selectImage' => __('Select Image', 'roi-theme'), - 'useImage' => __('Use Image', 'roi-theme'), - 'removeImage' => __('Remove Image', 'roi-theme'), - 'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'), - 'saved' => __('Settings saved successfully!', 'roi-theme'), - 'error' => __('An error occurred while saving settings.', 'roi-theme'), - ), - )); - - // No cargar Bootstrap ni otros assets del nuevo panel - return; - } - - // Bootstrap 5.3.2 CSS (solo para Componentes) - wp_enqueue_style( - 'bootstrap', - 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css', - array(), - '5.3.2' - ); - - // Bootstrap Icons - wp_enqueue_style( - 'bootstrap-icons', - 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css', - array(), - '1.11.1' - ); - - // Admin Panel CSS (Core) - wp_enqueue_style( - 'roi-admin-panel-css', - ROI_ADMIN_PANEL_URL . 'assets/css/admin-panel.css', - array('bootstrap'), - ROI_ADMIN_PANEL_VERSION - ); - - - // Bootstrap 5.3.2 JS - wp_enqueue_script( - 'bootstrap', - 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js', - array(), - '5.3.2', - true - ); - - // Axios (para AJAX) - wp_enqueue_script( - 'axios', - 'https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js', - array(), - '1.6.0', - true - ); - - - // Admin Panel JS (Core) - wp_enqueue_script( - 'roi-admin-panel-js', - ROI_ADMIN_PANEL_URL . 'assets/js/admin-app.js', - array('jquery', 'axios'), - ROI_ADMIN_PANEL_VERSION, - true - ); - - // Pasar datos a JavaScript - wp_localize_script('roi-admin-panel-js', 'roiAdminData', array( - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('roi_admin_nonce') - )); - } -} - -// Instanciar clase -new ROI_Admin_Menu(); diff --git a/admin/includes/class-db-manager.php b/admin/includes/class-db-manager.php deleted file mode 100644 index c6d4936d..00000000 --- a/admin/includes/class-db-manager.php +++ /dev/null @@ -1,318 +0,0 @@ -logDeprecation(__CLASS__, __FUNCTION__); - - // Hook para verificar/actualizar DB en cada carga - add_action('admin_init', array($this, 'maybe_create_tables')); - - // Inicializar adapter para mantener compatibilidad - $this->adapter = $this->getLegacyAdapter(); - } - - /** - * Obtener nombre completo de tabla con prefijo - * - * @deprecated 2.0.0 - * - * @param string $table_type Tipo de tabla: 'components' (personalizaciones) o 'defaults' (valores por defecto) - * @return string Nombre completo de la tabla con prefijo - */ - public function get_table_name($table_type = 'components') { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'Direct database access not recommended - use repositories' - ); - - global $wpdb; - - if ($table_type === 'defaults') { - return $wpdb->prefix . self::TABLE_DEFAULTS; - } - - return $wpdb->prefix . self::TABLE_COMPONENTS; - } - - /** - * Verificar si las tablas necesitan ser creadas o actualizadas - * - * @deprecated 2.0.0 Tables are now managed through database migrations - */ - public function maybe_create_tables() { - // Keep for backward compatibility but tables are managed differently now - $installed_version = get_option(self::DB_VERSION_OPTION); - - if ($installed_version !== self::DB_VERSION) { - $this->create_tables(); - update_option(self::DB_VERSION_OPTION, self::DB_VERSION); - } - } - - /** - * Crear tablas personalizadas - * - * @deprecated 2.0.0 Use DatabaseMigrator service instead - */ - public function create_tables() { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'ROITheme\Infrastructure\Services\DatabaseMigrator' - ); - - global $wpdb; - - $charset_collate = $wpdb->get_charset_collate(); - require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); - - // Table structure (kept for backward compatibility) - $table_structure = "( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - component_name VARCHAR(50) NOT NULL, - config_key VARCHAR(100) NOT NULL, - config_value TEXT NOT NULL, - data_type ENUM('string', 'boolean', 'integer', 'json') DEFAULT 'string', - version VARCHAR(10) DEFAULT NULL, - updated_at DATETIME NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY component_config (component_name, config_key), - INDEX idx_component (component_name), - INDEX idx_updated (updated_at) - ) $charset_collate;"; - - // Create components table - $table_components = $this->get_table_name('components'); - $sql_components = "CREATE TABLE IF NOT EXISTS $table_components $table_structure"; - dbDelta($sql_components); - - // Create defaults table - $table_defaults = $this->get_table_name('defaults'); - $sql_defaults = "CREATE TABLE IF NOT EXISTS $table_defaults $table_structure"; - dbDelta($sql_defaults); - } - - /** - * Verificar si una tabla existe - * - * @deprecated 2.0.0 - * - * @param string $table_type Tipo de tabla - * @return bool True si existe, false si no - */ - public function table_exists($table_type = 'components') { - global $wpdb; - $table_name = $this->get_table_name($table_type); - $query = $wpdb->prepare('SHOW TABLES LIKE %s', $table_name); - return $wpdb->get_var($query) === $table_name; - } - - /** - * Save component configuration - * - * @deprecated 2.0.0 Use SaveComponentUseCase::execute() instead - * - * @param string $component_name Component name (e.g., 'top_notification_bar', 'navbar', 'footer', 'hero_section') - * @param string $config_key Configuration key - * @param mixed $config_value Configuration value - * @param string $data_type Data type (string, boolean, integer, array) - * @param string|null $version Schema version - * @param string $table_type Table type (components or defaults) - * @return bool Success status - */ - public function save_config($component_name, $config_key, $config_value, $data_type = 'string', $version = null, $table_type = 'components') { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'ROITheme\Application\UseCases\SaveComponent\SaveComponentUseCase::execute()' - ); - - $this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args()); - - // Delegar al adapter para mantener funcionalidad - return $this->adapter->save_config( - $component_name, - $config_key, - $config_value, - $data_type, - $version, - $table_type - ); - } - - /** - * Get component configuration - * - * @deprecated 2.0.0 Use GetComponentUseCase::execute() instead - * - * @param string $component_name Component name - * @param string|null $config_key Specific configuration key (null for all) - * @param string $table_type Table type (components or defaults) - * @return mixed Configuration value(s) or null - */ - public function get_config($component_name, $config_key = null, $table_type = 'components') { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'ROITheme\Application\UseCases\GetComponent\GetComponentUseCase::execute()' - ); - - $this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args()); - - return $this->adapter->get_config($component_name, $config_key, $table_type); - } - - /** - * Delete component configuration - * - * @deprecated 2.0.0 Use DeleteComponentUseCase::execute() instead - * - * @param string $component_name Component name - * @param string|null $config_key Specific configuration key (null for all component) - * @param string $table_type Table type (components or defaults) - * @return bool Success status - */ - public function delete_config($component_name, $config_key = null, $table_type = 'components') { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'ROITheme\Application\UseCases\DeleteComponent\DeleteComponentUseCase::execute()' - ); - - $this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args()); - - // If deleting specific key, not supported in new architecture - // Delete entire component instead - return $this->adapter->delete_config($component_name, $table_type); - } - - /** - * List all components - * - * @deprecated 2.0.0 Use ComponentRepository::findAll() instead - * - * @param string $table_type Table type - * @return array List of components - */ - public function list_components($table_type = 'components') { - _deprecated_function( - __FUNCTION__, - '2.0.0', - 'ROITheme\Domain\Contracts\ComponentRepositoryInterface::findAll()' - ); - - $this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args()); - - global $wpdb; - $table = $this->get_table_name($table_type); - - $query = "SELECT DISTINCT component_name FROM $table ORDER BY component_name ASC"; - $components = $wpdb->get_col($query); - - return $components ?: array(); - } - - /** - * Get legacy adapter - * - * @return \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter - */ - private function getLegacyAdapter() { - if (!class_exists('ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter')) { - require_once get_template_directory() . '/vendor/autoload.php'; - } - - return new \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter(); - } - - /** - * Log deprecation usage - * - * @param string $class Class name - * @param string $method Method name - * @param array $args Arguments - */ - private function logDeprecation($class, $method, $args = array()) { - if (!class_exists('ROITheme\Infrastructure\Logging\DeprecationLogger')) { - require_once get_template_directory() . '/vendor/autoload.php'; - } - - $logger = \ROITheme\Infrastructure\Logging\DeprecationLogger::getInstance(); - $logger->log( - $class, - $method, - $args, - 'See documentation for Clean Architecture migration', - '2.0.0' - ); - } -} diff --git a/admin/includes/class-settings-manager.php b/admin/includes/class-settings-manager.php deleted file mode 100644 index 701d634e..00000000 --- a/admin/includes/class-settings-manager.php +++ /dev/null @@ -1,156 +0,0 @@ -get_defaults(); - - return wp_parse_args($settings, $defaults); - } - - /** - * Guardar configuraciones - */ - public function save_settings($data) { - // Validar - $validator = new ROI_Validator(); - $validation = $validator->validate($data); - - if (!$validation['valid']) { - return array( - 'success' => false, - 'message' => 'Error de validación', - 'errors' => $validation['errors'] - ); - } - - // Sanitizar - $sanitized = $this->sanitize_settings($data); - - // Agregar metadata - $sanitized['version'] = ROI_ADMIN_PANEL_VERSION; - $sanitized['updated_at'] = current_time('mysql'); - - // Guardar - update_option(self::OPTION_NAME, $sanitized, false); - - return array( - 'success' => true, - 'message' => 'Configuración guardada correctamente' - ); - } - - /** - * Valores por defecto - * Lee los defaults desde la tabla wp_roi_theme_components_defaults - */ - public function get_defaults() { - $db_manager = new ROI_DB_Manager(); - $component_names = $db_manager->list_components('defaults'); - - $defaults = array( - 'version' => ROI_ADMIN_PANEL_VERSION, - 'components' => array() - ); - - // Obtener configuraciones de cada componente desde la tabla de defaults - foreach ($component_names as $component_name) { - $defaults['components'][$component_name] = $db_manager->get_config($component_name, null, 'defaults'); - } - - return $defaults; - } - - /** - * Sanitizar configuraciones - * NOTA: Los sanitizers de componentes se ejecutarán aquí cuando se implementen - */ - public function sanitize_settings($data) { - $sanitized = array( - 'components' => array() - ); - - // Los componentes se sanitizarán aquí cuando se ejecute el algoritmo - - return $sanitized; - } - - /** - * AJAX: Obtener configuraciones - */ - public function ajax_get_settings() { - // Verificar nonce usando check_ajax_referer (método recomendado para AJAX) - check_ajax_referer('roi_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error('Permisos insuficientes'); - } - - $settings = $this->get_settings(); - wp_send_json_success($settings); - } - - /** - * AJAX: Guardar configuraciones - */ - public function ajax_save_settings() { - // Verificar nonce usando check_ajax_referer (método recomendado para AJAX) - check_ajax_referer('roi_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error('Permisos insuficientes'); - } - - // Los datos vienen como JSON string en $_POST['components'] - if (!isset($_POST['components'])) { - wp_send_json_error('Datos inválidos - falta components'); - } - - $components = json_decode(stripslashes($_POST['components']), true); - - if (!is_array($components)) { - wp_send_json_error('Datos inválidos - components no es un array válido'); - } - - $data = array( - 'components' => $components - ); - - $result = $this->save_settings($data); - - if ($result['success']) { - wp_send_json_success($result); - } else { - wp_send_json_error($result); - } - } -} - -// Instanciar clase -new ROI_Settings_Manager(); diff --git a/admin/includes/class-validator.php b/admin/includes/class-validator.php deleted file mode 100644 index ecfdae52..00000000 --- a/admin/includes/class-validator.php +++ /dev/null @@ -1,37 +0,0 @@ - false, 'errors' => $errors); - } - - // Los componentes se validarán aquí cuando se ejecute el algoritmo - - return array( - 'valid' => empty($errors), - 'errors' => $errors - ); - } -} diff --git a/admin/includes/sanitizers/class-sanitizer-helper.php b/admin/includes/sanitizers/class-sanitizer-helper.php deleted file mode 100644 index 4fdc9e06..00000000 --- a/admin/includes/sanitizers/class-sanitizer-helper.php +++ /dev/null @@ -1,271 +0,0 @@ - ['allowed' => [...], 'default' => ...]] - * @return array Array asociativo con valores enum sanitizados - */ - public static function sanitize_enums($data, $config) { - $result = array(); - foreach ($config as $key => $settings) { - $result[$key] = self::sanitize_enum( - $data, - $key, - $settings['allowed'], - $settings['default'] - ); - } - return $result; - } - - /** - * Sanitiza un valor entero con valor por defecto - * - * @param array $data Array de datos - * @param string $key Clave del dato - * @param int $default Valor por defecto - * @return int Entero sanitizado - */ - public static function sanitize_int($data, $key, $default = 0) { - return isset($data[$key]) ? intval($data[$key]) : $default; - } - - /** - * Sanitiza múltiples valores enteros - * - * @param array $data Array de datos - * @param array $config Array de configuración [key => default_value] - * @return array Array asociativo con enteros sanitizados - */ - public static function sanitize_ints($data, $config) { - $result = array(); - foreach ($config as $key => $default) { - $result[$key] = self::sanitize_int($data, $key, $default); - } - return $result; - } - - /** - * Sanitiza un valor float con valor por defecto - * - * @param array $data Array de datos - * @param string $key Clave del dato - * @param float $default Valor por defecto - * @return float Float sanitizado - */ - public static function sanitize_float($data, $key, $default = 0.0) { - return isset($data[$key]) ? floatval($data[$key]) : $default; - } - - /** - * Sanitiza múltiples valores float - * - * @param array $data Array de datos - * @param array $config Array de configuración [key => default_value] - * @return array Array asociativo con floats sanitizados - */ - public static function sanitize_floats($data, $config) { - $result = array(); - foreach ($config as $key => $default) { - $result[$key] = self::sanitize_float($data, $key, $default); - } - return $result; - } - - /** - * Sanitiza una URL con valor por defecto - * - * @param array $data Array de datos - * @param string $key Clave del dato - * @param string $default Valor por defecto (default: '') - * @return string URL sanitizada - */ - public static function sanitize_url($data, $key, $default = '') { - return esc_url_raw($data[$key] ?? $default); - } - - /** - * Sanitiza un array de strings - * - * @param array $data Array de datos - * @param string $key Clave del dato - * @param array $default Array por defecto - * @return array Array de strings sanitizados - */ - public static function sanitize_array_of_strings($data, $key, $default = array()) { - return isset($data[$key]) && is_array($data[$key]) - ? array_map('sanitize_text_field', $data[$key]) - : $default; - } - - /** - * Sanitiza un grupo de campos anidados (custom_styles, dropdown, etc.) - * - * @param array $data Array de datos completo - * @param string $group_key Clave del grupo (ej: 'custom_styles') - * @param array $sanitization_rules Reglas de sanitización por campo - * Formato: [ - * 'campo' => ['type' => 'text|color|int|float|enum|bool', 'default' => valor, 'allowed' => array()] - * ] - * @return array Array con campos del grupo sanitizados - */ - public static function sanitize_nested_group($data, $group_key, $sanitization_rules) { - $result = array(); - $group_data = $data[$group_key] ?? array(); - - foreach ($sanitization_rules as $field => $rule) { - $type = $rule['type']; - $default = $rule['default'] ?? null; - - switch ($type) { - case 'text': - $result[$field] = self::sanitize_text($group_data, $field, $default ?? ''); - break; - case 'color': - $result[$field] = self::sanitize_color($group_data, $field, $default ?? ''); - break; - case 'int': - $result[$field] = self::sanitize_int($group_data, $field, $default ?? 0); - break; - case 'float': - $result[$field] = self::sanitize_float($group_data, $field, $default ?? 0.0); - break; - case 'enum': - $result[$field] = self::sanitize_enum( - $group_data, - $field, - $rule['allowed'] ?? array(), - $default - ); - break; - case 'bool': - $result[$field] = self::sanitize_boolean($group_data, $field); - break; - default: - $result[$field] = $group_data[$field] ?? $default; - } - } - - return $result; - } -} diff --git a/admin/init.php b/admin/init.php deleted file mode 100644 index 56a36b46..00000000 --- a/admin/init.php +++ /dev/null @@ -1,31 +0,0 @@ - - -
    - - - - -
    - -
    - - -
    - - -
    -
    diff --git a/admin/theme-options/USAGE-EXAMPLES.php b/admin/theme-options/USAGE-EXAMPLES.php deleted file mode 100644 index 33afdbb5..00000000 --- a/admin/theme-options/USAGE-EXAMPLES.php +++ /dev/null @@ -1,394 +0,0 @@ - - - - - -

    - -

    - '; - echo 'Home'; - echo ' ' . esc_html($separator) . ' '; - - if (is_single()) { - the_category(' ' . esc_html($separator) . ' '); - echo ' ' . esc_html($separator) . ' '; - the_title(); - } elseif (is_category()) { - single_cat_title(); - } - - echo ''; - } -} - -/** - * EXAMPLE 3: Customizing excerpt - */ -function example_custom_excerpt_length($length) { - return roi_get_excerpt_length(); -} -add_filter('excerpt_length', 'example_custom_excerpt_length'); - -function example_custom_excerpt_more($more) { - return roi_get_excerpt_more(); -} -add_filter('excerpt_more', 'example_custom_excerpt_more'); - -/** - * EXAMPLE 4: Displaying related posts in single.php - */ -function example_display_related_posts() { - if (roi_show_related_posts() && is_single()) { - $count = roi_get_related_posts_count(); - $taxonomy = roi_get_related_posts_taxonomy(); - $title = roi_get_related_posts_title(); - - // Get related posts - $post_id = get_the_ID(); - $args = array( - 'posts_per_page' => $count, - 'post__not_in' => array($post_id), - ); - - if ($taxonomy === 'category') { - $categories = wp_get_post_categories($post_id); - if ($categories) { - $args['category__in'] = $categories; - } - } elseif ($taxonomy === 'tag') { - $tags = wp_get_post_tags($post_id, array('fields' => 'ids')); - if ($tags) { - $args['tag__in'] = $tags; - } - } - - $related = new WP_Query($args); - - if ($related->have_posts()) { - ?> - - -
    - -
    - -
    -
    - -
    -
    -

    -

    - - - -
    -
    - - - ' . wp_kses_post($copyright) . ''; - } -} - -/** - * EXAMPLE 10: Custom CSS in header - */ -function example_add_custom_css() { - $custom_css = roi_get_custom_css(); - - if ($custom_css) { - echo '\n"; - } -} -add_action('wp_head', 'example_add_custom_css', 100); - -/** - * EXAMPLE 11: Custom JS in header - */ -function example_add_custom_js_header() { - $custom_js = roi_get_custom_js_header(); - - if ($custom_js) { - echo '\n"; - } -} -add_action('wp_head', 'example_add_custom_js_header', 100); - -/** - * EXAMPLE 12: Custom JS in footer - */ -function example_add_custom_js_footer() { - $custom_js = roi_get_custom_js_footer(); - - if ($custom_js) { - echo '\n"; - } -} -add_action('wp_footer', 'example_add_custom_js_footer', 100); - -/** - * EXAMPLE 13: Posts per page for archives - */ -function example_set_archive_posts_per_page($query) { - if ($query->is_archive() && !is_admin() && $query->is_main_query()) { - $posts_per_page = roi_get_archive_posts_per_page(); - $query->set('posts_per_page', $posts_per_page); - } -} -add_action('pre_get_posts', 'example_set_archive_posts_per_page'); - -/** - * EXAMPLE 14: Performance optimizations - */ -function example_apply_performance_settings() { - // Remove emoji scripts - if (roi_is_performance_enabled('remove_emoji')) { - remove_action('wp_head', 'print_emoji_detection_script', 7); - remove_action('wp_print_styles', 'print_emoji_styles'); - } - - // Remove embeds - if (roi_is_performance_enabled('remove_embeds')) { - wp_deregister_script('wp-embed'); - } - - // Remove Dashicons for non-logged users - if (roi_is_performance_enabled('remove_dashicons') && !is_user_logged_in()) { - wp_deregister_style('dashicons'); - } -} -add_action('wp_enqueue_scripts', 'example_apply_performance_settings', 100); - -/** - * EXAMPLE 15: Lazy loading images - */ -function example_add_lazy_loading($attr, $attachment, $size) { - if (roi_is_lazy_loading_enabled()) { - $attr['loading'] = 'lazy'; - } - return $attr; -} -add_filter('wp_get_attachment_image_attributes', 'example_add_lazy_loading', 10, 3); - -/** - * EXAMPLE 16: Layout classes based on settings - */ -function example_get_layout_class() { - $layout = 'right-sidebar'; // default - - if (is_single()) { - $layout = roi_get_default_post_layout(); - } elseif (is_page()) { - $layout = roi_get_default_page_layout(); - } - - return 'layout-' . $layout; -} - -/** - * EXAMPLE 17: Display post meta conditionally - */ -function example_display_post_meta() { - if (!roi_get_option('show_post_meta', true)) { - return; - } - - ?> - - ', ', ', ''); - } -} - -/** - * EXAMPLE 19: Get all options (for debugging) - */ -function example_debug_all_options() { - if (current_user_can('manage_options') && isset($_GET['debug_options'])) { - $all_options = roi_get_all_options(); - echo '
    ';
    -        print_r($all_options);
    -        echo '
    '; - } -} -add_action('wp_footer', 'example_debug_all_options'); - -/** - * EXAMPLE 20: Check if specific feature is enabled - */ -function example_check_feature() { - // Multiple ways to check boolean options - - // Method 1: Using helper function - if (roi_is_option_enabled('enable_breadcrumbs')) { - // Breadcrumbs are enabled - } - - // Method 2: Using get_option with default - if (roi_get_option('enable_related_posts', true)) { - // Related posts are enabled - } - - // Method 3: Direct check - $options = roi_get_all_options(); - if (isset($options['enable_lazy_loading']) && $options['enable_lazy_loading']) { - // Lazy loading is enabled - } -} diff --git a/admin/theme-options/options-api.php b/admin/theme-options/options-api.php deleted file mode 100644 index 2bb1f27f..00000000 --- a/admin/theme-options/options-api.php +++ /dev/null @@ -1,237 +0,0 @@ - 'roi_sanitize_options', - 'default' => roi_get_default_options(), - ) - ); - - // General Settings Section - add_settings_section( - 'roi_general_section', - __('General Settings', 'roi-theme'), - 'roi_general_section_callback', - 'roitheme-options' - ); - - // Content Settings Section - add_settings_section( - 'roi_content_section', - __('Content Settings', 'roi-theme'), - 'roi_content_section_callback', - 'roitheme-options' - ); - - // Performance Settings Section - add_settings_section( - 'roi_performance_section', - __('Performance Settings', 'roi-theme'), - 'roi_performance_section_callback', - 'roitheme-options' - ); - - // Related Posts Settings Section - add_settings_section( - 'roi_related_posts_section', - __('Related Posts Settings', 'roi-theme'), - 'roi_related_posts_section_callback', - 'roitheme-options' - ); - - // Social Share Settings Section - add_settings_section( - 'roi_social_share_section', - __('Social Share Buttons', 'roi-theme'), - 'roi_social_share_section_callback', - 'roitheme-options' - ); -} -add_action('admin_init', 'roi_register_settings'); - -/** - * Get default options - * - * @return array - */ -function roi_get_default_options() { - return array( - // General - 'site_logo' => 0, - 'site_favicon' => 0, - 'enable_breadcrumbs' => true, - 'breadcrumb_separator' => '>', - 'date_format' => 'd/m/Y', - 'time_format' => 'H:i', - 'copyright_text' => sprintf(__('© %s %s. All rights reserved.', 'roi-theme'), date('Y'), get_bloginfo('name')), - 'social_facebook' => '', - 'social_twitter' => '', - 'social_instagram' => '', - 'social_linkedin' => '', - 'social_youtube' => '', - - // Content - 'excerpt_length' => 55, - 'excerpt_more' => '...', - 'default_post_layout' => 'right-sidebar', - 'default_page_layout' => 'right-sidebar', - 'archive_posts_per_page' => 10, - 'show_featured_image_single' => true, - 'show_author_box' => true, - 'enable_comments_posts' => true, - 'enable_comments_pages' => false, - 'show_post_meta' => true, - 'show_post_tags' => true, - 'show_post_categories' => true, - - // Performance - 'enable_lazy_loading' => true, - 'performance_remove_emoji' => true, - 'performance_remove_embeds' => false, - 'performance_remove_dashicons' => true, - 'performance_defer_js' => false, - 'performance_minify_html' => false, - 'performance_disable_gutenberg' => false, - - // Related Posts - 'enable_related_posts' => true, - 'related_posts_count' => 3, - 'related_posts_taxonomy' => 'category', - 'related_posts_title' => __('Related Posts', 'roi-theme'), - 'related_posts_columns' => 3, - - // Social Share Buttons - 'roi_enable_share_buttons' => '1', - 'roi_share_text' => __('Compartir:', 'roi-theme'), - - // Advanced - 'custom_css' => '', - 'custom_js_header' => '', - 'custom_js_footer' => '', - ); -} - -/** - * Section Callbacks - */ -function roi_general_section_callback() { - echo '

    ' . __('Configure general theme settings including logo, branding, and social media.', 'roi-theme') . '

    '; -} - -function roi_content_section_callback() { - echo '

    ' . __('Configure content display settings for posts, pages, and archives.', 'roi-theme') . '

    '; -} - -function roi_performance_section_callback() { - echo '

    ' . __('Optimize your site performance with these settings.', 'roi-theme') . '

    '; -} - -function roi_related_posts_section_callback() { - echo '

    ' . __('Configure related posts display on single post pages.', 'roi-theme') . '

    '; -} - -function roi_social_share_section_callback() { - echo '

    ' . __('Configure social share buttons display on single post pages.', 'roi-theme') . '

    '; -} - -/** - * Sanitize all options - * - * @param array $input The input array - * @return array The sanitized array - */ -function roi_sanitize_options($input) { - $sanitized = array(); - - if (!is_array($input)) { - return $sanitized; - } - - // General Settings - $sanitized['site_logo'] = isset($input['site_logo']) ? absint($input['site_logo']) : 0; - $sanitized['site_favicon'] = isset($input['site_favicon']) ? absint($input['site_favicon']) : 0; - $sanitized['enable_breadcrumbs'] = isset($input['enable_breadcrumbs']) ? (bool) $input['enable_breadcrumbs'] : false; - $sanitized['breadcrumb_separator'] = isset($input['breadcrumb_separator']) ? sanitize_text_field($input['breadcrumb_separator']) : '>'; - $sanitized['date_format'] = isset($input['date_format']) ? sanitize_text_field($input['date_format']) : 'd/m/Y'; - $sanitized['time_format'] = isset($input['time_format']) ? sanitize_text_field($input['time_format']) : 'H:i'; - $sanitized['copyright_text'] = isset($input['copyright_text']) ? wp_kses_post($input['copyright_text']) : ''; - - // Social Media - $social_fields = array('facebook', 'twitter', 'instagram', 'linkedin', 'youtube'); - foreach ($social_fields as $social) { - $key = 'social_' . $social; - $sanitized[$key] = isset($input[$key]) ? esc_url_raw($input[$key]) : ''; - } - - // Content Settings - $sanitized['excerpt_length'] = isset($input['excerpt_length']) ? absint($input['excerpt_length']) : 55; - $sanitized['excerpt_more'] = isset($input['excerpt_more']) ? sanitize_text_field($input['excerpt_more']) : '...'; - $sanitized['default_post_layout'] = isset($input['default_post_layout']) ? sanitize_text_field($input['default_post_layout']) : 'right-sidebar'; - $sanitized['default_page_layout'] = isset($input['default_page_layout']) ? sanitize_text_field($input['default_page_layout']) : 'right-sidebar'; - $sanitized['archive_posts_per_page'] = isset($input['archive_posts_per_page']) ? absint($input['archive_posts_per_page']) : 10; - $sanitized['show_featured_image_single'] = isset($input['show_featured_image_single']) ? (bool) $input['show_featured_image_single'] : false; - $sanitized['show_author_box'] = isset($input['show_author_box']) ? (bool) $input['show_author_box'] : false; - $sanitized['enable_comments_posts'] = isset($input['enable_comments_posts']) ? (bool) $input['enable_comments_posts'] : false; - $sanitized['enable_comments_pages'] = isset($input['enable_comments_pages']) ? (bool) $input['enable_comments_pages'] : false; - $sanitized['show_post_meta'] = isset($input['show_post_meta']) ? (bool) $input['show_post_meta'] : false; - $sanitized['show_post_tags'] = isset($input['show_post_tags']) ? (bool) $input['show_post_tags'] : false; - $sanitized['show_post_categories'] = isset($input['show_post_categories']) ? (bool) $input['show_post_categories'] : false; - - // Performance Settings - $sanitized['enable_lazy_loading'] = isset($input['enable_lazy_loading']) ? (bool) $input['enable_lazy_loading'] : false; - $sanitized['performance_remove_emoji'] = isset($input['performance_remove_emoji']) ? (bool) $input['performance_remove_emoji'] : false; - $sanitized['performance_remove_embeds'] = isset($input['performance_remove_embeds']) ? (bool) $input['performance_remove_embeds'] : false; - $sanitized['performance_remove_dashicons'] = isset($input['performance_remove_dashicons']) ? (bool) $input['performance_remove_dashicons'] : false; - $sanitized['performance_defer_js'] = isset($input['performance_defer_js']) ? (bool) $input['performance_defer_js'] : false; - $sanitized['performance_minify_html'] = isset($input['performance_minify_html']) ? (bool) $input['performance_minify_html'] : false; - $sanitized['performance_disable_gutenberg'] = isset($input['performance_disable_gutenberg']) ? (bool) $input['performance_disable_gutenberg'] : false; - - // Related Posts - $sanitized['enable_related_posts'] = isset($input['enable_related_posts']) ? (bool) $input['enable_related_posts'] : false; - $sanitized['related_posts_count'] = isset($input['related_posts_count']) ? absint($input['related_posts_count']) : 3; - $sanitized['related_posts_taxonomy'] = isset($input['related_posts_taxonomy']) ? sanitize_text_field($input['related_posts_taxonomy']) : 'category'; - $sanitized['related_posts_title'] = isset($input['related_posts_title']) ? sanitize_text_field($input['related_posts_title']) : __('Related Posts', 'roi-theme'); - $sanitized['related_posts_columns'] = isset($input['related_posts_columns']) ? absint($input['related_posts_columns']) : 3; - - // Social Share Buttons - $sanitized['roi_enable_share_buttons'] = isset($input['roi_enable_share_buttons']) ? sanitize_text_field($input['roi_enable_share_buttons']) : '1'; - $sanitized['roi_share_text'] = isset($input['roi_share_text']) ? sanitize_text_field($input['roi_share_text']) : __('Compartir:', 'roi-theme'); - - // Advanced Settings - $sanitized['custom_css'] = isset($input['custom_css']) ? roi_sanitize_css($input['custom_css']) : ''; - $sanitized['custom_js_header'] = isset($input['custom_js_header']) ? roi_sanitize_js($input['custom_js_header']) : ''; - $sanitized['custom_js_footer'] = isset($input['custom_js_footer']) ? roi_sanitize_js($input['custom_js_footer']) : ''; - - return $sanitized; -} - -/** - * NOTE: All sanitization functions have been moved to inc/sanitize-functions.php - * to avoid function redeclaration errors. This includes: - * - roi_sanitize_css() - * - roi_sanitize_js() - * - roi_sanitize_integer() - * - roi_sanitize_text() - * - roi_sanitize_url() - * - roi_sanitize_html() - * - roi_sanitize_checkbox() - * - roi_sanitize_select() - */ diff --git a/admin/theme-options/options-page-template.php b/admin/theme-options/options-page-template.php deleted file mode 100644 index 7938e440..00000000 --- a/admin/theme-options/options-page-template.php +++ /dev/null @@ -1,659 +0,0 @@ - - -
    -
    - -
    - - - -
    -
    - -
    - - -
    - -
    - -
    - - -
    - - -
    -

    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - -
    - -
    - 'roi-preview-image')); - } - ?> -
    - - -

    -
    -
    - - -
    - -
    - 'roi-preview-image')); - } - ?> -
    - - -

    -
    -
    - - - -

    -
    - - - -

    , /, »)', 'roi-theme'); ?>

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -
    - - - -
    - - - -
    - - - -
    - - - -
    -
    - - -
    -

    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    -
    - - -
    -

    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    -
    - - - - - -
    -

    -

    - - - - - - - - - - - - - - - - - - - -
    - - - -

    -
    - - - -

    -
    - - - -

    -
    -
    - -
    -
    - - -
    -
    - - - diff --git a/admin/theme-options/related-posts-options.php b/admin/theme-options/related-posts-options.php deleted file mode 100644 index fc8ab75e..00000000 --- a/admin/theme-options/related-posts-options.php +++ /dev/null @@ -1,272 +0,0 @@ - array( - 'key' => 'roi_related_posts_enabled', - 'value' => get_option('roi_related_posts_enabled', true), - 'type' => 'boolean', - 'default' => true, - 'label' => __('Enable Related Posts', 'roi-theme'), - 'description' => __('Show related posts section at the end of single posts', 'roi-theme'), - ), - 'title' => array( - 'key' => 'roi_related_posts_title', - 'value' => get_option('roi_related_posts_title', __('Related Posts', 'roi-theme')), - 'type' => 'text', - 'default' => __('Related Posts', 'roi-theme'), - 'label' => __('Section Title', 'roi-theme'), - 'description' => __('Title displayed above related posts', 'roi-theme'), - ), - 'count' => array( - 'key' => 'roi_related_posts_count', - 'value' => get_option('roi_related_posts_count', 3), - 'type' => 'number', - 'default' => 3, - 'min' => 1, - 'max' => 12, - 'label' => __('Number of Posts', 'roi-theme'), - 'description' => __('Maximum number of related posts to display', 'roi-theme'), - ), - 'columns' => array( - 'key' => 'roi_related_posts_columns', - 'value' => get_option('roi_related_posts_columns', 3), - 'type' => 'select', - 'default' => 3, - 'options' => array( - 1 => __('1 Column', 'roi-theme'), - 2 => __('2 Columns', 'roi-theme'), - 3 => __('3 Columns', 'roi-theme'), - 4 => __('4 Columns', 'roi-theme'), - ), - 'label' => __('Grid Columns', 'roi-theme'), - 'description' => __('Number of columns in the grid layout (responsive)', 'roi-theme'), - ), - 'show_excerpt' => array( - 'key' => 'roi_related_posts_show_excerpt', - 'value' => get_option('roi_related_posts_show_excerpt', true), - 'type' => 'boolean', - 'default' => true, - 'label' => __('Show Excerpt', 'roi-theme'), - 'description' => __('Display post excerpt in related posts cards', 'roi-theme'), - ), - 'excerpt_length' => array( - 'key' => 'roi_related_posts_excerpt_length', - 'value' => get_option('roi_related_posts_excerpt_length', 20), - 'type' => 'number', - 'default' => 20, - 'min' => 5, - 'max' => 100, - 'label' => __('Excerpt Length', 'roi-theme'), - 'description' => __('Number of words in the excerpt', 'roi-theme'), - ), - 'show_date' => array( - 'key' => 'roi_related_posts_show_date', - 'value' => get_option('roi_related_posts_show_date', true), - 'type' => 'boolean', - 'default' => true, - 'label' => __('Show Date', 'roi-theme'), - 'description' => __('Display publication date in related posts', 'roi-theme'), - ), - 'show_category' => array( - 'key' => 'roi_related_posts_show_category', - 'value' => get_option('roi_related_posts_show_category', true), - 'type' => 'boolean', - 'default' => true, - 'label' => __('Show Category', 'roi-theme'), - 'description' => __('Display category badge on related posts', 'roi-theme'), - ), - 'bg_colors' => array( - 'key' => 'roi_related_posts_bg_colors', - 'value' => get_option('roi_related_posts_bg_colors', array( - '#1a73e8', '#e91e63', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', - )), - 'type' => 'color_array', - 'default' => array( - '#1a73e8', // Blue - '#e91e63', // Pink - '#4caf50', // Green - '#ff9800', // Orange - '#9c27b0', // Purple - '#00bcd4', // Cyan - ), - 'label' => __('Background Colors', 'roi-theme'), - 'description' => __('Colors used for posts without featured images', 'roi-theme'), - ), - ); -} - -/** - * Update a related posts option - * - * @param string $option_key The option key (without 'roi_related_posts_' prefix) - * @param mixed $value The new value - * @return bool True if updated successfully - */ -function roi_update_related_posts_option($option_key, $value) { - $full_key = 'roi_related_posts_' . $option_key; - return update_option($full_key, $value); -} - -/** - * Reset related posts options to defaults - * - * @return bool True if reset successfully - */ -function roi_reset_related_posts_options() { - $options = roi_get_related_posts_options(); - $success = true; - - foreach ($options as $option) { - if (!update_option($option['key'], $option['default'])) { - $success = false; - } - } - - return $success; -} - -/** - * Example: Programmatically configure related posts - * - * This function shows how to configure related posts options programmatically. - * You can call this from your functions.php or a plugin. - * - * @return void - */ -function roi_example_configure_related_posts() { - // Example usage - uncomment to use: - - // Enable related posts - // update_option('roi_related_posts_enabled', true); - - // Set custom title - // update_option('roi_related_posts_title', __('You Might Also Like', 'roi-theme')); - - // Show 4 related posts - // update_option('roi_related_posts_count', 4); - - // Use 2 columns layout - // update_option('roi_related_posts_columns', 2); - - // Show excerpt with 30 words - // update_option('roi_related_posts_show_excerpt', true); - // update_option('roi_related_posts_excerpt_length', 30); - - // Show date and category - // update_option('roi_related_posts_show_date', true); - // update_option('roi_related_posts_show_category', true); - - // Custom background colors for posts without images - // update_option('roi_related_posts_bg_colors', array( - // '#FF6B6B', // Red - // '#4ECDC4', // Teal - // '#45B7D1', // Blue - // '#FFA07A', // Coral - // '#98D8C8', // Mint - // '#F7DC6F', // Yellow - // )); -} - -/** - * Filter hook example: Modify related posts query - * - * This example shows how to customize the related posts query. - * Add this to your functions.php or child theme. - */ -function roi_example_modify_related_posts_query($args, $post_id) { - // Example: Order by date instead of random - // $args['orderby'] = 'date'; - // $args['order'] = 'DESC'; - - // Example: Only show posts from the last 6 months - // $args['date_query'] = array( - // array( - // 'after' => '6 months ago', - // ), - // ); - - // Example: Exclude specific category - // $args['category__not_in'] = array(5); // Replace 5 with category ID - - return $args; -} -// add_filter('roi_related_posts_args', 'roi_example_modify_related_posts_query', 10, 2); - -/** - * Get documentation for related posts configuration - * - * @return array Documentation array - */ -function roi_get_related_posts_documentation() { - return array( - 'overview' => array( - 'title' => __('Related Posts Overview', 'roi-theme'), - 'content' => __( - 'The related posts feature automatically displays relevant posts at the end of each blog post. ' . - 'Posts are related based on shared categories and displayed in a responsive Bootstrap grid.', - 'roi-theme' - ), - ), - 'features' => array( - 'title' => __('Key Features', 'roi-theme'), - 'items' => array( - __('Automatic category-based matching', 'roi-theme'), - __('Responsive Bootstrap 5 grid layout', 'roi-theme'), - __('Configurable number of posts and columns', 'roi-theme'), - __('Support for posts with and without featured images', 'roi-theme'), - __('Beautiful color backgrounds for posts without images', 'roi-theme'), - __('Customizable excerpt length', 'roi-theme'), - __('Optional display of dates and categories', 'roi-theme'), - __('Smooth hover animations', 'roi-theme'), - __('Print-friendly styles', 'roi-theme'), - __('Dark mode support', 'roi-theme'), - ), - ), - 'configuration' => array( - 'title' => __('How to Configure', 'roi-theme'), - 'methods' => array( - 'database' => array( - 'title' => __('Via WordPress Options API', 'roi-theme'), - 'code' => "update_option('roi_related_posts_enabled', true);\nupdate_option('roi_related_posts_count', 4);", - ), - 'filter' => array( - 'title' => __('Via Filter Hook', 'roi-theme'), - 'code' => "add_filter('roi_related_posts_args', function(\$args, \$post_id) {\n \$args['posts_per_page'] = 6;\n return \$args;\n}, 10, 2);", - ), - ), - ), - 'customization' => array( - 'title' => __('Customization Examples', 'roi-theme'), - 'examples' => array( - array( - 'title' => __('Change title and layout', 'roi-theme'), - 'code' => "update_option('roi_related_posts_title', 'También te puede interesar');\nupdate_option('roi_related_posts_columns', 4);", - ), - array( - 'title' => __('Customize colors', 'roi-theme'), - 'code' => "update_option('roi_related_posts_bg_colors', array(\n '#FF6B6B',\n '#4ECDC4',\n '#45B7D1'\n));", - ), - ), - ), - ); -} diff --git a/admin/theme-options/theme-options.php b/admin/theme-options/theme-options.php deleted file mode 100644 index f23093b9..00000000 --- a/admin/theme-options/theme-options.php +++ /dev/null @@ -1,217 +0,0 @@ - admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('roi_admin_nonce'), - 'strings' => array( - 'selectImage' => __('Select Image', 'roi-theme'), - 'useImage' => __('Use Image', 'roi-theme'), - 'removeImage' => __('Remove Image', 'roi-theme'), - 'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'), - 'saved' => __('Settings saved successfully!', 'roi-theme'), - 'error' => __('An error occurred while saving settings.', 'roi-theme'), - ), - )); -} -add_action('admin_enqueue_scripts', 'roi_enqueue_admin_scripts'); - -/** - * Add settings link to theme actions - */ -function roi_add_settings_link($links) { - $settings_link = '' . __('Settings', 'roi-theme') . ''; - array_unshift($links, $settings_link); - return $links; -} -add_filter('theme_action_links_' . get_template(), 'roi_add_settings_link'); - -/** - * AJAX handler for resetting options - */ -function roi_reset_options_ajax() { - check_ajax_referer('roi_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme'))); - } - - // Delete options to reset to defaults - delete_option('roi_theme_options'); - - wp_send_json_success(array('message' => __('Options reset to defaults successfully.', 'roi-theme'))); -} -add_action('wp_ajax_roi_reset_options', 'roi_reset_options_ajax'); - -/** - * AJAX handler for exporting options - */ -function roi_export_options_ajax() { - check_ajax_referer('roi_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme'))); - } - - $options = get_option('roi_theme_options', array()); - - wp_send_json_success(array( - 'data' => json_encode($options, JSON_PRETTY_PRINT), - 'filename' => 'roi-theme-options-' . date('Y-m-d') . '.json' - )); -} -add_action('wp_ajax_roi_export_options', 'roi_export_options_ajax'); - -/** - * AJAX handler for importing options - */ -function roi_import_options_ajax() { - check_ajax_referer('roi_admin_nonce', 'nonce'); - - if (!current_user_can('manage_options')) { - wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme'))); - } - - if (!isset($_POST['import_data'])) { - wp_send_json_error(array('message' => __('No import data provided.', 'roi-theme'))); - } - - $import_data = json_decode(stripslashes($_POST['import_data']), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - wp_send_json_error(array('message' => __('Invalid JSON data.', 'roi-theme'))); - } - - // Sanitize imported data - $sanitized_data = roi_sanitize_options($import_data); - - // Update options - update_option('roi_theme_options', $sanitized_data); - - wp_send_json_success(array('message' => __('Options imported successfully.', 'roi-theme'))); -} -add_action('wp_ajax_roi_import_options', 'roi_import_options_ajax'); - -/** - * Add admin notices - */ -function roi_admin_notices() { - $screen = get_current_screen(); - if ($screen->id !== 'appearance_page_roi-theme-options') { - return; - } - - // Check if settings were updated - if (isset($_GET['settings-updated']) && $_GET['settings-updated'] === 'true') { - ?> -
    -

    -
    - add_panel('roi_theme_options', array( - 'title' => __('ROI Theme Options', 'roi-theme'), - 'description' => __('Configure theme options (Also available in Theme Options page)', 'roi-theme'), - 'priority' => 10, - )); - - // General Section - $wp_customize->add_section('roi_general', array( - 'title' => __('General Settings', 'roi-theme'), - 'panel' => 'roi_theme_options', - 'priority' => 10, - )); - - // Enable breadcrumbs - $wp_customize->add_setting('roi_theme_options[enable_breadcrumbs]', array( - 'default' => true, - 'type' => 'option', - 'sanitize_callback' => 'roi_sanitize_checkbox', - )); - - $wp_customize->add_control('roi_theme_options[enable_breadcrumbs]', array( - 'label' => __('Enable Breadcrumbs', 'roi-theme'), - 'section' => 'roi_general', - 'type' => 'checkbox', - )); -} -add_action('customize_register', 'roi_customize_register'); diff --git a/assets/css/componente-boton-lets-talk.css b/assets/css/componente-boton-lets-talk.css deleted file mode 100644 index d5d2d9dd..00000000 --- a/assets/css/componente-boton-lets-talk.css +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Buttons Styles - * - * RESPONSABILIDAD: Estilos de botones personalizados del tema - * - Botón Let's Talk (navbar) - * - Otros botones custom del tema - * - * REACTIVADO: Issue #121 - Arquitectura de separación de componentes - * El CSS NO debe estar en style.css sino en archivos individuales - * - * @package ROI_Theme - * @since 1.0.7 - */ - -/* ======================================== - Botón Let's Talk (Navbar) - ======================================== */ - -.btn-lets-talk { - background-color: var(--color-orange-primary) !important; - color: #ffffff !important; - font-weight: 600; - padding: 0.5rem 1.5rem; - border: none; - border-radius: 6px; - transition: all 0.3s ease; -} - -.btn-lets-talk:hover { - background-color: var(--color-orange-hover) !important; - color: #ffffff !important; -} - -.btn-lets-talk i { - color: #ffffff; -} diff --git a/assets/css/componente-cta-ab-testing.css b/assets/css/componente-cta-ab-testing.css deleted file mode 100644 index bd17c296..00000000 --- a/assets/css/componente-cta-ab-testing.css +++ /dev/null @@ -1,54 +0,0 @@ -/** - * CTA A/B Testing Styles - * - * CSS EXACTO copiado del template style.css (líneas 835-865) - * Sin extras, sin !important innecesario, sin media queries complicadas - * - * @package ROI_Theme - * @since 1.0.2 - */ - -.cta-section { - background: linear-gradient(135deg, var(--color-orange-primary) 0%, var(--color-orange-light) 100%); - box-shadow: 0 8px 24px rgba(255, 133, 0, 0.3); - border-radius: 12px; - padding: 2rem; -} - -.cta-section h3 { - color: #ffffff !important; -} - -.cta-section p { - color: rgba(255, 255, 255, 0.95) !important; -} - -.cta-button { - background-color: var(--color-orange-primary); - color: #ffffff; - font-weight: 600; - padding: 0.75rem 2rem; - border: none; - border-radius: 8px; - transition: all 0.3s ease; - text-decoration: none; - display: inline-block; -} - -.cta-button:hover { - background-color: var(--color-orange-hover); - color: #ffffff; - text-decoration: none; -} - -/* Responsive Mobile */ -@media (max-width: 768px) { - .cta-section { - padding: 1.5rem; - } - - .cta-button { - width: 100%; - margin-top: 1rem; - } -} diff --git a/assets/css/componente-cta-box-sidebar.css b/assets/css/componente-cta-box-sidebar.css deleted file mode 100644 index 2f0e72e3..00000000 --- a/assets/css/componente-cta-box-sidebar.css +++ /dev/null @@ -1,93 +0,0 @@ -/** - * CTA Box Sidebar Styles - * - * Styles for the CTA box component that appears in the sidebar - * below the Table of Contents on single posts. - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* ======================================== - CTA Box Container - ======================================== */ - -.cta-box-sidebar { - background: var(--color-orange-primary); - border-radius: 8px; - padding: 24px; - text-align: center; - margin-top: 0; - margin-bottom: 15px; - height: 250px; - display: flex; - flex-direction: column; - justify-content: center; - box-shadow: 0 4px 12px rgba(255, 133, 0, 0.2); -} - -/* ======================================== - CTA Box Content - ======================================== */ - -.cta-box-title { - color: #ffffff; - font-weight: 700; - font-size: 1.25rem; - margin-bottom: 1rem; -} - -.cta-box-text { - color: rgba(255, 255, 255, 0.95); - font-size: 0.9rem; - margin-bottom: 1rem; -} - -/* ======================================== - CTA Button - ======================================== */ - -.btn-cta-box { - background-color: #ffffff; - color: var(--color-orange-primary); - font-weight: 700; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 8px; - transition: all 0.3s ease; - font-size: 1rem; -} - -.btn-cta-box:hover { - background-color: var(--color-navy-primary); - color: #ffffff; -} - -/* ======================================== - Icon Spacing - ======================================== */ - -.btn-cta-box i { - vertical-align: middle; -} - -/* ======================================== - Responsive Design - ======================================== */ - -/* Hide on tablets and mobile */ -@media (max-width: 991px) { - .cta-box-sidebar { - display: none; /* Ocultar en móviles */ - } -} - -/* ======================================== - Print Styles - ======================================== */ - -@media print { - .cta-box-sidebar { - display: none; - } -} diff --git a/assets/css/componente-footer-contact-form.css b/assets/css/componente-footer-contact-form.css deleted file mode 100644 index 9c218161..00000000 --- a/assets/css/componente-footer-contact-form.css +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Footer Contact Form Styles - * - * Styles for the footer section including the contact form - * and contact information. - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* ======================================== - Contact Form Styles - ======================================== */ - -.form-control { - border: 2px solid var(--color-neutral-100); - border-radius: 6px; - padding: 0.625rem 1rem; - transition: all 0.3s ease; -} - -.form-control:focus { - border-color: var(--color-orange-primary); - outline: none; -} - -.btn-contact-submit { - background-color: var(--color-orange-primary); - color: #ffffff; - font-weight: 600; - padding: 0.75rem 2rem; - border: none; - border-radius: 6px; - transition: all 0.3s ease; -} - -.btn-contact-submit:hover { - background-color: var(--color-orange-hover); - color: #ffffff; -} - -.btn-submit-form { - background-color: var(--color-orange-primary); - color: #ffffff; - font-weight: 600; - padding: 0.75rem; - border: none; - border-radius: 6px; - transition: all 0.3s ease; -} - -.btn-submit-form:hover { - background-color: var(--color-orange-hover); - color: #ffffff; -} - -/* ======================================== - Contact Info Styles - ======================================== */ - -.contact-info i { - color: var(--color-orange-primary); -} - -/* ======================================== - Contact Section - Text Colors Override - ======================================== */ - -/* - * Fix Issue #128: Textos demasiado oscuros en sección de contacto - * Template usa #495057 (gris medio) en lugar de #212529 (gris oscuro) - * Aplicar solo a sección bg-secondary (sección de contacto arriba del footer) - */ - -section.bg-secondary h2, -section.bg-secondary h3, -section.bg-secondary p { - color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */ -} - -section.bg-secondary h6 { - color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */ -} - -/* NOTA: NO sobrescribir estilos Bootstrap h6 - Template usa defaults */ diff --git a/assets/css/componente-footer-principal.css b/assets/css/componente-footer-principal.css deleted file mode 100644 index 81fdbe40..00000000 --- a/assets/css/componente-footer-principal.css +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Footer Principal Styles - * - * RESPONSABILIDAD: Estilos del footer principal del sitio - * - Background navy dark - * - Títulos y enlaces - * - Botón de newsletter - * - Hover states - * - * @package ROI_Theme - * @since 1.0.19 - * @source roi-theme-template/css/style.css (líneas 987-1021) - * @reference CSS-ESPECIFICO.md - */ - -/* ======================================== - FOOTER - ======================================== */ - -footer { - background-color: var(--color-navy-dark); - color: rgba(255, 255, 255, 0.8); - padding: 3rem 0; -} - -footer h5 { - color: #ffffff; - font-weight: 600; - margin-bottom: 1rem; -} - -footer a { - color: rgba(255, 255, 255, 0.8); - text-decoration: none; - transition: color 0.3s ease; -} - -footer a:hover { - color: var(--color-orange-primary); -} - -footer .btn-primary { - background-color: var(--color-orange-primary); - border-color: var(--color-orange-primary); -} - -footer .btn-primary:hover { - background-color: var(--color-orange-hover); - border-color: var(--color-orange-hover); -} diff --git a/assets/css/componente-hero-section.css b/assets/css/componente-hero-section.css deleted file mode 100644 index 26239d9e..00000000 --- a/assets/css/componente-hero-section.css +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Hero Section Styles - * - * RESPONSABILIDAD: Estilos del componente Hero Section - * - Contenedor principal con gradiente navy - * - Título H1 del post - * - Category badges con efecto glassmorphism - * - * CORRECCIÓN Issue #121: Este archivo estaba vacío (solo comentarios) - * diciendo que los estilos estaban en style.css - * - * Ahora contiene el CSS correcto del template según: - * _planeacion/_desarrollo-tema-roi/theme-documentation/08-componente-hero-section/CSS-ESPECIFICO.md - * - * ELIMINADO: hero-section.css (duplicado con CSS incorrecto) - * - hero-section.css usaba clases .hero-section y .hero-category-badge - * - El HTML real usa .hero-title y .category-badge - * - Se consolidó todo en este archivo con las clases correctas - * - * @package ROI_Theme - * @since 1.0.9 - * @source roi-theme-template/css/style.css líneas 186-222 - */ - -/* ======================================== - Hero Section - ======================================== */ - -.hero-title { - background: linear-gradient(135deg, var(--color-navy-primary) 0%, var(--color-navy-light) 100%); - box-shadow: 0 4px 16px rgba(30, 58, 95, 0.25); - padding: 3rem 0; -} - -.hero-title h1 { - color: #ffffff !important; - font-weight: 700; - line-height: 1.4; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); - margin-bottom: 0; -} - -/* ======================================== - Category Badges (Glassmorphism Effect) - ======================================== */ - -.category-badge { - background: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.95); - padding: 0.375rem 0.875rem; - border-radius: 20px; - font-size: 0.813rem; - font-weight: 500; - text-decoration: none; - display: inline-block; - transition: all 0.3s ease; -} - -.category-badge:hover { - background: rgba(255, 133, 0, 0.2); - border-color: rgba(255, 133, 0, 0.4); - color: #ffffff; -} - -.category-badge i { - color: var(--color-orange-light); -} diff --git a/assets/css/componente-modal-contacto.css b/assets/css/componente-modal-contacto.css deleted file mode 100644 index dc2a48dc..00000000 --- a/assets/css/componente-modal-contacto.css +++ /dev/null @@ -1,419 +0,0 @@ -/** - * Modal de Contacto - Estilos - * - * Estilos para el modal de contacto con webhook - * Compatible con Bootstrap 5.3.2 - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* ========================================================================== - 1. ESTRUCTURA DEL MODAL - ========================================================================== */ - -.modal-content { - border-radius: 16px; - border: none; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - overflow: hidden; -} - -.modal-header { - padding: 1.5rem 1.5rem 1rem 1.5rem; - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); -} - -.modal-title { - font-size: 1.5rem; - color: #2c3e50; - font-weight: 700; -} - -.btn-close { - opacity: 0.6; - transition: opacity 0.3s ease; -} - -.btn-close:hover { - opacity: 1; -} - -.btn-close:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.25); - outline: none; -} - -.modal-body { - padding: 1rem 1.5rem 1.5rem 1.5rem; -} - -/* ========================================================================== - 2. FORMULARIO - ========================================================================== */ - -.form-label { - font-weight: 600; - color: #495057; - margin-bottom: 0.5rem; - font-size: 0.95rem; -} - -.form-label .text-danger { - font-weight: 700; - margin-left: 2px; -} - -.form-control { - border-radius: 8px; - border: 1px solid #dee2e6; - padding: 0.65rem 1rem; - transition: all 0.3s ease; - font-size: 0.95rem; -} - -.form-control:hover { - border-color: #adb5bd; -} - -.form-control:focus { - border-color: #FF8600; - box-shadow: 0 0 0 0.2rem rgba(255, 133, 0, 0.15); - outline: none; -} - -.form-control.is-invalid { - border-color: #dc3545; - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} - -.form-control.is-invalid:focus { - border-color: #dc3545; - box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); -} - -.form-control.is-valid { - border-color: #28a745; - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} - -.form-control.is-valid:focus { - border-color: #28a745; - box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); -} - -.invalid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 0.875em; - color: #dc3545; -} - -.form-control.is-invalid ~ .invalid-feedback { - display: block; -} - -textarea.form-control { - resize: vertical; - min-height: 80px; -} - -.form-text { - display: block; - margin-top: 0.25rem; - font-size: 0.875em; - color: #6c757d; -} - -/* ========================================================================== - 3. BOTÓN DE ENVÍO - ========================================================================== */ - -.btn-submit-form { - background: linear-gradient(135deg, #FF5722 0%, #FF6B35 100%); - color: #ffffff; - font-weight: 600; - padding: 0.75rem 1.5rem; - border: none; - border-radius: 8px; - transition: all 0.3s ease; - box-shadow: 0 4px 12px rgba(255, 87, 34, 0.3); - position: relative; - overflow: hidden; -} - -.btn-submit-form::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - transition: left 0.5s ease; -} - -.btn-submit-form:hover { - background: linear-gradient(135deg, #E64A19 0%, #FF5722 100%); - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(255, 87, 34, 0.4); -} - -.btn-submit-form:hover::before { - left: 100%; -} - -.btn-submit-form:active { - transform: translateY(0); - box-shadow: 0 2px 8px rgba(255, 87, 34, 0.3); -} - -.btn-submit-form:focus { - outline: none; - box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.5), 0 4px 12px rgba(255, 87, 34, 0.3); -} - -.btn-submit-form:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; - pointer-events: none; -} - -/* Spinner en botón */ -.spinner-border-sm { - width: 1rem; - height: 1rem; - border-width: 0.15em; -} - -/* ========================================================================== - 4. MENSAJES DE FEEDBACK - ========================================================================== */ - -#formMessage { - animation: slideDown 0.3s ease-out; - border-radius: 8px; - font-size: 0.9rem; -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.alert { - padding: 0.75rem 1rem; - margin-bottom: 0; - border: none; - border-radius: 8px; -} - -.alert-success { - background-color: #d4edda; - color: #155724; - border-left: 4px solid #28a745; -} - -.alert-danger { - background-color: #f8d7da; - color: #721c24; - border-left: 4px solid #dc3545; -} - -.alert-warning { - background-color: #fff3cd; - color: #856404; - border-left: 4px solid #ffc107; -} - -.alert-info { - background-color: #d1ecf1; - color: #0c5460; - border-left: 4px solid #17a2b8; -} - -/* ========================================================================== - 5. ANIMACIONES DEL MODAL - ========================================================================== */ - -.modal.fade .modal-dialog { - transition: transform 0.3s ease-out, opacity 0.3s ease-out; - transform: translate(0, -50px); -} - -.modal.show .modal-dialog { - transform: none; -} - -/* Backdrop personalizado */ -.modal-backdrop.show { - opacity: 0.6; -} - -/* ========================================================================== - 6. RESPONSIVE - ========================================================================== */ - -/* Tablets y dispositivos pequeños */ -@media (max-width: 768px) { - .modal-dialog { - margin: 1rem; - } - - .modal-header { - padding: 1rem; - } - - .modal-body { - padding: 0.75rem 1rem 1rem 1rem; - } - - .modal-title { - font-size: 1.25rem; - } - - .form-control { - font-size: 16px; /* Previene zoom en iOS */ - } -} - -/* Móviles pequeños */ -@media (max-width: 576px) { - .modal-dialog { - margin: 0.5rem; - max-width: calc(100% - 1rem); - } - - .modal-content { - border-radius: 12px; - } - - .modal-body { - padding: 0.5rem 0.75rem 0.75rem 0.75rem; - } - - .btn-submit-form { - padding: 0.65rem 1.25rem; - font-size: 0.95rem; - } -} - -/* ========================================================================== - 7. ACCESIBILIDAD - ========================================================================== */ - -/* Indicador de foco visible para navegación por teclado */ -.modal-content *:focus-visible { - outline: 2px solid #FF8600; - outline-offset: 2px; -} - -/* Mejora de contraste para lectores de pantalla */ -.screen-reader-text { - clip: rect(1px, 1px, 1px, 1px); - clip-path: inset(50%); - height: 1px; - width: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; -} - -/* High contrast mode support */ -@media (prefers-contrast: high) { - .form-control { - border-width: 2px; - } - - .btn-submit-form { - border: 2px solid #000; - } -} - -/* Reduced motion support */ -@media (prefers-reduced-motion: reduce) { - .modal.fade .modal-dialog, - .btn-submit-form, - .form-control, - .btn-close { - transition: none; - } - - .btn-submit-form::before { - display: none; - } - - #formMessage { - animation: none; - } -} - -/* ========================================================================== - 8. DARK MODE (OPCIONAL) - ========================================================================== */ - -@media (prefers-color-scheme: dark) { - .modal-content { - background-color: #2c3e50; - color: #ecf0f1; - } - - .modal-header { - background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%); - } - - .modal-title { - color: #ecf0f1; - } - - .form-label { - color: #bdc3c7; - } - - .form-control { - background-color: #34495e; - border-color: #4a5f7f; - color: #ecf0f1; - } - - .form-control:focus { - background-color: #34495e; - border-color: #FF8600; - } - - .form-text { - color: #95a5a6; - } - - .btn-close { - filter: invert(1); - } -} - -/* ========================================================================== - 9. PRINT STYLES - ========================================================================== */ - -@media print { - .modal, - .modal-backdrop { - display: none !important; - } -} diff --git a/assets/css/componente-navbar.css b/assets/css/componente-navbar.css deleted file mode 100644 index 195803e2..00000000 --- a/assets/css/componente-navbar.css +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Navbar Styles - * - * RESPONSABILIDAD: Estilos del componente de navegación principal - * - Navbar sticky - * - Navbar brand - * - Nav links y efectos hover - * - Dropdown menu - * - Dropdown items - * - * @package ROI_Theme - * @since 1.0.7 - * @source roi-theme-template/css/style.css - */ - -/* ======================================== - Navbar Principal - ======================================== */ - -.navbar { - position: sticky; - top: 0; - z-index: 1030; - background-color: var(--color-navy-primary) !important; - box-shadow: 0 4px 12px rgba(30, 58, 95, 0.15); - padding: 0.75rem 0; - transition: all 0.3s ease; -} - -.navbar.scrolled { - box-shadow: 0 6px 20px rgba(30, 58, 95, 0.25); -} - -/* ======================================== - Navbar Brand - ======================================== */ - -.navbar-brand { - color: #ffffff !important; - font-weight: 700; - font-size: 1.5rem; - transition: color 0.3s ease; -} - -.navbar-brand:hover { - color: var(--color-orange-primary) !important; -} - -/* ======================================== - Nav Links - ======================================== */ - -.nav-link { - color: rgba(255, 255, 255, 0.9) !important; - font-weight: 500; - position: relative; - padding: 0.5rem 0.65rem !important; - transition: all 0.3s ease; - font-size: 0.9rem; - white-space: nowrap; -} - -.nav-link::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%) scaleX(0); - width: 80%; - height: 2px; - background: var(--color-orange-primary); - transition: transform 0.3s ease; -} - -.nav-link:hover { - color: var(--color-orange-primary) !important; - background-color: rgba(255, 133, 0, 0.1); - border-radius: 4px; -} - -.nav-link:hover::after { - transform: translateX(-50%) scaleX(1); -} - -/* ======================================== - Dropdown Menu - ======================================== */ - -.dropdown-menu { - background: #ffffff; - border: none; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - border-radius: 8px; - padding: 0.5rem 0; - max-height: 70vh; - overflow-y: auto; - scroll-behavior: smooth; - scrollbar-width: thin; - scrollbar-color: var(--color-gray-400) transparent; -} - -/* Webkit browsers (Chrome, Safari, Edge) scrollbar */ -.dropdown-menu::-webkit-scrollbar { - width: 6px; -} - -.dropdown-menu::-webkit-scrollbar-track { - background: transparent; -} - -.dropdown-menu::-webkit-scrollbar-thumb { - background-color: var(--color-gray-400); - border-radius: 3px; -} - -.dropdown-menu::-webkit-scrollbar-thumb:hover { - background-color: var(--color-gray-500); -} - -/* ======================================== - Dropdown Items - ======================================== */ - -.dropdown-item { - color: var(--color-neutral-600); - padding: 0.5rem 1.25rem; - transition: all 0.3s ease; - font-weight: 500; -} - -.dropdown-item:hover { - background-color: rgba(255, 133, 0, 0.1); - color: var(--color-orange-primary); -} - -/* ======================================== - Dropdown Hover (Desktop Only) - ======================================== */ - -@media (min-width: 992px) { - .nav-item:hover > .dropdown-menu { - display: block; - } -} diff --git a/assets/css/componente-post-content.css b/assets/css/componente-post-content.css deleted file mode 100644 index 0910d668..00000000 --- a/assets/css/componente-post-content.css +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Post Content Component - * - * Estilos para el contenedor y contenido de posts - * Source: roi-theme-template/css/style.css líneas 245-298 - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* ============================================ - POST CONTENT CONTAINER - ============================================ */ - -.post-content { - background: #ffffff; - padding: 2rem; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); -} - -/* ============================================ - POST CONTENT TYPOGRAPHY - ============================================ */ - -.post-content h2 { - color: var(--color-navy-primary); - font-weight: 700; - margin-top: 2.5rem; - margin-bottom: 1.25rem; - padding-bottom: 0.75rem; - border-bottom: 3px solid var(--color-orange-primary); -} - -.post-content h3 { - color: var(--color-navy-light); - font-weight: 600; - margin-top: 2rem; - margin-bottom: 1rem; -} - -.post-content h4 { - font-size: 1.25rem; - font-weight: 600; - margin-top: 1.5rem; - margin-bottom: 0.75rem; - color: #495057; -} - -.post-content p { - color: var(--color-neutral-600); - line-height: 1.8; - margin-bottom: 1.25rem; -} - -.post-content ul, -.post-content ol { - margin-bottom: 1.5rem; - padding-left: 2rem; -} - -.post-content li { - margin-bottom: 0.5rem; - color: var(--color-neutral-600); -} - -.post-content strong { - color: var(--color-navy-primary); - font-weight: 600; -} - -.post-content a { - color: var(--color-orange-primary); - text-decoration: underline; - transition: color 0.3s ease; -} - -.post-content a:hover { - color: var(--color-orange-hover); -} - -.post-content blockquote { - border-left: 4px solid #0d6efd; - padding-left: 1.5rem; - margin: 2rem 0; - font-style: italic; - color: #6c757d; -} - -.post-content code { - background: #f8f9fa; - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-family: 'Courier New', monospace; - font-size: 0.95rem; - color: #e83e8c; -} - -.post-content pre { - background: #f8f9fa; - padding: 1rem; - border-radius: 8px; - overflow-x: auto; - margin: 1.5rem 0; -} - -.post-content pre code { - background: transparent; - padding: 0; - color: #212529; -} - -/* ============================================ - RESPONSIVE - ============================================ */ - -@media (max-width: 767.98px) { - .post-content { - padding: 1.5rem; - } - - .post-content h2 { - font-size: 1.5rem; - } - - .post-content h3 { - font-size: 1.25rem; - } - - .post-content p, - .post-content li { - font-size: 1rem; - } -} diff --git a/assets/css/componente-related-posts.css b/assets/css/componente-related-posts.css deleted file mode 100644 index 8641fc8a..00000000 --- a/assets/css/componente-related-posts.css +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Related Posts Component - * - * RESPONSABILIDAD: Estilos para las cards de posts relacionados - * - Cards con borde izquierdo navy de 4px - * - Hover effect con borde naranja - * - Cursor pointer - * - Integración con Bootstrap 5 cards - * - * @package ROI_Theme - * @since 1.0.17 - * @source roi-theme-template/css/style.css (líneas del template) - * @reference CSS-ESPECIFICO.md líneas 62-132 - */ - -/* ======================================== - Related Posts Section - ======================================== */ - -.related-posts { - margin: 3rem 0; -} - -.related-posts h2 { - color: var(--color-navy-primary); - font-weight: 700; - margin-bottom: 2rem; -} - -/* ======================================== - Related Posts Cards - ======================================== */ - -.related-posts .card { - cursor: pointer; - background: #ffffff !important; - border: 1px solid var(--color-neutral-100) !important; - border-left: 4px solid var(--color-neutral-600) !important; - transition: all 0.3s ease; - height: 100%; -} - -.related-posts .card:hover { - background: var(--color-neutral-50) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; - border-left-color: var(--color-orange-primary) !important; -} - -.related-posts .card-body { - padding: 1.5rem !important; -} - -.related-posts .card-title { - color: var(--color-navy-primary) !important; - font-weight: 600; - font-size: 0.95rem; - line-height: 1.4; -} - -.related-posts a { - text-decoration: none; -} - -.related-posts a:hover .card-title { - color: var(--color-orange-primary) !important; -} - -/* ======================================== - Pagination (si existe) - ======================================== */ - -.pagination .page-link { - color: var(--color-neutral-600); - border: 1px solid var(--color-neutral-100); - padding: 0.5rem 1rem; - margin: 0 0.25rem; - border-radius: 4px; - font-weight: 500; - transition: all 0.3s ease; -} - -.pagination .page-link:hover { - background-color: rgba(255, 133, 0, 0.1); - border-color: var(--color-orange-primary); - color: var(--color-orange-primary); -} - -.pagination .page-item.active .page-link { - background-color: var(--color-orange-primary); - border-color: var(--color-orange-primary); - color: #ffffff; -} diff --git a/assets/css/componente-share-buttons.css b/assets/css/componente-share-buttons.css deleted file mode 100644 index cecaa9ec..00000000 --- a/assets/css/componente-share-buttons.css +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Social Share Buttons - Estilos Mínimos - * - * Según CSS-ESPECIFICO.md de la documentación: - * Solo 2 reglas CSS simples. Bootstrap maneja el resto. - * - * Fuente: roi-theme-template/css/style.css líneas 795-806 - * - * @package ROI_Theme - * @since 1.0.0 - */ - -/* === SHARE BUTTONS === */ - -.share-buttons .btn { - transition: all 0.3s ease; - border-width: 2px; -} - -.share-buttons .btn:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} diff --git a/assets/css/componente-sidebar-toc.css b/assets/css/componente-sidebar-toc.css deleted file mode 100644 index 60229871..00000000 --- a/assets/css/componente-sidebar-toc.css +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Sidebar TOC (Table of Contents) Styles - * - * RESPONSABILIDAD: Estilos del componente TOC Sidebar - * - Contenedor sticky (.sidebar-sticky) - * - Contenedor TOC (.toc-container) - * - Título del TOC (.toc-container h4) - * - Lista de enlaces (.toc-list) - * - Items y enlaces del TOC - * - Scrollbar personalizado - * - * @package ROI_Theme - * @since 1.0.5 - * @source roi-theme-template/css/style.css líneas 663-746 - */ - -/* ======================================== - Contenedor Sticky del Sidebar - ======================================== */ - -.sidebar-sticky { - position: sticky; - top: 85px; - display: flex; - flex-direction: column; -} - -/* ======================================== - Contenedor del TOC - ======================================== */ - -.toc-container { - margin-bottom: 13px; - background: #ffffff; - border: 1px solid var(--color-neutral-100); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - padding: 12px 16px; - max-height: calc(100vh - 71px - 10px - 250px - 15px - 15px); - display: flex; - flex-direction: column; -} - -/* ======================================== - Título del TOC - ======================================== */ - -.toc-container h4 { - color: var(--color-navy-primary); - padding-bottom: 8px; - border-bottom: 2px solid var(--color-neutral-100); - margin-bottom: 0.75rem; - font-weight: 600; - text-align: left; - font-size: 1rem; - font-style: normal; -} - -/* ======================================== - Lista de Enlaces del TOC - ======================================== */ - -.toc-list { - overflow-y: auto; - padding-right: 0.5rem; - list-style: none; - flex: 1; - min-height: 0; -} - -.toc-container li { - margin-bottom: 0.15rem; -} - -/* ======================================== - Enlaces del TOC - ======================================== */ - -.toc-container a { - display: block; - padding: 0.3rem 0.85rem; - color: var(--color-neutral-600); - text-decoration: none; - border-left: 3px solid transparent; - transition: all 0.3s ease; - border-radius: 4px; - font-size: 0.9rem; - line-height: 1.3; -} - -.toc-container a:hover { - background: var(--color-neutral-50); - border-left-color: var(--color-navy-primary); - color: var(--color-navy-primary); -} - -.toc-container a.active { - background: var(--color-neutral-50); - border-left-color: var(--color-navy-primary); - color: var(--color-navy-primary); - font-weight: 600; -} - -/* ======================================== - Scrollbar Personalizado (Webkit) - ======================================== */ - -.toc-list::-webkit-scrollbar { - width: 6px; -} - -.toc-list::-webkit-scrollbar-track { - background: var(--color-neutral-50); - border-radius: 3px; -} - -.toc-list::-webkit-scrollbar-thumb { - background: var(--color-neutral-600); - border-radius: 3px; -} - -.toc-list::-webkit-scrollbar-thumb:hover { - background: var(--color-neutral-700); -} diff --git a/assets/css/componente-top-bar.css b/assets/css/componente-top-bar.css deleted file mode 100644 index 2e6667d3..00000000 --- a/assets/css/componente-top-bar.css +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Top Notification Bar Styles - * - * RESPONSABILIDAD: Estilos de la barra de notificación superior - * - Contenedor principal - * - Palabras destacadas (strong) - * - Iconos - * - Enlaces - * - * CORRECCIÓN Issue #121: Este archivo contenía CSS avanzado (position fixed, botón cerrar) - * que NO coincidía con el HTML simple del template en header.php - * - * Ahora contiene el CSS correcto del template según: - * _planeacion/_desarrollo-tema-roi/theme-documentation/05-componente-top-bar/CSS-ESPECIFICO.md - * - * @package ROI_Theme - * @since 1.0.8 - * @source roi-theme-template/css/style.css líneas 57-80 - */ - -/* ======================================== - Top Notification Bar - ======================================== */ - -.top-notification-bar { - background-color: var(--color-navy-dark); - color: #ffffff; - padding: 0.5rem 0; - font-size: 0.9rem; - text-align: center; -} - -.top-notification-bar strong { - color: var(--color-orange-primary); -} - -.top-notification-bar i { - color: var(--color-orange-primary); -} - -.top-notification-bar a { - color: #ffffff; - transition: color 0.3s ease; -} - -.top-notification-bar a:hover { - color: var(--color-orange-primary); -} diff --git a/assets/css/css-global-pagination.css b/assets/css/css-global-pagination.css index 9ee1788e..c73b37c6 100644 --- a/assets/css/css-global-pagination.css +++ b/assets/css/css-global-pagination.css @@ -37,9 +37,7 @@ color: var(--color-orange-primary); background-color: rgba(255, 133, 0, 0.1); border-color: var(--color-orange-primary); - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(255, 133, 0, 0.15); - z-index: 2; + text-decoration: none; } .page-link:focus { @@ -53,17 +51,8 @@ /* Active page */ .page-item.active .page-link { color: #ffffff; - background: var(--color-orange-primary); + background-color: var(--color-orange-primary); border-color: var(--color-orange-primary); - font-weight: 600; - box-shadow: 0 4px 12px rgba(255, 133, 0, 0.3); - z-index: 3; -} - -.page-item.active .page-link:hover { - background: var(--color-orange-light); - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(255, 133, 0, 0.4); } /* Disabled state */ diff --git a/footer.php b/footer.php index 4c92425a..c33a5b68 100644 --- a/footer.php +++ b/footer.php @@ -2,93 +2,19 @@ /** * Footer Template * - * Replica EXACTAMENTE la estructura del template (líneas 1093-1149) - * Footer con 3 columnas de navegación + newsletter simple (solo email). + * Renderiza el footer usando el componente dinámico desde BD. + * Los menús se gestionan desde Apariencia > Menús. + * La configuración se gestiona desde ROI Theme > Footer. * * @package ROI_Theme * @since 1.0.0 */ + +// Renderizar footer dinámico +echo roi_render_component('footer'); + +wp_footer(); ?> - -
    -
    -
    - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    - -
    - - -
    -
    -
    -

    -
    - - - -
    -
    -
    -
    - - -
    -

    © .

    - -
    -
    -
    - - - diff --git a/functions-addon.php b/functions-addon.php new file mode 100644 index 00000000..f1700126 --- /dev/null +++ b/functions-addon.php @@ -0,0 +1,232 @@ + get_template_directory() . '/Shared/', + 'ROITheme\\Public\\' => get_template_directory() . '/Public/', + 'ROITheme\\Admin\\' => get_template_directory() . '/Admin/', + ]; + + foreach ($prefixes as $prefix => $base_dir) { + $len = strlen($prefix); + if (strncmp($prefix, $class, $len) === 0) { + $relative_class = substr($class, $len); + $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; + + if (file_exists($file)) { + require $file; + return; + } + } + } +}); + +// ============================================================================= +// HELPER FUNCTION: roi_get_navbar_setting() +// ============================================================================= + +/** + * Obtiene un valor de configuración del navbar desde la BD + * + * @param string $group Nombre del grupo (ej: 'media', 'visibility') + * @param string $attribute Nombre del atributo (ej: 'show_brand', 'logo_url') + * @param mixed $default Valor por defecto si no existe + * @return mixed Valor del atributo + */ +function roi_get_navbar_setting(string $group, string $attribute, $default = null) { + global $wpdb; + + $table = $wpdb->prefix . 'roi_theme_component_settings'; + $value = $wpdb->get_var($wpdb->prepare( + "SELECT attribute_value FROM {$table} + WHERE component_name = 'navbar' + AND group_name = %s + AND attribute_name = %s", + $group, + $attribute + )); + + if ($value === null) { + return $default; + } + + // Convertir booleanos + if ($value === '1') return true; + if ($value === '0') return false; + + // Intentar decodificar JSON + $decoded = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + return $decoded; + } + + return $value; +} + +// ============================================================================= +// HELPER FUNCTION: roi_render_component() +// ============================================================================= + +/** + * Renderiza un componente por su nombre + * + * @param string $componentName Nombre del componente + * @return string HTML del componente renderizado + */ +function roi_render_component(string $componentName): string { + global $wpdb; + + // DEBUG: Trace component rendering + error_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}"); + + try { + // Obtener datos del componente desde BD normalizada + $table = $wpdb->prefix . 'roi_theme_component_settings'; + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT group_name, attribute_name, attribute_value + FROM {$table} + WHERE component_name = %s + ORDER BY group_name, attribute_name", + $componentName + )); + + if (empty($rows)) { + return ''; + } + + // Reconstruir estructura de datos agrupada + $data = []; + foreach ($rows as $row) { + if (!isset($data[$row->group_name])) { + $data[$row->group_name] = []; + } + // Decodificar valor + $value = $row->attribute_value; + + // Convertir booleanos almacenados como '1' o '0' + if ($value === '1' || $value === '0') { + $value = ($value === '1'); + } else { + // Intentar decodificar JSON + $decoded = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $value = $decoded; + } + } + + $data[$row->group_name][$row->attribute_name] = $value; + } + + // Crear Value Objects requeridos + $name = new \ROITheme\Shared\Domain\ValueObjects\ComponentName($componentName); + $configuration = new \ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration($data); + $visibility = \ROITheme\Shared\Domain\ValueObjects\ComponentVisibility::allDevices(); // Default: visible en todas partes + + // Crear instancia del componente + $component = new \ROITheme\Shared\Domain\Entities\Component( + $name, + $configuration, + $visibility + ); + + // Obtener renderer específico para el componente + $renderer = null; + + // Crear instancia del CSSGeneratorService (reutilizable para todos los renderers que lo necesiten) + $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); + + switch ($componentName) { + // Componentes nuevos (namespace PascalCase correcto) + case 'top-notification-bar': + $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator); + break; + case 'navbar': + $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator); + break; + case 'cta-lets-talk': + $renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator); + break; + case 'hero': + error_log("ROI Theme DEBUG: Creating HeroRenderer"); + $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator); + error_log("ROI Theme DEBUG: HeroRenderer created successfully"); + break; + // Componentes legacy (namespace minúsculas - pendiente migración) + case 'hero-section': + $renderer = new \ROITheme\Public\herosection\infrastructure\ui\HeroSectionRenderer(); + break; + case 'featured-image': + $renderer = new \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer($cssGenerator); + break; + case 'table-of-contents': + $renderer = new \ROITheme\Public\TableOfContents\Infrastructure\Ui\TableOfContentsRenderer($cssGenerator); + break; + case 'cta-box-sidebar': + $renderer = new \ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui\CtaBoxSidebarRenderer($cssGenerator); + break; + case 'social-share': + $renderer = new \ROITheme\Public\SocialShare\Infrastructure\Ui\SocialShareRenderer($cssGenerator); + break; + case 'cta-post': + $renderer = new \ROITheme\Public\CtaPost\Infrastructure\Ui\CtaPostRenderer($cssGenerator); + break; + case 'related-post': + $renderer = new \ROITheme\Public\RelatedPost\Infrastructure\Ui\RelatedPostRenderer($cssGenerator); + break; + case 'contact-form': + $renderer = new \ROITheme\Public\ContactForm\Infrastructure\Ui\ContactFormRenderer($cssGenerator); + break; + case 'footer': + $renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator); + break; + } + + if (!$renderer) { + error_log("ROI Theme DEBUG: No renderer for {$componentName}"); + return ''; + } + + error_log("ROI Theme DEBUG: Calling render() for {$componentName}"); + $output = $renderer->render($component); + error_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}"); + return $output; + + } catch (\Exception $e) { + // Always log errors for debugging + error_log('ROI Theme ERROR: Exception rendering component ' . $componentName . ': ' . $e->getMessage()); + error_log('ROI Theme ERROR: Stack trace: ' . $e->getTraceAsString()); + return ''; + } +} + +// ============================================================================= +// CARGAR ARCHIVOS DE INC/ +// ============================================================================= + +// CTA A/B Testing System +$cta_ab_testing = get_template_directory() . '/Inc/cta-ab-testing.php'; +if (file_exists($cta_ab_testing)) { + require_once $cta_ab_testing; +} + +// CTA Customizer Settings +$cta_customizer = get_template_directory() . '/Inc/customizer-cta.php'; +if (file_exists($cta_customizer)) { + require_once $cta_customizer; +} + +// ============================================================================= +// ESTILOS BASE PARA TOP NOTIFICATION BAR +// ============================================================================= + + +// ============================================================================= +// NOTA: Los estilos de TOC y CTA Box Sidebar se generan dinámicamente +// desde la base de datos a través de sus respectivos Renderers. +// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture. +// ============================================================================= diff --git a/functions.php b/functions.php index 279cfa0b..3b4a45a8 100644 --- a/functions.php +++ b/functions.php @@ -1,66 +1,240 @@ getAjaxController(); + +} catch (\Throwable $e) { + // Manejar errores de inicialización (no crítico, solo log) + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI Theme: Failed to initialize DI Container: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + } + // NO hacer return - permitir que el tema continúe funcionando + $container = null; } -// Initialize DI Container -use ROITheme\Infrastructure\DI\DIContainer; +// ============================================================================= +// 3.1. INICIALIZAR PANEL DE ADMINISTRACIÓN (Clean Architecture) +// ============================================================================= -/** - * Get DI Container instance - * - * Helper function to access the DI Container throughout the theme. - * - * @return DIContainer - */ -function roi_container(): DIContainer { - return DIContainer::getInstance(); +use ROITheme\Admin\Domain\ValueObjects\MenuItem; +use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase; +use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; +use ROITheme\Admin\Infrastructure\API\WordPress\AdminMenuRegistrar; +use ROITheme\Admin\Infrastructure\Services\AdminAssetEnqueuer; + +try { + // Obtener Use Case para cargar configuraciones + $getComponentSettingsUseCase = $container?->getGetComponentSettingsUseCase(); + + // Crear MenuItem con configuración del panel + $menuItem = new MenuItem( + pageTitle: 'ROI Theme - Panel de Administración', + menuTitle: 'ROI Theme', + capability: 'manage_options', + menuSlug: 'roi-theme-admin', + icon: 'dashicons-admin-settings', + position: 60 + ); + + // Crear renderer del dashboard con inyección del Use Case + $dashboardRenderer = new AdminDashboardRenderer($getComponentSettingsUseCase); + + // Crear caso de uso para renderizar + $renderDashboardUseCase = new RenderDashboardUseCase($dashboardRenderer); + + // Crear y registrar el menú de administración + $adminMenuRegistrar = new AdminMenuRegistrar($menuItem, $renderDashboardUseCase); + $adminMenuRegistrar->register(); + + // Crear y registrar el enqueuer de assets + $adminAssetEnqueuer = new AdminAssetEnqueuer(get_template_directory_uri()); + $adminAssetEnqueuer->register(); + + // Obtener Use Case para guardar configuraciones + $saveComponentSettingsUseCase = $container?->getSaveComponentSettingsUseCase(); + + // Crear y registrar el handler AJAX con inyección del Use Case + $adminAjaxHandler = new \ROITheme\Admin\Infrastructure\API\WordPress\AdminAjaxHandler($saveComponentSettingsUseCase); + $adminAjaxHandler->register(); + + // Crear y registrar el handler AJAX para el Contact Form (público) + $contactFormAjaxHandler = new \ROITheme\Public\ContactForm\Infrastructure\Api\WordPress\ContactFormAjaxHandler( + $container->getComponentSettingsRepository() + ); + $contactFormAjaxHandler->register(); + + // Crear y registrar el handler AJAX para Newsletter (público) + $newsletterAjaxHandler = new \ROITheme\Public\Footer\Infrastructure\Api\WordPress\NewsletterAjaxHandler( + $container->getComponentSettingsRepository() + ); + $newsletterAjaxHandler->register(); + + // Log en modo debug + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI Theme: Admin Panel initialized successfully'); + } + +} catch (\Throwable $e) { + // Manejar errores de inicialización del panel + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('ROI Theme: Failed to initialize Admin Panel: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + } } -/** - * ======================================================================== - * END BOOTSTRAP - * ======================================================================== - */ +// ============================================================================= +// 4. CONFIGURACIÓN DEL TEMA +// ============================================================================= /** - * ======================================================================== - * THEME DATABASE TABLES SETUP - * ======================================================================== + * Setup del tema * - * Crea las tablas del tema cuando se activa. - * Esto asegura que el tema sea portable y funcione en cualquier instalación WordPress. + * Configuraciones básicas de WordPress theme support */ +add_action('after_setup_theme', function() { + // Soporte para título del documento + add_theme_support('title-tag'); + + // Soporte para imágenes destacadas + add_theme_support('post-thumbnails'); + + // Soporte para HTML5 + add_theme_support('html5', [ + 'search-form', + 'comment-form', + 'comment-list', + 'gallery', + 'caption', + 'style', + 'script' + ]); + + // Soporte para feeds automáticos + add_theme_support('automatic-feed-links'); + + // Registro de ubicaciones de menús + register_nav_menus([ + 'primary' => __('Primary Menu', 'roi-theme'), + 'footer' => __('Footer Menu', 'roi-theme'), + 'footer_menu_1' => __('Footer Menu 1 (Widget 1)', 'roi-theme'), + 'footer_menu_2' => __('Footer Menu 2 (Widget 2)', 'roi-theme'), + 'footer_menu_3' => __('Footer Menu 3 (Widget 3)', 'roi-theme'), + ]); + + // TODO: Agregar más configuraciones según sea necesario +}); + +// ============================================================================= +// 5. HOOKS DE INICIALIZACIÓN (Para fases posteriores) +// ============================================================================= + +/** + * Hook para sincronización de schemas + * TODO: Implementar en Fase 6 + */ +// add_action('admin_init', function() use ($container) { +// $syncService = $container->getSchemaSyncService(); +// // Verificar si hay schemas desactualizados +// }); + +/** + * Hook para detección de schemas desactualizados + * TODO: Implementar en Fase 6 + */ +// add_action('admin_notices', function() use ($container) { +// // Mostrar aviso si hay schemas desactualizados +// }); + +// ============================================================================= +// 5. INFORMACIÓN DE DEBUG (Solo en desarrollo) +// ============================================================================= + +if (defined('WP_DEBUG') && WP_DEBUG) { + // Registrar que el tema se inicializó correctamente + error_log('ROI Theme: Bootstrap completed successfully'); +} + +// ============================================================================= +// 6. INSTALACIÓN DE TABLAS DEL TEMA +// ============================================================================= /** * Crear tablas del tema en la activación * * Este hook se ejecuta cuando el tema se activa en WordPress. * Crea las tablas necesarias si no existen. - * - * @since 1.0.19 */ add_action('after_switch_theme', function() { global $wpdb; @@ -68,39 +242,25 @@ add_action('after_switch_theme', function() { $charset_collate = $wpdb->get_charset_collate(); - // Tabla de components - $table_components = $wpdb->prefix . 'roi_theme_components'; - $sql_components = "CREATE TABLE {$table_components} ( + // Tabla de configuración de componentes (normalizada) + $table_settings = $wpdb->prefix . 'roi_theme_component_settings'; + $sql_settings = "CREATE TABLE {$table_settings} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - component_name VARCHAR(50) NOT NULL, - configuration LONGTEXT NOT NULL, - content LONGTEXT, - visibility TEXT NOT NULL, - is_enabled TINYINT(1) NOT NULL DEFAULT 1, - schema_version VARCHAR(20) NOT NULL, + component_name VARCHAR(100) NOT NULL, + group_name VARCHAR(100) NOT NULL, + attribute_name VARCHAR(100) NOT NULL, + attribute_value LONGTEXT NOT NULL, + is_editable TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), - UNIQUE KEY component_name (component_name), - INDEX idx_enabled (is_enabled), - INDEX idx_schema_version (schema_version) + UNIQUE KEY unique_setting (component_name, group_name, attribute_name), + INDEX idx_component (component_name), + INDEX idx_editable (is_editable) ) {$charset_collate};"; - // Tabla de defaults/schemas - $table_defaults = $wpdb->prefix . 'roi_theme_defaults'; - $sql_defaults = "CREATE TABLE {$table_defaults} ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - component_name VARCHAR(50) NOT NULL, - default_schema LONGTEXT NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY component_name (component_name) - ) {$charset_collate};"; - - // Crear/actualizar tablas - dbDelta($sql_components); - dbDelta($sql_defaults); + // Crear/actualizar tabla + dbDelta($sql_settings); // Log en modo debug if (defined('WP_DEBUG') && WP_DEBUG) { @@ -108,268 +268,11 @@ add_action('after_switch_theme', function() { } }); -/** - * Theme Version - */ -define('ROI_VERSION', '1.0.19'); - -/** - * Theme Setup - */ -function roi_theme_setup() { - // Make theme available for translation - load_theme_textdomain('roi-theme', get_template_directory() . '/languages'); - - // Let WordPress manage the document title - add_theme_support('title-tag'); - - // Enable support for Post Thumbnails - add_theme_support('post-thumbnails'); - - // Add image sizes - add_image_size('roi-thumbnail', 400, 300, true); - add_image_size('roi-medium', 800, 600, true); - add_image_size('roi-large', 1200, 900, true); - add_image_size('roi-featured-large', 1200, 600, true); - add_image_size('roi-featured-medium', 800, 400, true); - - // Switch default core markup to output valid HTML5 - add_theme_support('html5', array( - 'gallery', - 'caption', - 'style', - 'script', - )); - - // Set content width - if (!isset($content_width)) { - $content_width = 1200; - } - - // Register navigation menus - register_nav_menus(array( - 'primary' => __('Primary Menu', 'roi-theme'), - 'footer' => __('Footer Menu', 'roi-theme'), - )); -} -add_action('after_setup_theme', 'roi_theme_setup'); - -/** - * Set the content width in pixels - */ -function roi_content_width() { - $GLOBALS['content_width'] = apply_filters('roi_content_width', 1200); -} -add_action('after_setup_theme', 'roi_content_width', 0); - -/** - * ELIMINADO: roi_enqueue_scripts() - * - * Esta función estaba duplicando la carga de CSS. - * El sistema modular en inc/enqueue-scripts.php ya carga style.css como 'roi-main-style' (prioridad 5). - * Esta función duplicada lo cargaba otra vez como 'roi-theme-style' (prioridad 10). - * - * Fecha eliminación: 2025-01-08 - * Issue: #128 - Footer Contact Form - */ - -/** - * Register Widget Areas - */ -function roi_register_widget_areas() { - // Primary Sidebar - register_sidebar(array( - 'name' => __('Primary Sidebar', 'roi-theme'), - 'id' => 'sidebar-1', - 'description' => __('Main sidebar widget area', 'roi-theme'), - 'before_widget' => '
    ', - 'after_widget' => '
    ', - 'before_title' => '

    ', - 'after_title' => '

    ', - )); - - // Footer Contact Form (Issue #37) - ARRIBA de los 4 widgets - register_sidebar(array( - 'name' => __('Footer Contact Form', 'roi-theme'), - 'id' => 'footer-contact', - 'description' => __('Área de contacto arriba de los 4 widgets del footer', 'roi-theme'), - 'before_widget' => '', - 'before_title' => '

    ', - 'after_title' => '

    ', - )); - - // Footer Widget Areas - for ($i = 1; $i <= 4; $i++) { - register_sidebar(array( - 'name' => sprintf(__('Footer Column %d', 'roi-theme'), $i), - 'id' => 'footer-' . $i, - 'description' => sprintf(__('Footer widget area %d', 'roi-theme'), $i), - 'before_widget' => '
    ', - 'after_widget' => '
    ', - 'before_title' => '

    ', - 'after_title' => '

    ', - )); - } -} -add_action('widgets_init', 'roi_register_widget_areas'); - -/** - * Configure locale and date format - */ -function roi_configure_locale() { - // Set locale to es_MX - add_filter('locale', function($locale) { - return 'es_MX'; - }); -} -add_action('after_setup_theme', 'roi_configure_locale'); - -/** - * Custom date format - */ -function roi_custom_date_format($format) { - return 'd/m/Y'; // Format: day/month/year -} -add_filter('date_format', 'roi_custom_date_format'); - -/** - * Include modular files - */ -// Sanitize Functions (load first to avoid redeclaration errors) -if (file_exists(get_template_directory() . '/inc/sanitize-functions.php')) { - require_once get_template_directory() . '/inc/sanitize-functions.php'; -} - -// Theme Options Helpers (load first as other files may depend on it) -if (file_exists(get_template_directory() . '/inc/theme-options-helpers.php')) { - require_once get_template_directory() . '/inc/theme-options-helpers.php'; -} - -// Admin Options API (Theme Options) -// Cargar solo options-api.php para funciones auxiliares como roi_get_default_options() -// theme-options.php está desactivado porque el menú se registra en admin/includes/class-admin-menu.php -if (is_admin()) { - if (file_exists(get_template_directory() . '/admin/theme-options/options-api.php')) { - require_once get_template_directory() . '/admin/theme-options/options-api.php'; - } -} - -// Bootstrap Nav Walker -if (file_exists(get_template_directory() . '/inc/nav-walker.php')) { - require_once get_template_directory() . '/inc/nav-walker.php'; -} - -// Bootstrap and Script Enqueuing -if (file_exists(get_template_directory() . '/inc/enqueue-scripts.php')) { - require_once get_template_directory() . '/inc/enqueue-scripts.php'; -} - -// Font customizer options -if (file_exists(get_template_directory() . '/inc/customizer-fonts.php')) { - require_once get_template_directory() . '/inc/customizer-fonts.php'; -} - -// SEO optimizations and Rank Math compatibility -if (file_exists(get_template_directory() . '/inc/seo.php')) { - require_once get_template_directory() . '/inc/seo.php'; -} - -// Performance optimizations -if (file_exists(get_template_directory() . '/inc/performance.php')) { - require_once get_template_directory() . '/inc/performance.php'; -} - -// Critical CSS (optional, disabled by default) -if (file_exists(get_template_directory() . '/inc/critical-css.php')) { - require_once get_template_directory() . '/inc/critical-css.php'; -} - -// Image optimization -if (file_exists(get_template_directory() . '/inc/image-optimization.php')) { - require_once get_template_directory() . '/inc/image-optimization.php'; -} - -// Template functions -if (file_exists(get_template_directory() . '/inc/template-functions.php')) { - require_once get_template_directory() . '/inc/template-functions.php'; -} - -// Template tags -if (file_exists(get_template_directory() . '/inc/template-tags.php')) { - require_once get_template_directory() . '/inc/template-tags.php'; -} - -// Featured image functions -if (file_exists(get_template_directory() . '/inc/featured-image.php')) { - require_once get_template_directory() . '/inc/featured-image.php'; -} - -// Category badge functions -if (file_exists(get_template_directory() . '/inc/category-badge.php')) { - require_once get_template_directory() . '/inc/category-badge.php'; -} - -// AdSense delay loading -if (file_exists(get_template_directory() . '/inc/adsense-delay.php')) { - require_once get_template_directory() . '/inc/adsense-delay.php'; -} - -// Related posts functionality -if (file_exists(get_template_directory() . '/inc/related-posts.php')) { - require_once get_template_directory() . '/inc/related-posts.php'; -} - -// Related posts configuration options (admin helpers) -if (file_exists(get_template_directory() . '/admin/theme-options/related-posts-options.php')) { - require_once get_template_directory() . '/admin/theme-options/related-posts-options.php'; -} - -// Table of Contents -if (file_exists(get_template_directory() . '/inc/toc.php')) { - require_once get_template_directory() . '/inc/toc.php'; -} - -// APU Tables - Funciones para tablas de Análisis de Precios Unitarios (Issue #30) -if (file_exists(get_template_directory() . '/inc/apu-tables.php')) { - require_once get_template_directory() . '/inc/apu-tables.php'; -} - -// Desactivar búsqueda nativa (Issue #3) -if (file_exists(get_template_directory() . '/inc/search-disable.php')) { - require_once get_template_directory() . '/inc/search-disable.php'; -} - -// Desactivar comentarios (Issue #4) -if (file_exists(get_template_directory() . '/inc/comments-disable.php')) { - require_once get_template_directory() . '/inc/comments-disable.php'; -} - -// Social share buttons (Issue #31) -if (file_exists(get_template_directory() . '/inc/social-share.php')) { - require_once get_template_directory() . '/inc/social-share.php'; -} - -// CTA A/B Testing system (Issue #32) -if (file_exists(get_template_directory() . '/inc/cta-ab-testing.php')) { - require_once get_template_directory() . '/inc/cta-ab-testing.php'; -} - -// CTA Customizer options (Issue #32) -if (file_exists(get_template_directory() . '/inc/customizer-cta.php')) { - require_once get_template_directory() . '/inc/customizer-cta.php'; -} - -// Admin Panel Module (Phase 1-2: Base Structure) -if (file_exists(get_template_directory() . '/admin/init.php')) { - require_once get_template_directory() . '/admin/init.php'; -} - - // ============================================================================= // REGISTRO DE COMANDOS WP-CLI // ============================================================================= if (defined('WP_CLI') && WP_CLI) { - require_once get_template_directory() . '/src/Infrastructure/API/WordPress/MigrationCommand.php'; + require_once get_template_directory() . '/Shared/Infrastructure/Api/WordPress/MigrationCommand.php'; } + diff --git a/header.php b/header.php index 722c8666..6d264c9f 100644 --- a/header.php +++ b/header.php @@ -20,13 +20,44 @@ data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100"> - +
-HTML; - } - - private function buildVisibilityTab(array $data, string $componentId): string - { - $html = '
'; - $isEnabled = $data['visibility']['is_enabled'] ?? true; - $html .= $this->buildToggle('is_enabled', 'Mostrar hero section', $isEnabled, $componentId, 'visibility'); - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - $html .= $this->buildSelect('show_on_pages', 'Mostrar en', $showOn, ['all' => 'Todas las páginas', 'home' => 'Solo página de inicio', 'posts' => 'Solo posts individuales', 'pages' => 'Solo páginas', 'custom' => 'Tipos de post específicos'], $componentId, 'visibility'); - $customPostTypes = $data['visibility']['custom_post_types'] ?? ''; - $html .= $this->buildTextField('custom_post_types', 'Tipos de post personalizados', $customPostTypes, $componentId, 'visibility', 'Ej: post,page,producto', ['data-conditional-field' => 'show_on_pages', 'data-conditional-value' => 'custom']); - $html .= '
'; - return $html; - } - - private function buildCategoriesTab(array $data, string $componentId): string - { - $html = '
'; - $showCategories = $data['categories']['show_categories'] ?? true; - $html .= $this->buildToggle('show_categories', 'Mostrar badges de categorías', $showCategories, $componentId, 'categories'); - $categoriesSource = $data['categories']['categories_source'] ?? 'post_categories'; - $html .= $this->buildSelect('categories_source', 'Fuente de categorías', $categoriesSource, ['post_categories' => 'Categorías del post', 'post_tags' => 'Etiquetas del post', 'custom_taxonomy' => 'Taxonomía personalizada', 'custom_list' => 'Lista personalizada'], $componentId, 'categories', ['data-conditional-field' => 'show_categories', 'data-conditional-value' => 'true']); - $customTaxonomy = $data['categories']['custom_taxonomy_name'] ?? ''; - $html .= $this->buildTextField('custom_taxonomy_name', 'Nombre de taxonomía personalizada', $customTaxonomy, $componentId, 'categories', 'Ej: project_category', ['data-conditional-field' => 'categories_source', 'data-conditional-value' => 'custom_taxonomy']); - $customList = $data['categories']['custom_categories_list'] ?? ''; - $html .= $this->buildTextArea('custom_categories_list', 'Lista personalizada de categorías', $customList, $componentId, 'categories', 'Análisis de Precios|#', 5, ['data-conditional-field' => 'categories_source', 'data-conditional-value' => 'custom_list']); - $maxCategories = $data['categories']['max_categories'] ?? 5; - $html .= $this->buildNumberField('max_categories', 'Máximo de categorías a mostrar', $maxCategories, $componentId, 'categories', 1, 20, ['data-conditional-field' => 'show_categories', 'data-conditional-value' => 'true']); - $categoryIcon = $data['categories']['category_icon'] ?? 'bi-folder-fill'; - $html .= $this->buildTextField('category_icon', 'Ícono de categoría', $categoryIcon, $componentId, 'categories', 'Ej: bi-folder-fill', ['data-conditional-field' => 'show_categories', 'data-conditional-value' => 'true']); - $categoriesAlignment = $data['categories']['categories_alignment'] ?? 'center'; - $html .= $this->buildSelect('categories_alignment', 'Alineación de categorías', $categoriesAlignment, ['left' => 'Izquierda', 'center' => 'Centro', 'right' => 'Derecha'], $componentId, 'categories', ['data-conditional-field' => 'show_categories', 'data-conditional-value' => 'true']); - $html .= '
'; - return $html; - } - - private function buildTitleTab(array $data, string $componentId): string - { - $html = '
'; - $titleSource = $data['title']['title_source'] ?? 'post_title'; - $html .= $this->buildSelect('title_source', 'Fuente del título', $titleSource, ['post_title' => 'Título del post', 'custom_field' => 'Campo personalizado', 'custom_text' => 'Texto personalizado'], $componentId, 'title'); - $customField = $data['title']['custom_field_name'] ?? ''; - $html .= $this->buildTextField('custom_field_name', 'Nombre del campo personalizado', $customField, $componentId, 'title', 'Ej: hero_title', ['data-conditional-field' => 'title_source', 'data-conditional-value' => 'custom_field']); - $customText = $data['title']['custom_text'] ?? ''; - $html .= $this->buildTextArea('custom_text', 'Texto personalizado', $customText, $componentId, 'title', '', 3, ['data-conditional-field' => 'title_source', 'data-conditional-value' => 'custom_text']); - $titleTag = $data['title']['title_tag'] ?? 'h1'; - $html .= $this->buildSelect('title_tag', 'Etiqueta HTML del título', $titleTag, ['h1' => 'H1', 'h2' => 'H2', 'h3' => 'H3', 'div' => 'DIV'], $componentId, 'title'); - $titleClasses = $data['title']['title_classes'] ?? 'display-5 fw-bold'; - $html .= $this->buildTextField('title_classes', 'Clases CSS adicionales', $titleClasses, $componentId, 'title', 'Ej: display-5 fw-bold'); - $titleAlignment = $data['title']['title_alignment'] ?? 'center'; - $html .= $this->buildSelect('title_alignment', 'Alineación del título', $titleAlignment, ['left' => 'Izquierda', 'center' => 'Centro', 'right' => 'Derecha'], $componentId, 'title'); - $enableGradient = $data['title']['enable_gradient'] ?? false; - $html .= $this->buildToggle('enable_gradient', 'Activar gradiente en el texto', $enableGradient, $componentId, 'title'); - $gradientStart = $data['title']['gradient_color_start'] ?? '#1e3a5f'; - $html .= $this->buildColorField('gradient_color_start', 'Color inicial del gradiente', $gradientStart, $componentId, 'title', ['data-conditional-field' => 'enable_gradient', 'data-conditional-value' => 'true']); - $gradientEnd = $data['title']['gradient_color_end'] ?? '#FF8600'; - $html .= $this->buildColorField('gradient_color_end', 'Color final del gradiente', $gradientEnd, $componentId, 'title', ['data-conditional-field' => 'enable_gradient', 'data-conditional-value' => 'true']); - $gradientDirection = $data['title']['gradient_direction'] ?? 'to-right'; - $html .= $this->buildSelect('gradient_direction', 'Dirección del gradiente', $gradientDirection, ['to-right' => 'Izquierda a derecha', 'to-left' => 'Derecha a izquierda', 'to-bottom' => 'Arriba a abajo', 'to-top' => 'Abajo a arriba', 'diagonal' => 'Diagonal'], $componentId, 'title', ['data-conditional-field' => 'enable_gradient', 'data-conditional-value' => 'true']); - $html .= '
'; - return $html; - } - - private function buildStylesTab(array $data, string $componentId): string - { - $html = '
'; - $backgroundType = $data['styles']['background_type'] ?? 'gradient'; - $html .= $this->buildSelect('background_type', 'Tipo de fondo', $backgroundType, ['color' => 'Color sólido', 'gradient' => 'Gradiente', 'image' => 'Imagen', 'none' => 'Sin fondo'], $componentId, 'styles'); - $bgColor = $data['styles']['background_color'] ?? '#1e3a5f'; - $html .= $this->buildColorField('background_color', 'Color de fondo', $bgColor, $componentId, 'styles', ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'color']); - $gradientStart = $data['styles']['gradient_start_color'] ?? '#1e3a5f'; - $html .= $this->buildColorField('gradient_start_color', 'Color inicial del gradiente', $gradientStart, $componentId, 'styles', ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'gradient']); - $gradientEnd = $data['styles']['gradient_end_color'] ?? '#2c5282'; - $html .= $this->buildColorField('gradient_end_color', 'Color final del gradiente', $gradientEnd, $componentId, 'styles', ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'gradient']); - $gradientAngle = $data['styles']['gradient_angle'] ?? 135; - $html .= $this->buildNumberField('gradient_angle', 'Ángulo del gradiente (grados)', $gradientAngle, $componentId, 'styles', 0, 360, ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'gradient']); - $bgImage = $data['styles']['background_image_url'] ?? ''; - $html .= $this->buildMediaField('background_image_url', 'Imagen de fondo', $bgImage, $componentId, 'styles', ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'image']); - $bgOverlay = $data['styles']['background_overlay'] ?? true; - $html .= $this->buildToggle('background_overlay', 'Overlay oscuro sobre imagen', $bgOverlay, $componentId, 'styles', ['data-conditional-field' => 'background_type', 'data-conditional-value' => 'image']); - $overlayOpacity = $data['styles']['overlay_opacity'] ?? 60; - $html .= $this->buildNumberField('overlay_opacity', 'Opacidad del overlay (%)', $overlayOpacity, $componentId, 'styles', 0, 100, ['data-conditional-field' => 'background_overlay', 'data-conditional-value' => 'true']); - $textColor = $data['styles']['text_color'] ?? '#FFFFFF'; - $html .= $this->buildColorField('text_color', 'Color del texto', $textColor, $componentId, 'styles'); - $padding = $data['styles']['padding_vertical'] ?? 'normal'; - $html .= $this->buildSelect('padding_vertical', 'Padding vertical', $padding, ['compact' => 'Compacto (2rem)', 'normal' => 'Normal (3rem)', 'spacious' => 'Espacioso (4rem)', 'extra-spacious' => 'Extra espacioso (5rem)'], $componentId, 'styles'); - $margin = $data['styles']['margin_bottom'] ?? 'normal'; - $html .= $this->buildSelect('margin_bottom', 'Margen inferior', $margin, ['none' => 'Sin margen', 'small' => 'Pequeño (1rem)', 'normal' => 'Normal (1.5rem)', 'large' => 'Grande (2rem)'], $componentId, 'styles'); - $badgeBg = $data['styles']['category_badge_background'] ?? 'rgba(255, 255, 255, 0.2)'; - $html .= $this->buildTextField('category_badge_background', 'Fondo de badges', $badgeBg, $componentId, 'styles'); - $badgeText = $data['styles']['category_badge_text_color'] ?? '#FFFFFF'; - $html .= $this->buildColorField('category_badge_text_color', 'Color del texto de badges', $badgeText, $componentId, 'styles'); - $badgeBlur = $data['styles']['category_badge_blur'] ?? true; - $html .= $this->buildToggle('category_badge_blur', 'Efecto blur en badges', $badgeBlur, $componentId, 'styles'); - $html .= '
'; - return $html; - } - - private function buildToggle(string $name, string $label, bool $value, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $checked = $value ? 'checked' : ''; - $attrString = $this->buildAttributesString($attrs); - return sprintf('
', esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), $checked, $attrString, esc_attr($fieldId), esc_html($label)); - } - - private function buildTextField(string $name, string $label, string $value, string $componentId, string $group, string $placeholder = '', array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - return sprintf('
', esc_attr($fieldId), esc_html($label), esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), esc_attr($value), esc_attr($placeholder), $attrString); - } - - private function buildTextArea(string $name, string $label, string $value, string $componentId, string $group, string $placeholder = '', int $rows = 3, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - return sprintf('
', esc_attr($fieldId), esc_html($label), esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), $rows, esc_attr($placeholder), $attrString, esc_textarea($value)); - } - - private function buildSelect(string $name, string $label, string $value, array $options, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - $html = sprintf('
', esc_attr($fieldId), esc_html($label)); - $html .= sprintf('
'; - return $html; - } - - private function buildNumberField(string $name, string $label, $value, string $componentId, string $group, int $min = null, int $max = null, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrs['type'] = 'number'; - if ($min !== null) $attrs['min'] = $min; - if ($max !== null) $attrs['max'] = $max; - $attrString = $this->buildAttributesString($attrs); - return sprintf('
', esc_attr($fieldId), esc_html($label), esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), esc_attr($value), $attrString); - } - - private function buildColorField(string $name, string $label, string $value, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - return sprintf('
', esc_attr($fieldId), esc_html($label), esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), esc_attr($value), esc_attr($value)); - } - - private function buildMediaField(string $name, string $label, string $value, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - $html = sprintf('
', esc_attr($fieldId), esc_html($label)); - $html .= '
'; - $html .= sprintf('', esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), esc_attr($value), $attrString); - $html .= sprintf('', esc_attr($fieldId)); - $html .= '
'; - if (!empty($value)) { - $html .= sprintf('
Preview
', esc_url($value)); - } - $html .= '
'; - return $html; - } - - private function buildAttributesString(array $attrs): string - { - $attrString = ''; - foreach ($attrs as $key => $value) { - $attrString .= sprintf(' %s="%s"', esc_attr($key), esc_attr($value)); - } - return $attrString; - } - - private function buildFormScripts(string $componentId): string - { - return << -SCRIPT; - } - - public function getComponentId(): string - { - return $this->componentId; - } -} diff --git a/src/Navbar/Infrastructure/Presentation/Admin/NavbarFormBuilder.php b/src/Navbar/Infrastructure/Presentation/Admin/NavbarFormBuilder.php deleted file mode 100644 index d482f48f..00000000 --- a/src/Navbar/Infrastructure/Presentation/Admin/NavbarFormBuilder.php +++ /dev/null @@ -1,665 +0,0 @@ -componentId = $componentId; - $this->data = $data; - } - - public function build(): string - { - $data = $this->data; - $componentId = $this->componentId; - - $html = '
'; - - // Tabs navigation - $html .= $this->buildTabsNavigation(); - - // Tab content - $html .= '
'; - - // Visibility tab - $html .= $this->buildVisibilityTab($data, $componentId); - - // Logo tab - $html .= $this->buildLogoTab($data, $componentId); - - // Menu tab - $html .= $this->buildMenuTab($data, $componentId); - - // CTA Button tab - $html .= $this->buildCtaButtonTab($data, $componentId); - - // Styles tab - $html .= $this->buildStylesTab($data, $componentId); - - $html .= '
'; - $html .= '
'; - - $html .= $this->buildFormScripts($componentId); - - return $html; - } - - private function buildTabsNavigation(): string - { - return << - - - - - - -HTML; - } - - private function buildVisibilityTab(array $data, string $componentId): string - { - $html = '
'; - $html .= '
'; - - $isEnabled = $data['visibility']['is_enabled'] ?? true; - $html .= $this->buildToggle('is_enabled', 'Mostrar navbar', $isEnabled, $componentId, 'visibility'); - - $isSticky = $data['visibility']['is_sticky'] ?? true; - $html .= $this->buildToggle('is_sticky', 'Navbar fijo (sticky)', $isSticky, $componentId, 'visibility'); - - $hideOnScroll = $data['visibility']['hide_on_scroll'] ?? false; - $html .= $this->buildToggle('hide_on_scroll', 'Ocultar al hacer scroll hacia abajo', $hideOnScroll, $componentId, 'visibility'); - - $showOnMobile = $data['visibility']['show_on_mobile'] ?? true; - $html .= $this->buildToggle('show_on_mobile', 'Mostrar en dispositivos móviles', $showOnMobile, $componentId, 'visibility'); - - $html .= '
'; - $html .= '
'; - - return $html; - } - - private function buildLogoTab(array $data, string $componentId): string - { - $html = '
'; - $html .= '
'; - - $logoType = $data['logo']['logo_type'] ?? 'image'; - $html .= $this->buildSelect( - 'logo_type', - 'Tipo de logo', - $logoType, - ['image' => 'Imagen', 'text' => 'Texto', 'none' => 'Sin logo'], - $componentId, - 'logo' - ); - - $logoImageUrl = $data['logo']['logo_image_url'] ?? ''; - $html .= $this->buildMediaField( - 'logo_image_url', - 'Imagen del logo', - $logoImageUrl, - $componentId, - 'logo', - ['data-conditional-field' => 'logo_type', 'data-conditional-value' => 'image'] - ); - - $logoWidth = $data['logo']['logo_image_width'] ?? 150; - $html .= $this->buildNumberField( - 'logo_image_width', - 'Ancho del logo (px)', - $logoWidth, - $componentId, - 'logo', - 50, - 400, - ['data-conditional-field' => 'logo_type', 'data-conditional-value' => 'image'] - ); - - $logoText = $data['logo']['logo_text'] ?? ''; - $html .= $this->buildTextField( - 'logo_text', - 'Texto del logo', - $logoText, - $componentId, - 'logo', - '', - ['data-conditional-field' => 'logo_type', 'data-conditional-value' => 'text'] - ); - - $logoLink = $data['logo']['logo_link'] ?? ''; - $html .= $this->buildUrlField( - 'logo_link', - 'Enlace del logo', - $logoLink, - $componentId, - 'logo', - 'Dejar vacío para usar la URL del home' - ); - - $logoPosition = $data['logo']['logo_position'] ?? 'left'; - $html .= $this->buildSelect( - 'logo_position', - 'Posición del logo', - $logoPosition, - ['left' => 'Izquierda', 'center' => 'Centro', 'right' => 'Derecha'], - $componentId, - 'logo' - ); - - $html .= '
'; - $html .= '
'; - - return $html; - } - - private function buildMenuTab(array $data, string $componentId): string - { - $html = ''; - - return $html; - } - - private function buildCtaButtonTab(array $data, string $componentId): string - { - $html = '
'; - $html .= '
'; - - $buttonEnabled = $data['cta_button']['button_enabled'] ?? true; - $html .= $this->buildToggle('button_enabled', 'Mostrar botón CTA', $buttonEnabled, $componentId, 'cta_button'); - - $buttonText = $data['cta_button']['button_text'] ?? 'Let\'s Talk'; - $html .= $this->buildTextField( - 'button_text', - 'Texto del botón', - $buttonText, - $componentId, - 'cta_button', - '', - ['data-conditional-field' => 'button_enabled', 'data-conditional-value' => 'true'] - ); - - $buttonIcon = $data['cta_button']['button_icon'] ?? 'bi-lightning-charge-fill'; - $html .= $this->buildTextField( - 'button_icon', - 'Ícono del botón', - $buttonIcon, - $componentId, - 'cta_button', - 'Ej: bi-lightning-charge-fill', - ['data-conditional-field' => 'button_enabled', 'data-conditional-value' => 'true'] - ); - - $actionType = $data['cta_button']['button_action_type'] ?? 'modal'; - $html .= $this->buildSelect( - 'button_action_type', - 'Tipo de acción del botón', - $actionType, - ['modal' => 'Abrir modal', 'link' => 'Ir a URL', 'scroll' => 'Scroll a sección'], - $componentId, - 'cta_button', - ['data-conditional-field' => 'button_enabled', 'data-conditional-value' => 'true'] - ); - - $modalTarget = $data['cta_button']['button_modal_target'] ?? '#contactModal'; - $html .= $this->buildTextField( - 'button_modal_target', - 'ID del modal', - $modalTarget, - $componentId, - 'cta_button', - '#contactModal', - ['data-conditional-field' => 'button_action_type', 'data-conditional-value' => 'modal'] - ); - - $linkUrl = $data['cta_button']['button_link_url'] ?? ''; - $html .= $this->buildUrlField( - 'button_link_url', - 'URL del enlace', - $linkUrl, - $componentId, - 'cta_button', - 'https://', - ['data-conditional-field' => 'button_action_type', 'data-conditional-value' => 'link'] - ); - - $linkTarget = $data['cta_button']['button_link_target'] ?? '_self'; - $html .= $this->buildSelect( - 'button_link_target', - 'Abrir enlace en', - $linkTarget, - ['_self' => 'Misma ventana', '_blank' => 'Nueva ventana'], - $componentId, - 'cta_button', - ['data-conditional-field' => 'button_action_type', 'data-conditional-value' => 'link'] - ); - - $scrollTarget = $data['cta_button']['button_scroll_target'] ?? ''; - $html .= $this->buildTextField( - 'button_scroll_target', - 'ID de la sección', - $scrollTarget, - $componentId, - 'cta_button', - '#contact', - ['data-conditional-field' => 'button_action_type', 'data-conditional-value' => 'scroll'] - ); - - $buttonPosition = $data['cta_button']['button_position'] ?? 'right'; - $html .= $this->buildSelect( - 'button_position', - 'Posición del botón', - $buttonPosition, - ['left' => 'Antes del menú', 'right' => 'Después del menú'], - $componentId, - 'cta_button', - ['data-conditional-field' => 'button_enabled', 'data-conditional-value' => 'true'] - ); - - $html .= '
'; - $html .= '
'; - - return $html; - } - - private function buildStylesTab(array $data, string $componentId): string - { - $html = '
'; - $html .= '
'; - - $bgColor = $data['styles']['background_color'] ?? '#1e3a5f'; - $html .= $this->buildColorField('background_color', 'Color de fondo', $bgColor, $componentId, 'styles'); - - $textColor = $data['styles']['text_color'] ?? '#FFFFFF'; - $html .= $this->buildColorField('text_color', 'Color del texto', $textColor, $componentId, 'styles'); - - $hoverColor = $data['styles']['hover_color'] ?? '#FF8600'; - $html .= $this->buildColorField('hover_color', 'Color hover', $hoverColor, $componentId, 'styles'); - - $activeColor = $data['styles']['active_color'] ?? '#FF8600'; - $html .= $this->buildColorField('active_color', 'Color del item activo', $activeColor, $componentId, 'styles'); - - $buttonBg = $data['styles']['button_background'] ?? '#FF8600'; - $html .= $this->buildColorField('button_background', 'Color de fondo del botón', $buttonBg, $componentId, 'styles'); - - $buttonTextColor = $data['styles']['button_text_color'] ?? '#FFFFFF'; - $html .= $this->buildColorField('button_text_color', 'Color del texto del botón', $buttonTextColor, $componentId, 'styles'); - - $buttonHoverBg = $data['styles']['button_hover_background'] ?? '#FF6B35'; - $html .= $this->buildColorField('button_hover_background', 'Color hover del botón', $buttonHoverBg, $componentId, 'styles'); - - $padding = $data['styles']['padding_vertical'] ?? 'normal'; - $html .= $this->buildSelect( - 'padding_vertical', - 'Padding vertical', - $padding, - ['compact' => 'Compacto (0.5rem)', 'normal' => 'Normal (1rem)', 'spacious' => 'Espacioso (1.5rem)'], - $componentId, - 'styles' - ); - - $shadowEnabled = $data['styles']['shadow_enabled'] ?? true; - $html .= $this->buildToggle('shadow_enabled', 'Activar sombra', $shadowEnabled, $componentId, 'styles'); - - $shadowIntensity = $data['styles']['shadow_intensity'] ?? 'medium'; - $html .= $this->buildSelect( - 'shadow_intensity', - 'Intensidad de la sombra', - $shadowIntensity, - ['light' => 'Ligera', 'medium' => 'Media', 'strong' => 'Fuerte'], - $componentId, - 'styles', - ['data-conditional-field' => 'shadow_enabled', 'data-conditional-value' => 'true'] - ); - - $borderEnabled = $data['styles']['border_bottom_enabled'] ?? false; - $html .= $this->buildToggle('border_bottom_enabled', 'Borde inferior', $borderEnabled, $componentId, 'styles'); - - $borderColor = $data['styles']['border_bottom_color'] ?? '#FF8600'; - $html .= $this->buildColorField( - 'border_bottom_color', - 'Color del borde inferior', - $borderColor, - $componentId, - 'styles', - ['data-conditional-field' => 'border_bottom_enabled', 'data-conditional-value' => 'true'] - ); - - $borderWidth = $data['styles']['border_bottom_width'] ?? 3; - $html .= $this->buildNumberField( - 'border_bottom_width', - 'Grosor del borde (px)', - $borderWidth, - $componentId, - 'styles', - 1, - 10, - ['data-conditional-field' => 'border_bottom_enabled', 'data-conditional-value' => 'true'] - ); - - $html .= '
'; - $html .= '
'; - - return $html; - } - - // Helper methods (similar to TopNotificationBarFormBuilder) - private function buildToggle(string $name, string $label, bool $value, string $componentId, string $group): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $checked = $value ? 'checked' : ''; - - $html = '
'; - $html .= '
'; - $html .= sprintf( - '', - esc_attr($fieldId), - esc_attr($componentId), - esc_attr($group), - esc_attr($name), - $checked - ); - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= '
'; - $html .= '
'; - - return $html; - } - - private function buildTextField(string $name, string $label, string $value, string $componentId, string $group, string $placeholder = '', array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - - $html = '
'; - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= sprintf( - '', - esc_attr($fieldId), - esc_attr($componentId), - esc_attr($group), - esc_attr($name), - esc_attr($value), - esc_attr($placeholder), - $attrString - ); - $html .= '
'; - - return $html; - } - - private function buildSelect(string $name, string $label, string $value, array $options, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - - $html = '
'; - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= sprintf( - ''; - $html .= '
'; - - return $html; - } - - private function buildNumberField(string $name, string $label, $value, string $componentId, string $group, int $min = null, int $max = null, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - - $attrs['type'] = 'number'; - if ($min !== null) $attrs['min'] = $min; - if ($max !== null) $attrs['max'] = $max; - - $attrString = $this->buildAttributesString($attrs); - - $html = '
'; - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= sprintf( - '', - esc_attr($fieldId), - esc_attr($componentId), - esc_attr($group), - esc_attr($name), - esc_attr($value), - $attrString - ); - $html .= '
'; - - return $html; - } - - private function buildUrlField(string $name, string $label, string $value, string $componentId, string $group, string $placeholder = '', array $attrs = []): string - { - $attrs['type'] = 'url'; - return $this->buildTextField($name, $label, $value, $componentId, $group, $placeholder, $attrs); - } - - private function buildColorField(string $name, string $label, string $value, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - - $html = '
'; - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= '
'; - $html .= sprintf( - '', - esc_attr($fieldId), - esc_attr($componentId), - esc_attr($group), - esc_attr($name), - esc_attr($value) - ); - $html .= sprintf('', esc_attr($value)); - $html .= '
'; - $html .= '
'; - - return $html; - } - - private function buildMediaField(string $name, string $label, string $value, string $componentId, string $group, array $attrs = []): string - { - $fieldId = "roi_{$componentId}_{$group}_{$name}"; - $attrString = $this->buildAttributesString($attrs); - - $html = '
'; - $html .= sprintf('', esc_attr($fieldId), esc_html($label)); - $html .= '
'; - $html .= sprintf( - '', - esc_attr($fieldId), - esc_attr($componentId), - esc_attr($group), - esc_attr($name), - esc_attr($value), - $attrString - ); - $html .= sprintf('', esc_attr($fieldId)); - $html .= '
'; - - if (!empty($value)) { - $html .= sprintf('
Preview
', esc_url($value)); - } - - $html .= '
'; - - return $html; - } - - private function buildAttributesString(array $attrs): string - { - $attrString = ''; - foreach ($attrs as $key => $value) { - $attrString .= sprintf(' %s="%s"', esc_attr($key), esc_attr($value)); - } - return $attrString; - } - - private function buildFormScripts(string $componentId): string - { - return << -SCRIPT; - } - - public function getComponentId(): string - { - return $this->componentId; - } -} diff --git a/src/Navbar/Infrastructure/Presentation/Public/NavbarRenderer.php b/src/Navbar/Infrastructure/Presentation/Public/NavbarRenderer.php deleted file mode 100644 index 63444ee3..00000000 --- a/src/Navbar/Infrastructure/Presentation/Public/NavbarRenderer.php +++ /dev/null @@ -1,507 +0,0 @@ -getData(); - - if (!$this->isEnabled($data)) { - return ''; - } - - $classes = $this->buildNavbarClasses($data); - $styles = $this->buildInlineStyles($data); - $containerClasses = $this->buildContainerClasses($data); - - $html = sprintf( - ''; - - // Add custom styles - $html .= $this->buildCustomStyles($data); - - // Add sticky/hide on scroll scripts - if ($this->needsScrollScript($data)) { - $html .= $this->buildScrollScript($data); - } - - return $html; - } - - private function isEnabled(array $data): bool - { - return isset($data['visibility']['is_enabled']) && - $data['visibility']['is_enabled'] === true; - } - - private function shouldShowOnMobile(array $data): bool - { - return isset($data['visibility']['show_on_mobile']) && - $data['visibility']['show_on_mobile'] === true; - } - - private function shouldShowLogo(array $data): bool - { - $logoType = $data['logo']['logo_type'] ?? 'none'; - return $logoType !== 'none'; - } - - private function isButtonPosition(array $data, string $position): bool - { - if (!isset($data['cta_button']['button_enabled']) || !$data['cta_button']['button_enabled']) { - return false; - } - - $buttonPosition = $data['cta_button']['button_position'] ?? 'right'; - return $buttonPosition === $position; - } - - private function needsScrollScript(array $data): bool - { - return (isset($data['visibility']['is_sticky']) && $data['visibility']['is_sticky']) || - (isset($data['visibility']['hide_on_scroll']) && $data['visibility']['hide_on_scroll']); - } - - private function buildNavbarClasses(array $data): string - { - $classes = ['navbar', 'navbar-expand-' . ($data['menu']['mobile_breakpoint'] ?? 'lg'), 'navbar-dark']; - - if (isset($data['visibility']['is_sticky']) && $data['visibility']['is_sticky']) { - $classes[] = 'sticky-top'; - } - - if (isset($data['visibility']['hide_on_scroll']) && $data['visibility']['hide_on_scroll']) { - $classes[] = 'roi-navbar-autohide'; - } - - $paddingClass = $this->getPaddingClass($data['styles']['padding_vertical'] ?? 'normal'); - $classes[] = $paddingClass; - - return implode(' ', $classes); - } - - private function getPaddingClass(string $padding): string - { - $paddings = [ - 'compact' => 'py-2', - 'normal' => 'py-3', - 'spacious' => 'py-4' - ]; - - return $paddings[$padding] ?? 'py-3'; - } - - private function buildContainerClasses(array $data): string - { - return 'container'; - } - - private function buildInlineStyles(array $data): string - { - $styles = []; - - if (!empty($data['styles']['background_color'])) { - $styles[] = 'background-color: ' . $data['styles']['background_color']; - } - - if (isset($data['styles']['border_bottom_enabled']) && $data['styles']['border_bottom_enabled']) { - $borderColor = $data['styles']['border_bottom_color'] ?? '#FF8600'; - $borderWidth = $data['styles']['border_bottom_width'] ?? 3; - $styles[] = "border-bottom: {$borderWidth}px solid {$borderColor}"; - } - - return implode('; ', $styles); - } - - private function buildLogo(array $data): string - { - $logoType = $data['logo']['logo_type'] ?? 'image'; - $logoLink = $data['logo']['logo_link'] ?? home_url('/'); - $logoPosition = $data['logo']['logo_position'] ?? 'left'; - - $logoHtml = ''; - - switch ($logoType) { - case 'image': - $imageUrl = $data['logo']['logo_image_url'] ?? ''; - $imageWidth = $data['logo']['logo_image_width'] ?? 150; - if (!empty($imageUrl)) { - $logoHtml .= sprintf( - '%s', - esc_url($imageUrl), - esc_attr(get_bloginfo('name')), - (int)$imageWidth - ); - } - break; - - case 'text': - $logoText = $data['logo']['logo_text'] ?? get_bloginfo('name'); - $logoHtml .= esc_html($logoText); - break; - } - - $logoHtml .= ''; - - if ($logoPosition === 'center') { - $logoHtml = '
' . $logoHtml . '
'; - } - - return $logoHtml; - } - - private function buildMobileToggle(array $data): string - { - return ''; - } - - private function buildMenu(array $data): string - { - $menuLocation = $data['menu']['menu_location'] ?? 'primary'; - $menuAlignment = $data['menu']['menu_alignment'] ?? 'left'; - $enableDropdowns = $data['menu']['enable_dropdowns'] ?? true; - - $alignmentClasses = [ - 'left' => 'me-auto', - 'center' => 'mx-auto', - 'right' => 'ms-auto' - ]; - - $ulClass = 'navbar-nav mb-2 mb-lg-0 ' . ($alignmentClasses[$menuAlignment] ?? 'me-auto'); - - $args = [ - 'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation, - 'menu' => $menuLocation === 'custom' ? ($data['menu']['custom_menu_id'] ?? 0) : '', - 'container' => false, - 'menu_class' => $ulClass, - 'fallback_cb' => '__return_false', - 'items_wrap' => '
    %3$s
', - 'depth' => $enableDropdowns ? 2 : 1, - 'walker' => new ROI_Bootstrap_Nav_Walker() - ]; - - ob_start(); - wp_nav_menu($args); - return ob_get_clean(); - } - - private function buildCtaButton(array $data): string - { - if (!isset($data['cta_button']['button_enabled']) || !$data['cta_button']['button_enabled']) { - return ''; - } - - $buttonText = $data['cta_button']['button_text'] ?? 'Let\'s Talk'; - $buttonIcon = $data['cta_button']['button_icon'] ?? 'bi-lightning-charge-fill'; - $actionType = $data['cta_button']['button_action_type'] ?? 'modal'; - - $buttonClass = 'btn btn-lets-talk ms-lg-3'; - $buttonAttrs = []; - - switch ($actionType) { - case 'modal': - $modalTarget = $data['cta_button']['button_modal_target'] ?? '#contactModal'; - $buttonAttrs[] = 'data-bs-toggle="modal"'; - $buttonAttrs[] = 'data-bs-target="' . esc_attr($modalTarget) . '"'; - $element = 'button'; - $href = ''; - break; - - case 'link': - $linkUrl = $data['cta_button']['button_link_url'] ?? '#'; - $linkTarget = $data['cta_button']['button_link_target'] ?? '_self'; - $element = 'a'; - $href = ' href="' . esc_url($linkUrl) . '" target="' . esc_attr($linkTarget) . '"'; - break; - - case 'scroll': - $scrollTarget = $data['cta_button']['button_scroll_target'] ?? '#contact'; - $element = 'a'; - $href = ' href="' . esc_attr($scrollTarget) . '"'; - $buttonAttrs[] = 'data-scroll="true"'; - break; - - default: - $element = 'button'; - $href = ''; - } - - $attrString = !empty($buttonAttrs) ? ' ' . implode(' ', $buttonAttrs) : ''; - - $iconHtml = ''; - if (!empty($buttonIcon)) { - if (strpos($buttonIcon, 'bi-') !== 0) { - $buttonIcon = 'bi-' . $buttonIcon; - } - $iconHtml = sprintf('', esc_attr($buttonIcon)); - } - - return sprintf( - '<%s class="%s"%s%s>%s%s', - $element, - esc_attr($buttonClass), - $href, - $attrString, - $iconHtml, - esc_html($buttonText), - $element - ); - } - - private function buildCustomStyles(array $data): string - { - $textColor = $data['styles']['text_color'] ?? '#FFFFFF'; - $hoverColor = $data['styles']['hover_color'] ?? '#FF8600'; - $activeColor = $data['styles']['active_color'] ?? '#FF8600'; - $buttonBg = $data['styles']['button_background'] ?? '#FF8600'; - $buttonTextColor = $data['styles']['button_text_color'] ?? '#FFFFFF'; - $buttonHoverBg = $data['styles']['button_hover_background'] ?? '#FF6B35'; - $backgroundColor = $data['styles']['background_color'] ?? '#1e3a5f'; - - $shadowStyle = ''; - if (isset($data['styles']['shadow_enabled']) && $data['styles']['shadow_enabled']) { - $shadowIntensity = $data['styles']['shadow_intensity'] ?? 'medium'; - $shadows = [ - 'light' => '0 2px 4px rgba(0,0,0,0.1)', - 'medium' => '0 2px 8px rgba(0,0,0,0.15)', - 'strong' => '0 4px 12px rgba(0,0,0,0.25)' - ]; - $shadowStyle = 'box-shadow: ' . $shadows[$shadowIntensity] . ';'; - } - - return << -.navbar { - {$shadowStyle} -} -.navbar .nav-link { - color: {$textColor} !important; - transition: color 0.3s ease; -} -.navbar .nav-link:hover, -.navbar .nav-link:focus { - color: {$hoverColor} !important; -} -.navbar .nav-link.active, -.navbar .nav-item.current-menu-item > .nav-link { - color: {$activeColor} !important; -} -.navbar .dropdown-menu { - background-color: {$backgroundColor}; - border: none; -} -.navbar .dropdown-item { - color: {$textColor}; - transition: background-color 0.3s ease, color 0.3s ease; -} -.navbar .dropdown-item:hover, -.navbar .dropdown-item:focus { - background-color: rgba(255, 134, 0, 0.1); - color: {$hoverColor}; -} -.btn-lets-talk { - background-color: {$buttonBg}; - color: {$buttonTextColor}; - border: none; - padding: 0.5rem 1.5rem; - border-radius: 0.375rem; - font-weight: 500; - transition: all 0.3s ease; -} -.btn-lets-talk:hover, -.btn-lets-talk:focus { - background-color: {$buttonHoverBg}; - color: {$buttonTextColor}; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.2); -} - -STYLES; - } - - private function buildScrollScript(array $data): string - { - $isSticky = $data['visibility']['is_sticky'] ?? false; - $hideOnScroll = $data['visibility']['hide_on_scroll'] ?? false; - - if (!$isSticky && !$hideOnScroll) { - return ''; - } - - $script = ''; - - return $script; - } - - public function supports(string $componentType): bool - { - return $componentType === 'navbar'; - } -} - -/** - * Custom Walker for Bootstrap 5 Navigation - */ -class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu -{ - public function start_lvl(&$output, $depth = 0, $args = null) - { - $indent = str_repeat("\t", $depth); - $output .= "\n$indent
    \n"; - } - - public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) - { - $indent = ($depth) ? str_repeat("\t", $depth) : ''; - - $classes = empty($item->classes) ? [] : (array) $item->classes; - $classes[] = 'nav-item'; - - if ($args->walker->has_children) { - $classes[] = 'dropdown'; - } - - $class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth)); - $class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : ''; - - $id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth); - $id = $id ? ' id="' . esc_attr($id) . '"' : ''; - - $output .= $indent . ''; - - $atts = []; - $atts['title'] = !empty($item->attr_title) ? $item->attr_title : ''; - $atts['target'] = !empty($item->target) ? $item->target : ''; - $atts['rel'] = !empty($item->xfn) ? $item->xfn : ''; - $atts['href'] = !empty($item->url) ? $item->url : ''; - - if ($depth === 0) { - $atts['class'] = 'nav-link'; - if ($args->walker->has_children) { - $atts['class'] .= ' dropdown-toggle'; - $atts['data-bs-toggle'] = 'dropdown'; - $atts['role'] = 'button'; - $atts['aria-expanded'] = 'false'; - } - } else { - $atts['class'] = 'dropdown-item'; - } - - if (in_array('current-menu-item', $classes)) { - $atts['class'] .= ' active'; - } - - $atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth); - - $attributes = ''; - foreach ($atts as $attr => $value) { - if (!empty($value)) { - $value = ('href' === $attr) ? esc_url($value) : esc_attr($value); - $attributes .= ' ' . $attr . '="' . $value . '"'; - } - } - - $title = apply_filters('the_title', $item->title, $item->ID); - $title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth); - - $item_output = $args->before; - $item_output .= ''; - $item_output .= $args->link_before . $title . $args->link_after; - $item_output .= ''; - $item_output .= $args->after; - - $output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args); - } -} diff --git a/src/RelatedPosts/Infrastructure/Presentation/Admin/RelatedPostsFormBuilder.php b/src/RelatedPosts/Infrastructure/Presentation/Admin/RelatedPostsFormBuilder.php deleted file mode 100644 index 522490fe..00000000 --- a/src/RelatedPosts/Infrastructure/Presentation/Admin/RelatedPostsFormBuilder.php +++ /dev/null @@ -1,206 +0,0 @@ -componentId = $componentId; - $this->data = $data; - } - - public function build(): string - { - ob_start(); - ?> - - data['visibility']['is_enabled'] ?? true; - $sectionTitle = $this->data['visibility']['section_title'] ?? 'Descubre Más Contenido'; - - ob_start(); - ?> -
    -
    - > - -
    - Activa o desactiva el componente de posts relacionados -
    -
    - - - Título que aparece antes del grid de posts -
    - data['query']['posts_per_page'] ?? 12; - $postSelection = $this->data['query']['post_selection'] ?? 'category'; - $excludeCurrent = $this->data['query']['exclude_current_post'] ?? true; - - ob_start(); - ?> -
    - - - Cantidad de posts a mostrar por página -
    -
    - - - Cómo seleccionar los posts relacionados -
    -
    -
    - > - -
    - Excluye el post actual de los resultados -
    - data['layout']['columns'] ?? '3'; - $cardHeight = $this->data['layout']['card_height'] ?? 'equal'; - - ob_start(); - ?> -
    - - - Número de columnas en el grid -
    -
    - - - Controla cómo se muestran las cards -
    - data['pagination']['show_pagination'] ?? true; - $paginationPosition = $this->data['pagination']['pagination_position'] ?? 'center'; - - ob_start(); - ?> -
    -
    - > - -
    - Muestra u oculta la paginación -
    -
    - - - Alineación de la paginación -
    - data['styles']['container_classes'] ?? 'my-5 related-posts'; - $gridGap = $this->data['styles']['grid_gap'] ?? '4'; - - ob_start(); - ?> -
    - - - Clases CSS adicionales para el contenedor principal -
    -
    - - - Espaciado entre cards -
    -
    -
    Notas:
    -
      -
    • Los posts se seleccionan según el criterio elegido (categoría, tags, etc.)
    • -
    • Si no hay posts relacionados, se mostrarán los más recientes
    • -
    • La paginación solo aparece si hay más de una página de resultados
    • -
    -
    - componentId; - } -} diff --git a/src/RelatedPosts/Infrastructure/Presentation/Public/RelatedPostsRenderer.php b/src/RelatedPosts/Infrastructure/Presentation/Public/RelatedPostsRenderer.php deleted file mode 100644 index 0bd5dcc4..00000000 --- a/src/RelatedPosts/Infrastructure/Presentation/Public/RelatedPostsRenderer.php +++ /dev/null @@ -1,242 +0,0 @@ -currentPostId = get_the_ID() ?: 0; - } - - public function render(Component $component): string - { - $data = $component->getData(); - - if (!$this->isEnabled($data)) { - return ''; - } - - $this->query = $this->buildQuery($data); - - if (!$this->query->have_posts()) { - $this->query = $this->buildFallbackQuery($data); - } - - if (!$this->query->have_posts()) { - wp_reset_postdata(); - return ''; - } - - $containerClasses = $data['styles']['container_classes'] ?? 'my-5 related-posts'; - $sectionTitle = $data['visibility']['section_title'] ?? 'Descubre Más Contenido'; - - ob_start(); - ?> -
    -

    - renderPostsGrid($data); ?> - renderPagination($data); ?> -
    - 'post', - 'post_status' => 'publish', - 'posts_per_page' => $postsPerPage, - 'paged' => get_query_var('paged') ? get_query_var('paged') : 1, - 'ignore_sticky_posts' => true, - ]; - - if ($excludeCurrent) { - $args['post__not_in'] = [$this->currentPostId]; - } - - switch ($postSelection) { - case 'category': - $args['category__in'] = wp_get_post_categories($this->currentPostId); - break; - - case 'tags': - $tags = wp_get_post_tags($this->currentPostId, ['fields' => 'ids']); - if (!empty($tags)) { - $args['tag__in'] = $tags; - } - break; - - case 'both': - $args['category__in'] = wp_get_post_categories($this->currentPostId); - $tags = wp_get_post_tags($this->currentPostId, ['fields' => 'ids']); - if (!empty($tags)) { - $args['tag__in'] = $tags; - } - break; - - case 'random': - $args['orderby'] = 'rand'; - break; - - case 'recent': - default: - $args['orderby'] = 'date'; - $args['order'] = 'DESC'; - break; - } - - return new WP_Query($args); - } - - private function buildFallbackQuery(array $data): WP_Query - { - $postsPerPage = $data['query']['posts_per_page'] ?? 12; - - $args = [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'posts_per_page' => $postsPerPage, - 'orderby' => 'date', - 'order' => 'DESC', - 'post__not_in' => [$this->currentPostId], - 'paged' => get_query_var('paged') ? get_query_var('paged') : 1, - ]; - - return new WP_Query($args); - } - - private function renderPostsGrid(array $data): string - { - $columns = $data['layout']['columns'] ?? '3'; - $gridGap = 'g-' . ($data['styles']['grid_gap'] ?? '4'); - $columnClass = $this->getColumnClass($columns); - - ob_start(); - ?> -
    - query->have_posts()) { - $this->query->the_post(); - echo $this->renderPostCard($data, $columnClass); - } - ?> -
    - 'col-12', - '2' => 'col-md-6', - '3' => 'col-md-4', - '4' => 'col-md-3', - ]; - - return $columnMap[$columns] ?? 'col-md-4'; - } - - private function renderPostCard(array $data, string $columnClass): string - { - $cardHeight = $data['layout']['card_height'] ?? 'equal'; - $heightClass = $cardHeight === 'equal' ? 'h-100' : ''; - $textCenterClass = $cardHeight === 'equal' ? 'text-center' : ''; - $dFlexClass = $cardHeight === 'equal' ? 'd-flex align-items-center justify-content-center' : ''; - - ob_start(); - ?> - - query->max_num_pages <= 1) { - return ''; - } - - $paginationPosition = $data['pagination']['pagination_position'] ?? 'center'; - $positionClass = 'justify-content-' . $paginationPosition; - - $currentPage = max(1, get_query_var('paged')); - $totalPages = $this->query->max_num_pages; - - ob_start(); - ?> - - componentId = $componentId; - $this->data = $data; - } - - public function build(): string - { - ob_start(); - ?> - - data['visibility']['is_enabled'] ?? true; - $showLabel = $this->data['visibility']['show_label'] ?? true; - $labelText = $this->data['visibility']['label_text'] ?? 'Compartir:'; - - ob_start(); - ?> -
    -
    - > - -
    - Activa o desactiva los botones de compartir en redes sociales -
    -
    -
    - > - -
    - Muestra u oculta el texto de etiqueta antes de los botones -
    -
    - - - Texto que aparece antes de los botones -
    - data['networks']['enabled_networks'] ?? ['facebook', 'instagram', 'linkedin', 'whatsapp', 'twitter', 'email']; - $showLabels = $this->data['networks']['show_network_labels'] ?? false; - - $networks = [ - 'facebook' => 'Facebook', - 'instagram' => 'Instagram', - 'linkedin' => 'LinkedIn', - 'whatsapp' => 'WhatsApp', - 'twitter' => 'Twitter / X', - 'email' => 'Email', - ]; - - ob_start(); - ?> -
    - -

    Selecciona qué redes sociales mostrar en los botones de compartir

    - $label): ?> -
    - > - -
    - -
    -
    -
    - > - -
    - Muestra el nombre de la red social junto al icono -
    - data['button_styles']['button_style'] ?? 'outline'; - $buttonSize = $this->data['button_styles']['button_size'] ?? 'sm'; - - ob_start(); - ?> -
    - - - Estilo visual de los botones -
    -
    - - - Tamaño de los botones -
    - data['advanced']['container_classes'] ?? 'my-5 py-4 border-top'; - $wrapperClasses = $this->data['advanced']['buttons_wrapper_classes'] ?? 'd-flex gap-2 flex-wrap share-buttons'; - - ob_start(); - ?> -
    - - - Clases CSS adicionales para el contenedor principal -
    -
    - - - Clases CSS para el wrapper de los botones -
    -
    -
    Notas:
    -
      -
    • Instagram no tiene API de compartir directa, se muestra como enlace inactivo (#)
    • -
    • Las URLs de compartir se generan automáticamente con el permalink del post
    • -
    • Todos los botones incluyen aria-label para lectores de pantalla
    • -
    -
    - componentId; - } -} diff --git a/src/ShareButtons/Infrastructure/Presentation/Public/ShareButtonsRenderer.php b/src/ShareButtons/Infrastructure/Presentation/Public/ShareButtonsRenderer.php deleted file mode 100644 index 67429759..00000000 --- a/src/ShareButtons/Infrastructure/Presentation/Public/ShareButtonsRenderer.php +++ /dev/null @@ -1,159 +0,0 @@ - [ - 'url_template' => 'https://www.facebook.com/sharer/sharer.php?u={url}', - 'icon_class' => 'bi bi-facebook', - 'button_class' => 'btn-outline-primary', - 'aria_label' => 'Compartir en Facebook', - ], - 'instagram' => [ - 'url_template' => '#', - 'icon_class' => 'bi bi-instagram', - 'button_class' => 'btn-outline-danger', - 'aria_label' => 'Compartir en Instagram', - ], - 'linkedin' => [ - 'url_template' => 'https://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}', - 'icon_class' => 'bi bi-linkedin', - 'button_class' => 'btn-outline-info', - 'aria_label' => 'Compartir en LinkedIn', - ], - 'whatsapp' => [ - 'url_template' => 'https://wa.me/?text={title}%20{url}', - 'icon_class' => 'bi bi-whatsapp', - 'button_class' => 'btn-outline-success', - 'aria_label' => 'Compartir en WhatsApp', - ], - 'twitter' => [ - 'url_template' => 'https://twitter.com/intent/tweet?url={url}&text={title}', - 'icon_class' => 'bi bi-twitter-x', - 'button_class' => 'btn-outline-dark', - 'aria_label' => 'Compartir en X', - ], - 'email' => [ - 'url_template' => 'mailto:?subject={title}&body={url}', - 'icon_class' => 'bi bi-envelope', - 'button_class' => 'btn-outline-secondary', - 'aria_label' => 'Compartir por Email', - ], - ]; - - public function render(Component $component): string - { - $data = $component->getData(); - - if (!$this->isEnabled($data)) { - return ''; - } - - $enabledNetworks = $data['networks']['enabled_networks'] ?? []; - if (empty($enabledNetworks)) { - return ''; - } - - $containerClasses = $data['advanced']['container_classes'] ?? 'my-5 py-4 border-top'; - $wrapperClasses = $data['advanced']['buttons_wrapper_classes'] ?? 'd-flex gap-2 flex-wrap share-buttons'; - - $html = sprintf('
    ', esc_attr($containerClasses)); - - if ($this->shouldShowLabel($data)) { - $labelText = $data['visibility']['label_text'] ?? 'Compartir:'; - $html .= sprintf('

    %s

    ', esc_html($labelText)); - } - - $html .= sprintf('
    ', esc_attr($wrapperClasses)); - $html .= $this->renderButtons($data, $enabledNetworks); - $html .= '
    '; - $html .= '
    '; - - return $html; - } - - private function isEnabled(array $data): bool - { - return isset($data['visibility']['is_enabled']) && - $data['visibility']['is_enabled'] === true; - } - - private function shouldShowLabel(array $data): bool - { - return isset($data['visibility']['show_label']) && - $data['visibility']['show_label'] === true; - } - - private function renderButtons(array $data, array $enabledNetworks): string - { - $postUrl = get_permalink() ?: ''; - $postTitle = get_the_title() ?: ''; - $buttonStyle = $data['button_styles']['button_style'] ?? 'outline'; - $buttonSize = $data['button_styles']['button_size'] ?? 'sm'; - $showLabels = $data['networks']['show_network_labels'] ?? false; - - $output = ''; - - foreach ($enabledNetworks as $network) { - if (!isset($this->networksConfig[$network])) { - continue; - } - - $networkConfig = $this->networksConfig[$network]; - $url = $this->buildShareUrl($network, $networkConfig['url_template'], $postUrl, $postTitle); - $buttonClass = $this->getButtonClass($networkConfig['button_class'], $buttonStyle, $buttonSize); - - $label = $showLabels ? ' ' . esc_html(ucfirst($network)) : ''; - - $output .= sprintf( - ' - %s - ', - esc_url($url), - esc_attr($buttonClass), - esc_attr($networkConfig['aria_label']), - esc_attr($networkConfig['icon_class']), - $label - ); - } - - return $output; - } - - private function buildShareUrl(string $network, string $template, string $postUrl, string $postTitle): string - { - if ($network === 'instagram') { - return '#'; - } - - $replacements = [ - '{url}' => urlencode($postUrl), - '{title}' => urlencode($postTitle), - ]; - - return str_replace(array_keys($replacements), array_values($replacements), $template); - } - - private function getButtonClass(string $networkClass, string $buttonStyle, string $buttonSize): string - { - $baseClass = 'btn'; - $sizeClass = 'btn-' . $buttonSize; - - if ($buttonStyle === 'solid') { - $networkClass = str_replace('btn-outline-', 'btn-', $networkClass); - } - - return "{$baseClass} {$networkClass} {$sizeClass}"; - } - - public function supports(string $componentType): bool - { - return $componentType === 'share-buttons'; - } -} diff --git a/src/Shared/Domain/.gitkeep b/src/Shared/Domain/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/TableOfContents/Infrastructure/Presentation/Admin/TableOfContentsFormBuilder.php b/src/TableOfContents/Infrastructure/Presentation/Admin/TableOfContentsFormBuilder.php deleted file mode 100644 index 383e45bb..00000000 --- a/src/TableOfContents/Infrastructure/Presentation/Admin/TableOfContentsFormBuilder.php +++ /dev/null @@ -1,309 +0,0 @@ -componentId = $componentId; - $this->data = $data; - } - - public function build(): string - { - ob_start(); - ?> -
    - -
    -
    buildVisibilityTab(); ?>
    -
    buildConfigTab(); ?>
    -
    buildManualItemsTab(); ?>
    -
    buildStylesTab(); ?>
    -
    -
    - data['visibility']['is_enabled'] ?? true; - $sticky = $this->data['visibility']['sticky'] ?? true; - $showOnMobile = $this->data['visibility']['show_on_mobile'] ?? false; - - ob_start(); - ?> -
    -
    - > - -
    - Activa o desactiva el componente de tabla de contenido -
    -
    -
    - > - -
    - Mantiene el TOC visible mientras el usuario navega -
    -
    -
    - > - -
    - Muestra el TOC en pantallas pequeñas -
    - data['config']['title'] ?? 'Tabla de Contenido'; - $headingLevels = $this->data['config']['heading_levels'] ?? ['h2', 'h3']; - $autoGenerate = $this->data['config']['auto_generate'] ?? true; - $offsetTop = $this->data['config']['offset_top'] ?? 100; - $smoothScroll = $this->data['config']['smooth_scroll'] ?? true; - $maxHeight = $this->data['config']['max_height'] ?? 'calc(100vh - 400px)'; - $customCssClass = $this->data['config']['custom_css_class'] ?? ''; - - ob_start(); - ?> -
    - - - Título que aparece en el encabezado del TOC -
    -
    - - Selecciona qué niveles incluir en el TOC - 'H2', 'h3' => 'H3', 'h4' => 'H4', 'h5' => 'H5', 'h6' => 'H6']; - foreach ($availableLevels as $level => $label): - ?> -
    - > - -
    - -
    -
    -
    - > - -
    - Si está desactivado, usa items manuales -
    -
    - - - Offset desde el top para el ScrollSpy -
    -
    -
    - > - -
    - Activar scroll suave al hacer clic en enlaces -
    -
    - - - CSS válido (ej: 500px, calc(100vh - 200px)) -
    -
    - - - Opcional: agregar clase CSS adicional -
    - data['manual_items']['items'] ?? []; - - ob_start(); - ?> -
    - Nota: Los items manuales solo se usan si "Generar automáticamente" está desactivado en la pestaña Configuración. -
    -
    - '', 'anchor' => '', 'level' => 2]]; - } - - foreach ($items as $index => $item): - ?> -
    -
    - Item # - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    - -
    - - - - data['styles']['background_color'] ?? '#ffffff'; - $borderColor = $this->data['styles']['border_color'] ?? '#E6E9ED'; - $titleColor = $this->data['styles']['title_color'] ?? '#0E2337'; - $linkColor = $this->data['styles']['link_color'] ?? '#6B7280'; - $linkHoverColor = $this->data['styles']['link_hover_color'] ?? '#0E2337'; - $activeBorderColor = $this->data['styles']['active_border_color'] ?? '#0E2337'; - $activeBgColor = $this->data['styles']['active_bg_color'] ?? '#F9FAFB'; - - ob_start(); - ?> -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    Notas sobre el TOC:
    -
      -
    • El ScrollSpy requiere Bootstrap 5 cargado en el sitio
    • -
    • Los IDs se generan automáticamente desde los encabezados del contenido
    • -
    • El TOC es responsive y se adapta a pantallas pequeñas
    • -
    -
    - componentId; - } -} diff --git a/src/TableOfContents/Infrastructure/Presentation/Public/TableOfContentsRenderer.php b/src/TableOfContents/Infrastructure/Presentation/Public/TableOfContentsRenderer.php deleted file mode 100644 index 5b2e8fe1..00000000 --- a/src/TableOfContents/Infrastructure/Presentation/Public/TableOfContentsRenderer.php +++ /dev/null @@ -1,265 +0,0 @@ -currentPostId = get_the_ID() ?: 0; - } - - public function render(Component $component): string - { - $data = $component->getData(); - - if (!$this->isEnabled($data)) { - return ''; - } - - $autoGenerate = $data['config']['auto_generate'] ?? true; - $tocItems = $autoGenerate - ? $this->generateTocFromContent($data) - : $this->getManualItems($data); - - if (empty($tocItems)) { - return ''; - } - - $componentId = $component->getId(); - $customStyles = $this->generateCustomStyles($componentId, $data['styles'] ?? []); - $stickyClass = ($data['visibility']['sticky'] ?? true) ? 'sidebar-sticky' : ''; - $mobileClass = !($data['visibility']['show_on_mobile'] ?? false) ? 'd-none d-lg-block' : ''; - $title = $data['config']['title'] ?? 'Tabla de Contenido'; - $maxHeight = $data['config']['max_height'] ?? 'calc(100vh - 400px)'; - $offsetTop = $data['config']['offset_top'] ?? 100; - - ob_start(); - ?> - - - - -
    - -

    - -
      - - 2 ? 'toc-level-' . $level : ''; - ?> -
    1. - - - -
    2. - -
    -
    - - - - - - post_content)) { - return []; - } - - $headingLevels = $data['config']['heading_levels'] ?? ['h2', 'h3']; - $content = apply_filters('the_content', $post->post_content); - - $dom = new DOMDocument(); - libxml_use_internal_errors(true); - $dom->loadHTML('' . $content); - libxml_clear_errors(); - - $xpath = new DOMXPath($dom); - $xpathQuery = implode(' | ', array_map(function($level) { - return '//' . $level; - }, $headingLevels)); - - $headings = $xpath->query($xpathQuery); - - if ($headings->length === 0) { - return []; - } - - $tocItems = []; - $headingCounter = []; - - foreach ($headings as $heading) { - $tagName = strtolower($heading->tagName); - $level = intval(substr($tagName, 1)); - $text = trim($heading->textContent); - - if (empty($text)) { - continue; - } - - $existingId = $heading->getAttribute('id'); - $anchor = !empty($existingId) - ? $existingId - : $this->generateAnchorId($text, $headingCounter); - - $tocItems[] = [ - 'text' => $text, - 'anchor' => $anchor, - 'level' => $level - ]; - } - - return $tocItems; - } - - private function getManualItems(array $data): array - { - return $data['manual_items']['items'] ?? []; - } - - private function generateAnchorId(string $text, array &$counter): string - { - $id = strtolower($text); - $id = remove_accents($id); - $id = preg_replace('/[^a-z0-9]+/', '-', $id); - $id = trim($id, '-'); - - $baseId = $id; - $count = 1; - - while (isset($counter[$id])) { - $id = $baseId . '-' . $count; - $count++; - } - - $counter[$id] = true; - - return $id; - } - - private function generateCustomStyles(string $componentId, array $styles): string - { - if (empty($styles)) { - return ''; - } - - $css = []; - - if (isset($styles['background_color'])) { - $css[] = "#$componentId.toc-container { background: {$styles['background_color']}; }"; - } - - if (isset($styles['border_color'])) { - $css[] = "#$componentId.toc-container { border-color: {$styles['border_color']}; }"; - } - - if (isset($styles['title_color'])) { - $css[] = "#$componentId.toc-container h4 { color: {$styles['title_color']}; }"; - } - - if (isset($styles['link_color'])) { - $css[] = "#$componentId .toc-link { color: {$styles['link_color']}; }"; - } - - if (isset($styles['link_hover_color'])) { - $css[] = "#$componentId .toc-link:hover { color: {$styles['link_hover_color']}; }"; - } - - if (isset($styles['active_border_color'])) { - $css[] = "#$componentId .toc-link.active { border-left-color: {$styles['active_border_color']}; }"; - } - - if (isset($styles['active_bg_color'])) { - $css[] = "#$componentId .toc-link.active { background-color: {$styles['active_bg_color']}; }"; - } - - return implode("\n", $css); - } - - public function supports(string $componentType): bool - { - return $componentType === 'table-of-contents'; - } -} diff --git a/src/TopNotificationBar/Infrastructure/Presentation/Public/TopNotificationBarRenderer.php b/src/TopNotificationBar/Infrastructure/Presentation/Public/TopNotificationBarRenderer.php deleted file mode 100644 index 016a677c..00000000 --- a/src/TopNotificationBar/Infrastructure/Presentation/Public/TopNotificationBarRenderer.php +++ /dev/null @@ -1,348 +0,0 @@ -getData(); - - // Verificar si el componente está habilitado - if (!$this->isEnabled($data)) { - return ''; - } - - // Verificar si debe mostrarse en la página actual - if (!$this->shouldShowOnCurrentPage($data)) { - return ''; - } - - // Verificar si el usuario ha cerrado la barra (si es dismissible) - if ($this->isDismissed($data)) { - return ''; - } - - $classes = $this->buildClasses($data); - $styles = $this->buildInlineStyles($data); - $content = $this->buildContent($data); - - $html = sprintf( - '
    %s
    ', - esc_attr($classes), - $styles ? ' style="' . esc_attr($styles) . '"' : '', - $this->buildDataAttributes($data), - $content - ); - - // Agregar script si es dismissible - if ($this->isDismissible($data)) { - $html .= $this->buildDismissScript($data); - } - - // Agregar estilos de animación si está habilitada - if ($this->isAnimationEnabled($data)) { - $html .= $this->buildAnimationStyles($data); - } - - return $html; - } - - private function isEnabled(array $data): bool - { - return isset($data['visibility']['is_enabled']) && - $data['visibility']['is_enabled'] === true; - } - - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'all'; - - switch ($showOn) { - case 'all': - return true; - - case 'home': - return is_front_page(); - - case 'posts': - return is_single(); - - case 'pages': - return is_page(); - - case 'custom': - $pageIds = $data['visibility']['custom_page_ids'] ?? ''; - $allowedIds = array_map('trim', explode(',', $pageIds)); - return in_array((string)get_the_ID(), $allowedIds, true); - - default: - return true; - } - } - - private function isDismissed(array $data): bool - { - if (!$this->isDismissible($data)) { - return false; - } - - $cookieName = 'roi_notification_bar_dismissed'; - return isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === '1'; - } - - private function isDismissible(array $data): bool - { - return isset($data['visibility']['is_dismissible']) && - $data['visibility']['is_dismissible'] === true; - } - - private function isAnimationEnabled(array $data): bool - { - return isset($data['styles']['animation_enabled']) && - $data['styles']['animation_enabled'] === true; - } - - private function buildClasses(array $data): string - { - $classes = ['top-notification-bar']; - - // Ocultar en móvil si está configurado - if (isset($data['visibility']['hide_on_mobile']) && $data['visibility']['hide_on_mobile']) { - $classes[] = 'd-none'; - $classes[] = 'd-md-block'; - } - - // Agregar clase de animación - if ($this->isAnimationEnabled($data)) { - $animationType = $data['styles']['animation_type'] ?? 'slide-down'; - $classes[] = 'roi-animated'; - $classes[] = 'roi-' . $animationType; - } - - return implode(' ', $classes); - } - - private function buildInlineStyles(array $data): string - { - $styles = []; - - // Color de fondo - if (!empty($data['styles']['background_color'])) { - $styles[] = 'background-color: ' . $data['styles']['background_color']; - } - - // Color de texto - if (!empty($data['styles']['text_color'])) { - $styles[] = 'color: ' . $data['styles']['text_color']; - } - - // Tamaño de fuente - $fontSize = $data['styles']['font_size'] ?? 'small'; - $fontSizes = [ - 'extra-small' => '0.75rem', - 'small' => '0.875rem', - 'normal' => '1rem', - 'large' => '1.125rem' - ]; - if (isset($fontSizes[$fontSize])) { - $styles[] = 'font-size: ' . $fontSizes[$fontSize]; - } - - // Padding vertical - $padding = $data['styles']['padding_vertical'] ?? 'normal'; - $paddings = [ - 'compact' => '0.5rem 0', - 'normal' => '0.75rem 0', - 'spacious' => '1rem 0' - ]; - if (isset($paddings[$padding])) { - $styles[] = 'padding: ' . $paddings[$padding]; - } - - return implode('; ', $styles); - } - - private function buildContent(array $data): string - { - $alignment = $data['styles']['text_alignment'] ?? 'center'; - $alignmentClasses = [ - 'left' => 'justify-content-start', - 'center' => 'justify-content-center', - 'right' => 'justify-content-end' - ]; - $alignmentClass = $alignmentClasses[$alignment] ?? 'justify-content-center'; - - $html = '
    '; - $html .= sprintf('
    ', esc_attr($alignmentClass)); - - // Ícono - $html .= $this->buildIcon($data); - - // Texto del anuncio - $html .= $this->buildAnnouncementText($data); - - // Enlace - $html .= $this->buildLink($data); - - // Botón de cerrar (si es dismissible) - if ($this->isDismissible($data)) { - $html .= $this->buildDismissButton(); - } - - $html .= '
    '; - $html .= '
    '; - - return $html; - } - - private function buildIcon(array $data): string - { - $iconType = $data['content']['icon_type'] ?? 'bootstrap'; - - switch ($iconType) { - case 'bootstrap': - $iconClass = $data['content']['bootstrap_icon'] ?? 'bi-megaphone-fill'; - // Asegurar que tenga el prefijo 'bi' - if (strpos($iconClass, 'bi-') !== 0) { - $iconClass = 'bi-' . $iconClass; - } - return sprintf('', esc_attr($iconClass)); - - case 'custom': - $iconUrl = $data['content']['custom_icon_url'] ?? ''; - if (!empty($iconUrl)) { - return sprintf( - 'Icon', - esc_url($iconUrl) - ); - } - return ''; - - case 'none': - default: - return ''; - } - } - - private function buildAnnouncementText(array $data): string - { - $label = $data['content']['announcement_label'] ?? ''; - $text = $data['content']['announcement_text'] ?? ''; - - if (empty($text)) { - return ''; - } - - $html = ''; - - if (!empty($label)) { - $html .= sprintf('%s ', esc_html($label)); - } - - $html .= esc_html($text); - $html .= ''; - - return $html; - } - - private function buildLink(array $data): string - { - $linkEnabled = $data['content']['link_enabled'] ?? true; - - if (!$linkEnabled) { - return ''; - } - - $linkText = $data['content']['link_text'] ?? ''; - $linkUrl = $data['content']['link_url'] ?? '#'; - $linkTarget = $data['content']['link_target'] ?? '_self'; - $linkColor = $data['styles']['link_color'] ?? '#FFFFFF'; - - if (empty($linkText)) { - return ''; - } - - return sprintf( - '%s', - esc_url($linkUrl), - esc_attr($linkTarget), - esc_attr($linkColor), - esc_html($linkText) - ); - } - - private function buildDismissButton(): string - { - return ''; - } - - private function buildDataAttributes(array $data): string - { - if (!$this->isDismissible($data)) { - return ''; - } - - $days = $data['visibility']['dismissible_cookie_days'] ?? 7; - return sprintf(' data-dismissible-days="%d"', (int)$days); - } - - private function buildDismissScript(array $data): string - { - $days = $data['visibility']['dismissible_cookie_days'] ?? 7; - - return << -SCRIPT; - } - - private function buildAnimationStyles(array $data): string - { - $animationType = $data['styles']['animation_type'] ?? 'slide-down'; - - $animations = [ - 'slide-down' => [ - 'keyframes' => '@keyframes roiSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }', - 'animation' => 'roiSlideDown 0.5s ease-out' - ], - 'fade-in' => [ - 'keyframes' => '@keyframes roiFadeIn { from { opacity: 0; } to { opacity: 1; } }', - 'animation' => 'roiFadeIn 0.5s ease-out' - ] - ]; - - $anim = $animations[$animationType] ?? $animations['slide-down']; - - return << -STYLE; - } - - public function supports(string $componentType): bool - { - return $componentType === 'top_notification_bar'; - } -} diff --git a/style.css b/style.css index b8da6045..f9da2863 100644 --- a/style.css +++ b/style.css @@ -1,24 +1,29 @@ /* Theme Name: ROI Theme -Theme URI: https://analisisdepreciosunitarios.com -Author: ROI Development Team -Author URI: https://analisisdepreciosunitarios.com -Description: High-performance WordPress theme engineered for maximum ROI. Optimized for Core Web Vitals, SEO excellence, and conversion rate optimization. Zero bloat, maximum results. -Version: 1.0.0 +Theme URI: https://dev.analisisdepreciosunitarios.com +Author: ROI Team +Author URI: https://dev.analisisdepreciosunitarios.com +Description: Tema personalizado con arquitectura limpia para Análisis de Precios Unitarios. Incluye sistema de componentes modulares con Clean Architecture. +Version: 1.0.19 Requires at least: 6.0 -Tested up to: 6.4 +Tested up to: 6.8 Requires PHP: 8.0 License: GNU General Public License v2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Text Domain: roi-theme -Domain Path: /languages -Tags: blog, custom-menu, featured-images, footer-widgets, performance, seo, conversion, roi, accessibility-ready, translation-ready +Tags: custom, clean-architecture, modular, bootstrap + +ROI Theme, Copyright 2024 +ROI Theme is distributed under the terms of the GNU GPL. */ /* - * IMPORTANTE: Los estilos CSS del tema están en: - * assets/css/style.css + * IMPORTANTE: Este archivo style.css es OBLIGATORIO para WordPress. * - * Este archivo solo contiene el header obligatorio de WordPress. - * Los estilos se cargan mediante wp_enqueue_style() en functions.php + * Los estilos del tema NO se cargan desde aquí. + * Los estilos se cargan desde /assets/css/ mediante wp_enqueue_style() + * en el archivo /inc/enqueue-scripts.php + * + * Este archivo solo contiene el header requerido por WordPress + * para reconocer el tema como válido. */ diff --git a/templates/admin/.gitkeep b/templates/admin/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/public/.gitkeep b/templates/public/.gitkeep deleted file mode 100644 index e69de29b..00000000