fix(structure): Correct case-sensitivity for Linux compatibility

Rename folders to match PHP PSR-4 autoloading conventions:
- schemas → Schemas
- shared → Shared
- Wordpress → WordPress (in all locations)

Fixes deployment issues on Linux servers where filesystem is case-sensitive.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-26 22:53:34 -06:00
parent a2548ab5c2
commit 90863cd8f5
92 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,697 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\UI;
use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Shared\Domain\Contracts\FormBuilderInterface;
/**
* TopNotificationBarFormBuilder - Construye formulario de configuración
*
* RESPONSABILIDAD: Generar formulario HTML del admin para Top Notification Bar
*
* CARACTERÍSTICAS:
* - 3 secciones: Visibilidad, Contenido, Estilos
* - 19 campos configurables
* - Lógica condicional (data-conditional-field)
* - WordPress Media Library integration
* - Vista previa en tiempo real
*
* @package ROITheme\Shared\Infrastructure\UI
*/
final class TopNotificationBarFormBuilder implements FormBuilderInterface
{
public function build(Component $component): string
{
$data = $component->getData();
$componentId = $component->getName();
$html = '<div class="roi-form-builder roi-top-notification-bar-form">';
// Sección de Visibilidad
$html .= $this->buildVisibilitySection($data, $componentId);
// Sección de Contenido
$html .= $this->buildContentSection($data, $componentId);
// Sección de Estilos
$html .= $this->buildStylesSection($data, $componentId);
// Vista previa
$html .= $this->buildPreviewSection($data);
$html .= '</div>';
// Agregar scripts de formulario
$html .= $this->buildFormScripts($componentId);
return $html;
}
private function buildVisibilitySection(array $data, string $componentId): string
{
$html = '<div class="roi-form-section" data-section="visibility">';
$html .= '<h3 class="roi-form-section-title">Visibilidad</h3>';
$html .= '<div class="roi-form-section-content">';
// Is Enabled
$isEnabled = $data['visibility']['is_enabled'] ?? true;
$html .= $this->buildToggle(
'is_enabled',
'Mostrar barra de notificación',
$isEnabled,
$componentId,
'Activa o desactiva la barra de notificación superior'
);
// Show On Pages
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
$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' => 'Páginas específicas'
],
$componentId,
'Define en qué páginas se mostrará la barra'
);
// Custom Page IDs
$customPageIds = $data['visibility']['custom_page_ids'] ?? '';
$html .= $this->buildTextField(
'custom_page_ids',
'IDs de páginas específicas',
$customPageIds,
$componentId,
'IDs de páginas separados por comas',
'Ej: 1,5,10',
['data-conditional-field' => 'show_on_pages', 'data-conditional-value' => 'custom']
);
// Hide On Mobile
$hideOnMobile = $data['visibility']['hide_on_mobile'] ?? false;
$html .= $this->buildToggle(
'hide_on_mobile',
'Ocultar en dispositivos móviles',
$hideOnMobile,
$componentId,
'Oculta la barra en pantallas menores a 768px'
);
// Is Dismissible
$isDismissible = $data['visibility']['is_dismissible'] ?? false;
$html .= $this->buildToggle(
'is_dismissible',
'Permitir cerrar',
$isDismissible,
$componentId,
'Agrega botón X para que el usuario pueda cerrar la barra'
);
// Dismissible Cookie Days
$cookieDays = $data['visibility']['dismissible_cookie_days'] ?? 7;
$html .= $this->buildNumberField(
'dismissible_cookie_days',
'Días antes de volver a mostrar',
$cookieDays,
$componentId,
'Días que permanece oculta después de cerrarla',
1,
365,
['data-conditional-field' => 'is_dismissible', 'data-conditional-value' => 'true']
);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildContentSection(array $data, string $componentId): string
{
$html = '<div class="roi-form-section" data-section="content">';
$html .= '<h3 class="roi-form-section-title">Contenido</h3>';
$html .= '<div class="roi-form-section-content">';
// Icon Type
$iconType = $data['content']['icon_type'] ?? 'bootstrap';
$html .= $this->buildSelect(
'icon_type',
'Tipo de ícono',
$iconType,
[
'bootstrap' => 'Bootstrap Icons',
'custom' => 'Imagen personalizada',
'none' => 'Sin ícono'
],
$componentId,
'Selecciona el tipo de ícono a mostrar'
);
// Bootstrap Icon
$bootstrapIcon = $data['content']['bootstrap_icon'] ?? 'bi-megaphone-fill';
$html .= $this->buildTextField(
'bootstrap_icon',
'Clase de ícono Bootstrap',
$bootstrapIcon,
$componentId,
'Nombre de la clase del ícono sin el prefijo \'bi\' (ej: megaphone-fill)',
'Ej: bi-megaphone-fill',
['data-conditional-field' => 'icon_type', 'data-conditional-value' => 'bootstrap']
);
// Custom Icon URL
$customIconUrl = $data['content']['custom_icon_url'] ?? '';
$html .= $this->buildMediaField(
'custom_icon_url',
'Imagen personalizada',
$customIconUrl,
$componentId,
'Sube una imagen personalizada (recomendado: PNG 24x24px)',
['data-conditional-field' => 'icon_type', 'data-conditional-value' => 'custom']
);
// Announcement Label
$announcementLabel = $data['content']['announcement_label'] ?? 'Nuevo:';
$html .= $this->buildTextField(
'announcement_label',
'Etiqueta del anuncio',
$announcementLabel,
$componentId,
'Texto destacado en negrita antes del mensaje',
'Ej: Nuevo:, Importante:, Aviso:'
);
// Announcement Text
$announcementText = $data['content']['announcement_text'] ?? 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
$html .= $this->buildTextArea(
'announcement_text',
'Texto del anuncio',
$announcementText,
$componentId,
'Mensaje principal del anuncio (máximo 200 caracteres)',
3
);
// Link Enabled
$linkEnabled = $data['content']['link_enabled'] ?? true;
$html .= $this->buildToggle(
'link_enabled',
'Mostrar enlace',
$linkEnabled,
$componentId,
'Activa o desactiva el enlace de acción'
);
// Link Text
$linkText = $data['content']['link_text'] ?? 'Ver Catálogo';
$html .= $this->buildTextField(
'link_text',
'Texto del enlace',
$linkText,
$componentId,
'Texto del enlace de acción',
'',
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
);
// Link URL
$linkUrl = $data['content']['link_url'] ?? '#';
$html .= $this->buildUrlField(
'link_url',
'URL del enlace',
$linkUrl,
$componentId,
'URL de destino del enlace',
'https://',
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
);
// Link Target
$linkTarget = $data['content']['link_target'] ?? '_self';
$html .= $this->buildSelect(
'link_target',
'Abrir enlace en',
$linkTarget,
[
'_self' => 'Misma ventana',
'_blank' => 'Nueva ventana'
],
$componentId,
'Define cómo se abrirá el enlace',
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildStylesSection(array $data, string $componentId): string
{
$html = '<div class="roi-form-section" data-section="styles">';
$html .= '<h3 class="roi-form-section-title">Estilos</h3>';
$html .= '<div class="roi-form-section-content">';
// Background Color
$bgColor = $data['styles']['background_color'] ?? '#FF8600';
$html .= $this->buildColorField(
'background_color',
'Color de fondo',
$bgColor,
$componentId,
'Color de fondo de la barra (por defecto: orange primary)'
);
// Text Color
$textColor = $data['styles']['text_color'] ?? '#FFFFFF';
$html .= $this->buildColorField(
'text_color',
'Color del texto',
$textColor,
$componentId,
'Color del texto del anuncio'
);
// Link Color
$linkColor = $data['styles']['link_color'] ?? '#FFFFFF';
$html .= $this->buildColorField(
'link_color',
'Color del enlace',
$linkColor,
$componentId,
'Color del enlace de acción'
);
// Font Size
$fontSize = $data['styles']['font_size'] ?? 'small';
$html .= $this->buildSelect(
'font_size',
'Tamaño de fuente',
$fontSize,
[
'extra-small' => 'Muy pequeño (0.75rem)',
'small' => 'Pequeño (0.875rem)',
'normal' => 'Normal (1rem)',
'large' => 'Grande (1.125rem)'
],
$componentId,
'Tamaño del texto del anuncio'
);
// Padding Vertical
$padding = $data['styles']['padding_vertical'] ?? 'normal';
$html .= $this->buildSelect(
'padding_vertical',
'Padding vertical',
$padding,
[
'compact' => 'Compacto (0.5rem)',
'normal' => 'Normal (0.75rem)',
'spacious' => 'Espacioso (1rem)'
],
$componentId,
'Espaciado vertical interno de la barra'
);
// Text Alignment
$alignment = $data['styles']['text_alignment'] ?? 'center';
$html .= $this->buildSelect(
'text_alignment',
'Alineación del texto',
$alignment,
[
'left' => 'Izquierda',
'center' => 'Centro',
'right' => 'Derecha'
],
$componentId,
'Alineación del contenido de la barra'
);
// Animation Enabled
$animationEnabled = $data['styles']['animation_enabled'] ?? false;
$html .= $this->buildToggle(
'animation_enabled',
'Activar animación',
$animationEnabled,
$componentId,
'Activa animación de entrada al cargar la página'
);
// Animation Type
$animationType = $data['styles']['animation_type'] ?? 'slide-down';
$html .= $this->buildSelect(
'animation_type',
'Tipo de animación',
$animationType,
[
'slide-down' => 'Deslizar desde arriba',
'fade-in' => 'Aparecer gradualmente'
],
$componentId,
'Tipo de animación de entrada',
['data-conditional-field' => 'animation_enabled', 'data-conditional-value' => 'true']
);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildPreviewSection(array $data): string
{
$html = '<div class="roi-form-section roi-preview-section">';
$html .= '<h3 class="roi-form-section-title">Vista Previa</h3>';
$html .= '<div class="roi-form-section-content">';
$html .= '<div id="roi-component-preview" class="border rounded p-3 bg-light">';
$html .= '<p class="text-muted">La vista previa se actualizará automáticamente al modificar los campos.</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildToggle(string $name, string $label, bool $value, string $componentId, string $description = ''): string
{
$fieldId = "roi_{$componentId}_{$name}";
$checked = $value ? 'checked' : '';
$html = '<div class="roi-form-field roi-form-field-toggle mb-3">';
$html .= '<div class="form-check form-switch">';
$html .= sprintf(
'<input type="checkbox" class="form-check-input" id="%s" name="roi_component[%s][%s]" value="1" %s>',
esc_attr($fieldId),
esc_attr($componentId),
esc_attr($name),
$checked
);
$html .= sprintf('<label class="form-check-label" for="%s">%s</label>', esc_attr($fieldId), esc_html($label));
$html .= '</div>';
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildTextField(string $name, string $label, string $value, string $componentId, string $description = '', string $placeholder = '', array $attrs = []): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-text mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$attrString = $this->buildAttributesString($attrs);
$html .= sprintf(
'<input type="text" class="form-control" id="%s" name="roi_component[%s][%s]" value="%s" placeholder="%s"%s>',
esc_attr($fieldId),
esc_attr($componentId),
esc_attr($name),
esc_attr($value),
esc_attr($placeholder),
$attrString
);
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildTextArea(string $name, string $label, string $value, string $componentId, string $description = '', int $rows = 3, array $attrs = []): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-textarea mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$attrString = $this->buildAttributesString($attrs);
$html .= sprintf(
'<textarea class="form-control" id="%s" name="roi_component[%s][%s]" rows="%d"%s>%s</textarea>',
esc_attr($fieldId),
esc_attr($componentId),
esc_attr($name),
$rows,
$attrString,
esc_textarea($value)
);
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildSelect(string $name, string $label, string $value, array $options, string $componentId, string $description = '', array $attrs = []): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-select mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$attrString = $this->buildAttributesString($attrs);
$html .= sprintf(
'<select class="form-select" id="%s" name="roi_component[%s][%s]"%s>',
esc_attr($fieldId),
esc_attr($componentId),
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>';
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildNumberField(string $name, string $label, $value, string $componentId, string $description = '', int $min = null, int $max = null, array $attrs = []): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-number mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$attrs['type'] = 'number';
if ($min !== null) {
$attrs['min'] = $min;
}
if ($max !== null) {
$attrs['max'] = $max;
}
$attrString = $this->buildAttributesString($attrs);
$html .= sprintf(
'<input class="form-control" id="%s" name="roi_component[%s][%s]" value="%s"%s>',
esc_attr($fieldId),
esc_attr($componentId),
esc_attr($name),
esc_attr($value),
$attrString
);
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildUrlField(string $name, string $label, string $value, string $componentId, string $description = '', string $placeholder = '', array $attrs = []): string
{
$attrs['type'] = 'url';
return $this->buildTextField($name, $label, $value, $componentId, $description, $placeholder, $attrs);
}
private function buildColorField(string $name, string $label, string $value, string $componentId, string $description = ''): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-color mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$html .= '<div class="input-group">';
$html .= sprintf(
'<input type="color" class="form-control form-control-color" id="%s" name="roi_component[%s][%s]" value="%s">',
esc_attr($fieldId),
esc_attr($componentId),
esc_attr($name),
esc_attr($value)
);
$html .= sprintf(
'<input type="text" class="form-control" value="%s" readonly>',
esc_attr($value)
);
$html .= '</div>';
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$html .= '</div>';
return $html;
}
private function buildMediaField(string $name, string $label, string $value, string $componentId, string $description = '', array $attrs = []): string
{
$fieldId = "roi_{$componentId}_{$name}";
$html = '<div class="roi-form-field roi-form-field-media mb-3">';
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
$html .= '<div class="input-group">';
$attrString = $this->buildAttributesString($attrs);
$html .= sprintf(
'<input type="text" class="form-control" id="%s" name="roi_component[%s][%s]" value="%s" readonly%s>',
esc_attr($fieldId),
esc_attr($componentId),
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: 100px; height: auto;"></div>', esc_url($value));
}
if (!empty($description)) {
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
}
$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() {
// Conditional logic
$('[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;
if (targetField.is(':checkbox')) {
currentValue = targetField.is(':checked') ? 'true' : 'false';
} else {
currentValue = targetField.val();
}
if (currentValue === targetValue) {
field.closest('.roi-form-field').show();
} else {
field.closest('.roi-form-field').hide();
}
}
targetField.on('change', updateVisibility);
updateVisibility();
});
// Media upload
$('.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-media').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: 100px; height: auto;"></div>');
}
});
mediaUploader.open();
});
// Color picker sync
$('.form-control-color').on('change', function() {
$(this).next('input[type="text"]').val($(this).val());
});
// Auto-update preview
$('.roi-form-field input, .roi-form-field select, .roi-form-field textarea').on('change keyup', function() {
updatePreview();
});
function updatePreview() {
// Aquí iría la lógica para actualizar la vista previa en tiempo real
console.log('Preview updated');
}
});
})(jQuery);
</script>
SCRIPT;
}
public function supports(string $componentType): bool
{
return $componentType === 'top-notification-bar';
}
}