Compare commits
2 Commits
6be292e085
...
c23dc22d76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23dc22d76 | ||
|
|
b79569c5e7 |
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping;
|
||||||
|
|
||||||
|
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field Mapper para Archive Header
|
||||||
|
*
|
||||||
|
* RESPONSABILIDAD:
|
||||||
|
* - Mapear field IDs del formulario a atributos de BD
|
||||||
|
* - Solo conoce sus propios campos (modularidad)
|
||||||
|
*/
|
||||||
|
final class ArchiveHeaderFieldMapper implements FieldMapperInterface
|
||||||
|
{
|
||||||
|
public function getComponentName(): string
|
||||||
|
{
|
||||||
|
return 'archive-header';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFieldMapping(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Visibility
|
||||||
|
'archiveHeaderEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
|
'archiveHeaderShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
|
'archiveHeaderShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
|
|
||||||
|
// Page Visibility (grupo especial _page_visibility)
|
||||||
|
'archiveHeaderVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||||
|
'archiveHeaderVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||||
|
'archiveHeaderVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||||
|
'archiveHeaderVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||||
|
'archiveHeaderVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions)
|
||||||
|
'archiveHeaderExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'archiveHeaderExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'archiveHeaderExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'archiveHeaderExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
|
// Content
|
||||||
|
'archiveHeaderBlogTitle' => ['group' => 'content', 'attribute' => 'blog_title'],
|
||||||
|
'archiveHeaderShowPostCount' => ['group' => 'content', 'attribute' => 'show_post_count'],
|
||||||
|
'archiveHeaderShowDescription' => ['group' => 'content', 'attribute' => 'show_description'],
|
||||||
|
'archiveHeaderCategoryPrefix' => ['group' => 'content', 'attribute' => 'category_prefix'],
|
||||||
|
'archiveHeaderTagPrefix' => ['group' => 'content', 'attribute' => 'tag_prefix'],
|
||||||
|
'archiveHeaderAuthorPrefix' => ['group' => 'content', 'attribute' => 'author_prefix'],
|
||||||
|
'archiveHeaderDatePrefix' => ['group' => 'content', 'attribute' => 'date_prefix'],
|
||||||
|
'archiveHeaderSearchPrefix' => ['group' => 'content', 'attribute' => 'search_prefix'],
|
||||||
|
'archiveHeaderCountSingular' => ['group' => 'content', 'attribute' => 'posts_count_singular'],
|
||||||
|
'archiveHeaderCountPlural' => ['group' => 'content', 'attribute' => 'posts_count_plural'],
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
'archiveHeaderHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||||
|
'archiveHeaderTitleSize' => ['group' => 'typography', 'attribute' => 'title_size'],
|
||||||
|
'archiveHeaderTitleWeight' => ['group' => 'typography', 'attribute' => 'title_weight'],
|
||||||
|
'archiveHeaderDescriptionSize' => ['group' => 'typography', 'attribute' => 'description_size'],
|
||||||
|
'archiveHeaderCountSize' => ['group' => 'typography', 'attribute' => 'count_size'],
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
'archiveHeaderTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||||
|
'archiveHeaderPrefixColor' => ['group' => 'colors', 'attribute' => 'prefix_color'],
|
||||||
|
'archiveHeaderDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||||
|
'archiveHeaderCountBgColor' => ['group' => 'colors', 'attribute' => 'count_bg_color'],
|
||||||
|
'archiveHeaderCountTextColor' => ['group' => 'colors', 'attribute' => 'count_text_color'],
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
'archiveHeaderMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||||
|
'archiveHeaderMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||||
|
'archiveHeaderPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||||
|
'archiveHeaderTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||||
|
'archiveHeaderCountPadding' => ['group' => 'spacing', 'attribute' => 'count_padding'],
|
||||||
|
|
||||||
|
// Behavior
|
||||||
|
'archiveHeaderIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||||
|
'archiveHeaderStickyOffset' => ['group' => 'behavior', 'attribute' => 'sticky_offset'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\Ui;
|
||||||
|
|
||||||
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormBuilder para Archive Header
|
||||||
|
*
|
||||||
|
* @package ROITheme\Admin\ArchiveHeader\Infrastructure\Ui
|
||||||
|
*/
|
||||||
|
final class ArchiveHeaderFormBuilder
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AdminDashboardRenderer $renderer
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function buildForm(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
$html .= $this->buildHeader($componentId);
|
||||||
|
|
||||||
|
$html .= '<div class="row g-3">';
|
||||||
|
|
||||||
|
// Columna izquierda
|
||||||
|
$html .= '<div class="col-lg-6">';
|
||||||
|
$html .= $this->buildVisibilityGroup($componentId);
|
||||||
|
$html .= $this->buildContentGroup($componentId);
|
||||||
|
$html .= $this->buildBehaviorGroup($componentId);
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Columna derecha
|
||||||
|
$html .= '<div class="col-lg-6">';
|
||||||
|
$html .= $this->buildTypographyGroup($componentId);
|
||||||
|
$html .= $this->buildColorsGroup($componentId);
|
||||||
|
$html .= $this->buildSpacingGroup($componentId);
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildHeader(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||||
|
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||||
|
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||||
|
$html .= ' <div>';
|
||||||
|
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||||
|
$html .= ' <i class="bi bi-layout-text-window me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Configuracion de Cabecera de Archivo';
|
||||||
|
$html .= ' </h3>';
|
||||||
|
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||||
|
$html .= ' Cabecera dinamica para paginas de listados (blog, categorias, tags, autor, fecha, busqueda)';
|
||||||
|
$html .= ' </p>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="archive-header">';
|
||||||
|
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||||
|
$html .= ' Restaurar valores por defecto';
|
||||||
|
$html .= ' </button>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildVisibilityGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Visibilidad';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||||
|
|
||||||
|
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||||
|
|
||||||
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
|
// Page visibility checkboxes
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
|
$html .= ' </p>';
|
||||||
|
|
||||||
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
|
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', false);
|
||||||
|
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||||
|
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||||
|
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Exclusions
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'archiveHeader');
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildContentGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Contenido';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Blog Title
|
||||||
|
$blogTitle = $this->renderer->getFieldValue($componentId, 'content', 'blog_title', 'Blog');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="archiveHeaderBlogTitle" class="form-label small mb-1 fw-semibold">Titulo del blog</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderBlogTitle" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($blogTitle) . '">';
|
||||||
|
$html .= ' <small class="text-muted">Mostrado en la pagina principal del blog</small>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Switches
|
||||||
|
$showPostCount = $this->renderer->getFieldValue($componentId, 'content', 'show_post_count', true);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderShowPostCount', 'Mostrar contador de posts', 'bi-hash', $showPostCount);
|
||||||
|
|
||||||
|
$showDescription = $this->renderer->getFieldValue($componentId, 'content', 'show_description', true);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderShowDescription', 'Mostrar descripcion', 'bi-text-paragraph', $showDescription);
|
||||||
|
|
||||||
|
// Prefixes section
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Prefijos de titulo';
|
||||||
|
$html .= ' </p>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2">';
|
||||||
|
|
||||||
|
$categoryPrefix = $this->renderer->getFieldValue($componentId, 'content', 'category_prefix', 'Categoria:');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderCategoryPrefix" class="form-label small mb-1">Categoria</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderCategoryPrefix" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($categoryPrefix) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$tagPrefix = $this->renderer->getFieldValue($componentId, 'content', 'tag_prefix', 'Etiqueta:');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderTagPrefix" class="form-label small mb-1">Etiqueta</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderTagPrefix" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($tagPrefix) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$authorPrefix = $this->renderer->getFieldValue($componentId, 'content', 'author_prefix', 'Articulos de:');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderAuthorPrefix" class="form-label small mb-1">Autor</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderAuthorPrefix" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($authorPrefix) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$datePrefix = $this->renderer->getFieldValue($componentId, 'content', 'date_prefix', 'Archivo:');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderDatePrefix" class="form-label small mb-1">Fecha</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderDatePrefix" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($datePrefix) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$searchPrefix = $this->renderer->getFieldValue($componentId, 'content', 'search_prefix', 'Resultados para:');
|
||||||
|
$html .= ' <div class="col-12">';
|
||||||
|
$html .= ' <label for="archiveHeaderSearchPrefix" class="form-label small mb-1">Busqueda</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderSearchPrefix" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($searchPrefix) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Post count texts
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-123 me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Textos del contador';
|
||||||
|
$html .= ' </p>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2">';
|
||||||
|
|
||||||
|
$countSingular = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_singular', 'publicacion');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderCountSingular" class="form-label small mb-1">Singular</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderCountSingular" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($countSingular) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$countPlural = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_plural', 'publicaciones');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderCountPlural" class="form-label small mb-1">Plural</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderCountPlural" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($countPlural) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildBehaviorGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Comportamiento';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', false);
|
||||||
|
$html .= $this->buildSwitch('archiveHeaderIsSticky', 'Header fijo al hacer scroll', 'bi-pin-angle', $isSticky);
|
||||||
|
|
||||||
|
$stickyOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'sticky_offset', '0');
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$html .= ' <label for="archiveHeaderStickyOffset" class="form-label small mb-1 fw-semibold">Offset sticky</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderStickyOffset" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($stickyOffset) . '">';
|
||||||
|
$html .= ' <small class="text-muted">Distancia desde el top cuando es sticky (ej: 60px)</small>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTypographyGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Tipografia';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Heading level
|
||||||
|
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h1');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="archiveHeaderHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||||
|
$html .= ' <select id="archiveHeaderHeadingLevel" class="form-select form-select-sm">';
|
||||||
|
foreach (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $level) {
|
||||||
|
$selected = $headingLevel === $level ? ' selected' : '';
|
||||||
|
$html .= sprintf(' <option value="%s"%s>%s</option>', $level, $selected, strtoupper($level));
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' <small class="text-muted">Importante para SEO y accesibilidad</small>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$titleSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_size', '2rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderTitleSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($titleSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$titleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_weight', '700');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderTitleWeight" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($titleWeight) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-0">';
|
||||||
|
|
||||||
|
$descriptionSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_size', '1rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderDescriptionSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderDescriptionSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($descriptionSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$countSize = $this->renderer->getFieldValue($componentId, 'typography', 'count_size', '0.875rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderCountSize" class="form-label small mb-1 fw-semibold">Tamano contador</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderCountSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($countSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildColorsGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Colores';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||||
|
$html .= $this->buildColorPicker('archiveHeaderTitleColor', 'Titulo', $titleColor);
|
||||||
|
|
||||||
|
$prefixColor = $this->renderer->getFieldValue($componentId, 'colors', 'prefix_color', '#6b7280');
|
||||||
|
$html .= $this->buildColorPicker('archiveHeaderPrefixColor', 'Prefijo', $prefixColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$descriptionColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#6b7280');
|
||||||
|
$html .= $this->buildColorPicker('archiveHeaderDescriptionColor', 'Descripcion', $descriptionColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">Contador de posts</p>';
|
||||||
|
$html .= ' <div class="row g-2 mb-0">';
|
||||||
|
|
||||||
|
$countBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_bg_color', '#FF8600');
|
||||||
|
$html .= $this->buildColorPicker('archiveHeaderCountBgColor', 'Fondo', $countBgColor);
|
||||||
|
|
||||||
|
$countTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_text_color', '#ffffff');
|
||||||
|
$html .= $this->buildColorPicker('archiveHeaderCountTextColor', 'Texto', $countTextColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSpacingGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Espaciado';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '2rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderMarginTop" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderMarginBottom" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '1.5rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderPadding" class="form-label small mb-1 fw-semibold">Padding</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderPadding" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($padding) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="archiveHeaderTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderTitleMarginBottom" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$countPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'count_padding', '0.25rem 0.75rem');
|
||||||
|
$html .= ' <label for="archiveHeaderCountPadding" class="form-label small mb-1 fw-semibold">Padding contador</label>';
|
||||||
|
$html .= ' <input type="text" id="archiveHeaderCountPadding" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($countPadding) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="mb-2">';
|
||||||
|
$html .= ' <div class="form-check form-switch">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildColorPicker(string $id, string $label, string $value): string
|
||||||
|
{
|
||||||
|
$html = ' <div class="col-6">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-label small fw-semibold">%s</label>',
|
||||||
|
esc_html($label)
|
||||||
|
);
|
||||||
|
$html .= ' <div class="input-group input-group-sm">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_attr($value)
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_html(strtoupper($value))
|
||||||
|
);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ final class CtaBoxSidebarFormBuilder
|
|||||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||||
|
|
||||||
// Grid 3 columnas según Design System
|
// Grid 3 columnas según Design System
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
|||||||
'label' => 'Related Posts',
|
'label' => 'Related Posts',
|
||||||
'icon' => 'bi-grid-3x3-gap',
|
'icon' => 'bi-grid-3x3-gap',
|
||||||
],
|
],
|
||||||
|
'archive-header' => [
|
||||||
|
'id' => 'archive-header',
|
||||||
|
'label' => 'Archive Header',
|
||||||
|
'icon' => 'bi-layout-text-window',
|
||||||
|
],
|
||||||
|
'post-grid' => [
|
||||||
|
'id' => 'post-grid',
|
||||||
|
'label' => 'Post Grid',
|
||||||
|
'icon' => 'bi-grid-3x3',
|
||||||
|
],
|
||||||
'contact-form' => [
|
'contact-form' => [
|
||||||
'id' => 'contact-form',
|
'id' => 'contact-form',
|
||||||
'label' => 'Contact Form',
|
'label' => 'Contact Form',
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final class ComponentGroupRegistry
|
|||||||
'label' => __('Contenido Principal', 'roi-theme'),
|
'label' => __('Contenido Principal', 'roi-theme'),
|
||||||
'icon' => 'bi-file-richtext',
|
'icon' => 'bi-file-richtext',
|
||||||
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
||||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post']
|
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post', 'archive-header', 'post-grid']
|
||||||
],
|
],
|
||||||
'ctas-conversion' => [
|
'ctas-conversion' => [
|
||||||
'label' => __('CTAs & Conversión', 'roi-theme'),
|
'label' => __('CTAs & Conversión', 'roi-theme'),
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\PostGrid\Infrastructure\FieldMapping;
|
||||||
|
|
||||||
|
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field Mapper para Post Grid
|
||||||
|
*
|
||||||
|
* RESPONSABILIDAD:
|
||||||
|
* - Mapear field IDs del formulario a atributos de BD
|
||||||
|
* - Solo conoce sus propios campos (modularidad)
|
||||||
|
*/
|
||||||
|
final class PostGridFieldMapper implements FieldMapperInterface
|
||||||
|
{
|
||||||
|
public function getComponentName(): string
|
||||||
|
{
|
||||||
|
return 'post-grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFieldMapping(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Visibility
|
||||||
|
'postGridEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
|
'postGridShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
|
'postGridShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
|
|
||||||
|
// Page Visibility (grupo especial _page_visibility)
|
||||||
|
'postGridVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||||
|
'postGridVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||||
|
'postGridVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||||
|
'postGridVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||||
|
'postGridVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions)
|
||||||
|
'postGridExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'postGridExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'postGridExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'postGridExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
|
// Content
|
||||||
|
'postGridShowThumbnail' => ['group' => 'content', 'attribute' => 'show_thumbnail'],
|
||||||
|
'postGridShowExcerpt' => ['group' => 'content', 'attribute' => 'show_excerpt'],
|
||||||
|
'postGridShowMeta' => ['group' => 'content', 'attribute' => 'show_meta'],
|
||||||
|
'postGridShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||||
|
'postGridExcerptLength' => ['group' => 'content', 'attribute' => 'excerpt_length'],
|
||||||
|
'postGridReadMoreText' => ['group' => 'content', 'attribute' => 'read_more_text'],
|
||||||
|
'postGridNoPostsMessage' => ['group' => 'content', 'attribute' => 'no_posts_message'],
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
'postGridColumnsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||||
|
'postGridColumnsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||||
|
'postGridColumnsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||||
|
'postGridImagePosition' => ['group' => 'layout', 'attribute' => 'image_position'],
|
||||||
|
|
||||||
|
// Media
|
||||||
|
'postGridFallbackImage' => ['group' => 'media', 'attribute' => 'fallback_image'],
|
||||||
|
'postGridFallbackImageAlt' => ['group' => 'media', 'attribute' => 'fallback_image_alt'],
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
'postGridHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||||
|
'postGridCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||||
|
'postGridCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||||
|
'postGridExcerptSize' => ['group' => 'typography', 'attribute' => 'excerpt_size'],
|
||||||
|
'postGridMetaSize' => ['group' => 'typography', 'attribute' => 'meta_size'],
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
'postGridCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||||
|
'postGridCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||||
|
'postGridCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||||
|
'postGridCardBorderColor' => ['group' => 'colors', 'attribute' => 'card_border_color'],
|
||||||
|
'postGridCardHoverBorderColor' => ['group' => 'colors', 'attribute' => 'card_hover_border_color'],
|
||||||
|
'postGridExcerptColor' => ['group' => 'colors', 'attribute' => 'excerpt_color'],
|
||||||
|
'postGridMetaColor' => ['group' => 'colors', 'attribute' => 'meta_color'],
|
||||||
|
'postGridCategoryBgColor' => ['group' => 'colors', 'attribute' => 'category_bg_color'],
|
||||||
|
'postGridCategoryTextColor' => ['group' => 'colors', 'attribute' => 'category_text_color'],
|
||||||
|
'postGridPaginationColor' => ['group' => 'colors', 'attribute' => 'pagination_color'],
|
||||||
|
'postGridPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||||
|
'postGridPaginationActiveColor' => ['group' => 'colors', 'attribute' => 'pagination_active_color'],
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
'postGridGridGap' => ['group' => 'spacing', 'attribute' => 'grid_gap'],
|
||||||
|
'postGridCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||||
|
'postGridSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||||
|
'postGridSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||||
|
|
||||||
|
// Visual Effects
|
||||||
|
'postGridCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||||
|
'postGridCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||||
|
'postGridCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||||
|
'postGridCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||||
|
'postGridImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'image_border_radius'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
619
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
619
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\PostGrid\Infrastructure\Ui;
|
||||||
|
|
||||||
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PostGridFormBuilder - Genera formulario admin para Post Grid
|
||||||
|
*
|
||||||
|
* Sigue el mismo patron visual que RelatedPostFormBuilder:
|
||||||
|
* - Header con gradiente navy
|
||||||
|
* - Layout de 2 columnas
|
||||||
|
* - Cards con borde izquierdo
|
||||||
|
* - Inputs compactos (form-control-sm)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Admin\PostGrid\Infrastructure\Ui
|
||||||
|
*/
|
||||||
|
final class PostGridFormBuilder
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AdminDashboardRenderer $renderer
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function buildForm(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
$html .= $this->buildHeader();
|
||||||
|
|
||||||
|
$html .= '<div class="row g-3">';
|
||||||
|
|
||||||
|
// Columna izquierda
|
||||||
|
$html .= '<div class="col-lg-6">';
|
||||||
|
$html .= $this->buildVisibilityGroup($componentId);
|
||||||
|
$html .= $this->buildContentGroup($componentId);
|
||||||
|
$html .= $this->buildLayoutGroup($componentId);
|
||||||
|
$html .= $this->buildMediaGroup($componentId);
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Columna derecha
|
||||||
|
$html .= '<div class="col-lg-6">';
|
||||||
|
$html .= $this->buildTypographyGroup($componentId);
|
||||||
|
$html .= $this->buildColorsGroup($componentId);
|
||||||
|
$html .= $this->buildSpacingGroup($componentId);
|
||||||
|
$html .= $this->buildEffectsGroup($componentId);
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildHeader(): string
|
||||||
|
{
|
||||||
|
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||||
|
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||||
|
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||||
|
$html .= ' <div>';
|
||||||
|
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||||
|
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Configuracion de Post Grid';
|
||||||
|
$html .= ' </h3>';
|
||||||
|
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||||
|
$html .= ' Grid de posts para listados, archivos y resultados de busqueda';
|
||||||
|
$html .= ' </p>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="post-grid">';
|
||||||
|
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||||
|
$html .= ' Restaurar valores por defecto';
|
||||||
|
$html .= ' </button>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildVisibilityGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Visibilidad';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||||
|
$html .= $this->buildSwitch('postGridEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||||
|
|
||||||
|
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||||
|
|
||||||
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
|
// Checkboxes de visibilidad por tipo de página
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
|
$html .= ' </p>';
|
||||||
|
|
||||||
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
|
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', false);
|
||||||
|
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||||
|
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||||
|
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Reglas de exclusion
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'postGrid');
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildContentGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Contenido';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Switches de contenido
|
||||||
|
$showThumbnail = $this->renderer->getFieldValue($componentId, 'content', 'show_thumbnail', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowThumbnail', 'Mostrar imagen destacada', 'bi-image', $showThumbnail);
|
||||||
|
|
||||||
|
$showExcerpt = $this->renderer->getFieldValue($componentId, 'content', 'show_excerpt', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowExcerpt', 'Mostrar extracto', 'bi-text-paragraph', $showExcerpt);
|
||||||
|
|
||||||
|
$showMeta = $this->renderer->getFieldValue($componentId, 'content', 'show_meta', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowMeta', 'Mostrar metadatos', 'bi-info-circle', $showMeta);
|
||||||
|
|
||||||
|
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||||
|
$html .= $this->buildSwitch('postGridShowCategories', 'Mostrar categorias', 'bi-folder', $showCategories);
|
||||||
|
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
|
||||||
|
// Excerpt length
|
||||||
|
$excerptLength = $this->renderer->getFieldValue($componentId, 'content', 'excerpt_length', '20');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridExcerptLength" class="form-label small mb-1 fw-semibold">Longitud del extracto</label>';
|
||||||
|
$html .= ' <select id="postGridExcerptLength" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="10"' . ($excerptLength === '10' ? ' selected' : '') . '>10 palabras</option>';
|
||||||
|
$html .= ' <option value="15"' . ($excerptLength === '15' ? ' selected' : '') . '>15 palabras</option>';
|
||||||
|
$html .= ' <option value="20"' . ($excerptLength === '20' ? ' selected' : '') . '>20 palabras</option>';
|
||||||
|
$html .= ' <option value="25"' . ($excerptLength === '25' ? ' selected' : '') . '>25 palabras</option>';
|
||||||
|
$html .= ' <option value="30"' . ($excerptLength === '30' ? ' selected' : '') . '>30 palabras</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Read more text
|
||||||
|
$readMoreText = $this->renderer->getFieldValue($componentId, 'content', 'read_more_text', 'Leer mas');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridReadMoreText" class="form-label small mb-1 fw-semibold">Texto de leer mas</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridReadMoreText" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($readMoreText) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// No posts message
|
||||||
|
$noPostsMessage = $this->renderer->getFieldValue($componentId, 'content', 'no_posts_message', 'No se encontraron publicaciones');
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$html .= ' <label for="postGridNoPostsMessage" class="form-label small mb-1 fw-semibold">Mensaje sin posts</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridNoPostsMessage" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($noPostsMessage) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLayoutGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Disposicion';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Columns desktop
|
||||||
|
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridColsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||||
|
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Columnas escritorio';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select id="postGridColsDesktop" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||||
|
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||||
|
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Columns tablet
|
||||||
|
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridColsTablet" class="form-label small mb-1 fw-semibold">';
|
||||||
|
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Columnas tablet';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select id="postGridColsTablet" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||||
|
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||||
|
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Columns mobile
|
||||||
|
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridColsMobile" class="form-label small mb-1 fw-semibold">';
|
||||||
|
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Columnas movil';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select id="postGridColsMobile" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||||
|
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Image position
|
||||||
|
$imagePosition = $this->renderer->getFieldValue($componentId, 'layout', 'image_position', 'top');
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$html .= ' <label for="postGridImagePosition" class="form-label small mb-1 fw-semibold">';
|
||||||
|
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Posicion de imagen';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select id="postGridImagePosition" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="top"' . ($imagePosition === 'top' ? ' selected' : '') . '>Arriba</option>';
|
||||||
|
$html .= ' <option value="left"' . ($imagePosition === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||||
|
$html .= ' <option value="none"' . ($imagePosition === 'none' ? ' selected' : '') . '>Sin imagen</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMediaGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Medios';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Fallback image
|
||||||
|
$fallbackImage = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image', '');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridFallbackImage" class="form-label small mb-1 fw-semibold">URL imagen por defecto</label>';
|
||||||
|
$html .= ' <input type="url" id="postGridFallbackImage" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_url($fallbackImage) . '" placeholder="https://...">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Fallback image alt
|
||||||
|
$fallbackImageAlt = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image_alt', 'Imagen por defecto');
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$html .= ' <label for="postGridFallbackImageAlt" class="form-label small mb-1 fw-semibold">Texto alternativo</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridFallbackImageAlt" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($fallbackImageAlt) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTypographyGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Tipografia';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Heading level
|
||||||
|
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h3');
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$html .= ' <label for="postGridHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||||
|
$html .= ' <select id="postGridHeadingLevel" class="form-select form-select-sm">';
|
||||||
|
$html .= ' <option value="h2"' . ($headingLevel === 'h2' ? ' selected' : '') . '>H2</option>';
|
||||||
|
$html .= ' <option value="h3"' . ($headingLevel === 'h3' ? ' selected' : '') . '>H3</option>';
|
||||||
|
$html .= ' <option value="h4"' . ($headingLevel === 'h4' ? ' selected' : '') . '>H4</option>';
|
||||||
|
$html .= ' <option value="h5"' . ($headingLevel === 'h5' ? ' selected' : '') . '>H5</option>';
|
||||||
|
$html .= ' <option value="h6"' . ($headingLevel === 'h6' ? ' selected' : '') . '>H6</option>';
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1.1rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardTitleSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '600');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardTitleWeight" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-0">';
|
||||||
|
|
||||||
|
$excerptSize = $this->renderer->getFieldValue($componentId, 'typography', 'excerpt_size', '0.9rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridExcerptSize" class="form-label small mb-1 fw-semibold">Tamano extracto</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridExcerptSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($excerptSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$metaSize = $this->renderer->getFieldValue($componentId, 'typography', 'meta_size', '0.8rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridMetaSize" class="form-label small mb-1 fw-semibold">Tamano metadatos</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridMetaSize" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($metaSize) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildColorsGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Colores';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||||
|
$html .= $this->buildColorPicker('postGridCardBgColor', 'Fondo', $cardBgColor);
|
||||||
|
|
||||||
|
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#0E2337');
|
||||||
|
$html .= $this->buildColorPicker('postGridCardTitleColor', 'Titulo', $cardTitleColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f9fafb');
|
||||||
|
$html .= $this->buildColorPicker('postGridCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||||
|
|
||||||
|
$cardBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_border_color', '#e5e7eb');
|
||||||
|
$html .= $this->buildColorPicker('postGridCardBorderColor', 'Borde', $cardBorderColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$cardHoverBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_border_color', '#FF8600');
|
||||||
|
$html .= $this->buildColorPicker('postGridCardHoverBorderColor', 'Borde hover', $cardHoverBorderColor);
|
||||||
|
|
||||||
|
$excerptColor = $this->renderer->getFieldValue($componentId, 'colors', 'excerpt_color', '#6b7280');
|
||||||
|
$html .= $this->buildColorPicker('postGridExcerptColor', 'Extracto', $excerptColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$metaColor = $this->renderer->getFieldValue($componentId, 'colors', 'meta_color', '#9ca3af');
|
||||||
|
$html .= $this->buildColorPicker('postGridMetaColor', 'Metadatos', $metaColor);
|
||||||
|
|
||||||
|
$categoryBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_bg_color', '#FFF5EB');
|
||||||
|
$html .= $this->buildColorPicker('postGridCategoryBgColor', 'Fondo cat.', $categoryBgColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$categoryTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_text_color', '#FF8600');
|
||||||
|
$html .= $this->buildColorPicker('postGridCategoryTextColor', 'Texto cat.', $categoryTextColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Paginacion
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$paginationColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_color', '#0E2337');
|
||||||
|
$html .= $this->buildColorPicker('postGridPaginationColor', 'Color', $paginationColor);
|
||||||
|
|
||||||
|
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#FF8600');
|
||||||
|
$html .= $this->buildColorPicker('postGridPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-0">';
|
||||||
|
|
||||||
|
$paginationActiveColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_color', '#ffffff');
|
||||||
|
$html .= $this->buildColorPicker('postGridPaginationActiveColor', 'Activo texto', $paginationActiveColor);
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSpacingGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Espaciado';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridGridGap" class="form-label small mb-1 fw-semibold">Espacio entre cards</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridGridGap" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($gridGap) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.25rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridCardPadding" class="form-label small mb-1 fw-semibold">Padding card</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardPadding" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardPadding) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-0">';
|
||||||
|
|
||||||
|
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '0');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridSectionMarginTop" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '2rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridSectionMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridSectionMarginBottom" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($sectionMarginBottom) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEffectsGroup(string $componentId): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Efectos Visuales';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-3">';
|
||||||
|
|
||||||
|
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.5rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardBorderRadius" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$imageBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'image_border_radius', '0.375rem');
|
||||||
|
$html .= ' <div class="col-6">';
|
||||||
|
$html .= ' <label for="postGridImageBorderRadius" class="form-label small mb-1 fw-semibold">Radio imagen</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridImageBorderRadius" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($imageBorderRadius) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', 'all 0.3s ease');
|
||||||
|
$html .= ' <label for="postGridCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardTransition" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="mb-3">';
|
||||||
|
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 1px 3px rgba(0,0,0,0.1)');
|
||||||
|
$html .= ' <label for="postGridCardShadow" class="form-label small mb-1 fw-semibold">Sombra normal</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardShadow" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' <div class="mb-0">';
|
||||||
|
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 4px 12px rgba(0,0,0,0.15)');
|
||||||
|
$html .= ' <label for="postGridCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||||
|
$html .= ' <input type="text" id="postGridCardHoverShadow" class="form-control form-control-sm" ';
|
||||||
|
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="mb-2">';
|
||||||
|
$html .= ' <div class="form-check form-switch">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildColorPicker(string $id, string $label, string $value): string
|
||||||
|
{
|
||||||
|
$html = ' <div class="col-6">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-label small fw-semibold">%s</label>',
|
||||||
|
esc_html($label)
|
||||||
|
);
|
||||||
|
$html .= ' <div class="input-group input-group-sm">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_attr($value)
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_html(strtoupper($value))
|
||||||
|
);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ final class FieldMapperProvider
|
|||||||
'Footer',
|
'Footer',
|
||||||
'ThemeSettings',
|
'ThemeSettings',
|
||||||
'AdsensePlacement',
|
'AdsensePlacement',
|
||||||
|
'ArchiveHeader',
|
||||||
|
'PostGrid',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
314
Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
Normal file
314
Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Public\ArchiveHeader\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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArchiveHeaderRenderer - Renderiza cabecera dinamica para paginas de archivo
|
||||||
|
*
|
||||||
|
* RESPONSABILIDAD: Generar HTML y CSS del componente Archive Header
|
||||||
|
*
|
||||||
|
* CARACTERISTICAS:
|
||||||
|
* - Deteccion automatica del tipo de archivo (categoria, tag, autor, fecha, busqueda)
|
||||||
|
* - Titulo y descripcion dinamicos
|
||||||
|
* - Contador de posts opcional
|
||||||
|
* - Estilos 100% desde BD via CSSGenerator
|
||||||
|
*
|
||||||
|
* @package ROITheme\Public\ArchiveHeader\Infrastructure\Ui
|
||||||
|
*/
|
||||||
|
final class ArchiveHeaderRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
private const COMPONENT_NAME = 'archive-header';
|
||||||
|
|
||||||
|
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'] ?? [];
|
||||||
|
$typography = $data['typography'] ?? [];
|
||||||
|
$behavior = $data['behavior'] ?? [];
|
||||||
|
|
||||||
|
$cssRules = [];
|
||||||
|
|
||||||
|
// Container
|
||||||
|
$marginTop = $spacing['margin_top'] ?? '2rem';
|
||||||
|
$marginBottom = $spacing['margin_bottom'] ?? '2rem';
|
||||||
|
$padding = $spacing['padding'] ?? '1.5rem';
|
||||||
|
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header', [
|
||||||
|
'margin-top' => $marginTop,
|
||||||
|
'margin-bottom' => $marginBottom,
|
||||||
|
'padding' => $padding,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sticky behavior
|
||||||
|
$isSticky = $behavior['is_sticky'] ?? false;
|
||||||
|
$isSticky = $isSticky === true || $isSticky === '1' || $isSticky === 1;
|
||||||
|
|
||||||
|
if ($isSticky) {
|
||||||
|
$stickyOffset = $behavior['sticky_offset'] ?? '0';
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header', [
|
||||||
|
'position' => 'sticky',
|
||||||
|
'top' => $stickyOffset,
|
||||||
|
'z-index' => '100',
|
||||||
|
'background' => '#ffffff',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
$titleColor = $colors['title_color'] ?? '#0E2337';
|
||||||
|
$titleSize = $typography['title_size'] ?? '2rem';
|
||||||
|
$titleWeight = $typography['title_weight'] ?? '700';
|
||||||
|
$titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.5rem';
|
||||||
|
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header__title', [
|
||||||
|
'color' => $titleColor,
|
||||||
|
'font-size' => $titleSize,
|
||||||
|
'font-weight' => $titleWeight,
|
||||||
|
'margin-bottom' => $titleMarginBottom,
|
||||||
|
'line-height' => '1.2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prefix
|
||||||
|
$prefixColor = $colors['prefix_color'] ?? '#6b7280';
|
||||||
|
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header__prefix', [
|
||||||
|
'color' => $prefixColor,
|
||||||
|
'font-weight' => '400',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
$descColor = $colors['description_color'] ?? '#6b7280';
|
||||||
|
$descSize = $typography['description_size'] ?? '1rem';
|
||||||
|
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header__description', [
|
||||||
|
'color' => $descColor,
|
||||||
|
'font-size' => $descSize,
|
||||||
|
'margin-top' => '0.5rem',
|
||||||
|
'line-height' => '1.6',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Post count badge
|
||||||
|
$countBgColor = $colors['count_bg_color'] ?? '#FF8600';
|
||||||
|
$countTextColor = $colors['count_text_color'] ?? '#ffffff';
|
||||||
|
$countSize = $typography['count_size'] ?? '0.875rem';
|
||||||
|
$countPadding = $spacing['count_padding'] ?? '0.25rem 0.75rem';
|
||||||
|
|
||||||
|
$cssRules[] = $this->cssGenerator->generate('.archive-header__count', [
|
||||||
|
'background-color' => $countBgColor,
|
||||||
|
'color' => $countTextColor,
|
||||||
|
'font-size' => $countSize,
|
||||||
|
'padding' => $countPadding,
|
||||||
|
'border-radius' => '9999px',
|
||||||
|
'font-weight' => '500',
|
||||||
|
'display' => 'inline-block',
|
||||||
|
'margin-left' => '0.75rem',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return implode("\n", $cssRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildHTML(array $data, string $visibilityClass): string
|
||||||
|
{
|
||||||
|
$content = $data['content'] ?? [];
|
||||||
|
$typography = $data['typography'] ?? [];
|
||||||
|
|
||||||
|
$headingLevel = $typography['heading_level'] ?? 'h1';
|
||||||
|
$showPostCount = $content['show_post_count'] ?? true;
|
||||||
|
$showPostCount = $showPostCount === true || $showPostCount === '1' || $showPostCount === 1;
|
||||||
|
$showDescription = $content['show_description'] ?? true;
|
||||||
|
$showDescription = $showDescription === true || $showDescription === '1' || $showDescription === 1;
|
||||||
|
|
||||||
|
// Get context-specific title and description
|
||||||
|
$titleData = $this->getContextualTitle($content);
|
||||||
|
$title = $titleData['title'];
|
||||||
|
$prefix = $titleData['prefix'];
|
||||||
|
$description = $showDescription ? $titleData['description'] : '';
|
||||||
|
|
||||||
|
// Get post count
|
||||||
|
$postCount = $this->getPostCount();
|
||||||
|
$countSingular = $content['posts_count_singular'] ?? 'publicacion';
|
||||||
|
$countPlural = $content['posts_count_plural'] ?? 'publicaciones';
|
||||||
|
$countText = $postCount === 1 ? $countSingular : $countPlural;
|
||||||
|
|
||||||
|
$containerClass = 'archive-header';
|
||||||
|
if (!empty($visibilityClass)) {
|
||||||
|
$containerClass .= ' ' . $visibilityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
||||||
|
|
||||||
|
// Title with optional prefix
|
||||||
|
$html .= sprintf('<%s class="archive-header__title">', esc_attr($headingLevel));
|
||||||
|
|
||||||
|
if (!empty($prefix)) {
|
||||||
|
$html .= sprintf(
|
||||||
|
'<span class="archive-header__prefix">%s</span> ',
|
||||||
|
esc_html($prefix)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= esc_html($title);
|
||||||
|
|
||||||
|
// Post count badge
|
||||||
|
if ($showPostCount && $postCount > 0) {
|
||||||
|
$html .= sprintf(
|
||||||
|
'<span class="archive-header__count">%d %s</span>',
|
||||||
|
$postCount,
|
||||||
|
esc_html($countText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= sprintf('</%s>', esc_attr($headingLevel));
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (!empty($description)) {
|
||||||
|
$html .= sprintf(
|
||||||
|
'<p class="archive-header__description">%s</p>',
|
||||||
|
esc_html($description)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contextual title based on current page type
|
||||||
|
*
|
||||||
|
* @param array $content Content settings from schema
|
||||||
|
* @return array{title: string, prefix: string, description: string}
|
||||||
|
*/
|
||||||
|
private function getContextualTitle(array $content): array
|
||||||
|
{
|
||||||
|
$title = '';
|
||||||
|
$prefix = '';
|
||||||
|
$description = '';
|
||||||
|
|
||||||
|
if (is_category()) {
|
||||||
|
$prefix = $content['category_prefix'] ?? 'Categoria:';
|
||||||
|
$title = single_cat_title('', false) ?: '';
|
||||||
|
$description = category_description() ?: '';
|
||||||
|
} elseif (is_tag()) {
|
||||||
|
$prefix = $content['tag_prefix'] ?? 'Etiqueta:';
|
||||||
|
$title = single_tag_title('', false) ?: '';
|
||||||
|
$description = tag_description() ?: '';
|
||||||
|
} elseif (is_author()) {
|
||||||
|
$prefix = $content['author_prefix'] ?? 'Articulos de:';
|
||||||
|
$title = get_the_author() ?: '';
|
||||||
|
$description = get_the_author_meta('description') ?: '';
|
||||||
|
} elseif (is_date()) {
|
||||||
|
$prefix = $content['date_prefix'] ?? 'Archivo:';
|
||||||
|
$title = $this->getDateArchiveTitle();
|
||||||
|
$description = '';
|
||||||
|
} elseif (is_search()) {
|
||||||
|
$prefix = $content['search_prefix'] ?? 'Resultados para:';
|
||||||
|
$title = get_search_query() ?: '';
|
||||||
|
$description = '';
|
||||||
|
} elseif (is_home()) {
|
||||||
|
$prefix = '';
|
||||||
|
$title = $content['blog_title'] ?? 'Blog';
|
||||||
|
$description = '';
|
||||||
|
} elseif (is_archive()) {
|
||||||
|
$prefix = '';
|
||||||
|
$title = get_the_archive_title() ?: 'Archivo';
|
||||||
|
$description = get_the_archive_description() ?: '';
|
||||||
|
} else {
|
||||||
|
$prefix = '';
|
||||||
|
$title = $content['blog_title'] ?? 'Blog';
|
||||||
|
$description = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title,
|
||||||
|
'prefix' => $prefix,
|
||||||
|
'description' => strip_tags($description),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted title for date archives
|
||||||
|
*/
|
||||||
|
private function getDateArchiveTitle(): string
|
||||||
|
{
|
||||||
|
if (is_day()) {
|
||||||
|
return get_the_date();
|
||||||
|
} elseif (is_month()) {
|
||||||
|
return get_the_date('F Y');
|
||||||
|
} elseif (is_year()) {
|
||||||
|
return get_the_date('Y');
|
||||||
|
}
|
||||||
|
return get_the_archive_title() ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total post count for current query
|
||||||
|
*/
|
||||||
|
private function getPostCount(): int
|
||||||
|
{
|
||||||
|
global $wp_query;
|
||||||
|
return $wp_query->found_posts ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
572
Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
Normal file
572
Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
233
Schemas/archive-header.json
Normal file
233
Schemas/archive-header.json
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
{
|
||||||
|
"component_name": "archive-header",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Cabecera dinamica para paginas de archivo con titulo y descripcion contextual",
|
||||||
|
"groups": {
|
||||||
|
"visibility": {
|
||||||
|
"label": "Visibilidad",
|
||||||
|
"priority": 10,
|
||||||
|
"fields": {
|
||||||
|
"is_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Activar componente",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"show_on_desktop": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Mostrar en escritorio",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Muestra el componente en pantallas >= 992px"
|
||||||
|
},
|
||||||
|
"show_on_mobile": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Mostrar en movil",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Muestra el componente en pantallas < 992px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"label": "Contenido",
|
||||||
|
"priority": 20,
|
||||||
|
"fields": {
|
||||||
|
"blog_title": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Titulo del blog",
|
||||||
|
"default": "Blog",
|
||||||
|
"editable": true,
|
||||||
|
"description": "Titulo mostrado en la pagina principal del blog"
|
||||||
|
},
|
||||||
|
"show_post_count": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Mostrar contador de posts",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Muestra el numero de posts encontrados"
|
||||||
|
},
|
||||||
|
"show_description": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Mostrar descripcion",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Muestra la descripcion de categoria/tag si existe"
|
||||||
|
},
|
||||||
|
"category_prefix": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Prefijo categoria",
|
||||||
|
"default": "Categoria:",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"tag_prefix": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Prefijo etiqueta",
|
||||||
|
"default": "Etiqueta:",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"author_prefix": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Prefijo autor",
|
||||||
|
"default": "Articulos de:",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"date_prefix": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Prefijo fecha",
|
||||||
|
"default": "Archivo:",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"search_prefix": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Prefijo busqueda",
|
||||||
|
"default": "Resultados para:",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"posts_count_singular": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Texto singular posts",
|
||||||
|
"default": "publicacion",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"posts_count_plural": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Texto plural posts",
|
||||||
|
"default": "publicaciones",
|
||||||
|
"editable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typography": {
|
||||||
|
"label": "Tipografia",
|
||||||
|
"priority": 30,
|
||||||
|
"fields": {
|
||||||
|
"heading_level": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Nivel de encabezado",
|
||||||
|
"default": "h1",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||||
|
"description": "Nivel semantico del titulo para SEO"
|
||||||
|
},
|
||||||
|
"title_size": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Tamano titulo",
|
||||||
|
"default": "2rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"title_weight": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Peso titulo",
|
||||||
|
"default": "700",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"description_size": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Tamano descripcion",
|
||||||
|
"default": "1rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"count_size": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Tamano contador",
|
||||||
|
"default": "0.875rem",
|
||||||
|
"editable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"label": "Colores",
|
||||||
|
"priority": 40,
|
||||||
|
"fields": {
|
||||||
|
"title_color": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Color titulo",
|
||||||
|
"default": "#0E2337",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"description_color": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Color descripcion",
|
||||||
|
"default": "#6b7280",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"count_bg_color": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Fondo contador",
|
||||||
|
"default": "#FF8600",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"count_text_color": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Texto contador",
|
||||||
|
"default": "#ffffff",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"prefix_color": {
|
||||||
|
"type": "color",
|
||||||
|
"label": "Color prefijo",
|
||||||
|
"default": "#6b7280",
|
||||||
|
"editable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"label": "Espaciado",
|
||||||
|
"priority": 50,
|
||||||
|
"fields": {
|
||||||
|
"margin_top": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Margen superior",
|
||||||
|
"default": "2rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"margin_bottom": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Margen inferior",
|
||||||
|
"default": "2rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"padding": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Padding interno",
|
||||||
|
"default": "1.5rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"title_margin_bottom": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Margen inferior titulo",
|
||||||
|
"default": "0.5rem",
|
||||||
|
"editable": true
|
||||||
|
},
|
||||||
|
"count_padding": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Padding contador",
|
||||||
|
"default": "0.25rem 0.75rem",
|
||||||
|
"editable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"behavior": {
|
||||||
|
"label": "Comportamiento",
|
||||||
|
"priority": 70,
|
||||||
|
"fields": {
|
||||||
|
"is_sticky": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Header fijo",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Mantiene el header visible al hacer scroll"
|
||||||
|
},
|
||||||
|
"sticky_offset": {
|
||||||
|
"type": "text",
|
||||||
|
"label": "Offset sticky",
|
||||||
|
"default": "0",
|
||||||
|
"editable": true,
|
||||||
|
"description": "Distancia desde el top cuando es sticky"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
Schemas/post-grid.json
Normal file
271
Schemas/post-grid.json
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
{
|
||||||
|
"component_name": "post-grid",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Grid de posts para templates de listados usando el loop principal de WordPress",
|
||||||
|
"groups": {
|
||||||
|
"visibility": {
|
||||||
|
"label": "Visibilidad",
|
||||||
|
"priority": 10,
|
||||||
|
"fields": {
|
||||||
|
"is_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Habilitar componente"
|
||||||
|
},
|
||||||
|
"show_on_desktop": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar en desktop"
|
||||||
|
},
|
||||||
|
"show_on_mobile": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar en movil"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"label": "Contenido",
|
||||||
|
"priority": 20,
|
||||||
|
"fields": {
|
||||||
|
"show_thumbnail": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar imagen destacada"
|
||||||
|
},
|
||||||
|
"show_excerpt": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar extracto"
|
||||||
|
},
|
||||||
|
"show_meta": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar metadatos (fecha, autor)"
|
||||||
|
},
|
||||||
|
"show_categories": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"label": "Mostrar categorias"
|
||||||
|
},
|
||||||
|
"excerpt_length": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "20",
|
||||||
|
"label": "Longitud del extracto (palabras)",
|
||||||
|
"options": ["10", "15", "20", "25", "30"]
|
||||||
|
},
|
||||||
|
"read_more_text": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "Leer mas",
|
||||||
|
"label": "Texto de leer mas"
|
||||||
|
},
|
||||||
|
"no_posts_message": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "No se encontraron publicaciones",
|
||||||
|
"label": "Mensaje cuando no hay posts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typography": {
|
||||||
|
"label": "Tipografia",
|
||||||
|
"priority": 30,
|
||||||
|
"fields": {
|
||||||
|
"heading_level": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "h3",
|
||||||
|
"label": "Nivel de encabezado de tarjetas",
|
||||||
|
"options": ["h2", "h3", "h4", "h5", "h6"]
|
||||||
|
},
|
||||||
|
"card_title_size": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "1.1rem",
|
||||||
|
"label": "Tamano titulo de tarjeta"
|
||||||
|
},
|
||||||
|
"card_title_weight": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "600",
|
||||||
|
"label": "Peso titulo de tarjeta"
|
||||||
|
},
|
||||||
|
"excerpt_size": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0.9rem",
|
||||||
|
"label": "Tamano de extracto"
|
||||||
|
},
|
||||||
|
"meta_size": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0.8rem",
|
||||||
|
"label": "Tamano de metadatos"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"label": "Colores",
|
||||||
|
"priority": 40,
|
||||||
|
"fields": {
|
||||||
|
"card_bg_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#ffffff",
|
||||||
|
"label": "Fondo de tarjeta"
|
||||||
|
},
|
||||||
|
"card_title_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#0E2337",
|
||||||
|
"label": "Color titulo de tarjeta"
|
||||||
|
},
|
||||||
|
"card_hover_bg_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#f9fafb",
|
||||||
|
"label": "Fondo hover de tarjeta"
|
||||||
|
},
|
||||||
|
"card_border_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#e5e7eb",
|
||||||
|
"label": "Color borde de tarjeta"
|
||||||
|
},
|
||||||
|
"card_hover_border_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#FF8600",
|
||||||
|
"label": "Color borde hover"
|
||||||
|
},
|
||||||
|
"excerpt_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#6b7280",
|
||||||
|
"label": "Color de extracto"
|
||||||
|
},
|
||||||
|
"meta_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#9ca3af",
|
||||||
|
"label": "Color de metadatos"
|
||||||
|
},
|
||||||
|
"category_bg_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#FFF5EB",
|
||||||
|
"label": "Fondo de categoria"
|
||||||
|
},
|
||||||
|
"category_text_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#FF8600",
|
||||||
|
"label": "Color texto categoria"
|
||||||
|
},
|
||||||
|
"pagination_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#0E2337",
|
||||||
|
"label": "Color de paginacion"
|
||||||
|
},
|
||||||
|
"pagination_active_bg": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#FF8600",
|
||||||
|
"label": "Fondo paginacion activa"
|
||||||
|
},
|
||||||
|
"pagination_active_color": {
|
||||||
|
"type": "color",
|
||||||
|
"default": "#ffffff",
|
||||||
|
"label": "Color texto paginacion activa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"label": "Espaciado",
|
||||||
|
"priority": 50,
|
||||||
|
"fields": {
|
||||||
|
"grid_gap": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "1.5rem",
|
||||||
|
"label": "Espacio entre tarjetas"
|
||||||
|
},
|
||||||
|
"card_padding": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "1.25rem",
|
||||||
|
"label": "Padding interno de tarjeta"
|
||||||
|
},
|
||||||
|
"section_margin_top": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0",
|
||||||
|
"label": "Margen superior de seccion"
|
||||||
|
},
|
||||||
|
"section_margin_bottom": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "2rem",
|
||||||
|
"label": "Margen inferior de seccion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visual_effects": {
|
||||||
|
"label": "Efectos Visuales",
|
||||||
|
"priority": 60,
|
||||||
|
"fields": {
|
||||||
|
"card_border_radius": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0.5rem",
|
||||||
|
"label": "Radio de borde de tarjeta"
|
||||||
|
},
|
||||||
|
"card_shadow": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0 1px 3px rgba(0,0,0,0.1)",
|
||||||
|
"label": "Sombra de tarjeta"
|
||||||
|
},
|
||||||
|
"card_hover_shadow": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0 4px 12px rgba(0,0,0,0.15)",
|
||||||
|
"label": "Sombra hover de tarjeta"
|
||||||
|
},
|
||||||
|
"card_transition": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "all 0.3s ease",
|
||||||
|
"label": "Transicion de tarjeta"
|
||||||
|
},
|
||||||
|
"image_border_radius": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "0.375rem",
|
||||||
|
"label": "Radio de borde de imagen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"label": "Disposicion",
|
||||||
|
"priority": 80,
|
||||||
|
"fields": {
|
||||||
|
"columns_desktop": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "3",
|
||||||
|
"label": "Columnas en desktop",
|
||||||
|
"options": ["2", "3", "4"]
|
||||||
|
},
|
||||||
|
"columns_tablet": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "2",
|
||||||
|
"label": "Columnas en tablet",
|
||||||
|
"options": ["1", "2", "3"]
|
||||||
|
},
|
||||||
|
"columns_mobile": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "1",
|
||||||
|
"label": "Columnas en movil",
|
||||||
|
"options": ["1", "2"]
|
||||||
|
},
|
||||||
|
"image_position": {
|
||||||
|
"type": "select",
|
||||||
|
"default": "top",
|
||||||
|
"label": "Posicion de imagen",
|
||||||
|
"options": ["top", "left", "none"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"label": "Medios",
|
||||||
|
"priority": 90,
|
||||||
|
"fields": {
|
||||||
|
"fallback_image": {
|
||||||
|
"type": "url",
|
||||||
|
"default": "",
|
||||||
|
"label": "Imagen por defecto (URL)"
|
||||||
|
},
|
||||||
|
"fallback_image_alt": {
|
||||||
|
"type": "text",
|
||||||
|
"default": "Imagen por defecto",
|
||||||
|
"label": "Texto alternativo imagen por defecto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,15 +22,15 @@ final class EvaluatePageVisibilityUseCase
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evalúa si el componente debe mostrarse en la página actual
|
* Evalua si el componente debe mostrarse en la pagina actual
|
||||||
*/
|
*/
|
||||||
public function execute(string $componentName): bool
|
public function execute(string $componentName): bool
|
||||||
{
|
{
|
||||||
$config = $this->visibilityRepository->getVisibilityConfig($componentName);
|
$config = $this->visibilityRepository->getVisibilityConfig($componentName);
|
||||||
|
|
||||||
if (empty($config)) {
|
if (empty($config)) {
|
||||||
// Usar constante compartida (DRY)
|
// Usar defaults especificos por componente si existen
|
||||||
$config = VisibilityDefaults::DEFAULT_VISIBILITY;
|
$config = VisibilityDefaults::getForComponent($componentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pageType = $this->pageTypeDetector->detect();
|
$pageType = $this->pageTypeDetector->detect();
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ namespace ROITheme\Shared\Domain\Constants;
|
|||||||
final class VisibilityDefaults
|
final class VisibilityDefaults
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Configuración de visibilidad por defecto para nuevos componentes
|
* Configuracion de visibilidad por defecto para componentes generales
|
||||||
*
|
*
|
||||||
* - Home: SÍ mostrar (página principal)
|
* - Home: SI mostrar (pagina principal)
|
||||||
* - Posts: SÍ mostrar (artículos del blog)
|
* - Posts: SI mostrar (articulos del blog)
|
||||||
* - Pages: SÍ mostrar (páginas estáticas)
|
* - Pages: SI mostrar (paginas estaticas)
|
||||||
* - Archives: NO mostrar (listados de categorías/tags)
|
* - Archives: NO mostrar (listados de categorias/tags)
|
||||||
* - Search: NO mostrar (resultados de búsqueda)
|
* - Search: NO mostrar (resultados de busqueda)
|
||||||
*/
|
*/
|
||||||
public const DEFAULT_VISIBILITY = [
|
public const DEFAULT_VISIBILITY = [
|
||||||
'show_on_home' => true,
|
'show_on_home' => true,
|
||||||
@@ -33,7 +33,39 @@ final class VisibilityDefaults
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lista de campos de visibilidad válidos
|
* Defaults especificos por componente (sobrescriben DEFAULT_VISIBILITY)
|
||||||
|
*
|
||||||
|
* Componentes de listados:
|
||||||
|
* - archive-header: Solo en archives (home para blog title)
|
||||||
|
* - post-grid: En home, archives y search
|
||||||
|
* - cta-box-sidebar: Tambien en archives
|
||||||
|
*/
|
||||||
|
public const COMPONENT_VISIBILITY = [
|
||||||
|
'archive-header' => [
|
||||||
|
'show_on_home' => true, // Para mostrar blog_title
|
||||||
|
'show_on_posts' => false,
|
||||||
|
'show_on_pages' => false,
|
||||||
|
'show_on_archives' => true, // Proposito principal
|
||||||
|
'show_on_search' => true, // Mostrar "Resultados: X"
|
||||||
|
],
|
||||||
|
'post-grid' => [
|
||||||
|
'show_on_home' => true, // Blog principal
|
||||||
|
'show_on_posts' => false,
|
||||||
|
'show_on_pages' => false,
|
||||||
|
'show_on_archives' => true, // Listados de categoria/tag
|
||||||
|
'show_on_search' => true, // Resultados de busqueda
|
||||||
|
],
|
||||||
|
'cta-box-sidebar' => [
|
||||||
|
'show_on_home' => true,
|
||||||
|
'show_on_posts' => true,
|
||||||
|
'show_on_pages' => true,
|
||||||
|
'show_on_archives' => true, // Visible en archives
|
||||||
|
'show_on_search' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista de campos de visibilidad validos
|
||||||
*/
|
*/
|
||||||
public const VISIBILITY_FIELDS = [
|
public const VISIBILITY_FIELDS = [
|
||||||
'show_on_home',
|
'show_on_home',
|
||||||
@@ -42,4 +74,15 @@ final class VisibilityDefaults
|
|||||||
'show_on_archives',
|
'show_on_archives',
|
||||||
'show_on_search',
|
'show_on_search',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene los defaults para un componente especifico
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return array<string, bool> Configuracion de visibilidad
|
||||||
|
*/
|
||||||
|
public static function getForComponent(string $componentName): array
|
||||||
|
{
|
||||||
|
return self::COMPONENT_VISIBILITY[$componentName] ?? self::DEFAULT_VISIBILITY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ final class MigratePageVisibilityService
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ejecuta la migración para todos los componentes
|
* Ejecuta la migracion para todos los componentes
|
||||||
*
|
*
|
||||||
* @return array{created: int, skipped: int}
|
* @return array{created: int, skipped: int}
|
||||||
*/
|
*/
|
||||||
@@ -37,10 +37,10 @@ final class MigratePageVisibilityService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usar constante compartida (DRY)
|
// Usar defaults especificos por componente si existen
|
||||||
$this->visibilityRepository->createDefaultVisibility(
|
$this->visibilityRepository->createDefaultVisibility(
|
||||||
$componentName,
|
$componentName,
|
||||||
VisibilityDefaults::DEFAULT_VISIBILITY
|
VisibilityDefaults::getForComponent($componentName)
|
||||||
);
|
);
|
||||||
$created++;
|
$created++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ final class WordPressPageTypeDetector implements PageTypeDetectorInterface
|
|||||||
|
|
||||||
public function isHome(): bool
|
public function isHome(): bool
|
||||||
{
|
{
|
||||||
return is_front_page();
|
// is_front_page() = pagina de inicio configurada
|
||||||
|
// is_home() = pagina de posts (blog)
|
||||||
|
// Ambas cuentan como "home" para visibilidad
|
||||||
|
return is_front_page() || is_home();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPost(): bool
|
public function isPost(): bool
|
||||||
|
|||||||
264
archive.php
264
archive.php
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying archive pages
|
* The template for displaying archive pages
|
||||||
*
|
*
|
||||||
* This template displays date-based, category, tag, author, and post type
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* archives with a dynamic title, description, and post loop.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#archive
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#archive
|
||||||
*
|
*
|
||||||
@@ -16,211 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Archive Header -->
|
|
||||||
<header class="page-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Archive title
|
if (function_exists('roi_render_component')) {
|
||||||
the_archive_title( '<h1 class="page-title">', '</h1>' );
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
// Archive description
|
?>
|
||||||
$archive_description = get_the_archive_description();
|
|
||||||
if ( ! empty( $archive_description ) ) :
|
<!-- Archive Header - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('archive-header');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<?php
|
||||||
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<div class="archive-description">
|
|
||||||
<?php echo wp_kses_post( wpautop( $archive_description ) ); ?>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Archive Posts Loop -->
|
|
||||||
<div class="archive-posts">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
|
||||||
|
|
||||||
<!-- Post Thumbnail -->
|
|
||||||
<?php if ( has_post_thumbnail() ) : ?>
|
|
||||||
<div class="post-thumbnail">
|
|
||||||
<a href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
|
|
||||||
<?php
|
|
||||||
the_post_thumbnail(
|
|
||||||
'roi-featured-medium',
|
|
||||||
array(
|
|
||||||
'alt' => the_title_attribute(
|
|
||||||
array(
|
|
||||||
'echo' => false,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'loading' => 'lazy',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Post Content -->
|
</div><!-- .row -->
|
||||||
<div class="post-content">
|
</div><!-- .container -->
|
||||||
|
|
||||||
<!-- Post Header -->
|
|
||||||
<header class="entry-header">
|
|
||||||
|
|
||||||
<!-- Category Badges -->
|
|
||||||
<?php
|
|
||||||
$categories = get_the_category();
|
|
||||||
if ( ! empty( $categories ) ) :
|
|
||||||
?>
|
|
||||||
<div class="entry-categories">
|
|
||||||
<?php foreach ( $categories as $category ) : ?>
|
|
||||||
<a href="<?php echo esc_url( get_category_link( $category->term_id ) ); ?>"
|
|
||||||
class="category-badge"
|
|
||||||
rel="category tag">
|
|
||||||
<?php echo esc_html( $category->name ); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Post Title -->
|
|
||||||
<?php
|
|
||||||
if ( is_singular() ) :
|
|
||||||
the_title( '<h1 class="entry-title">', '</h1>' );
|
|
||||||
else :
|
|
||||||
the_title(
|
|
||||||
sprintf(
|
|
||||||
'<h2 class="entry-title"><a href="%s" rel="bookmark">',
|
|
||||||
esc_url( get_permalink() )
|
|
||||||
),
|
|
||||||
'</a></h2>'
|
|
||||||
);
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Post Meta -->
|
|
||||||
<div class="entry-meta">
|
|
||||||
<span class="posted-on">
|
|
||||||
<time class="entry-date published" datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>">
|
|
||||||
<?php echo esc_html( get_the_date() ); ?>
|
|
||||||
</time>
|
|
||||||
</span>
|
|
||||||
<span class="byline">
|
|
||||||
<span class="author vcard">
|
|
||||||
<a class="url fn n" href="<?php echo esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ); ?>">
|
|
||||||
<?php echo esc_html( get_the_author() ); ?>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div><!-- .entry-meta -->
|
|
||||||
|
|
||||||
</header><!-- .entry-header -->
|
|
||||||
|
|
||||||
<!-- Post Excerpt -->
|
|
||||||
<div class="entry-summary">
|
|
||||||
<?php the_excerpt(); ?>
|
|
||||||
</div><!-- .entry-summary -->
|
|
||||||
|
|
||||||
<!-- Read More Link -->
|
|
||||||
<div class="entry-footer">
|
|
||||||
<a href="<?php the_permalink(); ?>" class="read-more-link">
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
wp_kses(
|
|
||||||
/* translators: %s: Post title. Only visible to screen readers. */
|
|
||||||
__( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'roi-theme' ),
|
|
||||||
array(
|
|
||||||
'span' => array(
|
|
||||||
'class' => array(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
get_the_title()
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
<span class="read-more-icon" aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div><!-- .entry-footer -->
|
|
||||||
|
|
||||||
</div><!-- .post-content -->
|
|
||||||
|
|
||||||
</article><!-- #post-<?php the_ID(); ?> -->
|
|
||||||
|
|
||||||
<?php endwhile; ?>
|
|
||||||
|
|
||||||
</div><!-- .archive-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
<section class="no-results not-found">
|
|
||||||
<header class="page-header">
|
|
||||||
<h1 class="page-title">
|
|
||||||
<?php esc_html_e( 'Nothing Found', 'roi-theme' ); ?>
|
|
||||||
</h1>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<div class="page-content">
|
|
||||||
<p>
|
|
||||||
<?php esc_html_e( 'It seems we can’t find what you’re looking for. Perhaps searching can help.', 'roi-theme' ); ?>
|
|
||||||
</p>
|
|
||||||
</div><!-- .page-content -->
|
|
||||||
</section><!-- .no-results -->
|
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
195
author.php
195
author.php
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying author archive pages
|
* The template for displaying author archive pages
|
||||||
*
|
*
|
||||||
* This template displays posts from a specific author with author
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* bio and information at the top.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#author
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#author
|
||||||
*
|
*
|
||||||
@@ -16,148 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Author Archive Header -->
|
|
||||||
<header class="page-header author-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Get the author
|
if (function_exists('roi_render_component')) {
|
||||||
$author = get_queried_object();
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="author-info">
|
<!-- Archive Header - Componente dinamico (detecta autor automaticamente) -->
|
||||||
<!-- Author Avatar -->
|
|
||||||
<div class="author-avatar">
|
|
||||||
<?php
|
<?php
|
||||||
echo get_avatar(
|
if (function_exists('roi_render_component')) {
|
||||||
$author->ID,
|
echo roi_render_component('archive-header');
|
||||||
120,
|
}
|
||||||
'',
|
?>
|
||||||
sprintf(
|
|
||||||
/* translators: %s: author name */
|
<!-- Main Content Grid -->
|
||||||
esc_attr__( 'Avatar for %s', 'roi-theme' ),
|
<?php
|
||||||
esc_html( $author->display_name )
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
),
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
array(
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
'class' => 'author-avatar-img',
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
)
|
: false;
|
||||||
);
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Author Details -->
|
|
||||||
<div class="author-details">
|
|
||||||
<h1 class="page-title author-title">
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: author display name */
|
|
||||||
esc_html__( 'Posts by %s', 'roi-theme' ),
|
|
||||||
'<span class="author-name">' . esc_html( $author->display_name ) . '</span>'
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Author Bio -->
|
|
||||||
<?php
|
|
||||||
$author_bio = get_the_author_meta( 'description', $author->ID );
|
|
||||||
if ( ! empty( $author_bio ) ) :
|
|
||||||
?>
|
|
||||||
<div class="author-bio">
|
|
||||||
<?php echo wp_kses_post( wpautop( $author_bio ) ); ?>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Author Stats -->
|
</div><!-- .row -->
|
||||||
<div class="author-meta">
|
</div><!-- .container -->
|
||||||
<span class="author-posts-count">
|
|
||||||
<?php
|
|
||||||
$post_count = count_user_posts( $author->ID, 'post', true );
|
|
||||||
printf(
|
|
||||||
/* translators: %s: number of posts */
|
|
||||||
esc_html( _n( '%s post', '%s posts', $post_count, 'roi-theme' ) ),
|
|
||||||
esc_html( number_format_i18n( $post_count ) )
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Author Posts Loop -->
|
|
||||||
<div class="archive-posts author-posts">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include the Post-Type-specific template for the content.
|
|
||||||
* If you want to override this in a child theme, then include a file
|
|
||||||
* called content-___.php (where ___ is the Post Type name) and that will be used instead.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', get_post_type() );
|
|
||||||
|
|
||||||
endwhile;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .archive-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', 'none' );
|
|
||||||
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
171
category.php
171
category.php
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying category archive pages
|
* The template for displaying category archive pages
|
||||||
*
|
*
|
||||||
* This template displays posts from a specific category with category
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* information and description at the top.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#category
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#category
|
||||||
*
|
*
|
||||||
@@ -16,116 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Category Archive Header -->
|
|
||||||
<header class="page-header category-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Category title
|
if (function_exists('roi_render_component')) {
|
||||||
the_archive_title( '<h1 class="page-title category-title">', '</h1>' );
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
// Category description
|
|
||||||
$category_description = category_description();
|
|
||||||
if ( ! empty( $category_description ) ) :
|
|
||||||
?>
|
?>
|
||||||
<div class="archive-description category-description">
|
|
||||||
<?php echo wp_kses_post( wpautop( $category_description ) ); ?>
|
<!-- Archive Header - Componente dinamico (detecta categoria automaticamente) -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('archive-header');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<?php
|
||||||
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Category metadata -->
|
</div><!-- .row -->
|
||||||
<?php
|
</div><!-- .container -->
|
||||||
$category = get_queried_object();
|
|
||||||
if ( $category ) :
|
|
||||||
?>
|
|
||||||
<div class="category-meta">
|
|
||||||
<span class="category-count">
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: number of posts */
|
|
||||||
esc_html( _n( '%s post', '%s posts', $category->count, 'roi-theme' ) ),
|
|
||||||
esc_html( number_format_i18n( $category->count ) )
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Category Posts Loop -->
|
|
||||||
<div class="archive-posts category-posts">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include the Post-Type-specific template for the content.
|
|
||||||
* If you want to override this in a child theme, then include a file
|
|
||||||
* called content-___.php (where ___ is the Post Type name) and that will be used instead.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', get_post_type() );
|
|
||||||
|
|
||||||
endwhile;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .archive-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', 'none' );
|
|
||||||
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
168
date.php
168
date.php
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying date-based archive pages
|
* The template for displaying date-based archive pages
|
||||||
*
|
*
|
||||||
* This template displays posts from a specific date (year, month, or day)
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* with the date information displayed at the top.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#date
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#date
|
||||||
*
|
*
|
||||||
@@ -16,113 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Date Archive Header -->
|
|
||||||
<header class="page-header date-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Date archive title
|
if (function_exists('roi_render_component')) {
|
||||||
the_archive_title( '<h1 class="page-title date-title">', '</h1>' );
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
// Date archive description
|
|
||||||
$date_description = get_the_archive_description();
|
|
||||||
if ( ! empty( $date_description ) ) :
|
|
||||||
?>
|
?>
|
||||||
<div class="archive-description date-description">
|
|
||||||
<?php echo wp_kses_post( wpautop( $date_description ) ); ?>
|
<!-- Archive Header - Componente dinamico (detecta fecha automaticamente) -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('archive-header');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<?php
|
||||||
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Date metadata -->
|
</div><!-- .row -->
|
||||||
<div class="date-meta">
|
</div><!-- .container -->
|
||||||
<span class="posts-count">
|
|
||||||
<?php
|
|
||||||
global $wp_query;
|
|
||||||
$found_posts = $wp_query->found_posts;
|
|
||||||
printf(
|
|
||||||
/* translators: %s: number of posts */
|
|
||||||
esc_html( _n( '%s post', '%s posts', $found_posts, 'roi-theme' ) ),
|
|
||||||
esc_html( number_format_i18n( $found_posts ) )
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Date Posts Loop -->
|
|
||||||
<div class="archive-posts date-posts">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include the Post-Type-specific template for the content.
|
|
||||||
* If you want to override this in a child theme, then include a file
|
|
||||||
* called content-___.php (where ___ is the Post Type name) and that will be used instead.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', get_post_type() );
|
|
||||||
|
|
||||||
endwhile;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .archive-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', 'none' );
|
|
||||||
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
@@ -207,8 +207,17 @@ function roi_render_component(string $componentName): string {
|
|||||||
// Decodificar valor
|
// Decodificar valor
|
||||||
$value = $row->attribute_value;
|
$value = $row->attribute_value;
|
||||||
|
|
||||||
// Convertir booleanos almacenados como '1' o '0'
|
// Solo convertir a booleano campos que realmente son booleanos
|
||||||
if ($value === '1' || $value === '0') {
|
// Los grupos 'visibility', '_page_visibility' y campos que empiezan con 'show_', 'is_', 'enable'
|
||||||
|
$isBooleanField = (
|
||||||
|
$row->group_name === 'visibility' ||
|
||||||
|
$row->group_name === '_page_visibility' ||
|
||||||
|
str_starts_with($row->attribute_name, 'show_') ||
|
||||||
|
str_starts_with($row->attribute_name, 'is_') ||
|
||||||
|
str_starts_with($row->attribute_name, 'enable')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($isBooleanField && ($value === '1' || $value === '0')) {
|
||||||
$value = ($value === '1');
|
$value = ($value === '1');
|
||||||
} else {
|
} else {
|
||||||
// Intentar decodificar JSON
|
// Intentar decodificar JSON
|
||||||
@@ -321,6 +330,12 @@ function roi_render_component(string $componentName): string {
|
|||||||
case 'footer':
|
case 'footer':
|
||||||
$renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator);
|
$renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator);
|
||||||
break;
|
break;
|
||||||
|
case 'archive-header':
|
||||||
|
$renderer = new \ROITheme\Public\ArchiveHeader\Infrastructure\Ui\ArchiveHeaderRenderer($cssGenerator);
|
||||||
|
break;
|
||||||
|
case 'post-grid':
|
||||||
|
$renderer = new \ROITheme\Public\PostGrid\Infrastructure\Ui\PostGridRenderer($cssGenerator);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$renderer) {
|
if (!$renderer) {
|
||||||
|
|||||||
159
home.php
159
home.php
@@ -2,9 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying the blog posts index
|
* The template for displaying the blog posts index
|
||||||
*
|
*
|
||||||
* This template is used when the blog posts page is different from the front page.
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* It displays a listing of recent blog posts with pagination.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
* Set in WordPress Settings > Reading > "Posts page".
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#home
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#home
|
||||||
*
|
*
|
||||||
@@ -17,107 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Blog Header -->
|
|
||||||
<header class="page-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Display blog page title
|
if (function_exists('roi_render_component')) {
|
||||||
if ( is_home() && ! is_front_page() && get_option( 'page_for_posts' ) ) :
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<h1 class="page-title">
|
|
||||||
<?php echo esc_html( get_the_title( get_option( 'page_for_posts' ) ) ); ?>
|
<!-- Archive Header - Componente dinamico -->
|
||||||
</h1>
|
|
||||||
<?php
|
<?php
|
||||||
// Display blog page description if available
|
if (function_exists('roi_render_component')) {
|
||||||
$blog_page = get_post( get_option( 'page_for_posts' ) );
|
echo roi_render_component('archive-header');
|
||||||
if ( $blog_page && ! empty( $blog_page->post_content ) ) :
|
}
|
||||||
?>
|
?>
|
||||||
<div class="page-description">
|
|
||||||
<?php echo wp_kses_post( wpautop( $blog_page->post_excerpt ) ); ?>
|
<!-- Main Content Grid -->
|
||||||
|
<?php
|
||||||
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php else : ?>
|
|
||||||
<h1 class="page-title">
|
|
||||||
<?php esc_html_e( 'Blog', 'roi-theme' ); ?>
|
|
||||||
</h1>
|
|
||||||
<?php endif; ?>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Blog Posts Loop -->
|
</div><!-- .row -->
|
||||||
<div class="blog-posts">
|
</div><!-- .container -->
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include the Post-Type-specific template for the content.
|
|
||||||
* If you want to override this in a child theme, then include a file
|
|
||||||
* called content-___.php (where ___ is the Post Type name) and that will be used instead.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', get_post_type() );
|
|
||||||
|
|
||||||
endwhile;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .blog-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', 'none' );
|
|
||||||
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
417
openspec/specs/templates-unificados/spec.md
Normal file
417
openspec/specs/templates-unificados/spec.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# Especificacion de Templates Unificados para Blog/Archive
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define la arquitectura para unificar todos los templates de listados (blog, categorias, tags, archives) usando la misma estructura que `single.php`, aprovechando el sistema de visibilidad existente para controlar que componentes mostrar en cada contexto. Incluye la creacion de dos nuevos componentes: `archive-header` y `post-grid`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Template Unificado para Listados
|
||||||
|
|
||||||
|
All listing templates MUST use the same structure as single.php.
|
||||||
|
|
||||||
|
#### Scenario: Estructura base de templates de listado
|
||||||
|
- **WHEN** se implementa home.php, archive.php, category.php o tag.php
|
||||||
|
- **THEN** DEBE usar la misma estructura que single.php
|
||||||
|
- **AND** DEBE llamar a roi_render_component() para cada componente
|
||||||
|
- **AND** la visibilidad se controla via PageVisibilityHelper::shouldShow()
|
||||||
|
|
||||||
|
#### Scenario: Componentes que se llaman en templates de listado
|
||||||
|
- **WHEN** se renderiza un template de listado
|
||||||
|
- **THEN** DEBE llamar a roi_render_component('hero')
|
||||||
|
- **AND** DEBE llamar a roi_render_component('archive-header')
|
||||||
|
- **AND** DEBE llamar a roi_render_component('post-grid')
|
||||||
|
- **AND** DEBE llamar a roi_render_component('table-of-contents') en sidebar
|
||||||
|
- **AND** DEBE llamar a roi_render_component('cta-box-sidebar') en sidebar
|
||||||
|
- **AND** DEBE llamar a roi_render_component('contact-form')
|
||||||
|
- **AND** cada componente decide si renderiza segun show_on_archives
|
||||||
|
|
||||||
|
#### Scenario: Determinacion de sidebar en listados
|
||||||
|
- **WHEN** se determina si mostrar sidebar en un listado
|
||||||
|
- **THEN** DEBE usar roi_should_render_any_wrapper(['table-of-contents', 'cta-box-sidebar'])
|
||||||
|
- **AND** si retorna true usar col-lg-9 para contenido principal
|
||||||
|
- **AND** si retorna false usar col-lg-12 para contenido principal
|
||||||
|
|
||||||
|
#### Scenario: Paginacion en templates de listado
|
||||||
|
- **WHEN** se muestra paginacion en un listado
|
||||||
|
- **THEN** DEBE usar the_posts_pagination() de WordPress
|
||||||
|
- **AND** DEBE aplicar estilos Bootstrap via CSS del componente post-grid
|
||||||
|
|
||||||
|
#### Scenario: CSS de paginacion generado por post-grid
|
||||||
|
- **WHEN** PostGridRenderer renderiza la paginacion
|
||||||
|
- **THEN** el CSS de paginacion DEBE generarse via CSSGeneratorService
|
||||||
|
- **AND** DEBE aplicar estilos Bootstrap (nav-links, page-numbers)
|
||||||
|
- **AND** los colores DEBEN ser configurables via grupo colors del schema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Componente archive-header
|
||||||
|
|
||||||
|
The archive-header component MUST display dynamic title and description for archive pages.
|
||||||
|
|
||||||
|
#### Scenario: Ubicacion de archivos archive-header
|
||||||
|
- **WHEN** se crea el componente archive-header
|
||||||
|
- **THEN** schema DEBE estar en Schemas/archive-header.json
|
||||||
|
- **AND** Renderer DEBE estar en Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
|
||||||
|
- **AND** FormBuilder DEBE estar en Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php
|
||||||
|
- **AND** FieldMapper DEBE estar en Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php
|
||||||
|
|
||||||
|
#### Scenario: Namespaces PHP de archive-header
|
||||||
|
- **WHEN** se definen los namespaces para archive-header
|
||||||
|
- **THEN** Renderer DEBE usar namespace `ROITheme\Public\ArchiveHeader\Infrastructure\Ui`
|
||||||
|
- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\Ui`
|
||||||
|
- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping`
|
||||||
|
|
||||||
|
#### Scenario: Deteccion automatica de tipo de archivo
|
||||||
|
- **WHEN** ArchiveHeaderRenderer detecta el tipo de pagina
|
||||||
|
- **THEN** para categoria DEBE mostrar "Categoria: [nombre]"
|
||||||
|
- **AND** para tag DEBE mostrar "Etiqueta: [nombre]"
|
||||||
|
- **AND** para autor DEBE mostrar "Articulos de: [nombre]"
|
||||||
|
- **AND** para fecha DEBE mostrar "Archivo: [Mes Ano]"
|
||||||
|
- **AND** para busqueda DEBE mostrar "Resultados para: [termino]"
|
||||||
|
- **AND** para blog home DEBE mostrar el valor de blog_title del schema
|
||||||
|
|
||||||
|
#### Scenario: Grupos del schema archive-header
|
||||||
|
- **WHEN** se define el schema archive-header.json
|
||||||
|
- **THEN** DEBE incluir grupo visibility con priority 10
|
||||||
|
- **AND** DEBE incluir grupo content con priority 20
|
||||||
|
- **AND** DEBE incluir grupo typography con priority 30
|
||||||
|
- **AND** DEBE incluir grupo colors con priority 40
|
||||||
|
- **AND** DEBE incluir grupo spacing con priority 50
|
||||||
|
- **AND** DEBE incluir grupo behavior con priority 70
|
||||||
|
- **NOTE** archive-header NO incluye visual_effects (priority 60) porque es un componente de texto simple sin sombras, bordes redondeados ni transiciones hover
|
||||||
|
|
||||||
|
#### Scenario: Campos obligatorios de visibility en archive-header
|
||||||
|
- **WHEN** se define grupo visibility en schema
|
||||||
|
- **THEN** DEBE incluir is_enabled como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_on_desktop como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_on_mobile como boolean con default true
|
||||||
|
|
||||||
|
#### Scenario: Campos de _page_visibility en archive-header
|
||||||
|
- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper
|
||||||
|
- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default false
|
||||||
|
- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false
|
||||||
|
- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false
|
||||||
|
- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true
|
||||||
|
- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default false
|
||||||
|
- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults
|
||||||
|
- **NOTE** show_on_archives en true porque este componente solo tiene sentido en archives
|
||||||
|
|
||||||
|
#### Scenario: Campos de content en archive-header
|
||||||
|
- **WHEN** se define grupo content
|
||||||
|
- **THEN** DEBE incluir blog_title como text con default "Blog"
|
||||||
|
- **AND** DEBE incluir show_post_count como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_description como boolean con default true
|
||||||
|
|
||||||
|
#### Scenario: Campos de typography en archive-header
|
||||||
|
- **WHEN** se define grupo typography
|
||||||
|
- **THEN** DEBE incluir heading_level como select con options ["h1", "h2", "h3", "h4", "h5", "h6"] y default "h1"
|
||||||
|
- **AND** DEBE incluir title_size como text con default "2rem"
|
||||||
|
- **AND** DEBE incluir title_weight como text con default "700"
|
||||||
|
- **AND** DEBE incluir description_size como text con default "1rem"
|
||||||
|
|
||||||
|
#### Scenario: Campos de colors en archive-header
|
||||||
|
- **WHEN** se define grupo colors
|
||||||
|
- **THEN** DEBE incluir title_color como color con default "#0E2337"
|
||||||
|
- **AND** DEBE incluir description_color como color con default "#6b7280"
|
||||||
|
- **AND** DEBE incluir count_bg_color como color con default "#FF8600"
|
||||||
|
- **AND** DEBE incluir count_text_color como color con default "#ffffff"
|
||||||
|
|
||||||
|
#### Scenario: Campos de spacing en archive-header
|
||||||
|
- **WHEN** se define grupo spacing
|
||||||
|
- **THEN** DEBE incluir margin_top como text con default "2rem"
|
||||||
|
- **AND** DEBE incluir margin_bottom como text con default "2rem"
|
||||||
|
- **AND** DEBE incluir padding como text con default "1.5rem"
|
||||||
|
|
||||||
|
#### Scenario: Campos de behavior en archive-header
|
||||||
|
- **WHEN** se define grupo behavior
|
||||||
|
- **THEN** DEBE incluir is_sticky como boolean con default false
|
||||||
|
- **AND** DEBE incluir sticky_offset como text con default "0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Componente post-grid
|
||||||
|
|
||||||
|
The post-grid component MUST display posts from the main WordPress loop in a grid layout.
|
||||||
|
|
||||||
|
#### Scenario: Ubicacion de archivos post-grid
|
||||||
|
- **WHEN** se crea el componente post-grid
|
||||||
|
- **THEN** schema DEBE estar en Schemas/post-grid.json
|
||||||
|
- **AND** Renderer DEBE estar en Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
|
||||||
|
- **AND** FormBuilder DEBE estar en Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
|
||||||
|
- **AND** FieldMapper DEBE estar en Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php
|
||||||
|
|
||||||
|
#### Scenario: Namespaces PHP de post-grid
|
||||||
|
- **WHEN** se definen los namespaces para post-grid
|
||||||
|
- **THEN** Renderer DEBE usar namespace `ROITheme\Public\PostGrid\Infrastructure\Ui`
|
||||||
|
- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\Ui`
|
||||||
|
- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\FieldMapping`
|
||||||
|
|
||||||
|
#### Scenario: Diferencia entre post-grid y related-post
|
||||||
|
- **WHEN** PostGridRenderer obtiene los posts
|
||||||
|
- **THEN** DEBE usar global $wp_query para obtener posts del loop principal
|
||||||
|
- **AND** NO DEBE crear su propio WP_Query como hace RelatedPostRenderer
|
||||||
|
- **AND** DEBE llamar wp_reset_postdata() al finalizar si modifica el loop
|
||||||
|
|
||||||
|
#### Scenario: Grupos del schema post-grid
|
||||||
|
- **WHEN** se define el schema post-grid.json
|
||||||
|
- **THEN** DEBE incluir grupo visibility con priority 10
|
||||||
|
- **AND** DEBE incluir grupo content con priority 20
|
||||||
|
- **AND** DEBE incluir grupo typography con priority 30
|
||||||
|
- **AND** DEBE incluir grupo colors con priority 40
|
||||||
|
- **AND** DEBE incluir grupo spacing con priority 50
|
||||||
|
- **AND** DEBE incluir grupo visual_effects con priority 60
|
||||||
|
- **AND** DEBE incluir grupo layout con priority 80
|
||||||
|
- **AND** DEBE incluir grupo media con priority 90
|
||||||
|
|
||||||
|
#### Scenario: Campos obligatorios de visibility en post-grid
|
||||||
|
- **WHEN** se define grupo visibility en schema
|
||||||
|
- **THEN** DEBE incluir is_enabled como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_on_desktop como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_on_mobile como boolean con default true
|
||||||
|
|
||||||
|
#### Scenario: Campos de _page_visibility en post-grid
|
||||||
|
- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper
|
||||||
|
- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default true
|
||||||
|
- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false
|
||||||
|
- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false
|
||||||
|
- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true
|
||||||
|
- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default true
|
||||||
|
- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults
|
||||||
|
- **NOTE** show_on_home en true para mostrar grid en pagina de blog principal
|
||||||
|
- **NOTE** show_on_archives en true porque este componente es para listados
|
||||||
|
- **NOTE** show_on_search en true para mostrar resultados de busqueda
|
||||||
|
|
||||||
|
#### Scenario: Campos de content en post-grid
|
||||||
|
- **WHEN** se define grupo content
|
||||||
|
- **THEN** DEBE incluir show_thumbnail como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_excerpt como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_meta como boolean con default true
|
||||||
|
- **AND** DEBE incluir show_categories como boolean con default true
|
||||||
|
- **AND** DEBE incluir excerpt_length como select con options ["10", "15", "20", "25", "30"] y default "20"
|
||||||
|
- **AND** DEBE incluir read_more_text como text con default "Leer mas"
|
||||||
|
- **AND** DEBE incluir no_posts_message como text con default "No se encontraron publicaciones"
|
||||||
|
|
||||||
|
#### Scenario: Campos de media en post-grid
|
||||||
|
- **WHEN** se define grupo media
|
||||||
|
- **THEN** DEBE incluir fallback_image como url con default ""
|
||||||
|
- **AND** DEBE incluir fallback_image_alt como text con default "Imagen por defecto"
|
||||||
|
- **AND** fallback_image_alt es obligatorio para accesibilidad WCAG
|
||||||
|
|
||||||
|
#### Scenario: Campos de typography en post-grid
|
||||||
|
- **WHEN** se define grupo typography
|
||||||
|
- **THEN** DEBE incluir heading_level como select con options ["h2", "h3", "h4", "h5", "h6"] y default "h3"
|
||||||
|
- **AND** DEBE incluir card_title_size como text con default "1.1rem"
|
||||||
|
- **AND** DEBE incluir card_title_weight como text con default "600"
|
||||||
|
- **AND** DEBE incluir excerpt_size como text con default "0.9rem"
|
||||||
|
- **AND** DEBE incluir meta_size como text con default "0.8rem"
|
||||||
|
|
||||||
|
#### Scenario: Campos de colors en post-grid
|
||||||
|
- **WHEN** se define grupo colors
|
||||||
|
- **THEN** DEBE incluir card_bg_color como color con default "#ffffff"
|
||||||
|
- **AND** DEBE incluir card_title_color como color con default "#0E2337"
|
||||||
|
- **AND** DEBE incluir card_hover_bg_color como color con default "#f9fafb"
|
||||||
|
- **AND** DEBE incluir card_border_color como color con default "#e5e7eb"
|
||||||
|
- **AND** DEBE incluir card_hover_border_color como color con default "#FF8600"
|
||||||
|
- **AND** DEBE incluir excerpt_color como color con default "#6b7280"
|
||||||
|
- **AND** DEBE incluir meta_color como color con default "#9ca3af"
|
||||||
|
- **AND** DEBE incluir category_bg_color como color con default "#FFF5EB"
|
||||||
|
- **AND** DEBE incluir category_text_color como color con default "#FF8600"
|
||||||
|
|
||||||
|
#### Scenario: Campos de spacing en post-grid
|
||||||
|
- **WHEN** se define grupo spacing
|
||||||
|
- **THEN** DEBE incluir grid_gap como text con default "1.5rem"
|
||||||
|
- **AND** DEBE incluir card_padding como text con default "1.25rem"
|
||||||
|
- **AND** DEBE incluir section_margin_top como text con default "0"
|
||||||
|
- **AND** DEBE incluir section_margin_bottom como text con default "2rem"
|
||||||
|
|
||||||
|
#### Scenario: Campos de visual_effects en post-grid
|
||||||
|
- **WHEN** se define grupo visual_effects
|
||||||
|
- **THEN** DEBE incluir card_border_radius como text con default "0.5rem"
|
||||||
|
- **AND** DEBE incluir card_shadow como text con default "0 1px 3px rgba(0,0,0,0.1)"
|
||||||
|
- **AND** DEBE incluir card_hover_shadow como text con default "0 4px 12px rgba(0,0,0,0.15)"
|
||||||
|
- **AND** DEBE incluir card_transition como text con default "all 0.3s ease"
|
||||||
|
- **AND** DEBE incluir image_border_radius como text con default "0.375rem"
|
||||||
|
|
||||||
|
#### Scenario: Campos de layout en post-grid
|
||||||
|
- **WHEN** se define grupo layout
|
||||||
|
- **THEN** DEBE incluir columns_desktop como select con options ["2", "3", "4"] y default "3"
|
||||||
|
- **AND** DEBE incluir columns_tablet como select con options ["1", "2", "3"] y default "2"
|
||||||
|
- **AND** DEBE incluir columns_mobile como select con options ["1", "2"] y default "1"
|
||||||
|
- **AND** DEBE incluir image_position como select con options ["top", "left", "none"] y default "top"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Manejo Graceful de Contenido Faltante
|
||||||
|
|
||||||
|
The post-grid component MUST handle missing content gracefully.
|
||||||
|
|
||||||
|
#### Scenario: Post sin imagen destacada
|
||||||
|
- **WHEN** un post no tiene thumbnail y show_thumbnail es true
|
||||||
|
- **THEN** si fallback_image tiene valor DEBE mostrar esa imagen con fallback_image_alt
|
||||||
|
- **AND** si fallback_image esta vacio DEBE omitir la imagen sin romper el layout
|
||||||
|
- **AND** NO DEBE mostrar imagen rota o placeholder generico
|
||||||
|
|
||||||
|
#### Scenario: Post sin excerpt
|
||||||
|
- **WHEN** un post no tiene excerpt y show_excerpt es true
|
||||||
|
- **THEN** DEBE generar excerpt automatico desde post_content
|
||||||
|
- **AND** DEBE respetar excerpt_length del schema
|
||||||
|
- **AND** DEBE usar wp_trim_words() para truncar
|
||||||
|
|
||||||
|
#### Scenario: Post sin categorias
|
||||||
|
- **WHEN** un post no tiene categorias y show_categories es true
|
||||||
|
- **THEN** DEBE omitir la seccion de categorias
|
||||||
|
- **AND** NO DEBE mostrar "Sin categoria" u otro texto placeholder
|
||||||
|
|
||||||
|
#### Scenario: No posts found - Query vacia
|
||||||
|
- **WHEN** have_posts() retorna false en un template de listado
|
||||||
|
- **THEN** post-grid DEBE mostrar mensaje configurable de "no hay posts"
|
||||||
|
- **AND** el mensaje DEBE usar campo no_posts_message del schema con default "No se encontraron publicaciones"
|
||||||
|
- **AND** DEBE aplicar estilos consistentes con el design system
|
||||||
|
- **AND** NO DEBE romper el layout de la pagina
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Visibilidad por Tipo de Pagina
|
||||||
|
|
||||||
|
Components MUST respect the show_on_archives setting in _page_visibility group.
|
||||||
|
|
||||||
|
#### Scenario: Patron de visibilidad por tipo de pagina
|
||||||
|
- **WHEN** se implementa visibilidad por tipo de pagina
|
||||||
|
- **THEN** los campos show_on_home, show_on_posts, show_on_pages, show_on_archives, show_on_search
|
||||||
|
- **AND** DEBEN estar en grupo _page_visibility (NO en visibility)
|
||||||
|
- **AND** DEBEN mapearse via FieldMapper del componente
|
||||||
|
- **AND** DEBEN evaluarse via PageVisibilityHelper::shouldShow()
|
||||||
|
|
||||||
|
#### Scenario: Configuracion por defecto de show_on_archives para nuevos componentes
|
||||||
|
- **WHEN** se configura _page_visibility para componentes nuevos
|
||||||
|
- **THEN** archive-header DEBE tener show_on_archives true en _page_visibility
|
||||||
|
- **AND** post-grid DEBE tener show_on_archives true en _page_visibility
|
||||||
|
|
||||||
|
#### Scenario: Componentes existentes en archives
|
||||||
|
- **WHEN** se evalua que componentes mostrar en archives via _page_visibility
|
||||||
|
- **THEN** hero DEBE tener show_on_archives false por defecto (configurable)
|
||||||
|
- **AND** table-of-contents DEBE tener show_on_archives false
|
||||||
|
- **AND** featured-image DEBE tener show_on_archives false
|
||||||
|
- **AND** social-share DEBE tener show_on_archives false
|
||||||
|
- **AND** related-post DEBE tener show_on_archives false
|
||||||
|
- **AND** cta-box-sidebar DEBE tener show_on_archives true
|
||||||
|
- **AND** contact-form DEBE tener show_on_archives configurable
|
||||||
|
|
||||||
|
#### Scenario: Llamada a componente con visibilidad deshabilitada (Patron Template Unificado)
|
||||||
|
- **GIVEN** el template unificado llama a TODOS los componentes para mantener consistencia
|
||||||
|
- **WHEN** un template llama roi_render_component() para un componente
|
||||||
|
- **AND** ese componente tiene show_on_archives false
|
||||||
|
- **THEN** el componente NO DEBE renderizarse (retorna string vacio)
|
||||||
|
- **AND** esto es comportamiento correcto y esperado, NO un error
|
||||||
|
- **AND** permite que el admin habilite/deshabilite componentes sin modificar templates
|
||||||
|
- **NOTE** Por ejemplo: table-of-contents se llama en sidebar pero no renderiza en archives porque show_on_archives=false
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Templates a Modernizar
|
||||||
|
|
||||||
|
These templates MUST be updated to use the unified structure.
|
||||||
|
|
||||||
|
#### Scenario: Modernizar home.php
|
||||||
|
- **WHEN** se actualiza home.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada con hero, archive-header, post-grid
|
||||||
|
|
||||||
|
#### Scenario: Modernizar archive.php
|
||||||
|
- **WHEN** se actualiza archive.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada
|
||||||
|
|
||||||
|
#### Scenario: Modernizar category.php
|
||||||
|
- **GIVEN** category.php existe en roi-theme/ (verificado)
|
||||||
|
- **WHEN** se actualiza category.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada
|
||||||
|
|
||||||
|
#### Scenario: Modernizar tag.php
|
||||||
|
- **GIVEN** tag.php existe en roi-theme/ (verificado)
|
||||||
|
- **WHEN** se actualiza tag.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada
|
||||||
|
|
||||||
|
#### Scenario: Modernizar author.php
|
||||||
|
- **GIVEN** author.php existe en roi-theme/ (verificado)
|
||||||
|
- **WHEN** se actualiza author.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada
|
||||||
|
- **AND** archive-header detectara automaticamente contexto de autor
|
||||||
|
|
||||||
|
#### Scenario: Modernizar date.php
|
||||||
|
- **GIVEN** date.php existe en roi-theme/ (verificado)
|
||||||
|
- **WHEN** se actualiza date.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada
|
||||||
|
- **AND** archive-header detectara automaticamente contexto de fecha
|
||||||
|
|
||||||
|
#### Scenario: Modernizar search.php
|
||||||
|
- **GIVEN** search.php existe en roi-theme/ (verificado)
|
||||||
|
- **WHEN** se actualiza search.php
|
||||||
|
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||||
|
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||||
|
- **AND** DEBE usar estructura unificada con post-grid
|
||||||
|
- **AND** archive-header detectara automaticamente contexto de busqueda mostrando "Resultados: [termino]"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Orden de Implementacion
|
||||||
|
|
||||||
|
Components and templates MUST be implemented in a specific order.
|
||||||
|
|
||||||
|
#### Scenario: Secuencia de implementacion
|
||||||
|
- **WHEN** se implementa esta especificacion
|
||||||
|
- **THEN** Fase 1 es crear componente archive-header (5 pasos del flujo)
|
||||||
|
- **AND** Fase 2 es crear componente post-grid (5 pasos del flujo)
|
||||||
|
- **AND** Fase 3 es modernizar home.php
|
||||||
|
- **AND** Fase 4 es modernizar archive.php
|
||||||
|
- **AND** Fase 5 es modernizar category.php
|
||||||
|
- **AND** Fase 6 es modernizar tag.php
|
||||||
|
- **AND** Fase 7 es modernizar author.php
|
||||||
|
- **AND** Fase 8 es modernizar date.php
|
||||||
|
- **AND** Fase 9 es modernizar search.php
|
||||||
|
- **AND** Fase 10 es configurar visibilidad de componentes existentes
|
||||||
|
|
||||||
|
#### Scenario: Cada componente sigue flujo de 5 fases
|
||||||
|
- **WHEN** se crea archive-header o post-grid
|
||||||
|
- **THEN** DEBE seguir Fase 1: Schema JSON
|
||||||
|
- **AND** DEBE seguir Fase 2: Sincronizacion wp roi-theme sync-component
|
||||||
|
- **AND** DEBE seguir Fase 3: Renderer
|
||||||
|
- **AND** DEBE seguir Fase 4: FormBuilder
|
||||||
|
- **AND** DEBE seguir Fase 5: Validacion validate-architecture.php
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Dependencias Existentes
|
||||||
|
|
||||||
|
The implementation MUST use existing infrastructure.
|
||||||
|
|
||||||
|
#### Scenario: Uso de PageVisibilityHelper
|
||||||
|
- **WHEN** un Renderer verifica visibilidad
|
||||||
|
- **THEN** DEBE usar PageVisibilityHelper::shouldShow(componentName)
|
||||||
|
- **AND** esta en Shared/Infrastructure/Services/PageVisibilityHelper.php
|
||||||
|
|
||||||
|
#### Scenario: Uso de CSSGeneratorInterface
|
||||||
|
- **WHEN** un Renderer genera CSS
|
||||||
|
- **THEN** DEBE inyectar CSSGeneratorInterface via constructor
|
||||||
|
- **AND** DEBE usar $this->cssGenerator->generate()
|
||||||
|
|
||||||
|
#### Scenario: Uso de roi_should_render_any_wrapper
|
||||||
|
- **WHEN** un template determina si mostrar sidebar
|
||||||
|
- **THEN** DEBE usar roi_should_render_any_wrapper()
|
||||||
|
- **AND** esta definida en functions-addon.php linea 423
|
||||||
|
|
||||||
|
#### Scenario: Uso de DIContainer
|
||||||
|
- **WHEN** se instancian servicios
|
||||||
|
- **THEN** DEBE usar DIContainer::getInstance()
|
||||||
|
- **AND** NO DEBE instanciar servicios con new directamente
|
||||||
174
search.php
174
search.php
@@ -2,9 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying search results pages
|
* The template for displaying search results pages
|
||||||
*
|
*
|
||||||
* IMPORTANT: This theme has search functionality disabled.
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* All search attempts will be redirected to 404.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
* This template serves as a fallback and will display a 404 error.
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
|
*
|
||||||
|
* NOTA: Si se desea deshabilitar la busqueda, descomentar las lineas siguientes:
|
||||||
|
* status_header( 404 );
|
||||||
|
* nocache_headers();
|
||||||
|
* include( get_404_template() );
|
||||||
|
* exit;
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#search-result
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#search-result
|
||||||
*
|
*
|
||||||
@@ -12,117 +18,81 @@
|
|||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Force 404 status
|
|
||||||
status_header( 404 );
|
|
||||||
nocache_headers();
|
|
||||||
|
|
||||||
get_header();
|
get_header();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<section class="error-404 not-found search-disabled" aria-labelledby="search-error-title">
|
|
||||||
|
|
||||||
<!-- Error Header -->
|
|
||||||
<header class="page-header">
|
|
||||||
<h1 id="search-error-title" class="page-title">
|
|
||||||
<?php esc_html_e( 'Search Unavailable', 'roi-theme' ); ?>
|
|
||||||
</h1>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Error Content -->
|
|
||||||
<div class="page-content">
|
|
||||||
|
|
||||||
<p class="error-message">
|
|
||||||
<?php esc_html_e( 'The search functionality is currently disabled on this website.', 'roi-theme' ); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Helpful Actions -->
|
|
||||||
<div class="error-actions">
|
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'How to find content:', 'roi-theme' ); ?></h2>
|
|
||||||
|
|
||||||
<ul class="error-suggestions">
|
|
||||||
<li>
|
|
||||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
|
||||||
<?php esc_html_e( 'Go to the homepage', 'roi-theme' ); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<?php esc_html_e( 'Use the navigation menu above', 'roi-theme' ); ?>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<?php esc_html_e( 'Browse by category below', 'roi-theme' ); ?>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Categories -->
|
|
||||||
<?php
|
<?php
|
||||||
$categories = get_categories(
|
if (function_exists('roi_render_component')) {
|
||||||
array(
|
echo roi_render_component('hero');
|
||||||
'orderby' => 'count',
|
}
|
||||||
'order' => 'DESC',
|
|
||||||
'number' => 10,
|
|
||||||
'hide_empty' => true,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! empty( $categories ) ) :
|
|
||||||
?>
|
?>
|
||||||
<div class="categories-section">
|
|
||||||
<h3><?php esc_html_e( 'Browse by Category', 'roi-theme' ); ?></h3>
|
<!-- Archive Header - Componente dinamico (detecta busqueda automaticamente) -->
|
||||||
<ul class="categories-list" role="list">
|
<?php
|
||||||
<?php foreach ( $categories as $category ) : ?>
|
if (function_exists('roi_render_component')) {
|
||||||
<li>
|
echo roi_render_component('archive-header');
|
||||||
<a href="<?php echo esc_url( get_category_link( $category->term_id ) ); ?>">
|
}
|
||||||
<?php echo esc_html( $category->name ); ?>
|
?>
|
||||||
<span class="category-count">(<?php echo esc_html( $category->count ); ?>)</span>
|
|
||||||
</a>
|
<!-- Main Content Grid -->
|
||||||
</li>
|
<?php
|
||||||
<?php endforeach; ?>
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
</ul>
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Recent Posts -->
|
</div><!-- .row -->
|
||||||
<?php
|
</div><!-- .container -->
|
||||||
$recent_posts = wp_get_recent_posts(
|
|
||||||
array(
|
|
||||||
'numberposts' => 10,
|
|
||||||
'post_status' => 'publish',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! empty( $recent_posts ) ) :
|
|
||||||
?>
|
|
||||||
<div class="recent-posts-section">
|
|
||||||
<h3><?php esc_html_e( 'Recent Posts', 'roi-theme' ); ?></h3>
|
|
||||||
<ul class="recent-posts-list" role="list">
|
|
||||||
<?php foreach ( $recent_posts as $recent ) : ?>
|
|
||||||
<li>
|
|
||||||
<a href="<?php echo esc_url( get_permalink( $recent['ID'] ) ); ?>">
|
|
||||||
<?php echo esc_html( $recent['post_title'] ); ?>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
wp_reset_postdata();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .error-actions -->
|
|
||||||
|
|
||||||
</div><!-- .page-content -->
|
|
||||||
|
|
||||||
</section><!-- .error-404 -->
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
171
tag.php
171
tag.php
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying tag archive pages
|
* The template for displaying tag archive pages
|
||||||
*
|
*
|
||||||
* This template displays posts associated with a specific tag,
|
* Estructura unificada siguiendo el patron de single.php.
|
||||||
* with tag information displayed at the top.
|
* Usa roi_render_component() para todos los componentes.
|
||||||
|
* La visibilidad se controla via PageVisibilityHelper::shouldShow().
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#tag
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#tag
|
||||||
*
|
*
|
||||||
@@ -16,116 +17,76 @@ get_header();
|
|||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<!-- Hero Section - Componente dinamico -->
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
|
||||||
<div id="primary" class="content-area">
|
|
||||||
|
|
||||||
<?php if ( have_posts() ) : ?>
|
|
||||||
|
|
||||||
<!-- Tag Archive Header -->
|
|
||||||
<header class="page-header tag-header">
|
|
||||||
<?php
|
<?php
|
||||||
// Tag title
|
if (function_exists('roi_render_component')) {
|
||||||
the_archive_title( '<h1 class="page-title tag-title">', '</h1>' );
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
// Tag description
|
|
||||||
$tag_description = tag_description();
|
|
||||||
if ( ! empty( $tag_description ) ) :
|
|
||||||
?>
|
?>
|
||||||
<div class="archive-description tag-description">
|
|
||||||
<?php echo wp_kses_post( wpautop( $tag_description ) ); ?>
|
<!-- Archive Header - Componente dinamico (detecta tag automaticamente) -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('archive-header');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<?php
|
||||||
|
// Determinar si mostrar sidebar basandose en visibilidad de componentes
|
||||||
|
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||||
|
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||||
|
? roi_should_render_any_wrapper($sidebar_components)
|
||||||
|
: false;
|
||||||
|
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column -->
|
||||||
|
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||||
|
|
||||||
|
<!-- Post Grid - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('post-grid');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||||
|
|
||||||
|
<?php if ($show_sidebar): ?>
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Tag metadata -->
|
</div><!-- .row -->
|
||||||
<?php
|
</div><!-- .container -->
|
||||||
$tag = get_queried_object();
|
|
||||||
if ( $tag ) :
|
|
||||||
?>
|
|
||||||
<div class="tag-meta">
|
|
||||||
<span class="tag-count">
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: number of posts */
|
|
||||||
esc_html( _n( '%s post', '%s posts', $tag->count, 'roi-theme' ) ),
|
|
||||||
esc_html( number_format_i18n( $tag->count ) )
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</header><!-- .page-header -->
|
|
||||||
|
|
||||||
<!-- Tag Posts Loop -->
|
|
||||||
<div class="archive-posts tag-posts">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Start the WordPress Loop
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include the Post-Type-specific template for the content.
|
|
||||||
* If you want to override this in a child theme, then include a file
|
|
||||||
* called content-___.php (where ___ is the Post Type name) and that will be used instead.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', get_post_type() );
|
|
||||||
|
|
||||||
endwhile;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .archive-posts -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pagination
|
|
||||||
* Display navigation to next/previous set of posts when applicable.
|
|
||||||
*/
|
|
||||||
the_posts_pagination(
|
|
||||||
array(
|
|
||||||
'mid_size' => 2,
|
|
||||||
'prev_text' => sprintf(
|
|
||||||
'%s <span class="nav-prev-text">%s</span>',
|
|
||||||
'<span class="nav-icon" aria-hidden="true">«</span>',
|
|
||||||
esc_html__( 'Previous', 'roi-theme' )
|
|
||||||
),
|
|
||||||
'next_text' => sprintf(
|
|
||||||
'<span class="nav-next-text">%s</span> %s',
|
|
||||||
esc_html__( 'Next', 'roi-theme' ),
|
|
||||||
'<span class="nav-icon" aria-hidden="true">»</span>'
|
|
||||||
),
|
|
||||||
'before_page_number' => '<span class="screen-reader-text">' . esc_html__( 'Page', 'roi-theme' ) . ' </span>',
|
|
||||||
'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
else :
|
|
||||||
|
|
||||||
/**
|
|
||||||
* No posts found
|
|
||||||
* Display a message when no content is available.
|
|
||||||
*/
|
|
||||||
get_template_part( 'template-parts/content', 'none' );
|
|
||||||
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Sidebar
|
|
||||||
* Display the sidebar if it's active.
|
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinamico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
Reference in New Issue
Block a user