diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php index a5846e36..bf4aae2e 100644 --- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php +++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php @@ -95,6 +95,16 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface 'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'], 'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'], 'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'], + + // SEARCH RESULTS (ROI APU Search) + 'adsense-placementSearchAdsEnabled' => ['group' => 'search_results', 'attribute' => 'search_ads_enabled'], + 'adsense-placementSearchTopAdEnabled' => ['group' => 'search_results', 'attribute' => 'search_top_ad_enabled'], + 'adsense-placementSearchTopAdFormat' => ['group' => 'search_results', 'attribute' => 'search_top_ad_format'], + 'adsense-placementSearchBetweenEnabled' => ['group' => 'search_results', 'attribute' => 'search_between_enabled'], + 'adsense-placementSearchBetweenMax' => ['group' => 'search_results', 'attribute' => 'search_between_max'], + 'adsense-placementSearchBetweenFormat' => ['group' => 'search_results', 'attribute' => 'search_between_format'], + 'adsense-placementSearchBetweenPosition' => ['group' => 'search_results', 'attribute' => 'search_between_position'], + 'adsense-placementSearchBetweenEvery' => ['group' => 'search_results', 'attribute' => 'search_between_every'], ]; } } diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php index 5fb1bed4..b70f6da7 100644 --- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php +++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php @@ -57,6 +57,7 @@ final class AdsensePlacementFormBuilder $html .= $this->buildRailAdsGroup($componentId); $html .= $this->buildAnchorAdsGroup($componentId); $html .= $this->buildVignetteAdsGroup($componentId); + $html .= $this->buildSearchResultsGroup($componentId); $html .= ' '; $html .= ''; @@ -708,6 +709,101 @@ final class AdsensePlacementFormBuilder return $html; } + /** + * Seccion para anuncios en resultados de busqueda (ROI APU Search) + */ + private function buildSearchResultsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Resultados de Busqueda'; + $html .= ' ROI APU Search'; + $html .= '
'; + $html .= '

Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.

'; + + // Master switch + $searchAdsEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_ads_enabled', false); + $html .= $this->buildSwitch($cid . 'SearchAdsEnabled', 'Activar ads en busqueda', $searchAdsEnabled, 'bi-power'); + + // Anuncio superior + $html .= '
'; + $html .= '
'; + $html .= ' ANUNCIO SUPERIOR'; + $html .= ' Debajo del campo de busqueda'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true); + $html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled); + $html .= '
'; + $html .= '
'; + $topFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_format', 'auto'); + $html .= $this->buildSelect($cid . 'SearchTopAdFormat', 'Formato', + (string)$topFormat, + ['auto' => 'Auto (responsive)', 'display' => 'Display (fijo)', 'in-article' => 'In-Article (fluid)'] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + // Anuncios entre resultados + $html .= '
'; + $html .= '
'; + $html .= ' ENTRE RESULTADOS'; + $html .= ' Intercalados con los resultados'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true); + $html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled); + $html .= '
'; + $html .= '
'; + $betweenMax = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_max', '1'); + $html .= $this->buildSelect($cid . 'SearchBetweenMax', 'Maximo ads', + (string)$betweenMax, + ['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios (max)'] + ); + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $betweenFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_format', 'in-article'); + $html .= $this->buildSelect($cid . 'SearchBetweenFormat', 'Formato', + (string)$betweenFormat, + ['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)', 'autorelaxed' => 'Autorelaxed (feed)'] + ); + $html .= '
'; + $html .= '
'; + $betweenPosition = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_position', 'random'); + $html .= $this->buildSelect($cid . 'SearchBetweenPosition', 'Posicion', + (string)$betweenPosition, + ['random' => 'Aleatorio', 'fixed' => 'Fijo (cada N)', 'first_half' => 'Primera mitad'] + ); + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $betweenEvery = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_every', '5'); + $html .= $this->buildSelect($cid . 'SearchBetweenEvery', 'Cada N resultados (si es fijo)', + (string)$betweenEvery, + ['3' => 'Cada 3', '4' => 'Cada 4', '5' => 'Cada 5', '6' => 'Cada 6', '7' => 'Cada 7', '8' => 'Cada 8', '10' => 'Cada 10'] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + private function buildExclusionsGroup(string $cid): string { $html = '
'; diff --git a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php index 552d8c87..20329657 100644 --- a/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php +++ b/Admin/ContactForm/Infrastructure/FieldMapping/ContactFormFieldMapper.php @@ -26,7 +26,13 @@ final class ContactFormFieldMapper implements FieldMapperInterface 'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'], diff --git a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php index 9ff18a35..57cc938c 100644 --- a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php +++ b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php @@ -93,17 +93,38 @@ final class ContactFormFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -598,4 +619,26 @@ final class ContactFormFormBuilder 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/FieldMapping/CtaBoxSidebarFieldMapper.php b/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php index 1a594c73..0a544154 100644 --- a/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php +++ b/Admin/CtaBoxSidebar/Infrastructure/FieldMapping/CtaBoxSidebarFieldMapper.php @@ -30,7 +30,13 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface 'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'ctaVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'ctaVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'ctaVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'ctaVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'ctaTitle' => ['group' => 'content', 'attribute' => 'title'], diff --git a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php index 35f58809..08c7840c 100644 --- a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php +++ b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php @@ -94,18 +94,40 @@ final class CtaBoxSidebarFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false); $html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - // show_on_pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $html .= '
'; + $html .= '

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

'; + + // Obtener valores de _page_visibility (grupo especial) + $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); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + // Grid 3 columnas según Design System + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -515,4 +537,29 @@ final class CtaBoxSidebarFormBuilder return $html; } + + /** + * Genera un checkbox de visibilidad por tipo de pagina + * + * Sigue Design System: form-check-checkbox es obligatorio + */ + private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string + { + $html = '
'; + $html .= sprintf( + ' ', + esc_attr($id), + $checked ? 'checked' : '' + ); + $html .= sprintf( + ' '; + $html .= '
'; + + return $html; + } } diff --git a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php index 0249355e..c5cc4804 100644 --- a/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php +++ b/Admin/CtaLetsTalk/Infrastructure/FieldMapping/CtaLetsTalkFieldMapper.php @@ -26,7 +26,13 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface 'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'ctaLetsTalkVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'ctaLetsTalkVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'], diff --git a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php index a1e76b00..fecea221 100644 --- a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php +++ b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php @@ -120,16 +120,38 @@ final class CtaLetsTalkFormBuilder $html .= '
'; $html .= ' '; - // Select: Show on Pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -447,4 +469,26 @@ final class CtaLetsTalkFormBuilder 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/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php index 332e97b6..1c7f44ad 100644 --- a/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php +++ b/Admin/CtaPost/Infrastructure/FieldMapping/CtaPostFieldMapper.php @@ -26,7 +26,13 @@ final class CtaPostFieldMapper implements FieldMapperInterface 'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'ctaPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'ctaPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'], diff --git a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php index 5575894a..7406f633 100644 --- a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php +++ b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php @@ -85,17 +85,38 @@ final class CtaPostFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -437,4 +458,26 @@ final class CtaPostFormBuilder 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/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php index a1d51e0b..b7a08d27 100644 --- a/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php +++ b/Admin/FeaturedImage/Infrastructure/FieldMapping/FeaturedImageFieldMapper.php @@ -26,7 +26,13 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface 'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'], diff --git a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php index 05660e3e..6f47cb96 100644 --- a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php +++ b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php @@ -100,17 +100,38 @@ final class FeaturedImageFormBuilder $html .= ' '; $html .= ' '; - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $html .= '
'; + $html .= '

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

'; + + $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false); + $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); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -119,6 +140,28 @@ final class FeaturedImageFormBuilder 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; + } + private function buildContentGroup(string $componentId): string { $html = '
'; diff --git a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php index 0ad8eebd..0fd229ed 100644 --- a/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php +++ b/Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php @@ -26,9 +26,15 @@ final class HeroFieldMapper implements FieldMapperInterface 'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], 'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], + // Page Visibility (grupo especial _page_visibility) + 'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Content 'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'], 'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'], diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php index a9e13ec0..d6fe0337 100644 --- a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php +++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php @@ -102,18 +102,38 @@ final class HeroFormBuilder $html .= '
'; $html .= ' '; - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $html .= '
'; + $html .= '

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

'; + + $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false); + $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); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; // Switch: CSS Crítico @@ -427,4 +447,26 @@ final class HeroFormBuilder 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/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php index 2e25f22b..af1f9244 100644 --- a/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php +++ b/Admin/Navbar/Infrastructure/FieldMapping/NavbarFieldMapper.php @@ -26,10 +26,16 @@ final class NavbarFieldMapper implements FieldMapperInterface 'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], 'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], - 'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], 'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'], 'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], + // Page Visibility (grupo especial _page_visibility) + 'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Layout 'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'], 'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'], diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php index 596ad0d7..f71f64fb 100644 --- a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php +++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php @@ -105,16 +105,38 @@ final class NavbarFormBuilder $html .= '
'; $html .= ' '; - // Select: Show on Pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $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('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; // Switch: Sticky @@ -527,4 +549,26 @@ final class NavbarFormBuilder 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/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php index b5237712..6bcae3ce 100644 --- a/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php +++ b/Admin/RelatedPost/Infrastructure/FieldMapping/RelatedPostFieldMapper.php @@ -29,7 +29,13 @@ final class RelatedPostFieldMapper implements FieldMapperInterface 'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'], diff --git a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php index cde1c5d9..4cf3db80 100644 --- a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php +++ b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php @@ -86,17 +86,38 @@ final class RelatedPostFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -498,4 +519,26 @@ final class RelatedPostFormBuilder 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/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php index c0dc6162..827e9d70 100644 --- a/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php +++ b/Admin/SocialShare/Infrastructure/FieldMapping/SocialShareFieldMapper.php @@ -26,7 +26,13 @@ final class SocialShareFieldMapper implements FieldMapperInterface 'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'], diff --git a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php index 2fc4403a..7cf4741e 100644 --- a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php +++ b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php @@ -94,18 +94,38 @@ final class SocialShareFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - // show_on_pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -526,4 +546,26 @@ final class SocialShareFormBuilder 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/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php index 72dc09a7..8860fc5b 100644 --- a/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php +++ b/Admin/TableOfContents/Infrastructure/FieldMapping/TableOfContentsFieldMapper.php @@ -26,7 +26,13 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface 'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], 'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], - 'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], + + // Page Visibility (grupo especial _page_visibility) + 'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], // Content 'tocTitle' => ['group' => 'content', 'attribute' => 'title'], diff --git a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php index 6f0650f9..c0866d1e 100644 --- a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php +++ b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php @@ -94,18 +94,38 @@ final class TableOfContentsFormBuilder $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false); $html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile); - // show_on_pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $html .= '
'; + $html .= '

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

'; + + $showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false); + $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); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; $html .= '
'; @@ -585,4 +605,26 @@ final class TableOfContentsFormBuilder 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/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php index 5dd5997b..041a5501 100644 --- a/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php +++ b/Admin/TopNotificationBar/Infrastructure/FieldMapping/TopNotificationBarFieldMapper.php @@ -26,9 +26,15 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface 'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'], 'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'], 'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'], - 'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'], 'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'], + // Page Visibility (grupo especial _page_visibility) + 'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'], + 'topBarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'], + 'topBarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'], + 'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'], + 'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'], + // Content 'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'], 'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'], diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php index cd130552..5fed2d32 100644 --- a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php +++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php @@ -105,19 +105,38 @@ final class TopNotificationBarFormBuilder $html .= '
'; $html .= ' '; - // Select: Show on Pages - $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all'); - $html .= '
'; - $html .= ' '; - $html .= ' '; + // ============================================= + // Checkboxes de visibilidad por tipo de página + // Grupo especial: _page_visibility + // ============================================= + $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', true); + $showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true); + $showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false); + $showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false); + + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives); + $html .= '
'; + $html .= '
'; + $html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch); + $html .= '
'; $html .= '
'; // Switch: CSS Crítico @@ -319,4 +338,26 @@ final class TopNotificationBarFormBuilder 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/Assets/Js/adsense-loader.js b/Assets/Js/adsense-loader.js index 373d2ee9..37747bf5 100644 --- a/Assets/Js/adsense-loader.js +++ b/Assets/Js/adsense-loader.js @@ -182,10 +182,72 @@ }, CONFIG.timeout); } + /** + * Activa slots de AdSense insertados dinamicamente + * Escucha el evento 'roi-adsense-activate' disparado por otros scripts + */ + function setupDynamicAdsListener() { + window.addEventListener('roi-adsense-activate', function() { + debugLog('Evento roi-adsense-activate recibido'); + + // Si AdSense aun no ha cargado, forzar carga ahora + if (!adsenseLoaded) { + debugLog('AdSense no cargado, forzando carga...'); + loadAdSense(); + return; + } + + // AdSense ya cargado - activar nuevos slots + debugLog('Activando nuevos slots dinamicos...'); + activateDynamicSlots(); + }); + } + + /** + * Activa slots de AdSense que fueron insertados despues de la carga inicial + */ + function activateDynamicSlots() { + // Buscar scripts de push que aun no han sido ejecutados + var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]'); + + if (pendingPushScripts.length === 0) { + debugLog('No hay slots pendientes por activar'); + return; + } + + debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)'); + + // Asegurar que adsbygoogle existe + window.adsbygoogle = window.adsbygoogle || []; + + pendingPushScripts.forEach(function(oldScript) { + try { + // Crear nuevo script ejecutable + var newScript = document.createElement('script'); + newScript.type = 'text/javascript'; + newScript.innerHTML = oldScript.innerHTML; + + // Reemplazar el placeholder con el script real + oldScript.parentNode.replaceChild(newScript, oldScript); + } catch (e) { + debugLog('Error activando slot: ' + e.message); + } + }); + } + /** * Inicializa el cargador retrasado de AdSense */ function init() { + // ========================================================================= + // NUEVO: Siempre configurar listener para ads dinamicos + // IMPORTANTE: Esto debe ejecutarse ANTES del early return + // porque los ads dinamicos pueden necesitar activarse aunque + // el delay global este deshabilitado + // ========================================================================= + setupDynamicAdsListener(); + debugLog('Listener para ads dinamicos configurado'); + // Verificar si el retardo de AdSense está habilitado if (!window.roiAdsenseDelayed) { debugLog('Retardo de AdSense no habilitado'); diff --git a/Inc/featured-image.php b/Inc/featured-image.php index ff2e763a..1e8e040c 100644 --- a/Inc/featured-image.php +++ b/Inc/featured-image.php @@ -50,6 +50,13 @@ function roi_get_featured_image($post_id = null, $size = 'roi-featured-large', $ return ''; // No placeholder - retornar vacío } + // Verificar que el archivo físico exista, no solo el attachment ID + $thumbnailId = get_post_thumbnail_id($post_id); + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return ''; // Archivo no existe en servidor + } + // Obtener tipo de post $post_type = get_post_type($post_id); @@ -145,6 +152,13 @@ function roi_get_post_thumbnail($post_id = null, $with_link = true) { return ''; // No placeholder - retornar vacío } + // Verificar que el archivo físico exista + $thumbnailId = get_post_thumbnail_id($post_id); + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return ''; + } + // Obtener la imagen con clases Bootstrap $image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array( 'class' => 'img-fluid post-thumbnail', @@ -216,6 +230,13 @@ function roi_get_post_thumbnail_small($post_id = null, $with_link = true) { return ''; // No placeholder - retornar vacío } + // Verificar que el archivo físico exista + $thumbnailId = get_post_thumbnail_id($post_id); + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return ''; + } + // Obtener la imagen $image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array( 'class' => 'img-fluid post-thumbnail-small', @@ -287,6 +308,13 @@ function roi_should_show_featured_image($post_id = null) { return false; } + // Verificar que el archivo físico exista + $thumbnailId = get_post_thumbnail_id($post_id); + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return false; + } + // Obtener tipo de post $post_type = get_post_type($post_id); @@ -338,6 +366,13 @@ function roi_get_featured_image_url($post_id = null, $size = 'roi-featured-large return ''; // No placeholder - retornar vacío } + // Verificar que el archivo físico exista + $thumbnailId = get_post_thumbnail_id($post_id); + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return ''; + } + // Obtener URL de la imagen $image_url = get_the_post_thumbnail_url($post_id, $size); diff --git a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php index 86096dcf..a7f412d4 100644 --- a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php +++ b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\ContactForm\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; /** * ContactFormRenderer - Renderiza formulario de contacto con webhook @@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class ContactFormRenderer implements RendererInterface { + private const COMPONENT_NAME = 'contact-form'; + public function __construct( private CSSGeneratorInterface $cssGenerator ) {} @@ -34,7 +37,7 @@ final class ContactFormRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -67,7 +70,7 @@ final class ContactFormRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'contact-form'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -76,22 +79,6 @@ final class ContactFormRenderer implements RendererInterface return $value === true || $value === '1' || $value === 1; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'all'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function getVisibilityClass(array $data): ?string { $showDesktop = $data['visibility']['show_on_desktop'] ?? true; diff --git a/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php b/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php index 1e4ee4f8..42b847a5 100644 --- a/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php +++ b/Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaBoxSidebar\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; /** * CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar @@ -27,6 +28,12 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class CtaBoxSidebarRenderer implements RendererInterface { + /** + * Nombre del componente para visibilidad + * Evita strings hardcodeados y facilita mantenimiento + */ + private const COMPONENT_NAME = 'cta-box-sidebar'; + public function __construct( private CSSGeneratorInterface $cssGenerator ) {} @@ -39,7 +46,8 @@ final class CtaBoxSidebarRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + // Evaluar visibilidad por tipo de página (usa Helper, NO cambia constructor) + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -52,7 +60,7 @@ final class CtaBoxSidebarRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'cta-box-sidebar'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -60,22 +68,6 @@ final class CtaBoxSidebarRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function generateCSS(array $data): string { $colors = $data['colors'] ?? []; diff --git a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php index b15a3e52..d4b238fd 100644 --- a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php +++ b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaLetsTalk\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; /** * Class CtaLetsTalkRenderer @@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class CtaLetsTalkRenderer implements RendererInterface { + private const COMPONENT_NAME = 'cta-lets-talk'; + /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS */ @@ -54,7 +57,7 @@ final class CtaLetsTalkRenderer implements RendererInterface } // Validar visibilidad por página - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -77,7 +80,7 @@ final class CtaLetsTalkRenderer implements RendererInterface */ public function supports(string $componentType): bool { - return $componentType === 'cta-lets-talk'; + return $componentType === self::COMPONENT_NAME; } /** @@ -91,25 +94,6 @@ final class CtaLetsTalkRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - /** - * Verificar si debe mostrarse en la página actual - * - * @param array $data Datos del componente - * @return bool - */ - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'all'; - - return match ($showOn) { - 'all' => true, - 'home' => is_front_page(), - 'posts' => is_single(), - 'pages' => is_page(), - default => true, - }; - } - /** * Calcular clases de visibilidad responsive * diff --git a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php index 2ee6ea19..9982b72a 100644 --- a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php +++ b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaPost\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; /** * CtaPostRenderer - Renderiza CTA promocional debajo del contenido @@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class CtaPostRenderer implements RendererInterface { + private const COMPONENT_NAME = 'cta-post'; + public function __construct( private CSSGeneratorInterface $cssGenerator ) {} @@ -34,7 +37,7 @@ final class CtaPostRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -46,7 +49,7 @@ final class CtaPostRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'cta-post'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -55,22 +58,6 @@ final class CtaPostRenderer implements RendererInterface return $value === true || $value === '1' || $value === 1; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function generateCSS(array $data): string { $colors = $data['colors'] ?? []; diff --git a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php index 430ccdbc..ec44ee79 100644 --- a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php +++ b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\FeaturedImage\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; /** * FeaturedImageRenderer - Renderiza la imagen destacada del post @@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class FeaturedImageRenderer implements RendererInterface { + private const COMPONENT_NAME = 'featured-image'; + public function __construct( private CSSGeneratorInterface $cssGenerator ) {} @@ -39,7 +42,7 @@ final class FeaturedImageRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -63,7 +66,7 @@ final class FeaturedImageRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'featured-image'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -71,25 +74,24 @@ final class FeaturedImageRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function hasPostThumbnail(): bool { - return is_singular() && has_post_thumbnail(); + if (!is_singular() || !has_post_thumbnail()) { + return false; + } + + // Verificar que el archivo físico exista, no solo el attachment ID + $thumbnailId = get_post_thumbnail_id(); + if (!$thumbnailId) { + return false; + } + + $filePath = get_attached_file($thumbnailId); + if (empty($filePath) || !file_exists($filePath)) { + return false; + } + + return true; } /** diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php index 04389b3d..39b77341 100644 --- a/Public/Hero/Infrastructure/Ui/HeroRenderer.php +++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\Hero\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; /** * Class HeroRenderer @@ -33,6 +34,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class HeroRenderer implements RendererInterface { + private const COMPONENT_NAME = 'hero'; + /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS */ @@ -48,7 +51,7 @@ final class HeroRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -68,7 +71,7 @@ final class HeroRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'hero'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -76,24 +79,6 @@ final class HeroRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'home': - return is_front_page() || is_home(); - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - /** * Generar CSS usando CSSGeneratorService * diff --git a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php index 11cf590f..85cd8a87 100644 --- a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php +++ b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui; use ROITheme\Shared\Domain\Entities\Component; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; +use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper; use Walker_Nav_Menu; /** @@ -28,6 +29,8 @@ use Walker_Nav_Menu; */ final class NavbarRenderer implements RendererInterface { + private const COMPONENT_NAME = 'navbar'; + /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS */ @@ -43,6 +46,10 @@ final class NavbarRenderer implements RendererInterface return ''; } + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { + return ''; + } + $html = $this->buildMenu($data); // Si is_critical=true, CSS ya fue inyectado en por CriticalCSSService @@ -281,7 +288,7 @@ final class NavbarRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'navbar'; + return $componentType === self::COMPONENT_NAME; } } diff --git a/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php index 2d2dcc1c..23436d6a 100644 --- a/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php +++ b/Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\RelatedPost\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; use ROITheme\Shared\Domain\Entities\Component; +use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper; /** * RelatedPostRenderer - Renderiza seccion de posts relacionados @@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class RelatedPostRenderer implements RendererInterface { + private const COMPONENT_NAME = 'related-post'; + public function __construct( private CSSGeneratorInterface $cssGenerator ) {} @@ -34,7 +37,7 @@ final class RelatedPostRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -51,7 +54,7 @@ final class RelatedPostRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'related-post'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -60,22 +63,6 @@ final class RelatedPostRenderer implements RendererInterface return $value === true || $value === '1' || $value === 1; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function getVisibilityClass(array $data): ?string { $showDesktop = $data['visibility']['show_on_desktop'] ?? true; diff --git a/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php index c3a14276..e48e4fb8 100644 --- a/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php +++ b/Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\SocialShare\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; /** * SocialShareRenderer - Renderiza botones de compartir en redes sociales @@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class SocialShareRenderer implements RendererInterface { + private const COMPONENT_NAME = 'social-share'; + private const NETWORKS = [ 'facebook' => [ 'field' => 'show_facebook', @@ -84,7 +87,7 @@ final class SocialShareRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -96,7 +99,7 @@ final class SocialShareRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'social-share'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -105,22 +108,6 @@ final class SocialShareRenderer implements RendererInterface return $value === true || $value === '1' || $value === 1; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function generateCSS(array $data): string { $colors = $data['colors'] ?? []; diff --git a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php index 8493ed26..d09e5361 100644 --- a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php +++ b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\TableOfContents\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; use DOMDocument; use DOMXPath; @@ -30,6 +31,8 @@ use DOMXPath; */ final class TableOfContentsRenderer implements RendererInterface { + private const COMPONENT_NAME = 'table-of-contents'; + private array $headingCounter = []; public function __construct( @@ -44,7 +47,7 @@ final class TableOfContentsRenderer implements RendererInterface return ''; } - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -63,7 +66,7 @@ final class TableOfContentsRenderer implements RendererInterface public function supports(string $componentType): bool { - return $componentType === 'table-of-contents'; + return $componentType === self::COMPONENT_NAME; } private function isEnabled(array $data): bool @@ -71,22 +74,6 @@ final class TableOfContentsRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'posts'; - - switch ($showOn) { - case 'all': - return true; - case 'posts': - return is_single(); - case 'pages': - return is_page(); - default: - return true; - } - } - private function getVisibilityClasses(bool $desktop, bool $mobile): ?string { if (!$desktop && !$mobile) { diff --git a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php index 1f08d3dd..fa25e9cc 100644 --- a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php +++ b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php @@ -6,6 +6,7 @@ namespace ROITheme\Public\TopNotificationBar\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; /** * Class TopNotificationBarRenderer @@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component; */ final class TopNotificationBarRenderer implements RendererInterface { + private const COMPONENT_NAME = 'top-notification-bar'; + /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS */ @@ -54,7 +57,7 @@ final class TopNotificationBarRenderer implements RendererInterface } // Validar visibilidad por página - if (!$this->shouldShowOnCurrentPage($data)) { + if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) { return ''; } @@ -78,7 +81,7 @@ final class TopNotificationBarRenderer implements RendererInterface */ public function supports(string $componentType): bool { - return $componentType === 'top-notification-bar'; + return $componentType === self::COMPONENT_NAME; } /** @@ -92,46 +95,6 @@ final class TopNotificationBarRenderer implements RendererInterface return ($data['visibility']['is_enabled'] ?? false) === true; } - /** - * Verificar si debe mostrarse en la página actual - * - * @param array $data Datos del componente - * @return bool - */ - private function shouldShowOnCurrentPage(array $data): bool - { - $showOn = $data['visibility']['show_on_pages'] ?? 'all'; - - return match ($showOn) { - 'all' => true, - 'home' => is_front_page(), - 'posts' => is_single(), - 'pages' => is_page(), - 'custom' => $this->isInCustomPages($data), - default => true, - }; - } - - /** - * Verificar si está en páginas personalizadas - * - * @param array $data Datos del componente - * @return bool - */ - private function isInCustomPages(array $data): bool - { - $pageIds = $data['visibility']['custom_page_ids'] ?? ''; - - if (empty($pageIds)) { - return false; - } - - $allowedIds = array_map('trim', explode(',', $pageIds)); - $currentId = (string) get_the_ID(); - - return in_array($currentId, $allowedIds, true); - } - /** * Verificar si el componente fue dismissed por el usuario * diff --git a/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css b/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css index 049f7160..6b72b421 100644 --- a/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css +++ b/Public/YoutubeFacade/Infrastructure/Ui/Assets/Css/youtube-facade.css @@ -110,3 +110,14 @@ transform: rotate(360deg); } } + +/* ======================================== + FIX: Legacy wrapper with padding-top + Removes duplicate aspect-ratio from parent + containers that use the old padding-top trick + (prevents double spacing above videos) + ======================================== */ + +div[style*="padding-top"]:has(> .youtube-facade) { + padding-top: 0 !important; +} diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json index c22386a7..93716768 100644 --- a/Schemas/adsense-placement.json +++ b/Schemas/adsense-placement.json @@ -437,6 +437,72 @@ } } }, + "search_results": { + "label": "Resultados de Busqueda (ROI APU Search)", + "priority": 73, + "fields": { + "search_ads_enabled": { + "type": "boolean", + "label": "Activar ads en busqueda", + "default": false, + "editable": true, + "description": "Insertar anuncios en resultados del buscador APU" + }, + "search_top_ad_enabled": { + "type": "boolean", + "label": "Anuncio fijo arriba", + "default": true, + "editable": true, + "description": "Mostrar anuncio debajo del campo de busqueda" + }, + "search_top_ad_format": { + "type": "select", + "label": "Formato anuncio superior", + "default": "auto", + "editable": true, + "options": ["auto", "display", "in-article"] + }, + "search_between_enabled": { + "type": "boolean", + "label": "Anuncios entre resultados", + "default": true, + "editable": true + }, + "search_between_max": { + "type": "select", + "label": "Maximo anuncios entre resultados", + "default": "1", + "editable": true, + "options": ["1", "2", "3"], + "description": "Maximo 3 por politicas AdSense" + }, + "search_between_format": { + "type": "select", + "label": "Formato entre resultados", + "default": "in-article", + "editable": true, + "options": ["auto", "in-article", "autorelaxed"] + }, + "search_between_position": { + "type": "select", + "label": "Posicion de anuncios", + "default": "random", + "editable": true, + "options": { + "random": "Aleatorio", + "fixed": "Fijo (cada N resultados)", + "first_half": "Primera mitad" + } + }, + "search_between_every": { + "type": "select", + "label": "Cada N resultados (si es fijo)", + "default": "5", + "editable": true, + "options": ["3", "4", "5", "6", "7", "8", "10"] + } + } + }, "layout": { "label": "Ubicaciones Archivos/Globales", "priority": 80, diff --git a/Schemas/contact-form.json b/Schemas/contact-form.json index 3b801ed6..b03cf5d8 100644 --- a/Schemas/contact-form.json +++ b/Schemas/contact-form.json @@ -27,14 +27,6 @@ "default": true, "editable": true, "description": "Muestra el componente en pantallas < 992px" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "all", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" } } }, diff --git a/Schemas/cta-box-sidebar.json b/Schemas/cta-box-sidebar.json index 645eb54f..957c5e2a 100644 --- a/Schemas/cta-box-sidebar.json +++ b/Schemas/cta-box-sidebar.json @@ -27,14 +27,6 @@ "default": false, "editable": true, "description": "Muestra el componente en pantallas < 992px" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" } } }, diff --git a/Schemas/cta-lets-talk.json b/Schemas/cta-lets-talk.json index bdea8bd7..7f7aef0a 100644 --- a/Schemas/cta-lets-talk.json +++ b/Schemas/cta-lets-talk.json @@ -29,20 +29,6 @@ "editable": true, "description": "Muestra el botón en pantallas móviles (<992px). Por defecto oculto para ahorrar espacio en navbar móvil" }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "all", - "editable": true, - "required": true, - "options": { - "all": "Todas las páginas", - "home": "Solo página de inicio", - "posts": "Solo posts individuales", - "pages": "Solo páginas" - }, - "description": "Define en qué páginas se mostrará el botón" - }, "is_critical": { "type": "boolean", "label": "CSS Crítico", diff --git a/Schemas/cta-post.json b/Schemas/cta-post.json index 965a4e40..4d6d0034 100644 --- a/Schemas/cta-post.json +++ b/Schemas/cta-post.json @@ -27,14 +27,6 @@ "default": true, "editable": true, "description": "Muestra el componente en pantallas < 992px" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" } } }, diff --git a/Schemas/featured-image.json b/Schemas/featured-image.json index 70ca7f60..84444980 100644 --- a/Schemas/featured-image.json +++ b/Schemas/featured-image.json @@ -30,19 +30,6 @@ "editable": true, "required": true, "description": "Muestra la imagen en dispositivos moviles (<768px)" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "required": true, - "options": { - "all": "Todas las paginas", - "posts": "Solo posts individuales", - "pages": "Solo paginas" - }, - "description": "Define en que tipo de contenido se muestra la imagen" } } }, diff --git a/Schemas/hero.json b/Schemas/hero.json index 5a82caf9..b76e86af 100644 --- a/Schemas/hero.json +++ b/Schemas/hero.json @@ -31,20 +31,6 @@ "required": true, "description": "Muestra el hero en dispositivos móviles (<768px)" }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "required": true, - "options": { - "all": "Todas las páginas", - "posts": "Solo posts individuales", - "pages": "Solo páginas", - "home": "Solo página de inicio" - }, - "description": "Define en qué tipo de contenido se mostrará el hero" - }, "is_critical": { "type": "boolean", "label": "CSS Crítico", diff --git a/Schemas/navbar.json b/Schemas/navbar.json index d81a77bc..e61008f9 100644 --- a/Schemas/navbar.json +++ b/Schemas/navbar.json @@ -29,19 +29,6 @@ "editable": true, "description": "Muestra el menú en dispositivos de escritorio (≥768px)" }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "all", - "editable": true, - "options": { - "all": "Todas las páginas", - "home": "Solo página de inicio", - "posts": "Solo posts individuales", - "pages": "Solo páginas" - }, - "description": "Define en qué páginas se muestra el navbar" - }, "sticky_enabled": { "type": "boolean", "label": "Navbar fijo (sticky)", diff --git a/Schemas/related-post.json b/Schemas/related-post.json index 218c6f3b..9c89e725 100644 --- a/Schemas/related-post.json +++ b/Schemas/related-post.json @@ -27,14 +27,6 @@ "default": true, "editable": true, "description": "Muestra el componente en pantallas < 992px" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" } } }, diff --git a/Schemas/social-share.json b/Schemas/social-share.json index 456abe55..e7a13930 100644 --- a/Schemas/social-share.json +++ b/Schemas/social-share.json @@ -27,14 +27,6 @@ "default": true, "editable": true, "description": "Muestra el componente en pantallas < 992px" - }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" } } }, diff --git a/Schemas/table-of-contents.json b/Schemas/table-of-contents.json index f48efdc6..a5a9ea8f 100644 --- a/Schemas/table-of-contents.json +++ b/Schemas/table-of-contents.json @@ -28,14 +28,6 @@ "editable": true, "description": "Muestra el componente en pantallas < 992px" }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "posts", - "editable": true, - "options": ["all", "posts", "pages"], - "description": "Tipos de contenido donde se muestra" - }, "is_critical": { "type": "boolean", "label": "CSS Crítico", diff --git a/Schemas/top-notification-bar.json b/Schemas/top-notification-bar.json index 87f0134f..049075e8 100644 --- a/Schemas/top-notification-bar.json +++ b/Schemas/top-notification-bar.json @@ -15,20 +15,6 @@ "required": true, "description": "Activa o desactiva la barra de notificación superior" }, - "show_on_pages": { - "type": "select", - "label": "Mostrar en", - "default": "all", - "editable": true, - "required": true, - "options": { - "all": "Todas las páginas", - "home": "Solo página de inicio", - "posts": "Solo posts individuales", - "pages": "Solo páginas" - }, - "description": "Define en qué páginas se mostrará la barra" - }, "show_on_desktop": { "type": "boolean", "label": "Mostrar en desktop", diff --git a/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php new file mode 100644 index 00000000..57fc8e94 --- /dev/null +++ b/Shared/Application/UseCases/EvaluatePageVisibility/EvaluatePageVisibilityUseCase.php @@ -0,0 +1,46 @@ +visibilityRepository->getVisibilityConfig($componentName); + + if (empty($config)) { + // Usar constante compartida (DRY) + $config = VisibilityDefaults::DEFAULT_VISIBILITY; + } + + $pageType = $this->pageTypeDetector->detect(); + $visibilityField = $pageType->toVisibilityField(); + + return $this->toBool($config[$visibilityField] ?? true); + } + + private function toBool(mixed $value): bool + { + return $value === true || $value === '1' || $value === 1; + } +} diff --git a/Shared/Domain/Constants/VisibilityDefaults.php b/Shared/Domain/Constants/VisibilityDefaults.php new file mode 100644 index 00000000..ca810d2b --- /dev/null +++ b/Shared/Domain/Constants/VisibilityDefaults.php @@ -0,0 +1,45 @@ + true, + 'show_on_posts' => true, + 'show_on_pages' => true, + 'show_on_archives' => false, + 'show_on_search' => false, + ]; + + /** + * Lista de campos de visibilidad válidos + */ + public const VISIBILITY_FIELDS = [ + 'show_on_home', + 'show_on_posts', + 'show_on_pages', + 'show_on_archives', + 'show_on_search', + ]; +} diff --git a/Shared/Domain/Contracts/PageTypeDetectorInterface.php b/Shared/Domain/Contracts/PageTypeDetectorInterface.php new file mode 100644 index 00000000..75086568 --- /dev/null +++ b/Shared/Domain/Contracts/PageTypeDetectorInterface.php @@ -0,0 +1,25 @@ + Mapa de campo => habilitado + */ + public function getVisibilityConfig(string $componentName): array; + + /** + * Guarda la configuración de visibilidad de un componente + * + * @param string $componentName Nombre del componente + * @param array $config Configuración a guardar + */ + public function saveVisibilityConfig(string $componentName, array $config): void; + + /** + * Verifica si existe configuración de visibilidad para un componente + */ + public function hasVisibilityConfig(string $componentName): bool; + + /** + * Obtiene lista de todos los componentes registrados + * + * @return array Lista de nombres de componentes + */ + public function getAllComponentNames(): array; + + /** + * Crea configuración de visibilidad por defecto para un componente + * + * @param string $componentName Nombre del componente + * @param array $defaults Valores por defecto + */ + public function createDefaultVisibility(string $componentName, array $defaults): void; +} diff --git a/Shared/Domain/ValueObjects/ComponentConfiguration.php b/Shared/Domain/ValueObjects/ComponentConfiguration.php index 58de9dc5..3801287b 100644 --- a/Shared/Domain/ValueObjects/ComponentConfiguration.php +++ b/Shared/Domain/ValueObjects/ComponentConfiguration.php @@ -93,6 +93,9 @@ final readonly class ComponentConfiguration 'widget_3', // Widget 3 del footer (menú) 'newsletter', // Sección newsletter del footer 'footer_bottom', // Pie del footer (copyright) + + // Sistema de visibilidad por página + '_page_visibility', // Visibilidad por tipo de página (home, posts, pages, archives, search) ]; /** diff --git a/Shared/Domain/ValueObjects/PageType.php b/Shared/Domain/ValueObjects/PageType.php new file mode 100644 index 00000000..8e0e02e5 --- /dev/null +++ b/Shared/Domain/ValueObjects/PageType.php @@ -0,0 +1,90 @@ +value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } + + /** + * Retorna el nombre del campo de visibilidad correspondiente + */ + public function toVisibilityField(): string + { + return match ($this->value) { + self::HOME => 'show_on_home', + self::POST => 'show_on_posts', + self::PAGE => 'show_on_pages', + self::ARCHIVE => 'show_on_archives', + self::SEARCH => 'show_on_search', + default => 'show_on_posts', + }; + } +} diff --git a/Shared/Infrastructure/Api/WordPress/MigrationCommand.php b/Shared/Infrastructure/Api/WordPress/MigrationCommand.php index 404b7f4c..9de77fe3 100644 --- a/Shared/Infrastructure/Api/WordPress/MigrationCommand.php +++ b/Shared/Infrastructure/Api/WordPress/MigrationCommand.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace ROITheme\Shared\Infrastructure\Api\WordPress; +use ROITheme\Shared\Infrastructure\Di\DIContainer; + /** * WP-CLI Command para Sincronización de Schemas * @@ -297,6 +299,298 @@ final class MigrationCommand 'stats' => $stats ]; } + + /** + * Migra configuración de visibilidad para todos los componentes + * + * ## EXAMPLES + * + * wp roi-theme migrate-visibility + * + * @when after_wp_load + */ + public function migrate_visibility(): void + { + $container = DIContainer::getInstance(); + $service = $container->getMigratePageVisibilityService(); + + $result = $service->migrate(); + + \WP_CLI::success(sprintf( + 'Migración completada: %d creados, %d omitidos', + $result['created'], + $result['skipped'] + )); + } + + /** + * Shortcodes que DEBEN ser preservados + */ + private const PROTECTED_SHORTCODES = ['[roi_apu_search', '[roi_']; + + /** + * Máximo porcentaje de contenido que puede eliminarse + */ + private const MAX_CONTENT_LOSS_PERCENT = 50; + + /** + * Limpia contenido Thrive congelado de páginas (H2 y paginación) + * + * LIMPIEZA QUIRÚRGICA CON VALIDACIONES DE SEGURIDAD: + * - Elimina H2 con data-shortcode="tcb_post_title" + * - Elimina paginación rota ([tcb_pagination_current_page], [tcb_pagination_total_pages]) + * - PRESERVA todo el demás contenido incluyendo shortcodes [roi_apu_search] + * - Verifica que shortcodes importantes NO sean eliminados + * - Aborta si se detecta pérdida excesiva de contenido (>50%) + * + * ## OPTIONS + * + * [--dry-run] + * : Mostrar qué se limpiaría sin modificar nada (OBLIGATORIO primero) + * + * [--force] + * : Ejecutar la limpieza real después de verificar dry-run + * + * [--include-others] + * : Incluir otras páginas afectadas (Blog, Curso) + * + * ## EXAMPLES + * + * # Ver qué se limpiaría (modo seguro) - SIEMPRE PRIMERO + * wp roi-theme clean_thrive --dry-run + * + * # Ejecutar limpieza real (requiere --force) + * wp roi-theme clean_thrive --force + * + * @when after_wp_load + */ + public function clean_thrive(array $args, array $assoc_args): void + { + $affectedPageIds = [ + 107264, 107312, 107340, 107345, 107351, 107357, 107362, + 107369, 107374, 107379, 107384, 107389, 107395, 107399, + 107403, 107407, 107411, 107416, 107421, 107425, 185752 + ]; + $otherAffectedIds = [252030, 290709]; + + $dryRun = isset($assoc_args['dry-run']); + $includeOthers = isset($assoc_args['include-others']); + $force = isset($assoc_args['force']); + + $pageIds = $affectedPageIds; + if ($includeOthers) { + $pageIds = array_merge($pageIds, $otherAffectedIds); + } + + \WP_CLI::line(''); + \WP_CLI::line('╔══════════════════════════════════════════════════════════════════╗'); + \WP_CLI::line('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║'); + \WP_CLI::line('║ Con validaciones de seguridad para proteger shortcodes ║'); + \WP_CLI::line('╚══════════════════════════════════════════════════════════════════╝'); + \WP_CLI::line(''); + + if ($dryRun) { + \WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido'); + } else { + \WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false); + \WP_CLI::line(''); + \WP_CLI::line('Para ejecutar la limpieza real, primero revisa el dry-run:'); + \WP_CLI::line(' wp roi-theme clean_thrive --dry-run'); + \WP_CLI::line(''); + \WP_CLI::line('Si el dry-run es correcto y deseas ejecutar:'); + \WP_CLI::line(' wp roi-theme clean_thrive --force'); + + if (!$force) { + return; + } + \WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido'); + } + + \WP_CLI::line(''); + \WP_CLI::line('Páginas a procesar: ' . count($pageIds)); + \WP_CLI::line('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES)); + \WP_CLI::line('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%'); + \WP_CLI::line(''); + + $totalH2Removed = 0; + $totalPaginationRemoved = 0; + $totalBytesFreed = 0; + $pagesModified = 0; + $pagesSkipped = 0; + $errors = []; + + foreach ($pageIds as $id) { + $page = get_post($id); + if (!$page) { + \WP_CLI::warning("Página {$id} no encontrada, saltando..."); + continue; + } + + $originalContent = $page->post_content; + $originalSize = strlen($originalContent); + + $hasThrive = strpos($originalContent, 'tcb_post_title') !== false || + strpos($originalContent, 'tcb_pagination') !== false; + + if (!$hasThrive) { + \WP_CLI::line(sprintf("[SIN THRIVE] ID %d: %s", $id, mb_substr($page->post_title, 0, 50))); + continue; + } + + $h2Count = preg_match_all('/]*>\s*]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', $originalContent); + $protectedBefore = $this->countProtectedShortcodes($originalContent); + $cleanResult = $this->cleanThriveContentSafely($originalContent); + + if ($cleanResult['error']) { + $errors[] = "ID {$id}: {$cleanResult['error']}"; + \WP_CLI::error(sprintf("[ERROR] ID %d: %s - %s", $id, mb_substr($page->post_title, 0, 40), $cleanResult['error']), false); + $pagesSkipped++; + continue; + } + + $cleanedContent = $cleanResult['content']; + $newSize = strlen($cleanedContent); + $protectedAfter = $this->countProtectedShortcodes($cleanedContent); + + if ($protectedAfter < $protectedBefore) { + $errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})"; + \WP_CLI::error(sprintf("[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)", $id, $protectedBefore, $protectedAfter), false); + $pagesSkipped++; + continue; + } + + $lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0; + if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) { + $errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)"; + \WP_CLI::error(sprintf("[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)", $id, $lossPercent, self::MAX_CONTENT_LOSS_PERCENT), false); + $pagesSkipped++; + continue; + } + + $hasChanges = $originalContent !== $cleanedContent; + $bytesSaved = $originalSize - $newSize; + $paginationRemoved = (strpos($originalContent, 'tcb_pagination_current_page') !== false && strpos($cleanedContent, 'tcb_pagination_current_page') === false) ? 1 : 0; + + if ($hasChanges) { + $pagesModified++; + $totalH2Removed += $h2Count; + $totalPaginationRemoved += $paginationRemoved; + $totalBytesFreed += $bytesSaved; + + $status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]'; + \WP_CLI::line(sprintf("%s ID %d: %s", $status, $id, mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : ''))); + \WP_CLI::line(sprintf(" → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%", $h2Count, $paginationRemoved ? 'Sí' : 'No', $lossPercent)); + \WP_CLI::line(sprintf(" → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s", $protectedAfter, $this->formatBytes($bytesSaved))); + + if (!$dryRun && $force) { + wp_update_post(['ID' => $id, 'post_content' => $cleanedContent]); + } + } else { + \WP_CLI::line(sprintf("[SIN CAMBIOS] ID %d: %s", $id, mb_substr($page->post_title, 0, 50))); + } + } + + \WP_CLI::line(''); + \WP_CLI::line('════════════════════════════════════════════════════════════════════'); + \WP_CLI::line('RESUMEN:'); + \WP_CLI::line(sprintf(' Páginas modificadas: %d', $pagesModified)); + \WP_CLI::line(sprintf(' Páginas omitidas: %d', $pagesSkipped)); + \WP_CLI::line(sprintf(' Total H2 eliminados: %d', $totalH2Removed)); + \WP_CLI::line(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved)); + \WP_CLI::line(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed))); + \WP_CLI::line('════════════════════════════════════════════════════════════════════'); + + if (count($errors) > 0) { + \WP_CLI::line(''); + \WP_CLI::warning('ERRORES ENCONTRADOS:'); + foreach ($errors as $error) { + \WP_CLI::line(" - {$error}"); + } + } + + if ($dryRun && $pagesModified > 0 && count($errors) === 0) { + \WP_CLI::line(''); + \WP_CLI::success('Dry-run completado SIN errores.'); + \WP_CLI::line(''); + \WP_CLI::warning('Para ejecutar la limpieza real:'); + \WP_CLI::line(' wp roi-theme clean_thrive --force'); + } elseif (!$dryRun && $force && $pagesModified > 0) { + \WP_CLI::line(''); + \WP_CLI::success('Limpieza completada exitosamente.'); + \WP_CLI::line(''); + \WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.'); + } + } + + /** + * Limpia el contenido con validaciones de seguridad + * @return array{content: string, error: string|null} + */ + private function cleanThriveContentSafely(string $content): array + { + $originalContent = $content; + + // Patrón específico: H2 que contiene span con data-shortcode="tcb_post_title" + // Estructura:

...

+ $result = preg_replace('/]*>\s*]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', '', $content); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2']; + } + $content = $result; + + $result = preg_replace('/]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s', '', $content); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación']; + } + $content = $result; + + $result = preg_replace('/]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s', '', $content); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones']; + } + $content = $result; + + $content = str_replace('[tcb_pagination_current_page]', '', $content); + $content = str_replace('[tcb_pagination_total_pages]', '', $content); + + $result = preg_replace('/(\r?\n){3,}/', "\n\n", $content); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas']; + } + $content = trim($result); + + if (empty($content) && !empty($originalContent)) { + return ['content' => $originalContent, 'error' => 'El contenido quedó vacío']; + } + + return ['content' => $content, 'error' => null]; + } + + /** + * Cuenta shortcodes protegidos en el contenido + */ + private function countProtectedShortcodes(string $content): int + { + $count = 0; + foreach (self::PROTECTED_SHORTCODES as $shortcode) { + $count += substr_count($content, $shortcode); + } + return $count; + } + + /** + * Formatea bytes a formato legible + */ + private function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . ' B'; + } elseif ($bytes < 1048576) { + return round($bytes / 1024, 1) . ' KB'; + } else { + return round($bytes / 1048576, 2) . ' MB'; + } + } } // Registrar comando WP-CLI diff --git a/Shared/Infrastructure/CLI/CleanThriveContentCommand.php b/Shared/Infrastructure/CLI/CleanThriveContentCommand.php new file mode 100644 index 00000000..c068b8ba --- /dev/null +++ b/Shared/Infrastructure/CLI/CleanThriveContentCommand.php @@ -0,0 +1,352 @@ +50%) + * + * USO: + * wp roi-theme clean_thrive --dry-run # Ver qué se limpiaría (OBLIGATORIO primero) + * wp roi-theme clean_thrive # Ejecutar limpieza real + * + * SEGURIDAD: + * - Verifica preservación de shortcodes [roi_apu_search] + * - Máximo 50% de reducción de contenido permitida + * - Valida cada preg_replace para evitar null returns + */ +final class CleanThriveContentCommand +{ + /** + * IDs de páginas buscar-apus afectadas + */ + private const AFFECTED_PAGE_IDS = [ + 107264, 107312, 107340, 107345, 107351, 107357, 107362, + 107369, 107374, 107379, 107384, 107389, 107395, 107399, + 107403, 107407, 107411, 107416, 107421, 107425, 185752 + ]; + + /** + * Otras páginas con contenido Thrive (Blog, Curso) + */ + private const OTHER_AFFECTED_IDS = [252030, 290709]; + + /** + * Shortcodes que DEBEN ser preservados + */ + private const PROTECTED_SHORTCODES = [ + '[roi_apu_search', + '[roi_', + ]; + + /** + * Máximo porcentaje de contenido que puede eliminarse + */ + private const MAX_CONTENT_LOSS_PERCENT = 50; + + public function __invoke(array $args, array $assoc_args): void + { + $dryRun = isset($assoc_args['dry-run']); + $includeOthers = isset($assoc_args['include-others']); + $force = isset($assoc_args['force']); + + $pageIds = self::AFFECTED_PAGE_IDS; + if ($includeOthers) { + $pageIds = array_merge($pageIds, self::OTHER_AFFECTED_IDS); + } + + WP_CLI::log(''); + WP_CLI::log('╔══════════════════════════════════════════════════════════════════╗'); + WP_CLI::log('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║'); + WP_CLI::log('║ Con validaciones de seguridad para proteger shortcodes ║'); + WP_CLI::log('╚══════════════════════════════════════════════════════════════════╝'); + WP_CLI::log(''); + + if ($dryRun) { + WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido'); + } else { + WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false); + WP_CLI::log(''); + WP_CLI::log('Para ejecutar la limpieza real, primero revisa el dry-run:'); + WP_CLI::log(' wp roi-theme clean_thrive --dry-run'); + WP_CLI::log(''); + WP_CLI::log('Si el dry-run es correcto y deseas ejecutar:'); + WP_CLI::log(' wp roi-theme clean_thrive --force'); + + if (!$force) { + return; + } + WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido'); + } + + WP_CLI::log(''); + WP_CLI::log('Páginas a procesar: ' . count($pageIds)); + WP_CLI::log('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES)); + WP_CLI::log('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%'); + WP_CLI::log(''); + + $totalH2Removed = 0; + $totalPaginationRemoved = 0; + $totalBytesFreed = 0; + $pagesModified = 0; + $pagesSkipped = 0; + $errors = []; + + foreach ($pageIds as $id) { + $page = get_post($id); + if (!$page) { + WP_CLI::warning("Página {$id} no encontrada, saltando..."); + continue; + } + + $originalContent = $page->post_content; + $originalSize = strlen($originalContent); + + // Verificar si tiene contenido Thrive que limpiar + $hasThrive = strpos($originalContent, 'tcb_post_title') !== false || + strpos($originalContent, 'tcb_pagination') !== false; + + if (!$hasThrive) { + WP_CLI::log(sprintf( + "[SIN THRIVE] ID %d: %s", + $id, + mb_substr($page->post_title, 0, 50) + )); + continue; + } + + // Contar elementos antes de limpiar + $h2Count = preg_match_all('/]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s', $originalContent); + + // Contar shortcodes protegidos antes + $protectedBefore = $this->countProtectedShortcodes($originalContent); + + // Limpiar contenido con validación + $cleanResult = $this->cleanContentSafely($originalContent); + + if ($cleanResult['error']) { + $errors[] = "ID {$id}: {$cleanResult['error']}"; + WP_CLI::error(sprintf( + "[ERROR] ID %d: %s - %s", + $id, + mb_substr($page->post_title, 0, 40), + $cleanResult['error'] + ), false); + $pagesSkipped++; + continue; + } + + $cleanedContent = $cleanResult['content']; + $newSize = strlen($cleanedContent); + + // Contar shortcodes protegidos después + $protectedAfter = $this->countProtectedShortcodes($cleanedContent); + + // VALIDACIÓN CRÍTICA: Verificar shortcodes protegidos + if ($protectedAfter < $protectedBefore) { + $errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})"; + WP_CLI::error(sprintf( + "[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)", + $id, + $protectedBefore, + $protectedAfter + ), false); + $pagesSkipped++; + continue; + } + + // Verificar pérdida excesiva de contenido + $lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0; + if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) { + $errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)"; + WP_CLI::error(sprintf( + "[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)", + $id, + $lossPercent, + self::MAX_CONTENT_LOSS_PERCENT + ), false); + $pagesSkipped++; + continue; + } + + // Verificar si hubo cambios + $hasChanges = $originalContent !== $cleanedContent; + $bytesSaved = $originalSize - $newSize; + + // Contar paginación removida + $paginationRemoved = ( + strpos($originalContent, 'tcb_pagination_current_page') !== false && + strpos($cleanedContent, 'tcb_pagination_current_page') === false + ) ? 1 : 0; + + if ($hasChanges) { + $pagesModified++; + $totalH2Removed += $h2Count; + $totalPaginationRemoved += $paginationRemoved; + $totalBytesFreed += $bytesSaved; + + $status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]'; + WP_CLI::log(sprintf( + "%s ID %d: %s", + $status, + $id, + mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : '') + )); + WP_CLI::log(sprintf( + " → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%", + $h2Count, + $paginationRemoved ? 'Sí' : 'No', + $lossPercent + )); + WP_CLI::log(sprintf( + " → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s", + $protectedAfter, + $this->formatBytes($bytesSaved) + )); + + if (!$dryRun && $force) { + wp_update_post([ + 'ID' => $id, + 'post_content' => $cleanedContent + ]); + } + } else { + WP_CLI::log(sprintf( + "[SIN CAMBIOS] ID %d: %s", + $id, + mb_substr($page->post_title, 0, 50) + )); + } + } + + WP_CLI::log(''); + WP_CLI::log('════════════════════════════════════════════════════════════════════'); + WP_CLI::log('RESUMEN:'); + WP_CLI::log(sprintf(' Páginas modificadas: %d', $pagesModified)); + WP_CLI::log(sprintf(' Páginas omitidas: %d', $pagesSkipped)); + WP_CLI::log(sprintf(' Total H2 eliminados: %d', $totalH2Removed)); + WP_CLI::log(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved)); + WP_CLI::log(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed))); + WP_CLI::log('════════════════════════════════════════════════════════════════════'); + + if (count($errors) > 0) { + WP_CLI::log(''); + WP_CLI::warning('ERRORES ENCONTRADOS:'); + foreach ($errors as $error) { + WP_CLI::log(" - {$error}"); + } + } + + if ($dryRun && $pagesModified > 0 && count($errors) === 0) { + WP_CLI::log(''); + WP_CLI::success('Dry-run completado SIN errores.'); + WP_CLI::log(''); + WP_CLI::warning('Para ejecutar la limpieza real:'); + WP_CLI::log(' wp roi-theme clean_thrive --force'); + } elseif (!$dryRun && $force && $pagesModified > 0) { + WP_CLI::log(''); + WP_CLI::success('Limpieza completada exitosamente.'); + WP_CLI::log(''); + WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.'); + } + } + + /** + * Limpia el contenido con validaciones de seguridad + * + * @return array{content: string, error: string|null} + */ + private function cleanContentSafely(string $content): array + { + $originalContent = $content; + + // 1. Eliminar H2 con data-shortcode="tcb_post_title" + $result = preg_replace( + '/]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s', + '', + $content + ); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2']; + } + $content = $result; + + // 2. Eliminar paginación Thrive rota + $result = preg_replace( + '/]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s', + '', + $content + ); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación']; + } + $content = $result; + + // 3. Eliminar botones de paginación Thrive + $result = preg_replace( + '/]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s', + '', + $content + ); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones']; + } + $content = $result; + + // 4. Eliminar shortcodes Thrive huérfanos + $content = str_replace('[tcb_pagination_current_page]', '', $content); + $content = str_replace('[tcb_pagination_total_pages]', '', $content); + + // 5. Limpiar múltiples líneas vacías (con validación) + $result = preg_replace('/(\r?\n){3,}/', "\n\n", $content); + if ($result === null) { + return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas']; + } + $content = $result; + + // 6. Trim + $content = trim($content); + + // Validación final: no retornar vacío si original tenía contenido + if (empty($content) && !empty($originalContent)) { + return ['content' => $originalContent, 'error' => 'El contenido quedó vacío']; + } + + return ['content' => $content, 'error' => null]; + } + + /** + * Cuenta shortcodes protegidos en el contenido + */ + private function countProtectedShortcodes(string $content): int + { + $count = 0; + foreach (self::PROTECTED_SHORTCODES as $shortcode) { + $count += substr_count($content, $shortcode); + } + return $count; + } + + /** + * Formatea bytes a formato legible + */ + private function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . ' B'; + } elseif ($bytes < 1048576) { + return round($bytes / 1024, 1) . ' KB'; + } else { + return round($bytes / 1048576, 2) . ' MB'; + } + } +} diff --git a/Shared/Infrastructure/Di/DIContainer.php b/Shared/Infrastructure/Di/DIContainer.php index 8cf122f5..77054ccb 100644 --- a/Shared/Infrastructure/Di/DIContainer.php +++ b/Shared/Infrastructure/Di/DIContainer.php @@ -22,6 +22,12 @@ use ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector; use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase; use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase; use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer; +use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface; +use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface; +use ROITheme\Shared\Infrastructure\Services\WordPressPageTypeDetector; +use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressPageVisibilityRepository; +use ROITheme\Shared\Application\UseCases\EvaluatePageVisibility\EvaluatePageVisibilityUseCase; +use ROITheme\Shared\Infrastructure\Services\MigratePageVisibilityService; /** * DIContainer - Contenedor de Inyección de Dependencias @@ -46,10 +52,38 @@ final class DIContainer { private array $instances = []; + /** + * Instancia singleton del contenedor + * @var self|null + */ + private static ?self $instance = null; + + /** + * Obtiene la instancia singleton del contenedor + * + * NOTA: Se debe haber creado una instancia previamente en functions.php + * El constructor registra automáticamente la instancia. + * + * @return self + * @throws \RuntimeException Si no se ha inicializado el contenedor + */ + public static function getInstance(): self + { + if (self::$instance === null) { + throw new \RuntimeException( + 'DIContainer no ha sido inicializado. Asegúrate de que functions.php se haya ejecutado primero.' + ); + } + return self::$instance; + } + public function __construct( private \wpdb $wpdb, private string $schemasPath - ) {} + ) { + // Registrar como instancia singleton + self::$instance = $this; + } /** * Obtener repositorio de componentes @@ -272,4 +306,61 @@ final class DIContainer return $this->instances['criticalCSSCollector']; } + + // =============================== + // Page Visibility System + // =============================== + + /** + * Obtiene el repositorio de visibilidad de página + * + * IMPORTANTE: Inyecta $wpdb para consistencia con el resto del código + * (WordPressComponentSettingsRepository también recibe $wpdb por constructor) + */ + public function getPageVisibilityRepository(): PageVisibilityRepositoryInterface + { + if (!isset($this->instances['pageVisibilityRepository'])) { + // Inyectar $wpdb siguiendo el patrón existente + $this->instances['pageVisibilityRepository'] = new WordPressPageVisibilityRepository($this->wpdb); + } + return $this->instances['pageVisibilityRepository']; + } + + /** + * Obtiene el detector de tipo de página + */ + public function getPageTypeDetector(): PageTypeDetectorInterface + { + if (!isset($this->instances['pageTypeDetector'])) { + $this->instances['pageTypeDetector'] = new WordPressPageTypeDetector(); + } + return $this->instances['pageTypeDetector']; + } + + /** + * Obtiene el caso de uso de evaluación de visibilidad + */ + public function getEvaluatePageVisibilityUseCase(): EvaluatePageVisibilityUseCase + { + if (!isset($this->instances['evaluatePageVisibilityUseCase'])) { + $this->instances['evaluatePageVisibilityUseCase'] = new EvaluatePageVisibilityUseCase( + $this->getPageTypeDetector(), + $this->getPageVisibilityRepository() + ); + } + return $this->instances['evaluatePageVisibilityUseCase']; + } + + /** + * Obtiene el servicio de migración de visibilidad + */ + public function getMigratePageVisibilityService(): MigratePageVisibilityService + { + if (!isset($this->instances['migratePageVisibilityService'])) { + $this->instances['migratePageVisibilityService'] = new MigratePageVisibilityService( + $this->getPageVisibilityRepository() + ); + } + return $this->instances['migratePageVisibilityService']; + } } diff --git a/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php b/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php new file mode 100644 index 00000000..420ab1d0 --- /dev/null +++ b/Shared/Infrastructure/Persistence/WordPress/WordPressPageVisibilityRepository.php @@ -0,0 +1,146 @@ +wpdb->prefix . self::TABLE_SUFFIX; + + $results = $this->wpdb->get_results( + $this->wpdb->prepare( + "SELECT attribute_name, attribute_value + FROM {$table} + WHERE component_name = %s + AND group_name = %s", + $componentName, + self::GROUP_NAME + ), + ARRAY_A + ); + + if (empty($results)) { + return []; + } + + $config = []; + foreach ($results as $row) { + $config[$row['attribute_name']] = $row['attribute_value'] === '1'; + } + + return $config; + } + + public function saveVisibilityConfig(string $componentName, array $config): void + { + $table = $this->wpdb->prefix . self::TABLE_SUFFIX; + + foreach ($config as $field => $enabled) { + if (!in_array($field, self::VISIBILITY_FIELDS, true)) { + continue; + } + + $exists = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT COUNT(*) FROM {$table} + WHERE component_name = %s + AND group_name = %s + AND attribute_name = %s", + $componentName, + self::GROUP_NAME, + $field + )); + + $value = $enabled ? '1' : '0'; + + if ($exists) { + $this->wpdb->update( + $table, + [ + 'attribute_value' => $value, + 'updated_at' => current_time('mysql'), + ], + [ + 'component_name' => $componentName, + 'group_name' => self::GROUP_NAME, + 'attribute_name' => $field, + ] + ); + } else { + $this->wpdb->insert($table, [ + 'component_name' => $componentName, + 'group_name' => self::GROUP_NAME, + 'attribute_name' => $field, + 'attribute_value' => $value, + 'is_editable' => 1, + 'created_at' => current_time('mysql'), + 'updated_at' => current_time('mysql'), + ]); + } + } + } + + public function hasVisibilityConfig(string $componentName): bool + { + $table = $this->wpdb->prefix . self::TABLE_SUFFIX; + + $count = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT COUNT(*) FROM {$table} + WHERE component_name = %s + AND group_name = %s", + $componentName, + self::GROUP_NAME + )); + + return (int) $count > 0; + } + + public function getAllComponentNames(): array + { + $table = $this->wpdb->prefix . self::TABLE_SUFFIX; + + $results = $this->wpdb->get_col( + "SELECT DISTINCT component_name FROM {$table} ORDER BY component_name" + ); + + return $results ?: []; + } + + public function createDefaultVisibility(string $componentName, array $defaults): void + { + if ($this->hasVisibilityConfig($componentName)) { + return; + } + + $this->saveVisibilityConfig($componentName, $defaults); + } +} diff --git a/Shared/Infrastructure/Services/MigratePageVisibilityService.php b/Shared/Infrastructure/Services/MigratePageVisibilityService.php new file mode 100644 index 00000000..c86db621 --- /dev/null +++ b/Shared/Infrastructure/Services/MigratePageVisibilityService.php @@ -0,0 +1,53 @@ +visibilityRepository->getAllComponentNames(); + + foreach ($components as $componentName) { + if ($this->visibilityRepository->hasVisibilityConfig($componentName)) { + $skipped++; + continue; + } + + // Usar constante compartida (DRY) + $this->visibilityRepository->createDefaultVisibility( + $componentName, + VisibilityDefaults::DEFAULT_VISIBILITY + ); + $created++; + } + + return [ + 'created' => $created, + 'skipped' => $skipped, + ]; + } +} diff --git a/Shared/Infrastructure/Services/PageVisibilityHelper.php b/Shared/Infrastructure/Services/PageVisibilityHelper.php new file mode 100644 index 00000000..ba23de84 --- /dev/null +++ b/Shared/Infrastructure/Services/PageVisibilityHelper.php @@ -0,0 +1,39 @@ +getEvaluatePageVisibilityUseCase(); + + return $useCase->execute($componentName); + } +} diff --git a/Shared/Infrastructure/Services/WordPressPageTypeDetector.php b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php new file mode 100644 index 00000000..4d7eeb09 --- /dev/null +++ b/Shared/Infrastructure/Services/WordPressPageTypeDetector.php @@ -0,0 +1,65 @@ +isHome()) { + return PageType::home(); + } + + if ($this->isPost()) { + return PageType::post(); + } + + if ($this->isPage()) { + return PageType::page(); + } + + if ($this->isSearch()) { + return PageType::search(); + } + + if ($this->isArchive()) { + return PageType::archive(); + } + + return PageType::fromString(PageType::UNKNOWN); + } + + public function isHome(): bool + { + return is_front_page(); + } + + public function isPost(): bool + { + return is_single() && !is_front_page(); + } + + public function isPage(): bool + { + return is_page() && !is_front_page(); + } + + public function isArchive(): bool + { + return is_archive(); + } + + public function isSearch(): bool + { + return is_search(); + } +} diff --git a/front-page.php b/front-page.php index 7cb6b59c..e0c96ac6 100644 --- a/front-page.php +++ b/front-page.php @@ -2,7 +2,8 @@ /** * The template for displaying the static front page * - * Structure replicates template index.html lines 322-345 (Hero Section) + * Replica la estructura de single.php para consistencia visual. + * Grid layout: col-lg-9 (contenido) + col-lg-3 (sidebar) * * @link https://developer.wordpress.org/themes/basics/template-hierarchy/#front-page * @@ -13,118 +14,105 @@ get_header(); ?> - -
-
+ + +
+ + + + + +
+
+ + +
+ + 0 ) : - ?> -
-
- = 3 ) break; - if ( $category->slug === 'uncategorized' ) continue; - ?> - - - name ); ?> - - -
-
- - - -

- -

- -
-
- -
- - -
- - - -
> - - -
- '', - ) - ); - ?> -
- - - -
- "%s"', 'roi-theme' ), - array( - 'span' => array( - 'class' => array(), - ), - ) - ), - get_the_title() - ), - '', - '' - ); - ?> -
- - -
- - -
+ +
> + + wp_link_pages(array( + 'before' => '', + )); + ?> +
+ + + + + + + + + + + + + +
+ + +
+ +
+ +
+
+ + + + + + + prefix . 'roi_theme_component_settings'; + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT group_name, attribute_name, attribute_value + FROM {$table} + WHERE component_name = %s", + 'adsense-placement' + )); + + if (empty($rows)) { + return ['enabled' => false]; + } + + // Organizar en array asociativo por grupo/atributo + $settings = []; + foreach ($rows as $row) { + if (!isset($settings[$row->group_name])) { + $settings[$row->group_name] = []; + } + // Decodificar valor + $value = $row->attribute_value; + if ($value === '1') $value = true; + elseif ($value === '0') $value = false; + else { + $decoded = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $value = $decoded; + } + } + $settings[$row->group_name][$row->attribute_name] = $value; + } + + // Helper para obtener valor con default + $get = function(string $group, string $attr, $default = null) use ($settings) { + return $settings[$group][$attr] ?? $default; + }; + + // ========================================================================= + // VALIDAR CONDICIONES GLOBALES + // ========================================================================= + + // AdSense global deshabilitado + if ($get('visibility', 'is_enabled', false) !== true) { + return ['enabled' => false]; + } + + // Ads en busqueda deshabilitados + if ($get('search_results', 'search_ads_enabled', false) !== true) { + return ['enabled' => false]; + } + + // Publisher ID vacio + $publisherId = $get('content', 'publisher_id', ''); + if (empty($publisherId)) { + return ['enabled' => false]; + } + + // ========================================================================= + // VALIDAR EXCLUSIONES (igual que el resto del sistema) + // ========================================================================= + + // Ocultar para usuarios logueados + if ($get('visibility', 'hide_for_logged_in', false) === true && is_user_logged_in()) { + return ['enabled' => false]; + } + + // Visibilidad por dispositivo + $isMobile = wp_is_mobile(); + if ($isMobile && $get('visibility', 'show_on_mobile', true) !== true) { + return ['enabled' => false]; + } + if (!$isMobile && $get('visibility', 'show_on_desktop', true) !== true) { + return ['enabled' => false]; + } + + // ========================================================================= + // CONSTRUIR CONFIGURACION + // ========================================================================= + return [ + 'enabled' => true, + 'publisherId' => $publisherId, + 'slots' => [ + 'auto' => $get('content', 'slot_auto', ''), + 'inArticle' => $get('content', 'slot_inarticle', ''), + 'autorelaxed' => $get('content', 'slot_autorelaxed', ''), + 'display' => $get('content', 'slot_display', ''), + ], + 'topAd' => [ + 'enabled' => $get('search_results', 'search_top_ad_enabled', true) === true, + 'format' => $get('search_results', 'search_top_ad_format', 'auto'), + ], + 'betweenAds' => [ + 'enabled' => $get('search_results', 'search_between_enabled', true) === true, + 'max' => min(3, max(1, (int) $get('search_results', 'search_between_max', '1'))), + 'format' => $get('search_results', 'search_between_format', 'in-article'), + 'position' => $get('search_results', 'search_between_position', 'random'), + 'every' => (int) $get('search_results', 'search_between_every', '5'), + ], + 'delay' => [ + 'enabled' => $get('forms', 'delay_enabled', true) === true, + 'timeout' => (int) $get('forms', 'delay_timeout', '5000'), + ], + ]; +} diff --git a/minify-css.php b/minify-css.php new file mode 100644 index 00000000..18996c02 --- /dev/null +++ b/minify-css.php @@ -0,0 +1,60 @@ ++~])\s*/', '$1', $css); + + // Remove last semicolon before closing brace + $css = str_replace(';}', '}', $css); + + // Trim + $css = trim($css); + + return $css; +} + +$files = [ + 'Assets/Css/css-global-accessibility.css' => 'Assets/Css/css-global-accessibility.min.css', + 'Assets/Css/style.css' => 'Assets/Css/style.min.css', +]; + +$base_path = __DIR__ . '/'; + +foreach ($files as $source => $dest) { + $source_path = $base_path . $source; + $dest_path = $base_path . $dest; + + if (file_exists($source_path)) { + $css = file_get_contents($source_path); + $minified = minify_css($css); + + file_put_contents($dest_path, $minified); + + $original_size = strlen($css); + $minified_size = strlen($minified); + $savings = $original_size - $minified_size; + $percent = round(($savings / $original_size) * 100, 1); + + echo "Minified: $source\n"; + echo " Original: " . number_format($original_size) . " bytes\n"; + echo " Minified: " . number_format($minified_size) . " bytes\n"; + echo " Savings: " . number_format($savings) . " bytes ($percent%)\n\n"; + } else { + echo "File not found: $source\n"; + } +} + +echo "Done!\n";