PROBLEMA: - wp_head() se ejecuta ANTES de que los componentes rendericen - CriticalCSSCollector estaba vacío al momento de inyectar en <head> - Componentes retornaban solo HTML sin CSS → sitio sin estilos SOLUCIÓN: - Eliminar lógica condicional de is_critical en render() - Siempre incluir CSS inline con el componente (comportamiento anterior) - Campo is_critical reservado para futura implementación con output buffering Archivos modificados: - NavbarRenderer.php - TopNotificationBarRenderer.php - HeroRenderer.php 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
471 lines
14 KiB
PHP
471 lines
14 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
|
use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface;
|
|
use ROITheme\Shared\Domain\Entities\Component;
|
|
|
|
/**
|
|
* Class TopNotificationBarRenderer
|
|
*
|
|
* Renderizador del componente Top Notification Bar para el frontend.
|
|
*
|
|
* Responsabilidades:
|
|
* - Renderizar HTML del componente top-notification-bar
|
|
* - Delegar generación de CSS a CSSGeneratorInterface
|
|
* - Validar visibilidad (is_enabled, show_on_pages, hide_on_mobile)
|
|
* - Manejar visibilidad responsive con clases Bootstrap
|
|
* - Generar script para funcionalidad dismissible
|
|
* - 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\TopNotificationBar\Infrastructure\Ui
|
|
*/
|
|
final class TopNotificationBarRenderer implements RendererInterface
|
|
{
|
|
/**
|
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
|
* @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico
|
|
*/
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator,
|
|
private CriticalCSSCollectorInterface $criticalCollector
|
|
) {}
|
|
|
|
/**
|
|
* {@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);
|
|
|
|
// Siempre incluir CSS inline con el componente
|
|
// Nota: is_critical se reserva para futura implementación con output buffering
|
|
return sprintf(
|
|
"<style>%s</style>\n%s",
|
|
$css,
|
|
$html
|
|
);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'top-notification-bar';
|
|
}
|
|
|
|
/**
|
|
* 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(),
|
|
'custom' => $this->isInCustomPages($data),
|
|
default => true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verificar si está en páginas personalizadas
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return bool
|
|
*/
|
|
private function isInCustomPages(array $data): bool
|
|
{
|
|
$pageIds = $data['visibility']['custom_page_ids'] ?? '';
|
|
|
|
if (empty($pageIds)) {
|
|
return false;
|
|
}
|
|
|
|
$allowedIds = array_map('trim', explode(',', $pageIds));
|
|
$currentId = (string) get_the_ID();
|
|
|
|
return in_array($currentId, $allowedIds, true);
|
|
}
|
|
|
|
/**
|
|
* Verificar si el componente fue dismissed por el usuario
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return bool
|
|
*/
|
|
private function isDismissed(array $data): bool
|
|
{
|
|
if (!$this->isDismissible($data)) {
|
|
return false;
|
|
}
|
|
|
|
$cookieName = 'roi_notification_bar_dismissed';
|
|
return isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === '1';
|
|
}
|
|
|
|
/**
|
|
* Verificar si el componente es dismissible
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return bool
|
|
*/
|
|
private function isDismissible(array $data): bool
|
|
{
|
|
return ($data['behavior']['is_dismissible'] ?? false) === true;
|
|
}
|
|
|
|
/**
|
|
* Generar CSS usando CSSGeneratorService
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string CSS generado
|
|
*/
|
|
private function generateCSS(array $data): string
|
|
{
|
|
$css = '';
|
|
|
|
// Estilos base de la barra
|
|
$baseStyles = [
|
|
'background_color' => $data['styles']['background_color'] ?? '#0E2337',
|
|
'color' => $data['styles']['text_color'] ?? '#FFFFFF',
|
|
'font_size' => $data['styles']['font_size'] ?? '0.9rem',
|
|
'padding' => $data['styles']['padding'] ?? '0.5rem 0',
|
|
'width' => '100%',
|
|
'z_index' => '1050',
|
|
];
|
|
$css .= $this->cssGenerator->generate('.top-notification-bar', $baseStyles);
|
|
|
|
// Estilos del ícono
|
|
$iconStyles = [
|
|
'color' => $data['styles']['icon_color'] ?? '#FF8600',
|
|
];
|
|
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-icon', $iconStyles);
|
|
|
|
// Estilos de la etiqueta (label)
|
|
$labelStyles = [
|
|
'color' => $data['styles']['label_color'] ?? '#FF8600',
|
|
];
|
|
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-label', $labelStyles);
|
|
|
|
// Estilos del enlace
|
|
$linkStyles = [
|
|
'color' => $data['styles']['link_color'] ?? '#FFFFFF',
|
|
];
|
|
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link', $linkStyles);
|
|
|
|
// Estilos del enlace hover
|
|
$linkHoverStyles = [
|
|
'color' => $data['styles']['link_hover_color'] ?? '#FF8600',
|
|
];
|
|
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link:hover', $linkHoverStyles);
|
|
|
|
// Estilos del ícono personalizado
|
|
$customIconStyles = [
|
|
'width' => '24px',
|
|
'height' => '24px',
|
|
];
|
|
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .custom-icon', $customIconStyles);
|
|
|
|
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);
|
|
$content = $this->buildContent($data);
|
|
|
|
return sprintf(
|
|
'<div class="%s">%s</div>',
|
|
esc_attr($classes),
|
|
$content
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Construir clases CSS del componente
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string Clases CSS
|
|
*/
|
|
private function buildClasses(array $data): string
|
|
{
|
|
return 'top-notification-bar';
|
|
}
|
|
|
|
/**
|
|
* Construir atributos data para dismissible
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string Atributos HTML
|
|
*/
|
|
private function buildDismissAttributes(array $data): string
|
|
{
|
|
if (!$this->isDismissible($data)) {
|
|
return '';
|
|
}
|
|
|
|
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
|
|
return sprintf(' data-dismissible-days="%d"', $days);
|
|
}
|
|
|
|
/**
|
|
* Construir contenido del componente
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string HTML del contenido
|
|
*/
|
|
private function buildContent(array $data): string
|
|
{
|
|
$html = '<div class="container">';
|
|
$html .= '<div class="d-flex align-items-center justify-content-center">';
|
|
|
|
// Ícono
|
|
$html .= $this->buildIcon($data);
|
|
|
|
// Texto del anuncio
|
|
$html .= $this->buildAnnouncementText($data);
|
|
|
|
// Enlace
|
|
$html .= $this->buildLink($data);
|
|
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Construir ícono del componente
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string HTML del ícono
|
|
*/
|
|
private function buildIcon(array $data): string
|
|
{
|
|
// Siempre usar Bootstrap icon desde content.icon_class
|
|
$iconClass = $data['content']['icon_class'] ?? 'bi-megaphone-fill';
|
|
|
|
// Asegurar prefijo 'bi-'
|
|
if (strpos($iconClass, 'bi-') !== 0) {
|
|
$iconClass = 'bi-' . $iconClass;
|
|
}
|
|
|
|
return sprintf(
|
|
'<i class="bi %s notification-icon me-2"></i>',
|
|
esc_attr($iconClass)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Construir texto del anuncio
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string HTML del texto
|
|
*/
|
|
private function buildAnnouncementText(array $data): string
|
|
{
|
|
$label = $data['content']['label_text'] ?? '';
|
|
$text = $data['content']['message_text'] ?? '';
|
|
|
|
if (empty($text)) {
|
|
return '';
|
|
}
|
|
|
|
$html = '<span>';
|
|
|
|
if (!empty($label)) {
|
|
$html .= sprintf('<strong class="notification-label">%s</strong> ', esc_html($label));
|
|
}
|
|
|
|
$html .= esc_html($text);
|
|
$html .= '</span>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Construir enlace de acción
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string HTML del enlace
|
|
*/
|
|
private function buildLink(array $data): string
|
|
{
|
|
$linkText = $data['content']['link_text'] ?? '';
|
|
$linkUrl = $data['content']['link_url'] ?? '#';
|
|
|
|
if (empty($linkText)) {
|
|
return '';
|
|
}
|
|
|
|
return sprintf(
|
|
'<a href="%s" class="notification-link ms-2 text-decoration-underline">%s</a>',
|
|
esc_url($linkUrl),
|
|
esc_html($linkText)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Construir botón de cerrar
|
|
*
|
|
* @return string HTML del botón
|
|
*/
|
|
private function buildDismissButton(): string
|
|
{
|
|
return '<button type="button" class="btn-close btn-close-white ms-3 roi-dismiss-notification" aria-label="Cerrar"></button>';
|
|
}
|
|
|
|
/**
|
|
* Construir estilos CSS de animaciones
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string CSS de animaciones
|
|
*/
|
|
private function buildAnimationStyles(array $data): string
|
|
{
|
|
$animationType = $data['visual_effects']['animation_type'] ?? 'slide-down';
|
|
|
|
$animations = [
|
|
'slide-down' => [
|
|
'keyframes' => '@keyframes roiSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }',
|
|
'animation' => 'roiSlideDown 0.5s ease-out',
|
|
],
|
|
'fade-in' => [
|
|
'keyframes' => '@keyframes roiFadeIn { from { opacity: 0; } to { opacity: 1; } }',
|
|
'animation' => 'roiFadeIn 0.5s ease-out',
|
|
],
|
|
];
|
|
|
|
$anim = $animations[$animationType] ?? $animations['slide-down'];
|
|
|
|
return sprintf(
|
|
"%s\n.top-notification-bar.roi-animated.roi-%s { animation: %s; }",
|
|
$anim['keyframes'],
|
|
$animationType,
|
|
$anim['animation']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Construir script para funcionalidad dismissible
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string JavaScript
|
|
*/
|
|
private function buildDismissScript(array $data): string
|
|
{
|
|
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
|
|
|
|
return sprintf(
|
|
'<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const dismissBtn = document.querySelector(".roi-dismiss-notification");
|
|
if (dismissBtn) {
|
|
dismissBtn.addEventListener("click", function() {
|
|
const bar = document.querySelector(".top-notification-bar");
|
|
if (bar) {
|
|
bar.style.display = "none";
|
|
}
|
|
|
|
const days = %d;
|
|
const date = new Date();
|
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
const expires = "expires=" + date.toUTCString();
|
|
document.cookie = "roi_notification_bar_dismissed=1;" + expires + ";path=/";
|
|
});
|
|
}
|
|
});
|
|
</script>',
|
|
$days
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
|
|
*
|
|
* Implementa tabla de decisión según especificación (10.03):
|
|
* - Desktop Y Mobile = null (visible en ambos)
|
|
* - Solo Desktop = 'd-none d-lg-block'
|
|
* - Solo Mobile = 'd-lg-none'
|
|
* - Ninguno = 'd-none' (oculto)
|
|
*
|
|
* @param bool $desktop Mostrar en desktop
|
|
* @param bool $mobile Mostrar en mobile
|
|
* @return string|null Clases CSS o null si visible en ambos
|
|
*/
|
|
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
|
{
|
|
// Desktop Y Mobile = visible en ambos dispositivos
|
|
if ($desktop && $mobile) {
|
|
return null; // Sin clases = visible siempre
|
|
}
|
|
|
|
// Solo Desktop
|
|
if ($desktop && !$mobile) {
|
|
return 'd-none d-lg-block';
|
|
}
|
|
|
|
// Solo Mobile
|
|
if (!$desktop && $mobile) {
|
|
return 'd-lg-none';
|
|
}
|
|
|
|
// Ninguno = oculto completamente
|
|
return 'd-none';
|
|
}
|
|
}
|