- Add ArchiveHeader component (schema, renderer, formbuilder) - Add PostGrid component (schema, renderer, formbuilder) - Unify archive templates (home, archive, category, tag, author, date, search) - Add page visibility system with VisibilityDefaults - Register components in AdminDashboardRenderer - Fix boolean conversion in functions-addon.php - All 172 unit tests passed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
573 lines
18 KiB
PHP
573 lines
18 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\PostGrid\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;
|
|
|
|
/**
|
|
* PostGridRenderer - Renderiza grid de posts del loop principal de WordPress
|
|
*
|
|
* RESPONSABILIDAD: Generar HTML y CSS del componente Post Grid
|
|
*
|
|
* DIFERENCIA CON RelatedPostRenderer:
|
|
* - PostGrid usa global $wp_query (loop principal)
|
|
* - RelatedPost crea su propio WP_Query
|
|
*
|
|
* CARACTERISTICAS:
|
|
* - Grid responsive de cards con imagen, excerpt y meta
|
|
* - Usa loop principal de WordPress (no crea queries propias)
|
|
* - Paginacion nativa de WordPress
|
|
* - Estilos 100% desde BD via CSSGenerator
|
|
*
|
|
* @package ROITheme\Public\PostGrid\Infrastructure\Ui
|
|
*/
|
|
final class PostGridRenderer implements RendererInterface
|
|
{
|
|
private const COMPONENT_NAME = 'post-grid';
|
|
|
|
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 '';
|
|
}
|
|
|
|
global $wp_query;
|
|
|
|
// Si no hay posts, mostrar mensaje
|
|
if (!have_posts()) {
|
|
$noPostsMessage = $data['content']['no_posts_message'] ?? 'No se encontraron publicaciones';
|
|
return $this->renderNoPostsMessage($noPostsMessage, $visibilityClass, $data);
|
|
}
|
|
|
|
$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 renderNoPostsMessage(string $message, string $visibilityClass, array $data): string
|
|
{
|
|
$colors = $data['colors'] ?? [];
|
|
$spacing = $data['spacing'] ?? [];
|
|
|
|
$bgColor = $colors['card_bg_color'] ?? '#ffffff';
|
|
$textColor = $colors['excerpt_color'] ?? '#6b7280';
|
|
$borderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
|
$padding = $spacing['card_padding'] ?? '1.25rem';
|
|
|
|
$css = $this->cssGenerator->generate('.post-grid-no-posts', [
|
|
'background-color' => $bgColor,
|
|
'color' => $textColor,
|
|
'border' => "1px solid {$borderColor}",
|
|
'border-radius' => '0.5rem',
|
|
'padding' => '2rem',
|
|
'text-align' => 'center',
|
|
]);
|
|
|
|
$containerClass = 'post-grid-no-posts';
|
|
if (!empty($visibilityClass)) {
|
|
$containerClass .= ' ' . $visibilityClass;
|
|
}
|
|
|
|
$html = sprintf(
|
|
'<div class="%s"><p class="mb-0">%s</p></div>',
|
|
esc_attr($containerClass),
|
|
esc_html($message)
|
|
);
|
|
|
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
|
}
|
|
|
|
private function generateCSS(array $data): string
|
|
{
|
|
$colors = $data['colors'] ?? [];
|
|
$spacing = $data['spacing'] ?? [];
|
|
$effects = $data['visual_effects'] ?? [];
|
|
$typography = $data['typography'] ?? [];
|
|
$layout = $data['layout'] ?? [];
|
|
|
|
$cssRules = [];
|
|
|
|
// Colores
|
|
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
|
|
$cardTitleColor = $colors['card_title_color'] ?? '#0E2337';
|
|
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? '#f9fafb';
|
|
$cardBorderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
|
$cardHoverBorderColor = $colors['card_hover_border_color'] ?? '#FF8600';
|
|
$excerptColor = $colors['excerpt_color'] ?? '#6b7280';
|
|
$metaColor = $colors['meta_color'] ?? '#9ca3af';
|
|
$categoryBgColor = $colors['category_bg_color'] ?? '#FFF5EB';
|
|
$categoryTextColor = $colors['category_text_color'] ?? '#FF8600';
|
|
$paginationColor = $colors['pagination_color'] ?? '#0E2337';
|
|
$paginationActiveBg = $colors['pagination_active_bg'] ?? '#FF8600';
|
|
$paginationActiveColor = $colors['pagination_active_color'] ?? '#ffffff';
|
|
|
|
// Spacing
|
|
$gridGap = $spacing['grid_gap'] ?? '1.5rem';
|
|
$cardPadding = $spacing['card_padding'] ?? '1.25rem';
|
|
$sectionMarginTop = $spacing['section_margin_top'] ?? '0';
|
|
$sectionMarginBottom = $spacing['section_margin_bottom'] ?? '2rem';
|
|
|
|
// Visual effects
|
|
$cardBorderRadius = $effects['card_border_radius'] ?? '0.5rem';
|
|
$cardShadow = $effects['card_shadow'] ?? '0 1px 3px rgba(0,0,0,0.1)';
|
|
$cardHoverShadow = $effects['card_hover_shadow'] ?? '0 4px 12px rgba(0,0,0,0.15)';
|
|
$cardTransition = $effects['card_transition'] ?? 'all 0.3s ease';
|
|
$imageBorderRadius = $effects['image_border_radius'] ?? '0.375rem';
|
|
|
|
// Typography
|
|
$cardTitleSize = $typography['card_title_size'] ?? '1.1rem';
|
|
$cardTitleWeight = $typography['card_title_weight'] ?? '600';
|
|
$excerptSize = $typography['excerpt_size'] ?? '0.9rem';
|
|
$metaSize = $typography['meta_size'] ?? '0.8rem';
|
|
|
|
// Container
|
|
$cssRules[] = $this->cssGenerator->generate('.post-grid', [
|
|
'margin-top' => $sectionMarginTop,
|
|
'margin-bottom' => $sectionMarginBottom,
|
|
]);
|
|
|
|
// Row gap
|
|
$cssRules[] = $this->cssGenerator->generate('.post-grid .row', [
|
|
'gap' => $gridGap,
|
|
'row-gap' => $gridGap,
|
|
]);
|
|
|
|
// Card base
|
|
$cssRules[] = ".post-grid .card {
|
|
background: {$cardBgColor};
|
|
border: 1px solid {$cardBorderColor};
|
|
border-radius: {$cardBorderRadius};
|
|
box-shadow: {$cardShadow};
|
|
transition: {$cardTransition};
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}";
|
|
|
|
// Card hover
|
|
$cssRules[] = ".post-grid .card:hover {
|
|
background: {$cardHoverBgColor};
|
|
border-color: {$cardHoverBorderColor};
|
|
box-shadow: {$cardHoverShadow};
|
|
transform: translateY(-2px);
|
|
}";
|
|
|
|
// Card body
|
|
$cssRules[] = $this->cssGenerator->generate('.post-grid .card-body', [
|
|
'padding' => $cardPadding,
|
|
]);
|
|
|
|
// Card image
|
|
$cssRules[] = ".post-grid .card-img-top {
|
|
border-radius: {$imageBorderRadius} {$imageBorderRadius} 0 0;
|
|
object-fit: cover;
|
|
width: 100%;
|
|
height: 200px;
|
|
}";
|
|
|
|
// Card title
|
|
$cssRules[] = ".post-grid .card-title {
|
|
color: {$cardTitleColor};
|
|
font-size: {$cardTitleSize};
|
|
font-weight: {$cardTitleWeight};
|
|
line-height: 1.4;
|
|
margin-bottom: 0.75rem;
|
|
}";
|
|
|
|
// Card title hover
|
|
$cssRules[] = ".post-grid a:hover .card-title {
|
|
color: {$cardHoverBorderColor};
|
|
}";
|
|
|
|
// Excerpt
|
|
$cssRules[] = ".post-grid .card-text {
|
|
color: {$excerptColor};
|
|
font-size: {$excerptSize};
|
|
line-height: 1.6;
|
|
}";
|
|
|
|
// Meta
|
|
$cssRules[] = ".post-grid .post-meta {
|
|
color: {$metaColor};
|
|
font-size: {$metaSize};
|
|
}";
|
|
|
|
// Categories
|
|
$cssRules[] = ".post-grid .post-category {
|
|
background: {$categoryBgColor};
|
|
color: {$categoryTextColor};
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .post-category:hover {
|
|
background: {$categoryTextColor};
|
|
color: #ffffff;
|
|
}";
|
|
|
|
// Pagination
|
|
$cssRules[] = ".post-grid .pagination {
|
|
margin-top: 2rem;
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .page-link {
|
|
color: {$paginationColor};
|
|
border: 1px solid {$cardBorderColor};
|
|
padding: 0.5rem 1rem;
|
|
margin: 0 0.25rem;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .page-link:hover {
|
|
background-color: rgba(255, 133, 0, 0.1);
|
|
border-color: {$paginationActiveBg};
|
|
color: {$paginationActiveBg};
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .page-item.active .page-link,
|
|
.post-grid .nav-links .current {
|
|
background-color: {$paginationActiveBg};
|
|
border-color: {$paginationActiveBg};
|
|
color: {$paginationActiveColor};
|
|
}";
|
|
|
|
// WordPress pagination classes
|
|
$cssRules[] = ".post-grid .nav-links {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-top: 2rem;
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .nav-links a,
|
|
.post-grid .nav-links span {
|
|
color: {$paginationColor};
|
|
border: 1px solid {$cardBorderColor};
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
text-decoration: none;
|
|
}";
|
|
|
|
$cssRules[] = ".post-grid .nav-links a:hover {
|
|
background-color: rgba(255, 133, 0, 0.1);
|
|
border-color: {$paginationActiveBg};
|
|
color: {$paginationActiveBg};
|
|
}";
|
|
|
|
// Layout responsive columns
|
|
$colsDesktop = $layout['columns_desktop'] ?? '3';
|
|
$colsTablet = $layout['columns_tablet'] ?? '2';
|
|
$colsMobile = $layout['columns_mobile'] ?? '1';
|
|
|
|
// Mobile
|
|
$mobileWidth = $this->getColumnWidth($colsMobile);
|
|
$cssRules[] = "@media (max-width: 575.98px) {
|
|
.post-grid .post-card-col {
|
|
flex: 0 0 {$mobileWidth};
|
|
max-width: {$mobileWidth};
|
|
}
|
|
}";
|
|
|
|
// Tablet
|
|
$tabletWidth = $this->getColumnWidth($colsTablet);
|
|
$cssRules[] = "@media (min-width: 576px) and (max-width: 991.98px) {
|
|
.post-grid .post-card-col {
|
|
flex: 0 0 {$tabletWidth};
|
|
max-width: {$tabletWidth};
|
|
}
|
|
}";
|
|
|
|
// Desktop
|
|
$desktopWidth = $this->getColumnWidth($colsDesktop);
|
|
$cssRules[] = "@media (min-width: 992px) {
|
|
.post-grid .post-card-col {
|
|
flex: 0 0 {$desktopWidth};
|
|
max-width: {$desktopWidth};
|
|
}
|
|
}";
|
|
|
|
return implode("\n", $cssRules);
|
|
}
|
|
|
|
private function getColumnWidth(string $cols): string
|
|
{
|
|
$colCount = (int)$cols;
|
|
if ($colCount <= 0) {
|
|
$colCount = 1;
|
|
}
|
|
$percentage = 100 / $colCount;
|
|
return sprintf('%.4f%%', $percentage);
|
|
}
|
|
|
|
private function buildHTML(array $data, string $visibilityClass): string
|
|
{
|
|
$content = $data['content'] ?? [];
|
|
$typography = $data['typography'] ?? [];
|
|
$media = $data['media'] ?? [];
|
|
$layout = $data['layout'] ?? [];
|
|
|
|
$showThumbnail = $this->toBool($content['show_thumbnail'] ?? true);
|
|
$showExcerpt = $this->toBool($content['show_excerpt'] ?? true);
|
|
$showMeta = $this->toBool($content['show_meta'] ?? true);
|
|
$showCategories = $this->toBool($content['show_categories'] ?? true);
|
|
$excerptLength = (int)($content['excerpt_length'] ?? 20);
|
|
$readMoreText = $content['read_more_text'] ?? 'Leer mas';
|
|
$headingLevel = $typography['heading_level'] ?? 'h3';
|
|
$fallbackImage = $media['fallback_image'] ?? '';
|
|
$fallbackImageAlt = $media['fallback_image_alt'] ?? 'Imagen por defecto';
|
|
$imagePosition = $layout['image_position'] ?? 'top';
|
|
|
|
$containerClass = 'post-grid';
|
|
if (!empty($visibilityClass)) {
|
|
$containerClass .= ' ' . $visibilityClass;
|
|
}
|
|
|
|
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
|
$html .= '<div class="row">';
|
|
|
|
while (have_posts()) {
|
|
the_post();
|
|
$html .= $this->buildCardHTML(
|
|
$showThumbnail,
|
|
$showExcerpt,
|
|
$showMeta,
|
|
$showCategories,
|
|
$excerptLength,
|
|
$readMoreText,
|
|
$headingLevel,
|
|
$fallbackImage,
|
|
$fallbackImageAlt,
|
|
$imagePosition
|
|
);
|
|
}
|
|
|
|
$html .= '</div>';
|
|
|
|
// Paginacion nativa de WordPress
|
|
$html .= '<div class="pagination-wrapper">';
|
|
$html .= $this->buildPaginationHTML();
|
|
$html .= '</div>';
|
|
|
|
$html .= '</div>';
|
|
|
|
wp_reset_postdata();
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function toBool(mixed $value): bool
|
|
{
|
|
return $value === true || $value === '1' || $value === 1;
|
|
}
|
|
|
|
private function buildCardHTML(
|
|
bool $showThumbnail,
|
|
bool $showExcerpt,
|
|
bool $showMeta,
|
|
bool $showCategories,
|
|
int $excerptLength,
|
|
string $readMoreText,
|
|
string $headingLevel,
|
|
string $fallbackImage,
|
|
string $fallbackImageAlt,
|
|
string $imagePosition
|
|
): string {
|
|
$permalink = get_permalink();
|
|
$title = get_the_title();
|
|
|
|
$html = '<div class="post-card-col">';
|
|
$html .= sprintf(
|
|
'<a href="%s" class="text-decoration-none">',
|
|
esc_url($permalink)
|
|
);
|
|
|
|
$cardClass = 'card h-100';
|
|
if ($imagePosition === 'left') {
|
|
$cardClass .= ' flex-row';
|
|
}
|
|
|
|
$html .= sprintf('<div class="%s">', esc_attr($cardClass));
|
|
|
|
// Imagen
|
|
if ($showThumbnail && $imagePosition !== 'none') {
|
|
$html .= $this->buildImageHTML($fallbackImage, $fallbackImageAlt, $imagePosition);
|
|
}
|
|
|
|
$html .= '<div class="card-body">';
|
|
|
|
// Categorias
|
|
if ($showCategories) {
|
|
$html .= $this->buildCategoriesHTML();
|
|
}
|
|
|
|
// Titulo
|
|
$html .= sprintf(
|
|
'<%s class="card-title">%s</%s>',
|
|
esc_attr($headingLevel),
|
|
esc_html($title),
|
|
esc_attr($headingLevel)
|
|
);
|
|
|
|
// Meta
|
|
if ($showMeta) {
|
|
$html .= $this->buildMetaHTML();
|
|
}
|
|
|
|
// Excerpt
|
|
if ($showExcerpt) {
|
|
$html .= $this->buildExcerptHTML($excerptLength);
|
|
}
|
|
|
|
$html .= '</div>'; // card-body
|
|
$html .= '</div>'; // card
|
|
$html .= '</a>';
|
|
$html .= '</div>'; // col
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildImageHTML(string $fallbackImage, string $fallbackImageAlt, string $imagePosition): string
|
|
{
|
|
if (has_post_thumbnail()) {
|
|
$imageClass = $imagePosition === 'left' ? 'card-img-left' : 'card-img-top';
|
|
return get_the_post_thumbnail(
|
|
null,
|
|
'medium_large',
|
|
['class' => $imageClass, 'loading' => 'lazy']
|
|
);
|
|
}
|
|
|
|
if (!empty($fallbackImage)) {
|
|
$imageClass = $imagePosition === 'left' ? 'card-img-left' : 'card-img-top';
|
|
return sprintf(
|
|
'<img src="%s" alt="%s" class="%s" loading="lazy">',
|
|
esc_url($fallbackImage),
|
|
esc_attr($fallbackImageAlt),
|
|
esc_attr($imageClass)
|
|
);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function buildCategoriesHTML(): string
|
|
{
|
|
$categories = get_the_category();
|
|
if (empty($categories)) {
|
|
return '';
|
|
}
|
|
|
|
$html = '<div class="post-categories mb-2">';
|
|
foreach (array_slice($categories, 0, 2) as $category) {
|
|
$html .= sprintf(
|
|
'<span class="post-category">%s</span>',
|
|
esc_html($category->name)
|
|
);
|
|
}
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildMetaHTML(): string
|
|
{
|
|
$date = get_the_date();
|
|
$author = get_the_author();
|
|
|
|
return sprintf(
|
|
'<div class="post-meta mb-2"><small>%s | %s</small></div>',
|
|
esc_html($date),
|
|
esc_html($author)
|
|
);
|
|
}
|
|
|
|
private function buildExcerptHTML(int $length): string
|
|
{
|
|
$excerpt = get_the_excerpt();
|
|
|
|
if (empty($excerpt)) {
|
|
$excerpt = wp_trim_words(get_the_content(), $length, '...');
|
|
} else {
|
|
$excerpt = wp_trim_words($excerpt, $length, '...');
|
|
}
|
|
|
|
return sprintf(
|
|
'<p class="card-text">%s</p>',
|
|
esc_html($excerpt)
|
|
);
|
|
}
|
|
|
|
private function buildPaginationHTML(): string
|
|
{
|
|
ob_start();
|
|
|
|
the_posts_pagination([
|
|
'mid_size' => 2,
|
|
'prev_text' => __('« Anterior', 'roi-theme'),
|
|
'next_text' => __('Siguiente »', 'roi-theme'),
|
|
'screen_reader_text' => __('Navegacion de publicaciones', 'roi-theme'),
|
|
]);
|
|
|
|
return ob_get_clean();
|
|
}
|
|
}
|