- Añadir PageVisibility use case y repositorio - Implementar PageTypeDetector para detectar home/single/page/archive - Actualizar FieldMappers con soporte show_on_[page_type] - Extender FormBuilders con UI de visibilidad por página - Refactorizar Renderers para evaluar visibilidad dinámica - Limpiar schemas removiendo campos de visibilidad legacy - Añadir MigrationCommand para migrar configuraciones existentes - Implementar adsense-loader.js para carga lazy de ads - Actualizar front-page.php con nueva estructura - Extender DIContainer con nuevos servicios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
345 lines
10 KiB
PHP
345 lines
10 KiB
PHP
<?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;
|
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
private const COMPONENT_NAME = 'cta-lets-talk';
|
|
|
|
/**
|
|
* @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 (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
|
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 === self::COMPONENT_NAME;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public 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)
|
|
);
|
|
}
|
|
}
|