- 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>
273 lines
20 KiB
PHP
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;
|
|
}
|
|
}
|