Files
roi-theme/src/HeroSection/Infrastructure/Presentation/Admin/HeroSectionFormBuilder.php
FrankZamora 90de6df77c Fase-01: Preparación del entorno y estructura inicial
- Verificación de entorno XAMPP (PHP 8.0.30, Composer 2.9.1, WP-CLI 2.12.0)
- Configuración de Composer con PSR-4 para 24 namespaces
- Configuración de PHPUnit con 140 tests preparados
- Configuración de PHPCS con WordPress Coding Standards
- Scripts de backup y rollback con mejoras de seguridad
- Estructura de contextos (admin/, public/, shared/)
- Schemas JSON para 11 componentes del sistema
- Código fuente inicial con arquitectura limpia en src/
- Documentación de procedimientos de emergencia

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:34:49 -06:00

273 lines
20 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\HeroSection\Infrastructure\Presentation\Admin;
use ROITheme\Component\Domain\FormBuilderInterface;
final class HeroSectionFormBuilder implements FormBuilderInterface
{
private string $componentId;
private array $data;
public function __construct(string $componentId, array $data = [])
{
$this->componentId = $componentId;
$this->data = $data;
}
public function build(): string
{
$data = $this->data;
$componentId = $this->componentId;
$html = '<div class="roi-form-builder roi-hero-section-form">';
$html .= $this->buildTabsNavigation();
$html .= '<div class="tab-content mt-3">';
$html .= $this->buildVisibilityTab($data, $componentId);
$html .= $this->buildCategoriesTab($data, $componentId);
$html .= $this->buildTitleTab($data, $componentId);
$html .= $this->buildStylesTab($data, $componentId);
$html .= '</div></div>';
$html .= $this->buildFormScripts($componentId);
return $html;
}
private function buildTabsNavigation(): string
{
return <<<HTML
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#visibility-tab" type="button">Visibilidad</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#categories-tab" type="button">Categorías</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#title-tab" type="button">Título</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#styles-tab" type="button">Estilos</button></li>
</ul>
HTML;
}
private function buildVisibilityTab(array $data, string $componentId): string
{
$html = '<div class="tab-pane fade show active" id="visibility-tab"><div class="p-3">';
$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 .= '</div></div>';
return $html;
}
private function buildCategoriesTab(array $data, string $componentId): string
{
$html = '<div class="tab-pane fade" id="categories-tab"><div class="p-3">';
$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 .= '</div></div>';
return $html;
}
private function buildTitleTab(array $data, string $componentId): string
{
$html = '<div class="tab-pane fade" id="title-tab"><div class="p-3">';
$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 .= '</div></div>';
return $html;
}
private function buildStylesTab(array $data, string $componentId): string
{
$html = '<div class="tab-pane fade" id="styles-tab"><div class="p-3">';
$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 .= '</div></div>';
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('<div class="roi-form-field mb-3"><div class="form-check form-switch"><input type="checkbox" class="form-check-input" id="%s" name="roi_component[%s][%s][%s]" value="1" %s%s><label class="form-check-label" for="%s">%s</label></div></div>', 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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label><input type="text" class="form-control" id="%s" name="roi_component[%s][%s][%s]" value="%s" placeholder="%s"%s></div>', 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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label><textarea class="form-control" id="%s" name="roi_component[%s][%s][%s]" rows="%d" placeholder="%s"%s>%s</textarea></div>', 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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$html .= sprintf('<select class="form-select" id="%s" name="roi_component[%s][%s][%s]"%s>', esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), $attrString);
foreach ($options as $optValue => $optLabel) {
$selected = ($value === $optValue) ? 'selected' : '';
$html .= sprintf('<option value="%s" %s>%s</option>', esc_attr($optValue), $selected, esc_html($optLabel));
}
$html .= '</select></div>';
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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label><input class="form-control" id="%s" name="roi_component[%s][%s][%s]" value="%s"%s></div>', 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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label><div class="input-group"><input type="color" class="form-control form-control-color" id="%s" name="roi_component[%s][%s][%s]" value="%s"><input type="text" class="form-control" value="%s" readonly></div></div>', 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('<div class="roi-form-field mb-3"><label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$html .= '<div class="input-group">';
$html .= sprintf('<input type="text" class="form-control" id="%s" name="roi_component[%s][%s][%s]" value="%s" readonly%s>', esc_attr($fieldId), esc_attr($componentId), esc_attr($group), esc_attr($name), esc_attr($value), $attrString);
$html .= sprintf('<button type="button" class="btn btn-primary roi-media-upload-btn" data-target="%s">Seleccionar</button>', esc_attr($fieldId));
$html .= '</div>';
if (!empty($value)) {
$html .= sprintf('<div class="mt-2"><img src="%s" alt="Preview" style="max-width: 300px; height: auto;"></div>', esc_url($value));
}
$html .= '</div>';
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
<script>
(function($) {
'use strict';
$(document).ready(function() {
$('[data-conditional-field]').each(function() {
const field = $(this);
const targetFieldName = field.data('conditional-field');
const targetValue = field.data('conditional-value');
const targetField = $('[name*="[' + targetFieldName + ']"]');
function updateVisibility() {
let currentValue = targetField.is(':checkbox') ? (targetField.is(':checked') ? 'true' : 'false') : targetField.val();
field.closest('.roi-form-field')[currentValue === targetValue ? 'show' : 'hide']();
}
targetField.on('change', updateVisibility);
updateVisibility();
});
$('.roi-media-upload-btn').on('click', function(e) {
e.preventDefault();
const button = $(this);
const targetId = button.data('target');
const targetField = $('#' + targetId);
const mediaUploader = wp.media({title: 'Seleccionar imagen', button: {text: 'Usar esta imagen'}, multiple: false});
mediaUploader.on('select', function() {
const attachment = mediaUploader.state().get('selection').first().toJSON();
targetField.val(attachment.url);
const preview = targetField.closest('.roi-form-field').find('img');
if (preview.length) {
preview.attr('src', attachment.url);
} else {
targetField.closest('.input-group').after('<div class="mt-2"><img src="' + attachment.url + '" alt="Preview" style="max-width: 300px; height: auto;"></div>');
}
});
mediaUploader.open();
});
$('.form-control-color').on('change', function() {
$(this).next('input[type="text"]').val($(this).val());
});
});
})(jQuery);
</script>
SCRIPT;
}
public function getComponentId(): string
{
return $this->componentId;
}
}