diff --git a/Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php b/Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php new file mode 100644 index 00000000..62d0efe3 --- /dev/null +++ b/Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php @@ -0,0 +1,81 @@ + ['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'], + ]; + } +} diff --git a/Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php b/Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php new file mode 100644 index 00000000..454a51f4 --- /dev/null +++ b/Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php @@ -0,0 +1,492 @@ +buildHeader($componentId); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildBehaviorGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(string $componentId): string + { + $html = '
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 .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Mostrar en tipos de pagina'; + $html .= '

'; + + $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 .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; + $html .= '
'; + + // Exclusions + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'archiveHeader'); + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // Blog Title + $blogTitle = $this->renderer->getFieldValue($componentId, 'content', 'blog_title', 'Blog'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Mostrado en la pagina principal del blog'; + $html .= '
'; + + // 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 .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Prefijos de titulo'; + $html .= '

'; + + $html .= '
'; + + $categoryPrefix = $this->renderer->getFieldValue($componentId, 'content', 'category_prefix', 'Categoria:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $tagPrefix = $this->renderer->getFieldValue($componentId, 'content', 'tag_prefix', 'Etiqueta:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $authorPrefix = $this->renderer->getFieldValue($componentId, 'content', 'author_prefix', 'Articulos de:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $datePrefix = $this->renderer->getFieldValue($componentId, 'content', 'date_prefix', 'Archivo:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $searchPrefix = $this->renderer->getFieldValue($componentId, 'content', 'search_prefix', 'Resultados para:'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + // Post count texts + $html .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Textos del contador'; + $html .= '

'; + + $html .= '
'; + + $countSingular = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_singular', 'publicacion'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $countPlural = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_plural', 'publicaciones'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildBehaviorGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Comportamiento'; + $html .= '
'; + + $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 .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Distancia desde el top cuando es sticky (ej: 60px)'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + // Heading level + $headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h1'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= ' Importante para SEO y accesibilidad'; + $html .= '
'; + + $html .= '
'; + + $titleSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_size', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_weight', '700'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $descriptionSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_size', '1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $countSize = $this->renderer->getFieldValue($componentId, 'typography', 'count_size', '0.875rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $descriptionColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#6b7280'); + $html .= $this->buildColorPicker('archiveHeaderDescriptionColor', 'Descripcion', $descriptionColor); + + $html .= '
'; + + $html .= '

Contador de posts

'; + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $countPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'count_padding', '0.25rem 0.75rem'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php index 781973a8..d5e5def7 100644 --- a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php +++ b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php @@ -109,7 +109,7 @@ final class CtaBoxSidebarFormBuilder $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true); $showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', 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); // Grid 3 columnas según Design System diff --git a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php index 7517b079..cb05ddc8 100644 --- a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php +++ b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php @@ -98,6 +98,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface 'label' => 'Related Posts', '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' => [ 'id' => 'contact-form', 'label' => 'Contact Form', diff --git a/Admin/Infrastructure/Ui/ComponentGroupRegistry.php b/Admin/Infrastructure/Ui/ComponentGroupRegistry.php index 05464063..54eea99f 100644 --- a/Admin/Infrastructure/Ui/ComponentGroupRegistry.php +++ b/Admin/Infrastructure/Ui/ComponentGroupRegistry.php @@ -37,7 +37,7 @@ final class ComponentGroupRegistry 'label' => __('Contenido Principal', 'roi-theme'), 'icon' => 'bi-file-richtext', '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' => [ 'label' => __('CTAs & Conversión', 'roi-theme'), diff --git a/Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php b/Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php new file mode 100644 index 00000000..d7508f60 --- /dev/null +++ b/Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php @@ -0,0 +1,97 @@ + ['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'], + ]; + } +} diff --git a/Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php b/Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php new file mode 100644 index 00000000..e99112c1 --- /dev/null +++ b/Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php @@ -0,0 +1,619 @@ +buildHeader(); + + $html .= '
'; + + // Columna izquierda + $html .= '
'; + $html .= $this->buildVisibilityGroup($componentId); + $html .= $this->buildContentGroup($componentId); + $html .= $this->buildLayoutGroup($componentId); + $html .= $this->buildMediaGroup($componentId); + $html .= '
'; + + // Columna derecha + $html .= '
'; + $html .= $this->buildTypographyGroup($componentId); + $html .= $this->buildColorsGroup($componentId); + $html .= $this->buildSpacingGroup($componentId); + $html .= $this->buildEffectsGroup($componentId); + $html .= '
'; + + $html .= '
'; + + return $html; + } + + private function buildHeader(): string + { + $html = '
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 .= '
'; + $html .= '

'; + $html .= ' '; + $html .= ' Mostrar en tipos de pagina'; + $html .= '

'; + + $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 .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('postGridVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('postGridVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('postGridVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; + $html .= '
'; + + // Reglas de exclusion + $exclusionPartial = new ExclusionFormPartial($this->renderer); + $html .= $exclusionPartial->render($componentId, 'postGrid'); + + $html .= '
'; + $html .= ''; + + return $html; + } + + private function buildContentGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Contenido'; + $html .= '
'; + + // 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 .= '
'; + + // Excerpt length + $excerptLength = $this->renderer->getFieldValue($componentId, 'content', 'excerpt_length', '20'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Read more text + $readMoreText = $this->renderer->getFieldValue($componentId, 'content', 'read_more_text', 'Leer mas'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // No posts message + $noPostsMessage = $this->renderer->getFieldValue($componentId, 'content', 'no_posts_message', 'No se encontraron publicaciones'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildLayoutGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Disposicion'; + $html .= '
'; + + // Columns desktop + $colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Columns tablet + $colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Columns mobile + $colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Image position + $imagePosition = $this->renderer->getFieldValue($componentId, 'layout', 'image_position', 'top'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildMediaGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Medios'; + $html .= '
'; + + // Fallback image + $fallbackImage = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image', ''); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + // Fallback image alt + $fallbackImageAlt = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image_alt', 'Imagen por defecto'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildTypographyGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Tipografia'; + $html .= '
'; + + // Heading level + $headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h3'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1.1rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '600'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $excerptSize = $this->renderer->getFieldValue($componentId, 'typography', 'excerpt_size', '0.9rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $metaSize = $this->renderer->getFieldValue($componentId, 'typography', 'meta_size', '0.8rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Colores'; + $html .= '
'; + + // Cards + $html .= '

Cards

'; + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $categoryTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_text_color', '#FF8600'); + $html .= $this->buildColorPicker('postGridCategoryTextColor', 'Texto cat.', $categoryTextColor); + + $html .= '
'; + + // Paginacion + $html .= '

Paginacion

'; + $html .= '
'; + + $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 .= '
'; + + $html .= '
'; + + $paginationActiveColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_color', '#ffffff'); + $html .= $this->buildColorPicker('postGridPaginationActiveColor', 'Activo texto', $paginationActiveColor); + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSpacingGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Espaciado'; + $html .= '
'; + + $html .= '
'; + + $gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.25rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + + $sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '0'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '2rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildEffectsGroup(string $componentId): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Efectos Visuales'; + $html .= '
'; + + $html .= '
'; + + $cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.5rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $imageBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'image_border_radius', '0.375rem'); + $html .= '
'; + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', 'all 0.3s ease'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 1px 3px rgba(0,0,0,0.1)'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 4px 12px rgba(0,0,0,0.15)'); + $html .= ' '; + $html .= ' '; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildColorPicker(string $id, string $label, string $value): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_html($label) + ); + $html .= '
'; + $html .= sprintf( + ' ', + esc_attr($id), + esc_attr($value) + ); + $html .= sprintf( + ' %s', + esc_attr($id), + esc_html(strtoupper($value)) + ); + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string + { + $checked = $checked === true || $checked === '1' || $checked === 1; + + $html = '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + + return $html; + } +} diff --git a/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php b/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php index acdc7790..a945af90 100644 --- a/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php +++ b/Admin/Shared/Infrastructure/FieldMapping/FieldMapperProvider.php @@ -33,6 +33,8 @@ final class FieldMapperProvider 'Footer', 'ThemeSettings', 'AdsensePlacement', + 'ArchiveHeader', + 'PostGrid', ]; public function __construct( diff --git a/Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php b/Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php new file mode 100644 index 00000000..3de8dd15 --- /dev/null +++ b/Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php @@ -0,0 +1,314 @@ +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("\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('
', esc_attr($containerClass)); + + // Title with optional prefix + $html .= sprintf('<%s class="archive-header__title">', esc_attr($headingLevel)); + + if (!empty($prefix)) { + $html .= sprintf( + '%s ', + esc_html($prefix) + ); + } + + $html .= esc_html($title); + + // Post count badge + if ($showPostCount && $postCount > 0) { + $html .= sprintf( + '%d %s', + $postCount, + esc_html($countText) + ); + } + + $html .= sprintf('', esc_attr($headingLevel)); + + // Description + if (!empty($description)) { + $html .= sprintf( + '

%s

', + esc_html($description) + ); + } + + $html .= '
'; + + 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; + } +} diff --git a/Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php b/Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php new file mode 100644 index 00000000..07cf378d --- /dev/null +++ b/Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php @@ -0,0 +1,572 @@ +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("\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( + '

%s

', + esc_attr($containerClass), + esc_html($message) + ); + + return sprintf("\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('
', esc_attr($containerClass)); + $html .= '
'; + + while (have_posts()) { + the_post(); + $html .= $this->buildCardHTML( + $showThumbnail, + $showExcerpt, + $showMeta, + $showCategories, + $excerptLength, + $readMoreText, + $headingLevel, + $fallbackImage, + $fallbackImageAlt, + $imagePosition + ); + } + + $html .= '
'; + + // Paginacion nativa de WordPress + $html .= '
'; + $html .= $this->buildPaginationHTML(); + $html .= '
'; + + $html .= '
'; + + 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 = '
'; + $html .= sprintf( + '', + esc_url($permalink) + ); + + $cardClass = 'card h-100'; + if ($imagePosition === 'left') { + $cardClass .= ' flex-row'; + } + + $html .= sprintf('
', esc_attr($cardClass)); + + // Imagen + if ($showThumbnail && $imagePosition !== 'none') { + $html .= $this->buildImageHTML($fallbackImage, $fallbackImageAlt, $imagePosition); + } + + $html .= '
'; + + // Categorias + if ($showCategories) { + $html .= $this->buildCategoriesHTML(); + } + + // Titulo + $html .= sprintf( + '<%s class="card-title">%s', + esc_attr($headingLevel), + esc_html($title), + esc_attr($headingLevel) + ); + + // Meta + if ($showMeta) { + $html .= $this->buildMetaHTML(); + } + + // Excerpt + if ($showExcerpt) { + $html .= $this->buildExcerptHTML($excerptLength); + } + + $html .= '
'; // card-body + $html .= '
'; // card + $html .= '
'; + $html .= '
'; // 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( + '%s', + esc_url($fallbackImage), + esc_attr($fallbackImageAlt), + esc_attr($imageClass) + ); + } + + return ''; + } + + private function buildCategoriesHTML(): string + { + $categories = get_the_category(); + if (empty($categories)) { + return ''; + } + + $html = '
'; + foreach (array_slice($categories, 0, 2) as $category) { + $html .= sprintf( + '%s', + esc_html($category->name) + ); + } + $html .= '
'; + + return $html; + } + + private function buildMetaHTML(): string + { + $date = get_the_date(); + $author = get_the_author(); + + return sprintf( + '
%s | %s
', + 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( + '

%s

', + 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(); + } +} diff --git a/Schemas/archive-header.json b/Schemas/archive-header.json new file mode 100644 index 00000000..f940e8fa --- /dev/null +++ b/Schemas/archive-header.json @@ -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" + } + } + } + } +} diff --git a/Schemas/post-grid.json b/Schemas/post-grid.json new file mode 100644 index 00000000..9512e52f --- /dev/null +++ b/Schemas/post-grid.json @@ -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" + } + } + } + } +} diff --git a/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php index 57fc8e94..14c54829 100644 --- a/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php +++ b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php @@ -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 { $config = $this->visibilityRepository->getVisibilityConfig($componentName); if (empty($config)) { - // Usar constante compartida (DRY) - $config = VisibilityDefaults::DEFAULT_VISIBILITY; + // Usar defaults especificos por componente si existen + $config = VisibilityDefaults::getForComponent($componentName); } $pageType = $this->pageTypeDetector->detect(); diff --git a/Shared/Domain/Constants/VisibilityDefaults.php b/Shared/Domain/Constants/VisibilityDefaults.php index ca810d2b..73901c26 100644 --- a/Shared/Domain/Constants/VisibilityDefaults.php +++ b/Shared/Domain/Constants/VisibilityDefaults.php @@ -16,13 +16,13 @@ namespace ROITheme\Shared\Domain\Constants; 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) - * - Posts: SÍ mostrar (artículos del blog) - * - Pages: SÍ mostrar (páginas estáticas) - * - Archives: NO mostrar (listados de categorías/tags) - * - Search: NO mostrar (resultados de búsqueda) + * - Home: SI mostrar (pagina principal) + * - Posts: SI mostrar (articulos del blog) + * - Pages: SI mostrar (paginas estaticas) + * - Archives: NO mostrar (listados de categorias/tags) + * - Search: NO mostrar (resultados de busqueda) */ public const DEFAULT_VISIBILITY = [ '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 = [ 'show_on_home', @@ -42,4 +74,15 @@ final class VisibilityDefaults 'show_on_archives', 'show_on_search', ]; + + /** + * Obtiene los defaults para un componente especifico + * + * @param string $componentName Nombre del componente (kebab-case) + * @return array Configuracion de visibilidad + */ + public static function getForComponent(string $componentName): array + { + return self::COMPONENT_VISIBILITY[$componentName] ?? self::DEFAULT_VISIBILITY; + } } diff --git a/Shared/Infrastructure/Services/MigratePageVisibilityService.php b/Shared/Infrastructure/Services/MigratePageVisibilityService.php index c86db621..9c697849 100644 --- a/Shared/Infrastructure/Services/MigratePageVisibilityService.php +++ b/Shared/Infrastructure/Services/MigratePageVisibilityService.php @@ -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} */ @@ -37,10 +37,10 @@ final class MigratePageVisibilityService continue; } - // Usar constante compartida (DRY) + // Usar defaults especificos por componente si existen $this->visibilityRepository->createDefaultVisibility( $componentName, - VisibilityDefaults::DEFAULT_VISIBILITY + VisibilityDefaults::getForComponent($componentName) ); $created++; } diff --git a/Shared/Infrastructure/Services/WordPressPageTypeDetector.php b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php index 4d7eeb09..7dcaf162 100644 --- a/Shared/Infrastructure/Services/WordPressPageTypeDetector.php +++ b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php @@ -40,7 +40,10 @@ final class WordPressPageTypeDetector implements PageTypeDetectorInterface 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 diff --git a/archive.php b/archive.php index bd1d8f81..2dee3bf4 100644 --- a/archive.php +++ b/archive.php @@ -2,8 +2,9 @@ /** * The template for displaying archive pages * - * This template displays date-based, category, tag, author, and post type - * archives with a dynamic title, description, and post loop. + * Estructura unificada siguiendo el patron de single.php. + * 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 * @@ -14,213 +15,78 @@ get_header(); ?> -
+
-
+ + - -
+ + - + + +
+
- - + + - -
+
- + + +
+ +
+ - - -
- -
- +
+
- -
+
- -
- - - -
- - - name ); ?> - - -
- - - - ', '' ); - else : - the_title( - sprintf( - '

', - esc_url( get_permalink() ) - ), - '

' - ); - endif; - ?> - - - - -
- - -
- -
- - - - - - - - - - - - - 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - 'aria_label' => esc_attr__( 'Posts navigation', 'roi-theme' ), - ) - ); - - else : - - /** - * No posts found - * Display a message when no content is available. - */ - ?> -
- - -
-

- -

-
-
- - - - - - - - - -
+ + -
+
-
+ + - -
+ + - + + +
+
- - + + +
+
+ - -
+
+
- - /** - * 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; - ?> - -
- - 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - '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; - ?> - - - - - - - -
+ + -
+
-
+ + - -
+ + - + + +
+
- - +
- -
+ + +
+ +
+ - /** - * 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; - ?> +
- - - 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - '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; - ?> - - - - - - - -
+ + -
+
-
+ + - -
+ + - + + +
+
- - +
- -
+ + +
+ +
+ - /** - * 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; - ?> +
- - - 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - '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; - ?> - - - - - - - -
+ + attribute_value; - // Convertir booleanos almacenados como '1' o '0' - if ($value === '1' || $value === '0') { + // Solo convertir a booleano campos que realmente son booleanos + // 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'); } else { // Intentar decodificar JSON @@ -321,6 +330,12 @@ function roi_render_component(string $componentName): string { case 'footer': $renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator); 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) { diff --git a/home.php b/home.php index ee8271e6..70fdee2b 100644 --- a/home.php +++ b/home.php @@ -2,9 +2,9 @@ /** * The template for displaying the blog posts index * - * This template is used when the blog posts page is different from the front page. - * It displays a listing of recent blog posts with pagination. - * Set in WordPress Settings > Reading > "Posts page". + * Estructura unificada siguiendo el patron de single.php. + * Usa roi_render_component() para todos los componentes. + * La visibilidad se controla via PageVisibilityHelper::shouldShow(). * * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#home * @@ -15,109 +15,78 @@ get_header(); ?> -
+
-
+ + - -
+ + - + + +
+
- - + +
- -
+ + - - /** - * 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() ); + + +
+ +
+ -
+
+
- 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - '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; - ?> - - - - - - - -
+ + -
+
-
+ + -
+ + - - + + +
+
- -
+ +
-

- -

+ + - -
+
-

+ + +
+ +
+ - - 'count', - 'order' => 'DESC', - 'number' => 10, - 'hide_empty' => true, - ) - ); +
+
- if ( ! empty( $categories ) ) : - ?> - - +
- - 10, - 'post_status' => 'publish', - ) - ); - - if ( ! empty( $recent_posts ) ) : - ?> -
-

- -
- - - - - - - - - - -
+ + -
+
-
+ + - -
+ + - + + +
+
- - +
- -
+ + +
+ +
+ - /** - * 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; - ?> +
- - - 2, - 'prev_text' => sprintf( - '%s %s', - '', - esc_html__( 'Previous', 'roi-theme' ) - ), - 'next_text' => sprintf( - '%s %s', - esc_html__( 'Next', 'roi-theme' ), - '' - ), - 'before_page_number' => '' . esc_html__( 'Page', 'roi-theme' ) . ' ', - '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; - ?> - - - - - - - -
+ +