Phase 4.4 Accessibility fixes: - ContactFormRenderer: Change h6 info-labels to span (WhatsApp, Email, Location) - RelatedPostRenderer: Change h5 card-title to span (semantic hierarchy) - top-notification-bar schema: Change link_url default from # to /suscripcion-vip (identical links must have same destination) Fixes: "Headings not in sequential order" and "Identical links have different purposes" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
381 lines
12 KiB
PHP
381 lines
12 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
|
use ROITheme\Shared\Domain\Entities\Component;
|
|
|
|
/**
|
|
* RelatedPostRenderer - Renderiza seccion de posts relacionados
|
|
*
|
|
* RESPONSABILIDAD: Generar HTML y CSS del componente Related Posts
|
|
*
|
|
* CARACTERISTICAS:
|
|
* - Grid responsive de cards
|
|
* - Query dinamica de posts
|
|
* - Paginacion Bootstrap
|
|
* - Estilos 100% desde BD via CSSGenerator
|
|
*
|
|
* @package ROITheme\Public\RelatedPost\Infrastructure\Ui
|
|
*/
|
|
final class RelatedPostRenderer implements RendererInterface
|
|
{
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator
|
|
) {}
|
|
|
|
public function render(Component $component): string
|
|
{
|
|
$data = $component->getData();
|
|
|
|
if (!$this->isEnabled($data)) {
|
|
return '';
|
|
}
|
|
|
|
if (!$this->shouldShowOnCurrentPage($data)) {
|
|
return '';
|
|
}
|
|
|
|
$visibilityClass = $this->getVisibilityClass($data);
|
|
if ($visibilityClass === null) {
|
|
return '';
|
|
}
|
|
|
|
$css = $this->generateCSS($data);
|
|
$html = $this->buildHTML($data, $visibilityClass);
|
|
|
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
|
}
|
|
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'related-post';
|
|
}
|
|
|
|
private function isEnabled(array $data): bool
|
|
{
|
|
$value = $data['visibility']['is_enabled'] ?? false;
|
|
return $value === true || $value === '1' || $value === 1;
|
|
}
|
|
|
|
private function shouldShowOnCurrentPage(array $data): bool
|
|
{
|
|
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
|
|
|
switch ($showOn) {
|
|
case 'all':
|
|
return true;
|
|
case 'posts':
|
|
return is_single();
|
|
case 'pages':
|
|
return is_page();
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private function getVisibilityClass(array $data): ?string
|
|
{
|
|
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
|
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
|
|
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
|
|
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
|
|
|
|
if (!$showDesktop && !$showMobile) {
|
|
return null;
|
|
}
|
|
if (!$showDesktop && $showMobile) {
|
|
return 'd-lg-none';
|
|
}
|
|
if ($showDesktop && !$showMobile) {
|
|
return 'd-none d-lg-block';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private function generateCSS(array $data): string
|
|
{
|
|
$colors = $data['colors'] ?? [];
|
|
$spacing = $data['spacing'] ?? [];
|
|
$effects = $data['visual_effects'] ?? [];
|
|
$typography = $data['typography'] ?? [];
|
|
$visibility = $data['visibility'] ?? [];
|
|
|
|
$cssRules = [];
|
|
|
|
// Variables de colores del tema (defaults del template)
|
|
$colorNavyPrimary = $colors['section_title_color'] ?? '#0E2337';
|
|
$colorOrangePrimary = $colors['card_hover_border_color'] ?? '#FF8600';
|
|
$colorNeutral50 = '#f9fafb';
|
|
$colorNeutral100 = '#e5e7eb';
|
|
$colorNeutral600 = $colors['card_border_color'] ?? '#6b7280';
|
|
|
|
// Container - margin 3rem 0
|
|
$cssRules[] = $this->cssGenerator->generate('.related-posts', [
|
|
'margin' => '3rem 0',
|
|
]);
|
|
|
|
// Section title - color navy, font-weight 700, margin-bottom 2rem
|
|
$cssRules[] = $this->cssGenerator->generate('.related-posts h2', [
|
|
'color' => $colorNavyPrimary,
|
|
'font-weight' => '700',
|
|
'margin-bottom' => '2rem',
|
|
]);
|
|
|
|
// Card styles - cursor pointer, border, border-left 4px
|
|
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
|
|
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? $colorNeutral50;
|
|
|
|
$cssRules[] = ".related-posts .card {
|
|
cursor: pointer;
|
|
background: {$cardBgColor} !important;
|
|
border: 1px solid {$colorNeutral100} !important;
|
|
border-left: 4px solid {$colorNeutral600} !important;
|
|
transition: all 0.3s ease;
|
|
height: 100%;
|
|
}";
|
|
|
|
// Card hover - background change, shadow, border-left orange
|
|
$cssRules[] = ".related-posts .card:hover {
|
|
background: {$cardHoverBgColor} !important;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
|
border-left-color: {$colorOrangePrimary} !important;
|
|
}";
|
|
|
|
// Card body - padding 1.5rem
|
|
$cssRules[] = $this->cssGenerator->generate('.related-posts .card-body', [
|
|
'padding' => '1.5rem !important',
|
|
]);
|
|
|
|
// Card title - color navy, font-weight 600, font-size 0.95rem
|
|
$cardTitleColor = $colors['card_title_color'] ?? $colorNavyPrimary;
|
|
|
|
$cssRules[] = ".related-posts .card-title {
|
|
color: {$cardTitleColor} !important;
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
line-height: 1.4;
|
|
}";
|
|
|
|
// Link hover - title changes to orange
|
|
$cssRules[] = ".related-posts a:hover .card-title {
|
|
color: {$colorOrangePrimary} !important;
|
|
}";
|
|
|
|
// Pagination styles - matching template exactly
|
|
$cssRules[] = ".related-posts .page-link {
|
|
color: {$colorNeutral600};
|
|
border: 1px solid {$colorNeutral100};
|
|
padding: 0.5rem 1rem;
|
|
margin: 0 0.25rem;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}";
|
|
|
|
$cssRules[] = ".related-posts .page-link:hover {
|
|
background-color: rgba(255, 133, 0, 0.1);
|
|
border-color: {$colorOrangePrimary};
|
|
color: {$colorOrangePrimary};
|
|
}";
|
|
|
|
$cssRules[] = ".related-posts .page-item.active .page-link {
|
|
background-color: {$colorOrangePrimary};
|
|
border-color: {$colorOrangePrimary};
|
|
color: #ffffff;
|
|
}";
|
|
|
|
// Responsive visibility
|
|
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
|
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
|
|
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
|
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
|
|
|
|
if (!$showOnMobile) {
|
|
$cssRules[] = "@media (max-width: 991.98px) {
|
|
.related-posts { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
if (!$showOnDesktop) {
|
|
$cssRules[] = "@media (min-width: 992px) {
|
|
.related-posts { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
return implode("\n", $cssRules);
|
|
}
|
|
|
|
private function buildHTML(array $data, string $visibilityClass): string
|
|
{
|
|
$content = $data['content'] ?? [];
|
|
$layout = $data['layout'] ?? [];
|
|
|
|
$sectionTitle = $content['section_title'] ?? 'Descubre Mas Contenido';
|
|
$postsPerPage = (int)($content['posts_per_page'] ?? 12);
|
|
$orderby = $content['orderby'] ?? 'rand';
|
|
$order = $content['order'] ?? 'DESC';
|
|
$showPagination = $content['show_pagination'] ?? true;
|
|
$showPagination = $showPagination === true || $showPagination === '1' || $showPagination === 1;
|
|
|
|
// Layout columns (cast to string to handle boolean conversion from DB)
|
|
$colsDesktop = (string)($layout['columns_desktop'] ?? '3');
|
|
$colsTablet = (string)($layout['columns_tablet'] ?? '2');
|
|
$colsMobile = (string)($layout['columns_mobile'] ?? '1');
|
|
|
|
// Handle '1' stored as boolean true in DB
|
|
if ($colsDesktop === '1' || $colsDesktop === '') $colsDesktop = '3';
|
|
if ($colsTablet === '1' || $colsTablet === '') $colsTablet = '2';
|
|
if ($colsMobile === '1' || $colsMobile === '') $colsMobile = '1';
|
|
|
|
// Bootstrap column classes
|
|
$colClass = $this->getColumnClass($colsDesktop, $colsTablet, $colsMobile);
|
|
|
|
// Query related posts
|
|
$posts = $this->getRelatedPosts($postsPerPage, $orderby, $order);
|
|
|
|
if (empty($posts)) {
|
|
return '';
|
|
}
|
|
|
|
$containerClass = 'my-5 related-posts';
|
|
if (!empty($visibilityClass)) {
|
|
$containerClass .= ' ' . $visibilityClass;
|
|
}
|
|
|
|
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
|
$html .= sprintf(
|
|
'<h2 class="h3 mb-4">%s</h2>',
|
|
esc_html($sectionTitle)
|
|
);
|
|
$html .= '<div class="row g-4">';
|
|
|
|
foreach ($posts as $post) {
|
|
$html .= $this->buildCardHTML($post, $colClass);
|
|
}
|
|
|
|
$html .= '</div>';
|
|
|
|
if ($showPagination) {
|
|
$html .= $this->buildPaginationHTML($data);
|
|
}
|
|
|
|
$html .= '</div>';
|
|
|
|
// Reset post data
|
|
wp_reset_postdata();
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function getColumnClass(string $desktop, string $tablet, string $mobile): string
|
|
{
|
|
$desktopCols = 12 / (int)$desktop;
|
|
$tabletCols = 12 / (int)$tablet;
|
|
$mobileCols = 12 / (int)$mobile;
|
|
|
|
// Template original usa col-md-4 (3 columnas desde tablet)
|
|
// col-{mobile} col-md-{tablet/desktop}
|
|
return sprintf('col-%d col-md-%d', $mobileCols, $desktopCols);
|
|
}
|
|
|
|
private function getRelatedPosts(int $perPage, string $orderby, string $order): array
|
|
{
|
|
$currentPostId = get_the_ID();
|
|
|
|
$args = [
|
|
'post_type' => 'post',
|
|
'posts_per_page' => $perPage,
|
|
'post__not_in' => $currentPostId ? [$currentPostId] : [],
|
|
'orderby' => $orderby,
|
|
'order' => $order,
|
|
'no_found_rows' => true,
|
|
];
|
|
|
|
$query = new \WP_Query($args);
|
|
|
|
return $query->posts;
|
|
}
|
|
|
|
private function buildCardHTML(\WP_Post $post, string $colClass): string
|
|
{
|
|
$permalink = get_permalink($post);
|
|
$title = get_the_title($post);
|
|
|
|
$html = sprintf('<div class="%s">', esc_attr($colClass));
|
|
$html .= sprintf(
|
|
'<a href="%s" class="text-decoration-none">',
|
|
esc_url($permalink)
|
|
);
|
|
$html .= '<div class="card h-100">';
|
|
$html .= '<div class="card-body d-flex align-items-center justify-content-center">';
|
|
$html .= sprintf(
|
|
'<span class="card-title d-block h6 mb-0 text-center">%s</span>',
|
|
esc_html($title)
|
|
);
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
$html .= '</a>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildPaginationHTML(array $data): string
|
|
{
|
|
$content = $data['content'] ?? [];
|
|
|
|
$textFirst = $content['pagination_text_first'] ?? 'Inicio';
|
|
$textLast = $content['pagination_text_last'] ?? 'Fin';
|
|
$textMore = $content['pagination_text_more'] ?? 'Ver mas';
|
|
|
|
$html = '<nav aria-label="' . esc_attr__('Navegacion de posts relacionados', 'roi-theme') . '" class="mt-5">';
|
|
$html .= '<ul class="pagination justify-content-center">';
|
|
|
|
// First page
|
|
$html .= '<li class="page-item">';
|
|
$html .= sprintf(
|
|
'<a class="page-link" href="#" aria-label="%s">%s</a>',
|
|
esc_attr($textFirst),
|
|
esc_html($textFirst)
|
|
);
|
|
$html .= '</li>';
|
|
|
|
// Page numbers (static for now, can be enhanced with AJAX later)
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$activeClass = $i === 1 ? ' active' : '';
|
|
$ariaCurrent = $i === 1 ? ' aria-current="page"' : '';
|
|
$html .= sprintf(
|
|
'<li class="page-item%s"%s><a class="page-link" href="#">%d</a></li>',
|
|
$activeClass,
|
|
$ariaCurrent,
|
|
$i
|
|
);
|
|
}
|
|
|
|
// More link
|
|
$html .= '<li class="page-item">';
|
|
$html .= sprintf(
|
|
'<a class="page-link" href="#">%s</a>',
|
|
esc_html($textMore)
|
|
);
|
|
$html .= '</li>';
|
|
|
|
// Last page
|
|
$html .= '<li class="page-item">';
|
|
$html .= sprintf(
|
|
'<a class="page-link" href="#" aria-label="%s">%s</a>',
|
|
esc_attr($textLast),
|
|
esc_html($textLast)
|
|
);
|
|
$html .= '</li>';
|
|
|
|
$html .= '</ul>';
|
|
$html .= '</nav>';
|
|
|
|
return $html;
|
|
}
|
|
}
|