Files
roi-theme/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
FrankZamora ffe6ea8e65 feat(visibility): añadir opción "Ocultar para usuarios logueados" (Plan 99.16)
- Crear UserVisibilityHelper centralizado en Shared/Infrastructure/Services
- Añadir campo hide_for_logged_in en schemas de 4 componentes
- Integrar validación en Renderers: TopBar, LetsTalk, CTASidebar, CTAPost
- Añadir checkbox UI en FormBuilders de los 4 componentes
- Refactorizar adsense-placement.php para usar el helper centralizado
- Deprecar función roi_should_hide_for_logged_in() (backwards compatible)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:28:53 -06:00

289 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Public\CtaBoxSidebar\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;
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
/**
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
*
* RESPONSABILIDAD: Generar HTML y CSS del CTA Box Sidebar
*
* CARACTERISTICAS:
* - Titulo configurable
* - Descripcion configurable
* - Boton con icono y multiples acciones (modal, link, scroll)
* - Estilos 100% desde BD via CSSGenerator
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar CTA box)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui
*/
final class CtaBoxSidebarRenderer implements RendererInterface
{
/**
* Nombre del componente para visibilidad
* Evita strings hardcodeados y facilita mantenimiento
*/
private const COMPONENT_NAME = 'cta-box-sidebar';
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
// Evaluar visibilidad por tipo de página (usa Helper, NO cambia constructor)
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
return '';
}
// Validar visibilidad por usuario logueado
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
$script = $this->buildScript();
return sprintf("<style>%s</style>\n%s\n%s", $css, $html, $script);
}
public function supports(string $componentType): bool
{
return $componentType === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$typography = $data['typography'] ?? [];
$effects = $data['visual_effects'] ?? [];
$behavior = $data['behavior'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
// Container styles - Match template exactly (height: 250px, flexbox centering)
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar', [
'background' => $colors['background_color'] ?? '#FF8600',
'border-radius' => $effects['border_radius'] ?? '8px',
'padding' => $spacing['container_padding'] ?? '24px',
'text-align' => $behavior['text_align'] ?? 'center',
'box-shadow' => $effects['box_shadow'] ?? '0 4px 12px rgba(255, 133, 0, 0.2)',
'margin-top' => '0',
'margin-bottom' => '15px',
'height' => '250px',
'display' => 'flex',
'flex-direction' => 'column',
'justify-content' => 'center',
]);
// Title styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-title', [
'color' => $colors['title_color'] ?? '#ffffff',
'font-weight' => $typography['title_font_weight'] ?? '700',
'font-size' => $typography['title_font_size'] ?? '1.25rem',
'margin-bottom' => $spacing['title_margin_bottom'] ?? '1rem',
'margin-top' => '0',
]);
// Description styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-text', [
'color' => $colors['description_color'] ?? 'rgba(255, 255, 255, 0.95)',
'font-size' => $typography['description_font_size'] ?? '0.9rem',
'margin-bottom' => $spacing['description_margin_bottom'] ?? '1rem',
]);
// Button styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box', [
'background-color' => $colors['button_background_color'] ?? '#ffffff',
'color' => $colors['button_text_color'] ?? '#FF8600',
'font-weight' => $typography['button_font_weight'] ?? '700',
'font-size' => $typography['button_font_size'] ?? '1rem',
'border' => 'none',
'padding' => $spacing['button_padding'] ?? '0.75rem 1.5rem',
'border-radius' => $effects['button_border_radius'] ?? '8px',
'transition' => "all {$transitionDuration} ease",
'cursor' => 'pointer',
'display' => 'inline-flex',
'align-items' => 'center',
'justify-content' => 'center',
'width' => '100%',
]);
// Button hover styles (template uses --color-navy-primary = #1e3a5f)
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box:hover', [
'background-color' => $colors['button_hover_background'] ?? '#1e3a5f',
'color' => $colors['button_hover_text_color'] ?? '#ffffff',
]);
// Button icon spacing
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box i', [
'margin-right' => $spacing['icon_margin_right'] ?? '0.5rem',
]);
// Responsive visibility
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnMobile = $visibility['show_on_mobile'] ?? false;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.cta-box-sidebar { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.cta-box-sidebar { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$title = $content['title'] ?? '¿Listo para potenciar tus proyectos?';
$description = $content['description'] ?? 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.';
$buttonText = $content['button_text'] ?? 'Solicitar Demo';
$buttonIcon = $content['button_icon'] ?? 'bi bi-calendar-check';
$buttonAction = $content['button_action'] ?? 'modal';
$buttonLink = $content['button_link'] ?? '#contactModal';
// Build button attributes based on action type
$buttonAttributes = $this->getButtonAttributes($buttonAction, $buttonLink);
$html = '<div class="cta-box-sidebar">';
// Title (span instead of h5 for semantic heading hierarchy - Phase 4.4 Accessibility)
$html .= sprintf(
'<span class="cta-box-title d-block h5">%s</span>',
esc_html($title)
);
// Description
$html .= sprintf(
'<p class="cta-box-text">%s</p>',
esc_html($description)
);
// Button/Link
$iconHtml = !empty($buttonIcon)
? sprintf('<i class="%s"></i>', esc_attr($buttonIcon))
: '';
// Use <a> for link action, <button> for modal/scroll
if ($buttonAction === 'link') {
$html .= sprintf(
'<a href="%s" class="btn btn-cta-box">%s%s</a>',
esc_url($buttonLink),
$iconHtml,
esc_html($buttonText)
);
} else {
$html .= sprintf(
'<button class="btn btn-cta-box" %s>%s%s</button>',
$buttonAttributes,
$iconHtml,
esc_html($buttonText)
);
}
$html .= '</div>';
return $html;
}
private function getButtonAttributes(string $action, string $link): string
{
switch ($action) {
case 'modal':
// Extract modal ID from link (e.g., #contactModal -> contactModal)
$modalId = ltrim($link, '#');
return sprintf(
'type="button" data-bs-toggle="modal" data-bs-target="#%s"',
esc_attr($modalId)
);
case 'link':
return sprintf(
'type="button" data-cta-action="link" data-cta-href="%s"',
esc_url($link)
);
case 'scroll':
$targetId = ltrim($link, '#');
return sprintf(
'type="button" data-cta-action="scroll" data-cta-target="%s"',
esc_attr($targetId)
);
default:
return 'type="button"';
}
}
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 '';
}
private function buildScript(): string
{
return <<<JS
<script>
document.addEventListener('DOMContentLoaded', function() {
var ctaButtons = document.querySelectorAll('.btn-cta-box[data-cta-action]');
ctaButtons.forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.getAttribute('data-cta-action');
if (action === 'link') {
var href = this.getAttribute('data-cta-href');
if (href) window.location.href = href;
} else if (action === 'scroll') {
var target = this.getAttribute('data-cta-target');
var el = document.getElementById(target);
if (el) el.scrollIntoView({behavior: 'smooth'});
}
});
});
});
</script>
JS;
}
}