Files
roi-theme/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
FrankZamora 8735962f52 feat(visibility): sistema de visibilidad por tipo de página
- 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>
2025-12-03 09:16:34 -06:00

368 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;
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
/**
* 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
{
private const COMPONENT_NAME = 'related-post';
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
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 === self::COMPONENT_NAME;
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
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;
}
}