Plan 99.11 - Correcciones críticas: - FooterRenderer: Añadir PageVisibilityHelper::shouldShow() - HeroSectionRenderer: Añadir PageVisibilityHelper::shouldShow() - AdsensePlacementRenderer: Añadir PageVisibilityHelper::shouldShow() Mejoras adicionales: - UrlPatternExclusion: Soporte wildcards (*sct* → regex) - ExclusionFormPartial: UI mejorada con placeholders - ComponentConfiguration: Grupo _exclusions validado - 12 FormBuilders: Integración UI de exclusiones - 12 FieldMappers: Mapeo campos de exclusión Verificado: Footer oculto en post con categoría excluida SCT 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
456 lines
17 KiB
PHP
456 lines
17 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\Footer\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;
|
|
|
|
/**
|
|
* FooterRenderer - Renderiza el footer del sitio
|
|
*
|
|
* RESPONSABILIDAD: Generar HTML y CSS del footer con menus WP y newsletter
|
|
*
|
|
* SEGURIDAD:
|
|
* - Webhook URL nunca se expone al cliente
|
|
* - Escaping de todos los outputs
|
|
* - Nonce para formulario newsletter
|
|
*
|
|
* @package ROITheme\Public\Footer\Infrastructure\Ui
|
|
*/
|
|
final class FooterRenderer implements RendererInterface
|
|
{
|
|
private const NONCE_ACTION = 'roi_newsletter_nonce';
|
|
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator
|
|
) {}
|
|
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'footer';
|
|
}
|
|
|
|
public function render(Component $component): string
|
|
{
|
|
// Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
|
if (!PageVisibilityHelper::shouldShow('footer')) {
|
|
return '';
|
|
}
|
|
|
|
$data = $component->getData();
|
|
|
|
// Validar visibilidad básica
|
|
$visibility = $data['visibility'] ?? [];
|
|
if (!($visibility['is_enabled'] ?? true)) {
|
|
return '';
|
|
}
|
|
|
|
// Verificar visibilidad responsive
|
|
$showDesktop = $visibility['show_on_desktop'] ?? true;
|
|
$showMobile = $visibility['show_on_mobile'] ?? true;
|
|
|
|
if (!$showDesktop && !$showMobile) {
|
|
return '';
|
|
}
|
|
|
|
// Generar CSS
|
|
$css = $this->generateCSS($data, $showDesktop, $showMobile);
|
|
|
|
// Generar HTML
|
|
$html = $this->generateHTML($data);
|
|
|
|
// Generar JavaScript
|
|
$js = $this->generateJS($data);
|
|
|
|
return $css . $html . $js;
|
|
}
|
|
|
|
private function generateCSS(array $data, bool $showDesktop, bool $showMobile): string
|
|
{
|
|
$colors = $data['colors'] ?? [];
|
|
$spacing = $data['spacing'] ?? [];
|
|
$effects = $data['visual_effects'] ?? [];
|
|
|
|
// Valores con fallbacks
|
|
$bgColor = $colors['bg_color'] ?? '#212529';
|
|
$textColor = $colors['text_color'] ?? '#ffffff';
|
|
$titleColor = $colors['title_color'] ?? '#ffffff';
|
|
$linkColor = $colors['link_color'] ?? '#ffffff';
|
|
$linkHoverColor = $colors['link_hover_color'] ?? '#FF8600';
|
|
$inputBgColor = $colors['input_bg_color'] ?? '#ffffff';
|
|
$inputTextColor = $colors['input_text_color'] ?? '#212529';
|
|
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
|
|
$buttonBgColor = $colors['button_bg_color'] ?? '#0d6efd';
|
|
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
|
|
$buttonHoverBg = $colors['button_hover_bg'] ?? '#0b5ed7';
|
|
$borderTopColor = $colors['border_top_color'] ?? 'rgba(255, 255, 255, 0.2)';
|
|
|
|
$paddingY = $spacing['padding_y'] ?? '3rem';
|
|
$marginTop = $spacing['margin_top'] ?? '0';
|
|
$widgetTitleMb = $spacing['widget_title_margin_bottom'] ?? '1rem';
|
|
$linkMb = $spacing['link_margin_bottom'] ?? '0.5rem';
|
|
$copyrightPy = $spacing['copyright_padding_y'] ?? '1.5rem';
|
|
|
|
$inputRadius = $effects['input_border_radius'] ?? '6px';
|
|
$buttonRadius = $effects['button_border_radius'] ?? '6px';
|
|
$transition = $effects['transition_duration'] ?? '0.3s';
|
|
|
|
$cssRules = [];
|
|
|
|
// Footer principal
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer', [
|
|
'background-color' => $bgColor,
|
|
'color' => $textColor,
|
|
'padding-top' => $paddingY,
|
|
'padding-bottom' => $paddingY,
|
|
'margin-top' => $marginTop,
|
|
]);
|
|
|
|
// Grid custom para 3+3+3+4 = 13 columnas
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-grid', [
|
|
'display' => 'grid',
|
|
'grid-template-columns' => 'repeat(4, 1fr)',
|
|
'gap' => '2rem',
|
|
]);
|
|
|
|
// En desktop: distribucion 3+3+3+4
|
|
$cssRules[] = "@media (min-width: 768px) {
|
|
.roi-footer .footer-grid {
|
|
grid-template-columns: 23% 23% 23% 31%;
|
|
}
|
|
}";
|
|
|
|
// En mobile: 2 columnas
|
|
$cssRules[] = "@media (max-width: 767px) {
|
|
.roi-footer .footer-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
.roi-footer .footer-widget-newsletter {
|
|
grid-column: span 2;
|
|
}
|
|
}";
|
|
|
|
// Titulos de widgets
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .widget-title', [
|
|
'color' => $titleColor,
|
|
'font-size' => '1.25rem',
|
|
'font-weight' => '500',
|
|
'margin-bottom' => $widgetTitleMb,
|
|
]);
|
|
|
|
// Links de navegacion
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav', [
|
|
'list-style' => 'none',
|
|
'padding' => '0',
|
|
'margin' => '0',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav li', [
|
|
'margin-bottom' => $linkMb,
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a', [
|
|
'color' => $linkColor,
|
|
'text-decoration' => 'none',
|
|
'transition' => "color {$transition}",
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a:hover', [
|
|
'color' => $linkHoverColor,
|
|
]);
|
|
|
|
// Widget 1B spacing
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-widget-1b', [
|
|
'margin-top' => '1.5rem',
|
|
]);
|
|
|
|
// Newsletter description
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-description', [
|
|
'color' => $textColor,
|
|
'margin-bottom' => '1rem',
|
|
'opacity' => '0.9',
|
|
]);
|
|
|
|
// Input newsletter
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input', [
|
|
'width' => '100%',
|
|
'padding' => '0.75rem 1rem',
|
|
'background-color' => $inputBgColor,
|
|
'color' => $inputTextColor,
|
|
'border' => "1px solid {$inputBorderColor}",
|
|
'border-radius' => $inputRadius,
|
|
'margin-bottom' => '0.75rem',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input:focus', [
|
|
'outline' => 'none',
|
|
'border-color' => $buttonBgColor,
|
|
'box-shadow' => "0 0 0 0.2rem rgba(13, 110, 253, 0.25)",
|
|
]);
|
|
|
|
// Boton newsletter
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn', [
|
|
'width' => '100%',
|
|
'padding' => '0.75rem 1.5rem',
|
|
'background-color' => $buttonBgColor,
|
|
'color' => $buttonTextColor,
|
|
'border' => 'none',
|
|
'border-radius' => $buttonRadius,
|
|
'font-weight' => '500',
|
|
'cursor' => 'pointer',
|
|
'transition' => "background-color {$transition}",
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:hover', [
|
|
'background-color' => $buttonHoverBg,
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:disabled', [
|
|
'opacity' => '0.7',
|
|
'cursor' => 'not-allowed',
|
|
]);
|
|
|
|
// Mensaje newsletter
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message', [
|
|
'margin-top' => '0.75rem',
|
|
'padding' => '0.5rem',
|
|
'border-radius' => '4px',
|
|
'font-size' => '0.875rem',
|
|
'display' => 'none',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.success', [
|
|
'background-color' => '#d1e7dd',
|
|
'color' => '#0f5132',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.error', [
|
|
'background-color' => '#f8d7da',
|
|
'color' => '#842029',
|
|
]);
|
|
|
|
// Footer bottom (copyright)
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-bottom', [
|
|
'border-top' => "1px solid {$borderTopColor}",
|
|
'padding-top' => $copyrightPy,
|
|
'margin-top' => '2rem',
|
|
'text-align' => 'center',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.roi-footer .copyright-text', [
|
|
'margin' => '0',
|
|
'opacity' => '0.9',
|
|
]);
|
|
|
|
// Responsive visibility
|
|
if (!$showDesktop) {
|
|
$cssRules[] = "@media (min-width: 992px) { .roi-footer { display: none !important; } }";
|
|
}
|
|
if (!$showMobile) {
|
|
$cssRules[] = "@media (max-width: 991px) { .roi-footer { display: none !important; } }";
|
|
}
|
|
|
|
return '<style>' . implode("\n", $cssRules) . '</style>';
|
|
}
|
|
|
|
private function generateHTML(array $data): string
|
|
{
|
|
$widget1 = $data['widget_1'] ?? [];
|
|
$widget1b = $data['widget_1b'] ?? [];
|
|
$widget2 = $data['widget_2'] ?? [];
|
|
$widget3 = $data['widget_3'] ?? [];
|
|
$newsletter = $data['newsletter'] ?? [];
|
|
$footerBottom = $data['footer_bottom'] ?? [];
|
|
|
|
$widget1Visible = $this->toBool($widget1['widget_1_visible'] ?? true);
|
|
$widget2Visible = $this->toBool($widget2['widget_2_visible'] ?? true);
|
|
$widget3Visible = $this->toBool($widget3['widget_3_visible'] ?? true);
|
|
$newsletterVisible = $this->toBool($newsletter['newsletter_visible'] ?? true);
|
|
|
|
$widget1Title = esc_html($widget1['widget_1_title'] ?? 'Recursos');
|
|
$widget1bTitle = esc_html($widget1b['widget_1b_title'] ?? 'Bases de datos');
|
|
$widget2Title = esc_html($widget2['widget_2_title'] ?? 'Soporte');
|
|
$widget3Title = esc_html($widget3['widget_3_title'] ?? 'Empresa');
|
|
|
|
$newsletterTitle = esc_html($newsletter['newsletter_title'] ?? 'Suscribete al Newsletter');
|
|
$newsletterDesc = esc_html($newsletter['newsletter_description'] ?? 'Recibe las ultimas actualizaciones.');
|
|
$newsletterNamePlaceholder = esc_attr($newsletter['newsletter_name_placeholder'] ?? 'Nombre');
|
|
$newsletterEmailPlaceholder = esc_attr($newsletter['newsletter_email_placeholder'] ?? 'Email');
|
|
$newsletterWhatsappPlaceholder = esc_attr($newsletter['newsletter_whatsapp_placeholder'] ?? 'WhatsApp');
|
|
$newsletterBtnText = esc_html($newsletter['newsletter_button_text'] ?? 'Suscribirse');
|
|
|
|
$copyrightText = esc_html($footerBottom['copyright_text'] ?? date('Y') . ' Todos los derechos reservados.');
|
|
|
|
$nonce = wp_create_nonce(self::NONCE_ACTION);
|
|
$ajaxUrl = admin_url('admin-ajax.php');
|
|
|
|
$html = '<footer class="roi-footer">';
|
|
$html .= '<div class="container">';
|
|
$html .= '<div class="footer-grid">';
|
|
|
|
// Columna 1: Widget 1 + Widget 1B
|
|
if ($widget1Visible) {
|
|
$html .= '<div class="footer-column footer-column-1">';
|
|
|
|
// Widget 1
|
|
$html .= '<div class="footer-widget footer-widget-menu">';
|
|
$html .= '<span class="widget-title d-block h5">' . $widget1Title . '</span>';
|
|
$html .= $this->renderMenu('footer_menu_1');
|
|
$html .= '</div>';
|
|
|
|
// Widget 1B - Solo si tiene menu asignado
|
|
if (has_nav_menu('footer_menu_4')) {
|
|
$html .= '<div class="footer-widget footer-widget-menu footer-widget-1b">';
|
|
$html .= '<span class="widget-title d-block h5">' . $widget1bTitle . '</span>';
|
|
$html .= $this->renderMenu('footer_menu_4');
|
|
$html .= '</div>';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
}
|
|
|
|
// Widget 2
|
|
if ($widget2Visible) {
|
|
$html .= '<div class="footer-widget footer-widget-menu">';
|
|
$html .= '<span class="widget-title d-block h5">' . $widget2Title . '</span>';
|
|
$html .= $this->renderMenu('footer_menu_2');
|
|
$html .= '</div>';
|
|
}
|
|
|
|
// Widget 3
|
|
if ($widget3Visible) {
|
|
$html .= '<div class="footer-widget footer-widget-menu">';
|
|
$html .= '<span class="widget-title d-block h5">' . $widget3Title . '</span>';
|
|
$html .= $this->renderMenu('footer_menu_3');
|
|
$html .= '</div>';
|
|
}
|
|
|
|
// Widget Newsletter
|
|
if ($newsletterVisible) {
|
|
$html .= '<div class="footer-widget footer-widget-newsletter">';
|
|
$html .= '<span class="widget-title d-block h5">' . $newsletterTitle . '</span>';
|
|
$html .= '<p class="newsletter-description">' . $newsletterDesc . '</p>';
|
|
$html .= '<form id="roi-newsletter-form" class="newsletter-form">';
|
|
$html .= '<input type="hidden" name="action" value="roi_newsletter_subscribe">';
|
|
$html .= '<input type="hidden" name="nonce" value="' . esc_attr($nonce) . '">';
|
|
$html .= '<input type="text" name="name" class="newsletter-input" placeholder="' . $newsletterNamePlaceholder . '">';
|
|
$html .= '<input type="email" name="email" class="newsletter-input" placeholder="' . $newsletterEmailPlaceholder . '" required>';
|
|
$html .= '<input type="tel" name="whatsapp" class="newsletter-input" placeholder="' . $newsletterWhatsappPlaceholder . '">';
|
|
$html .= '<button type="submit" class="newsletter-btn">' . $newsletterBtnText . '</button>';
|
|
$html .= '<div class="newsletter-message"></div>';
|
|
$html .= '</form>';
|
|
$html .= '</div>';
|
|
}
|
|
|
|
$html .= '</div>'; // .footer-grid
|
|
|
|
// Footer bottom
|
|
$html .= '<div class="footer-bottom">';
|
|
$html .= '<p class="copyright-text">© ' . $copyrightText . '</p>';
|
|
$html .= '</div>';
|
|
|
|
$html .= '</div>'; // .container
|
|
$html .= '</footer>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function renderMenu(string $menuLocation): string
|
|
{
|
|
if (!has_nav_menu($menuLocation)) {
|
|
return '<p class="text-muted">Menu no asignado</p>';
|
|
}
|
|
|
|
return wp_nav_menu([
|
|
'theme_location' => $menuLocation,
|
|
'container' => false,
|
|
'menu_class' => 'footer-nav',
|
|
'fallback_cb' => false,
|
|
'echo' => false,
|
|
'depth' => 1,
|
|
]) ?: '';
|
|
}
|
|
|
|
private function generateJS(array $data): string
|
|
{
|
|
$newsletter = $data['newsletter'] ?? [];
|
|
$successMsg = esc_js($newsletter['newsletter_success_message'] ?? 'Gracias por suscribirte!');
|
|
$errorMsg = esc_js($newsletter['newsletter_error_message'] ?? 'Error al suscribirse. Intenta de nuevo.');
|
|
|
|
$ajaxUrl = admin_url('admin-ajax.php');
|
|
|
|
$js = <<<JS
|
|
<script>
|
|
(function() {
|
|
const form = document.getElementById('roi-newsletter-form');
|
|
if (!form) return;
|
|
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const btn = form.querySelector('.newsletter-btn');
|
|
const msgDiv = form.querySelector('.newsletter-message');
|
|
const emailInput = form.querySelector('input[name="email"]');
|
|
const originalText = btn.textContent;
|
|
|
|
// Reset message
|
|
msgDiv.style.display = 'none';
|
|
msgDiv.className = 'newsletter-message';
|
|
|
|
// Validate email
|
|
if (!emailInput.value || !emailInput.validity.valid) {
|
|
msgDiv.textContent = 'Por favor ingresa un email valido';
|
|
msgDiv.classList.add('error');
|
|
msgDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Disable button
|
|
btn.disabled = true;
|
|
btn.textContent = 'Enviando...';
|
|
|
|
try {
|
|
const formData = new FormData(form);
|
|
formData.append('pageUrl', window.location.href);
|
|
formData.append('pageTitle', document.title);
|
|
|
|
const response = await fetch('{$ajaxUrl}', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
msgDiv.textContent = '{$successMsg}';
|
|
msgDiv.classList.add('success');
|
|
emailInput.value = '';
|
|
} else {
|
|
msgDiv.textContent = result.data?.message || '{$errorMsg}';
|
|
msgDiv.classList.add('error');
|
|
}
|
|
} catch (error) {
|
|
msgDiv.textContent = '{$errorMsg}';
|
|
msgDiv.classList.add('error');
|
|
}
|
|
|
|
msgDiv.style.display = 'block';
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
});
|
|
})();
|
|
</script>
|
|
JS;
|
|
|
|
return $js;
|
|
}
|
|
|
|
private function toBool($value): bool
|
|
{
|
|
return $value === true || $value === '1' || $value === 1;
|
|
}
|
|
}
|