Migración completa a Clean Architecture con componentes funcionales
- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/ - 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero, FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost, RelatedPost, ContactForm, Footer - Panel de administración con tabs Bootstrap 5 funcionales - Schemas JSON para configuración de componentes - Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado) - FormBuilders para UI admin con Design System consistente - Fix: Bootstrap JS cargado en header para tabs funcionales - Fix: buildTextInput maneja valores mixed (bool/string) - Eliminación de estructura legacy (src/, admin/, assets/css/componente-*) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
360
Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
Normal file
360
Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkRenderer
|
||||
*
|
||||
* Renderizador del componente CTA "Let's Talk" para el frontend.
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar botón CTA "Let's Talk" en el navbar
|
||||
* - Delegar generación de CSS a CSSGeneratorInterface
|
||||
* - Validar visibilidad (is_enabled, show_on_pages, show_on_desktop, show_on_mobile)
|
||||
* - Manejar visibilidad responsive con clases Bootstrap
|
||||
* - Generar atributos para modal o URL personalizada
|
||||
* - Sanitizar todos los outputs
|
||||
*
|
||||
* NO responsable de:
|
||||
* - Generar string CSS (delega a CSSGeneratorService)
|
||||
* - Persistir datos (ya están en Component)
|
||||
* - Lógica de negocio (está en Domain)
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar este componente)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\CtaLetsTalk\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaLetsTalkRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->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(
|
||||
"<style>%s</style>\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</%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(
|
||||
'<i class="bi %s"></i>',
|
||||
esc_attr($iconClass)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user