Compare commits
10 Commits
ce66eeba6d
...
f4b45b7e17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4b45b7e17 | ||
|
|
c28fedd6e7 | ||
|
|
14138e7762 | ||
|
|
8735962f52 | ||
|
|
7fb5eda108 | ||
|
|
4cdc4db397 | ||
|
|
c732b5af05 | ||
|
|
29a69617e4 | ||
|
|
9e37ea93eb | ||
|
|
7472dbad11 |
@@ -95,6 +95,16 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
|||||||
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
|
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
|
||||||
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
|
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
|
||||||
'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
|
'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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ final class AdsensePlacementFormBuilder
|
|||||||
$html .= $this->buildRailAdsGroup($componentId);
|
$html .= $this->buildRailAdsGroup($componentId);
|
||||||
$html .= $this->buildAnchorAdsGroup($componentId);
|
$html .= $this->buildAnchorAdsGroup($componentId);
|
||||||
$html .= $this->buildVignetteAdsGroup($componentId);
|
$html .= $this->buildVignetteAdsGroup($componentId);
|
||||||
|
$html .= $this->buildSearchResultsGroup($componentId);
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
@@ -708,6 +709,101 @@ final class AdsensePlacementFormBuilder
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seccion para anuncios en resultados de busqueda (ROI APU Search)
|
||||||
|
*/
|
||||||
|
private function buildSearchResultsGroup(string $cid): string
|
||||||
|
{
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #fd7e14;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-search me-2" style="color: #fd7e14;"></i>';
|
||||||
|
$html .= ' Resultados de Busqueda';
|
||||||
|
$html .= ' <span class="badge bg-secondary ms-2">ROI APU Search</span>';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
$html .= ' <p class="small text-muted mb-3">Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.</p>';
|
||||||
|
|
||||||
|
// 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 .= '<div class="border rounded p-3 mb-3" style="background: #fff8f0;">';
|
||||||
|
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||||
|
$html .= ' <span class="badge" style="background: #fd7e14;">ANUNCIO SUPERIOR</span>';
|
||||||
|
$html .= ' <small class="text-muted">Debajo del campo de busqueda</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
|
||||||
|
$html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$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 .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Anuncios entre resultados
|
||||||
|
$html .= '<div class="border rounded p-3" style="background: #fff8f0;">';
|
||||||
|
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||||
|
$html .= ' <span class="badge" style="background: #fd7e14;">ENTRE RESULTADOS</span>';
|
||||||
|
$html .= ' <small class="text-muted">Intercalados con los resultados</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<div class="row g-2 mb-2">';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
|
||||||
|
$html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$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 .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<div class="row g-2 mb-2">';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$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 .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$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 .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$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 .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
private function buildExclusionsGroup(string $cid): string
|
private function buildExclusionsGroup(string $cid): string
|
||||||
{
|
{
|
||||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ final class ContactFormFieldMapper implements FieldMapperInterface
|
|||||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para Contact Form
|
* FormBuilder para Contact Form
|
||||||
@@ -93,19 +94,47 @@ final class ContactFormFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
// =============================================
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
// =============================================
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' </label>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'contactForm');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -598,4 +627,26 @@ final class ContactFormFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,19 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
|||||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'ctaExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'ctaExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'ctaExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'ctaExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para el CTA Box Sidebar
|
* FormBuilder para el CTA Box Sidebar
|
||||||
@@ -94,20 +95,49 @@ final class CtaBoxSidebarFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
// show_on_pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// =============================================
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' </label>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
|
||||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
// Obtener valores de _page_visibility (grupo especial)
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'cta');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -515,4 +545,29 @@ final class CtaBoxSidebarFormBuilder
|
|||||||
|
|
||||||
return $html;
|
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 = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
|||||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'letsTalkExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'letsTalkExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'letsTalkExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'letsTalkExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CtaLetsTalkFormBuilder
|
* Class CtaLetsTalkFormBuilder
|
||||||
@@ -120,18 +121,47 @@ final class CtaLetsTalkFormBuilder
|
|||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
// Select: Show on Pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-0">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
// =============================================
|
||||||
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' </select>';
|
|
||||||
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'letsTalk');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -447,4 +477,26 @@ final class CtaLetsTalkFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ final class CtaPostFieldMapper implements FieldMapperInterface
|
|||||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'ctaPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'ctaPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'ctaPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'ctaPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para CTA Post
|
* FormBuilder para CTA Post
|
||||||
@@ -85,19 +86,47 @@ final class CtaPostFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// =============================================
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
// =============================================
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' </label>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'ctaPost');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -437,4 +466,26 @@ final class CtaPostFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface
|
|||||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
final class FeaturedImageFormBuilder
|
final class FeaturedImageFormBuilder
|
||||||
{
|
{
|
||||||
@@ -100,25 +101,75 @@ final class FeaturedImageFormBuilder
|
|||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// =============================================
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
// =============================================
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' </label>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'featuredImage');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
private function buildContentGroup(string $componentId): string
|
private function buildContentGroup(string $componentId): string
|
||||||
{
|
{
|
||||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ final class FooterFieldMapper implements FieldMapperInterface
|
|||||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
|
|
||||||
|
// Page Visibility (grupo especial _page_visibility)
|
||||||
|
'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||||
|
'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||||
|
'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||||
|
'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||||
|
'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Widget 1
|
// Widget 1
|
||||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para Footer
|
* FormBuilder para Footer
|
||||||
@@ -90,6 +91,47 @@ final class FooterFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Checkboxes de visibilidad por tipo de página
|
||||||
|
// Grupo especial: _page_visibility
|
||||||
|
// =============================================
|
||||||
|
$html .= ' <hr class="my-3">';
|
||||||
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
|
$html .= ' </p>';
|
||||||
|
|
||||||
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
|
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', 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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'footer');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -410,4 +452,19 @@ final class FooterFormBuilder
|
|||||||
}
|
}
|
||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||||
|
{
|
||||||
|
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||||
|
|
||||||
|
$html = '<div class="form-check">';
|
||||||
|
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||||
|
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||||
|
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' ' . esc_html($label);
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,21 @@ final class HeroFieldMapper implements FieldMapperInterface
|
|||||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
|
||||||
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||||
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
final class HeroFormBuilder
|
final class HeroFormBuilder
|
||||||
{
|
{
|
||||||
@@ -102,20 +103,47 @@ final class HeroFormBuilder
|
|||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// =============================================
|
||||||
$html .= ' <div class="mb-2 mt-3">';
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
// =============================================
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' </label>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
|
||||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'hero');
|
||||||
|
|
||||||
// Switch: CSS Crítico
|
// Switch: CSS Crítico
|
||||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
$html .= ' <div class="mb-0 mt-3">';
|
||||||
@@ -427,4 +455,26 @@ final class HeroFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,15 @@ final class AdminAssetEnqueuer
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Script de toggle para exclusiones (Plan 99.11)
|
||||||
|
wp_enqueue_script(
|
||||||
|
'roi-exclusion-toggle',
|
||||||
|
$this->themeUri . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js',
|
||||||
|
['roi-admin-dashboard'],
|
||||||
|
filemtime(get_template_directory() . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Pasar variables al JavaScript
|
// Pasar variables al JavaScript
|
||||||
wp_localize_script(
|
wp_localize_script(
|
||||||
'roi-admin-dashboard',
|
'roi-admin-dashboard',
|
||||||
|
|||||||
@@ -26,10 +26,22 @@ final class NavbarFieldMapper implements FieldMapperInterface
|
|||||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
|
||||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||||
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||||
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
final class NavbarFormBuilder
|
final class NavbarFormBuilder
|
||||||
{
|
{
|
||||||
@@ -105,18 +106,47 @@ final class NavbarFormBuilder
|
|||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
// Select: Show on Pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-2">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
// =============================================
|
||||||
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' </select>';
|
|
||||||
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'navbar');
|
||||||
|
|
||||||
// Switch: Sticky
|
// Switch: Sticky
|
||||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||||
$html .= ' <div class="mb-2">';
|
$html .= ' <div class="mb-2">';
|
||||||
@@ -527,4 +557,26 @@ final class NavbarFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,19 @@ final class RelatedPostFieldMapper implements FieldMapperInterface
|
|||||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para Related Posts
|
* FormBuilder para Related Posts
|
||||||
@@ -86,19 +87,47 @@ final class RelatedPostFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// =============================================
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
// =============================================
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' </label>';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'relatedPost');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -498,4 +527,26 @@ final class RelatedPostFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace ROITheme\Admin\Shared\Infrastructure\Api\WordPress;
|
|||||||
|
|
||||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Services\ExclusionFieldProcessor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler para peticiones AJAX del panel de administracion
|
* Handler para peticiones AJAX del panel de administracion
|
||||||
@@ -73,10 +74,16 @@ final class AdminAjaxHandler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapea settings de field IDs a grupos/atributos
|
* Mapea settings de field IDs a grupos/atributos
|
||||||
|
*
|
||||||
|
* Soporta tipos especiales para campos de exclusion:
|
||||||
|
* - json_array: Convierte "a, b, c" a ["a", "b", "c"]
|
||||||
|
* - json_array_int: Convierte "1, 2, 3" a [1, 2, 3]
|
||||||
|
* - json_array_lines: Convierte lineas a array
|
||||||
*/
|
*/
|
||||||
private function mapSettings(array $settings, array $fieldMapping): array
|
private function mapSettings(array $settings, array $fieldMapping): array
|
||||||
{
|
{
|
||||||
$mappedSettings = [];
|
$mappedSettings = [];
|
||||||
|
$fieldProcessor = new ExclusionFieldProcessor();
|
||||||
|
|
||||||
foreach ($settings as $fieldId => $value) {
|
foreach ($settings as $fieldId => $value) {
|
||||||
if (!isset($fieldMapping[$fieldId])) {
|
if (!isset($fieldMapping[$fieldId])) {
|
||||||
@@ -86,11 +93,17 @@ final class AdminAjaxHandler
|
|||||||
$mapping = $fieldMapping[$fieldId];
|
$mapping = $fieldMapping[$fieldId];
|
||||||
$groupName = $mapping['group'];
|
$groupName = $mapping['group'];
|
||||||
$attributeName = $mapping['attribute'];
|
$attributeName = $mapping['attribute'];
|
||||||
|
$type = $mapping['type'] ?? null;
|
||||||
|
|
||||||
if (!isset($mappedSettings[$groupName])) {
|
if (!isset($mappedSettings[$groupName])) {
|
||||||
$mappedSettings[$groupName] = [];
|
$mappedSettings[$groupName] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Procesar valor segun tipo
|
||||||
|
if ($type !== null && is_string($value)) {
|
||||||
|
$value = $fieldProcessor->process($value, $type);
|
||||||
|
}
|
||||||
|
|
||||||
$mappedSettings[$groupName][$attributeName] = $value;
|
$mappedSettings[$groupName][$attributeName] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servicio para procesar campos de exclusion antes de guardar en BD
|
||||||
|
*
|
||||||
|
* Convierte formatos de UI a JSON para almacenamiento.
|
||||||
|
*
|
||||||
|
* v1.1: Extraido de AdminAjaxHandler (SRP)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Admin\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class ExclusionFieldProcessor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Procesa un valor de campo de exclusion segun su tipo
|
||||||
|
*
|
||||||
|
* @param string $value Valor del campo (desde UI)
|
||||||
|
* @param string $type Tipo de campo: json_array, json_array_int, json_array_lines
|
||||||
|
* @return string JSON string para almacenar en BD
|
||||||
|
*/
|
||||||
|
public function process(string $value, string $type): string
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
'json_array' => $this->processJsonArray($value),
|
||||||
|
'json_array_int' => $this->processJsonArrayInt($value),
|
||||||
|
'json_array_lines' => $this->processJsonArrayLines($value),
|
||||||
|
default => $value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "a, b, c" -> ["a", "b", "c"]
|
||||||
|
*/
|
||||||
|
private function processJsonArray(string $value): string
|
||||||
|
{
|
||||||
|
$items = array_map('trim', explode(',', $value));
|
||||||
|
$items = array_filter($items, fn($item) => $item !== '');
|
||||||
|
return json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "1, 2, 3" -> [1, 2, 3]
|
||||||
|
*/
|
||||||
|
private function processJsonArrayInt(string $value): string
|
||||||
|
{
|
||||||
|
$items = array_map('trim', explode(',', $value));
|
||||||
|
$items = array_filter($items, 'is_numeric');
|
||||||
|
$items = array_map('intval', $items);
|
||||||
|
return json_encode(array_values($items));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lineas separadas -> array
|
||||||
|
*/
|
||||||
|
private function processJsonArrayLines(string $value): string
|
||||||
|
{
|
||||||
|
$items = preg_split('/\r\n|\r|\n/', $value);
|
||||||
|
$items = array_map('trim', $items);
|
||||||
|
$items = array_filter($items, fn($item) => $item !== '');
|
||||||
|
return json_encode(array_values($items), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Toggle para mostrar/ocultar reglas de exclusion en FormBuilders
|
||||||
|
*
|
||||||
|
* Escucha cambios en checkboxes con ID que termine en "ExclusionsEnabled"
|
||||||
|
* y muestra/oculta el contenedor de reglas correspondiente.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Admin
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function initExclusionToggles() {
|
||||||
|
document.querySelectorAll('[id$="ExclusionsEnabled"]').forEach(function(checkbox) {
|
||||||
|
// Handler para cambios
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
const prefix = this.id.replace('ExclusionsEnabled', '');
|
||||||
|
const rulesContainer = document.getElementById(prefix + 'ExclusionRules');
|
||||||
|
if (rulesContainer) {
|
||||||
|
rulesContainer.style.display = this.checked ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar cuando DOM este listo
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initExclusionToggles);
|
||||||
|
} else {
|
||||||
|
initExclusionToggles();
|
||||||
|
}
|
||||||
|
})();
|
||||||
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||||
|
|
||||||
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Componente UI parcial reutilizable para reglas de exclusion
|
||||||
|
*
|
||||||
|
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
||||||
|
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
||||||
|
*
|
||||||
|
* Uso en FormBuilder:
|
||||||
|
* ```php
|
||||||
|
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
||||||
|
*/
|
||||||
|
final class ExclusionFormPartial
|
||||||
|
{
|
||||||
|
private const GROUP_NAME = '_exclusions';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly AdminDashboardRenderer $renderer
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderiza la seccion de exclusiones
|
||||||
|
*
|
||||||
|
* @param string $componentId ID del componente (kebab-case)
|
||||||
|
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
||||||
|
* @return string HTML de la seccion
|
||||||
|
*/
|
||||||
|
public function render(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
$html .= $this->buildExclusionHeader();
|
||||||
|
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
||||||
|
$html .= $this->buildExclusionRules($componentId, $prefix);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExclusionHeader(): string
|
||||||
|
{
|
||||||
|
$html = '<hr class="my-3">';
|
||||||
|
$html .= '<p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Reglas de exclusion avanzadas';
|
||||||
|
$html .= '</p>';
|
||||||
|
$html .= '<p class="small text-muted mb-2">';
|
||||||
|
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
||||||
|
$html .= '</p>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExclusionToggle(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$enabled = $this->renderer->getFieldValue(
|
||||||
|
$componentId,
|
||||||
|
self::GROUP_NAME,
|
||||||
|
'exclusions_enabled',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$checked = $this->toBool($enabled);
|
||||||
|
|
||||||
|
$id = $prefix . 'ExclusionsEnabled';
|
||||||
|
|
||||||
|
$html = '<div class="mb-3">';
|
||||||
|
$html .= ' <div class="form-check form-switch">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExclusionRules(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$enabled = $this->renderer->getFieldValue(
|
||||||
|
$componentId,
|
||||||
|
self::GROUP_NAME,
|
||||||
|
'exclusions_enabled',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$display = $this->toBool($enabled) ? 'block' : 'none';
|
||||||
|
|
||||||
|
$html = sprintf(
|
||||||
|
'<div id="%sExclusionRules" style="display: %s;">',
|
||||||
|
esc_attr($prefix),
|
||||||
|
$display
|
||||||
|
);
|
||||||
|
|
||||||
|
$html .= $this->buildCategoryField($componentId, $prefix);
|
||||||
|
$html .= $this->buildPostIdsField($componentId, $prefix);
|
||||||
|
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCategoryField(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$value = $this->renderer->getFieldValue(
|
||||||
|
$componentId,
|
||||||
|
self::GROUP_NAME,
|
||||||
|
'exclude_categories',
|
||||||
|
'[]'
|
||||||
|
);
|
||||||
|
$categories = $this->jsonToCommaList($value);
|
||||||
|
|
||||||
|
$id = $prefix . 'ExcludeCategories';
|
||||||
|
|
||||||
|
$html = '<div class="mb-3">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Excluir en categorias';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_attr($categories)
|
||||||
|
);
|
||||||
|
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPostIdsField(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$value = $this->renderer->getFieldValue(
|
||||||
|
$componentId,
|
||||||
|
self::GROUP_NAME,
|
||||||
|
'exclude_post_ids',
|
||||||
|
'[]'
|
||||||
|
);
|
||||||
|
$postIds = $this->jsonToCommaList($value);
|
||||||
|
|
||||||
|
$id = $prefix . 'ExcludePostIds';
|
||||||
|
|
||||||
|
$html = '<div class="mb-3">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Excluir en posts/paginas';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_attr($postIds)
|
||||||
|
);
|
||||||
|
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
||||||
|
{
|
||||||
|
$value = $this->renderer->getFieldValue(
|
||||||
|
$componentId,
|
||||||
|
self::GROUP_NAME,
|
||||||
|
'exclude_url_patterns',
|
||||||
|
'[]'
|
||||||
|
);
|
||||||
|
$patterns = $this->jsonToLineList($value);
|
||||||
|
|
||||||
|
$id = $prefix . 'ExcludeUrlPatterns';
|
||||||
|
|
||||||
|
$html = '<div class="mb-0">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||||
|
$html .= ' Excluir por patrones URL';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
||||||
|
esc_attr($id),
|
||||||
|
esc_textarea($patterns)
|
||||||
|
);
|
||||||
|
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte JSON array o array a lista separada por comas
|
||||||
|
*
|
||||||
|
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||||
|
*/
|
||||||
|
private function jsonToCommaList(string|array $value): string
|
||||||
|
{
|
||||||
|
// Si ya es array, usarlo directamente
|
||||||
|
if (is_array($value)) {
|
||||||
|
return empty($value) ? '' : implode(', ', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es string, intentar decodificar JSON
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded) || empty($decoded)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(', ', $decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte JSON array o array a lista separada por lineas
|
||||||
|
*
|
||||||
|
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||||
|
*/
|
||||||
|
private function jsonToLineList(string|array $value): string
|
||||||
|
{
|
||||||
|
// Si ya es array, usarlo directamente
|
||||||
|
if (is_array($value)) {
|
||||||
|
return empty($value) ? '' : implode("\n", $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es string, intentar decodificar JSON
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded) || empty($decoded)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toBool(mixed $value): bool
|
||||||
|
{
|
||||||
|
return $value === true || $value === '1' || $value === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,19 @@ final class SocialShareFieldMapper implements FieldMapperInterface
|
|||||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para Social Share
|
* FormBuilder para Social Share
|
||||||
@@ -94,20 +95,47 @@ final class SocialShareFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
// show_on_pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// =============================================
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' </label>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
|
||||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'socialShare');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -526,4 +554,26 @@ final class SocialShareFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface
|
|||||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FormBuilder para la Tabla de Contenido
|
* FormBuilder para la Tabla de Contenido
|
||||||
@@ -94,20 +95,47 @@ final class TableOfContentsFormBuilder
|
|||||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||||
|
|
||||||
// show_on_pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
|
// =============================================
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' </label>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
|
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'toc');
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
@@ -585,4 +613,26 @@ final class TableOfContentsFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,21 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
|||||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||||
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
|
||||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
'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'],
|
||||||
|
|
||||||
|
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||||
|
'topBarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||||
|
'topBarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
|
'topBarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
|
'topBarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||||
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||||
|
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||||
|
|
||||||
final class TopNotificationBarFormBuilder
|
final class TopNotificationBarFormBuilder
|
||||||
{
|
{
|
||||||
@@ -105,21 +106,47 @@ final class TopNotificationBarFormBuilder
|
|||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
// Select: Show on Pages
|
// =============================================
|
||||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
// Checkboxes de visibilidad por tipo de página
|
||||||
$html .= ' <div class="mb-2 mt-3">';
|
// Grupo especial: _page_visibility
|
||||||
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
// =============================================
|
||||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
$html .= ' <hr class="my-3">';
|
||||||
$html .= ' Mostrar en';
|
$html .= ' <p class="small fw-semibold mb-2">';
|
||||||
$html .= ' </label>';
|
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||||
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
|
$html .= ' Mostrar en tipos de pagina';
|
||||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
$html .= ' </p>';
|
||||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
|
||||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||||
$html .= ' </select>';
|
$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 .= ' <div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-4">';
|
||||||
|
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||||
|
$html .= ' </div>';
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Reglas de exclusion avanzadas
|
||||||
|
// Grupo especial: _exclusions (Plan 99.11)
|
||||||
|
// =============================================
|
||||||
|
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||||
|
$html .= $exclusionPartial->render($componentId, 'topBar');
|
||||||
|
|
||||||
// Switch: CSS Crítico
|
// Switch: CSS Crítico
|
||||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||||
$html .= ' <div class="mb-0 mt-3">';
|
$html .= ' <div class="mb-0 mt-3">';
|
||||||
@@ -319,4 +346,26 @@ final class TopNotificationBarFormBuilder
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||||
|
{
|
||||||
|
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||||
|
|
||||||
|
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||||
|
$html .= sprintf(
|
||||||
|
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||||
|
esc_attr($id),
|
||||||
|
$checked ? 'checked' : ''
|
||||||
|
);
|
||||||
|
$html .= sprintf(
|
||||||
|
' <label class="form-check-label small" for="%s">',
|
||||||
|
esc_attr($id)
|
||||||
|
);
|
||||||
|
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||||
|
$html .= sprintf(' %s', esc_html($label));
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
||||||
*
|
*
|
||||||
* Componentes Bootstrap incluidos:
|
* Componentes Bootstrap incluidos:
|
||||||
* - System Fonts (CERO flash - sin @font-face externos)
|
* - Fonts (@font-face Poppins)
|
||||||
* - Variables CSS (:root)
|
* - Variables CSS (:root)
|
||||||
* - Resets (box-sizing, body)
|
* - Resets (box-sizing, body)
|
||||||
* - Container system
|
* - Container system
|
||||||
@@ -30,29 +30,45 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
SYSTEM FONTS - CERO Flash (sin fuentes externas)
|
CRITICAL FONTS (Poppins - LCP optimization)
|
||||||
|
|
||||||
Usa fuentes nativas del sistema operativo:
|
font-display: swap + preload = fuente carga rapido y siempre se muestra
|
||||||
- macOS/iOS: -apple-system, BlinkMacSystemFont
|
size-adjust: 100.6% = fallback casi identico a Poppins (minimiza CLS)
|
||||||
- Windows: Segoe UI
|
|
||||||
- Android: Roboto
|
|
||||||
- Linux: Ubuntu/Cantarell
|
|
||||||
- Fallback: sans-serif
|
|
||||||
|
|
||||||
VENTAJAS:
|
|
||||||
- 0 KB descarga (fuentes ya instaladas)
|
|
||||||
- 0 flash/parpadeo (disponibles instantaneamente)
|
|
||||||
- Mejor rendimiento LCP/FCP
|
|
||||||
- Familiar para usuarios (fuentes nativas)
|
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins Fallback';
|
||||||
|
src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
|
||||||
|
size-adjust: 106%;
|
||||||
|
ascent-override: 105%;
|
||||||
|
descent-override: 35%;
|
||||||
|
line-gap-override: 10%;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* System Font Stack - CERO flash garantizado */
|
/* Fonts */
|
||||||
--font-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||||
"Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
|
--bs-body-font-family: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
||||||
--font-primary: var(--font-system);
|
|
||||||
--bs-body-font-family: var(--font-system);
|
|
||||||
|
|
||||||
/* Theme Colors (críticos para above-the-fold) */
|
/* Theme Colors (críticos para above-the-fold) */
|
||||||
--color-navy-dark: #0E2337;
|
--color-navy-dark: #0E2337;
|
||||||
@@ -372,7 +388,7 @@ button:focus:not(:focus-visible) {
|
|||||||
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||||
--bs-navbar-toggler-focus-width: 0.25rem;
|
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||||
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||||
position: relative;
|
/* position: controlado por CriticalCSSService según sticky_enabled */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
* Sistema de Tipografías - ROI Theme
|
* Sistema de Tipografías - ROI Theme
|
||||||
*
|
*
|
||||||
* RESPONSABILIDAD: SOLO definición de fuentes y variables tipográficas
|
* RESPONSABILIDAD: SOLO definición de fuentes y variables tipográficas
|
||||||
|
* - Declaraciones @font-face (comentadas - usar Google Fonts)
|
||||||
* - Variables CSS de tipografía (:root)
|
* - Variables CSS de tipografía (:root)
|
||||||
* - Clases utilitarias de fuentes
|
* - Clases utilitarias de fuentes
|
||||||
*
|
*
|
||||||
* NOTA: Usando SYSTEM FONTS para CERO flash/parpadeo
|
|
||||||
* Las fuentes del sistema están disponibles instantáneamente.
|
|
||||||
*
|
|
||||||
* NO debe contener:
|
* NO debe contener:
|
||||||
* - Estilos de body (van en style.css)
|
* - Estilos de body (van en style.css)
|
||||||
* - Estilos de elementos HTML (van en style.css)
|
* - Estilos de elementos HTML (van en style.css)
|
||||||
@@ -18,20 +16,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
SYSTEM FONTS - CERO Flash
|
SYSTEM FONTS (Por defecto - Recomendado)
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Stack de fuentes del sistema - disponibles instantáneamente */
|
/* Stack de fuentes del sistema - Fallback */
|
||||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans',
|
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
|
||||||
|
|
||||||
/* Fuente primaria - System fonts (CERO flash) */
|
/* Fuente primaria - Poppins con fallback ajustado (Fase 4.3 PageSpeed)
|
||||||
--font-primary: var(--font-system);
|
'Poppins Fallback' tiene size-adjust para reducir CLS durante font swap */
|
||||||
|
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||||
|
|
||||||
/* Fuente para encabezados - System fonts */
|
/* Fuente para encabezados - Poppins con fallback ajustado */
|
||||||
--font-headings: var(--font-system);
|
--font-headings: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||||
|
|
||||||
/* Fuente para código (monospace) */
|
/* Fuente para código (monospace) */
|
||||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
||||||
@@ -48,22 +46,70 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
POPPINS - DESHABILITADO
|
POPPINS (Self-hosted)
|
||||||
============================================
|
============================================
|
||||||
|
|
||||||
Las @font-face de Poppins fueron eliminadas para
|
Fuentes Poppins alojadas localmente para:
|
||||||
garantizar CERO flash/parpadeo en la carga de página.
|
- Eliminar dependencia de Google Fonts
|
||||||
|
- Mejorar rendimiento (sin requests externos)
|
||||||
|
- Cumplimiento GDPR (sin tracking de Google)
|
||||||
|
|
||||||
El sitio ahora usa fuentes del sistema (--font-system)
|
Pesos incluidos: 400, 500, 600, 700
|
||||||
que están disponibles instantáneamente en todos los
|
Formato: WOFF2 (mejor compresión)
|
||||||
dispositivos sin necesidad de descarga.
|
|
||||||
|
|
||||||
Para reactivar Poppins en el futuro, descomentar las
|
Fase 4.3 PageSpeed: Fallback con size-adjust para reducir CLS
|
||||||
declaraciones @font-face y actualizar las variables
|
- size-adjust: 100.6% ajustado para coincidir mejor con Poppins
|
||||||
--font-primary y --font-headings.
|
- font-display: swap + preload = carga rapida sin salto visual
|
||||||
|
- Preload en CriticalCSSInjector P:-2 acelera descarga de fuentes
|
||||||
|
|
||||||
|
NOTA: El valor 100.6% fue calibrado empiricamente.
|
||||||
|
- 106% causaba un salto visual notable (navbar se "achicaba")
|
||||||
|
- 100.6% minimiza el CLS manteniendo legibilidad del fallback
|
||||||
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
|
/* Fallback font con metricas ajustadas para Poppins */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins Fallback';
|
||||||
|
src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('sans-serif');
|
||||||
|
size-adjust: 106%;
|
||||||
|
ascent-override: 105%;
|
||||||
|
descent-override: 35%;
|
||||||
|
line-gap-override: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
UTILIDADES DE FUENTES
|
UTILIDADES DE FUENTES
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -182,10 +182,72 @@
|
|||||||
}, CONFIG.timeout);
|
}, 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
|
* Inicializa el cargador retrasado de AdSense
|
||||||
*/
|
*/
|
||||||
function init() {
|
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
|
// Verificar si el retardo de AdSense está habilitado
|
||||||
if (!window.roiAdsenseDelayed) {
|
if (!window.roiAdsenseDelayed) {
|
||||||
debugLog('Retardo de AdSense no habilitado');
|
debugLog('Retardo de AdSense no habilitado');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -41,7 +41,7 @@ define('ROI_DEFERRED_CSS', [
|
|||||||
'roi-utilities',
|
'roi-utilities',
|
||||||
'roi-accessibility',
|
'roi-accessibility',
|
||||||
'roi-responsive',
|
'roi-responsive',
|
||||||
'bootstrap-icons',
|
// NOTA: bootstrap-icons REMOVIDO de diferido - ahora crítico para evitar flash
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,19 +125,19 @@ function roi_enqueue_bootstrap() {
|
|||||||
'roi-bootstrap',
|
'roi-bootstrap',
|
||||||
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css',
|
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css',
|
||||||
array('roi-fonts'),
|
array('roi-fonts'),
|
||||||
'5.3.2-subset',
|
'5.3.2-subset-2', // v2: removed position:relative from .navbar
|
||||||
'print' // DIFERIDO - critical CSS inline evita CLS
|
'print' // DIFERIDO - critical CSS inline evita CLS
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
|
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
|
||||||
// Original: 211 KB (2050 iconos) -> Subset: 13 KB (104 iconos) = 94% reduccion
|
// Original: 211 KB (2050 iconos) -> Subset: 13 KB (104 iconos) = 94% reduccion
|
||||||
// DIFERIDO: Fase 4.3 - no crítico para renderizado inicial
|
// CRITICO: Carga inmediata para evitar flash de iconos (4.4KB)
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'bootstrap-icons',
|
'bootstrap-icons',
|
||||||
get_template_directory_uri() . '/Assets/Vendor/bootstrap-icons-subset.min.css',
|
get_template_directory_uri() . '/Assets/Vendor/bootstrap-icons-subset.min.css',
|
||||||
array('roi-bootstrap'),
|
array('roi-bootstrap'),
|
||||||
ROI_VERSION,
|
ROI_VERSION,
|
||||||
'print'
|
'all' // CRITICO - no diferir para evitar parpadeo de iconos
|
||||||
);
|
);
|
||||||
|
|
||||||
// Variables CSS del Template RDash - DIFERIDO
|
// Variables CSS del Template RDash - DIFERIDO
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ function roi_get_featured_image($post_id = null, $size = 'roi-featured-large', $
|
|||||||
return ''; // No placeholder - retornar vacío
|
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
|
// Obtener tipo de post
|
||||||
$post_type = get_post_type($post_id);
|
$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
|
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
|
// Obtener la imagen con clases Bootstrap
|
||||||
$image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array(
|
$image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array(
|
||||||
'class' => 'img-fluid post-thumbnail',
|
'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
|
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
|
// Obtener la imagen
|
||||||
$image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array(
|
$image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array(
|
||||||
'class' => 'img-fluid post-thumbnail-small',
|
'class' => 'img-fluid post-thumbnail-small',
|
||||||
@@ -287,6 +308,13 @@ function roi_should_show_featured_image($post_id = null) {
|
|||||||
return false;
|
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
|
// Obtener tipo de post
|
||||||
$post_type = get_post_type($post_id);
|
$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
|
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
|
// Obtener URL de la imagen
|
||||||
$image_url = get_the_post_thumbnail_url($post_id, $size);
|
$image_url = get_the_post_thumbnail_url($post_id, $size);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui;
|
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui;
|
||||||
|
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renderer para slots de AdSense
|
* Renderer para slots de AdSense
|
||||||
@@ -36,6 +37,11 @@ final class AdsensePlacementRenderer
|
|||||||
*/
|
*/
|
||||||
public function renderSlot(array $settings, string $location): string
|
public function renderSlot(array $settings, string $location): string
|
||||||
{
|
{
|
||||||
|
// 0. Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||||
|
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Validar is_enabled
|
// 1. Validar is_enabled
|
||||||
if (!($settings['visibility']['is_enabled'] ?? false)) {
|
if (!($settings['visibility']['is_enabled'] ?? false)) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContactFormRenderer - Renderiza formulario de contacto con webhook
|
* ContactFormRenderer - Renderiza formulario de contacto con webhook
|
||||||
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class ContactFormRenderer implements RendererInterface
|
final class ContactFormRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'contact-form';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator
|
private CSSGeneratorInterface $cssGenerator
|
||||||
) {}
|
) {}
|
||||||
@@ -34,7 +37,7 @@ final class ContactFormRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +70,7 @@ final class ContactFormRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'contact-form';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -76,22 +79,6 @@ final class ContactFormRenderer implements RendererInterface
|
|||||||
return $value === true || $value === '1' || $value === 1;
|
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
|
private function getVisibilityClass(array $data): ?string
|
||||||
{
|
{
|
||||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
|
|||||||
* Inyecta CSS critico en wp_head
|
* Inyecta CSS critico en wp_head
|
||||||
*
|
*
|
||||||
* Prioridades:
|
* Prioridades:
|
||||||
|
* - P:-2 Font preload (antes de variables)
|
||||||
* - P:-1 Variables CSS (antes de Bootstrap)
|
* - P:-1 Variables CSS (antes de Bootstrap)
|
||||||
* - P:2 Responsive critico (despues de Bootstrap critico)
|
* - P:2 Responsive critico (despues de Bootstrap critico)
|
||||||
*
|
*
|
||||||
* NOTA: Font preload deshabilitado - usando system fonts para CERO flash
|
|
||||||
*
|
|
||||||
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
|
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
|
||||||
*/
|
*/
|
||||||
final class CriticalCSSInjector
|
final class CriticalCSSInjector
|
||||||
@@ -22,11 +21,23 @@ final class CriticalCSSInjector
|
|||||||
private readonly CriticalCSSCacheInterface $cache
|
private readonly CriticalCSSCacheInterface $cache
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fuentes criticas para preload (pesos usados en navbar above-the-fold)
|
||||||
|
*/
|
||||||
|
private const CRITICAL_FONTS = [
|
||||||
|
'/Assets/Fonts/poppins-v24-latin-regular.woff2', // 400 - body text
|
||||||
|
'/Assets/Fonts/poppins-v24-latin-600.woff2', // 600 - navbar brand
|
||||||
|
'/Assets/Fonts/poppins-v24-latin-700.woff2', // 700 - headings
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra hooks de WordPress
|
* Registra hooks de WordPress
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
// Font preload: P:-2 (antes de todo, incluso variables)
|
||||||
|
add_action('wp_head', [$this, 'preloadFonts'], -2);
|
||||||
|
|
||||||
// Variables CSS: P:-1 (antes de CriticalBootstrapService P:0)
|
// Variables CSS: P:-1 (antes de CriticalBootstrapService P:0)
|
||||||
add_action('wp_head', [$this, 'injectVariables'], -1);
|
add_action('wp_head', [$this, 'injectVariables'], -1);
|
||||||
|
|
||||||
@@ -38,6 +49,25 @@ final class CriticalCSSInjector
|
|||||||
add_action('wp_enqueue_scripts', [$this, 'dequeueInlinedCSS'], 999);
|
add_action('wp_enqueue_scripts', [$this, 'dequeueInlinedCSS'], 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inyecta preload links para fuentes criticas
|
||||||
|
*
|
||||||
|
* Resuelve el problema de "font swap" donde el fallback (106% size-adjust)
|
||||||
|
* causa un salto visual cuando Poppins se carga.
|
||||||
|
* Con preload, las fuentes llegan antes del primer paint.
|
||||||
|
*/
|
||||||
|
public function preloadFonts(): void
|
||||||
|
{
|
||||||
|
echo "<!-- TIPO 4: Font preload para evitar CLS -->\n";
|
||||||
|
|
||||||
|
foreach (self::CRITICAL_FONTS as $font) {
|
||||||
|
printf(
|
||||||
|
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin>' . "\n",
|
||||||
|
esc_url(get_template_directory_uri() . $font)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inyecta variables CSS criticas
|
* Inyecta variables CSS criticas
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
|
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
|
||||||
@@ -27,6 +28,12 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class CtaBoxSidebarRenderer implements RendererInterface
|
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(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator
|
private CSSGeneratorInterface $cssGenerator
|
||||||
) {}
|
) {}
|
||||||
@@ -39,7 +46,8 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
// Evaluar visibilidad por tipo de página (usa Helper, NO cambia constructor)
|
||||||
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +60,7 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'cta-box-sidebar';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -60,22 +68,6 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
|||||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
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
|
private function generateCSS(array $data): string
|
||||||
{
|
{
|
||||||
$colors = $data['colors'] ?? [];
|
$colors = $data['colors'] ?? [];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CtaLetsTalkRenderer
|
* Class CtaLetsTalkRenderer
|
||||||
@@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class CtaLetsTalkRenderer implements RendererInterface
|
final class CtaLetsTalkRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'cta-lets-talk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +57,7 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validar visibilidad por página
|
// Validar visibilidad por página
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
|||||||
*/
|
*/
|
||||||
public function supports(string $componentType): bool
|
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;
|
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
|
* Calcular clases de visibilidad responsive
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\CtaPost\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
|
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
|
||||||
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class CtaPostRenderer implements RendererInterface
|
final class CtaPostRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'cta-post';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator
|
private CSSGeneratorInterface $cssGenerator
|
||||||
) {}
|
) {}
|
||||||
@@ -34,7 +37,7 @@ final class CtaPostRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ final class CtaPostRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'cta-post';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -55,22 +58,6 @@ final class CtaPostRenderer implements RendererInterface
|
|||||||
return $value === true || $value === '1' || $value === 1;
|
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
|
private function generateCSS(array $data): string
|
||||||
{
|
{
|
||||||
$colors = $data['colors'] ?? [];
|
$colors = $data['colors'] ?? [];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FeaturedImageRenderer - Renderiza la imagen destacada del post
|
* FeaturedImageRenderer - Renderiza la imagen destacada del post
|
||||||
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class FeaturedImageRenderer implements RendererInterface
|
final class FeaturedImageRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'featured-image';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator
|
private CSSGeneratorInterface $cssGenerator
|
||||||
) {}
|
) {}
|
||||||
@@ -39,7 +42,7 @@ final class FeaturedImageRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ final class FeaturedImageRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'featured-image';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -71,25 +74,24 @@ final class FeaturedImageRenderer implements RendererInterface
|
|||||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
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
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Footer\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FooterRenderer - Renderiza el footer del sitio
|
* FooterRenderer - Renderiza el footer del sitio
|
||||||
@@ -34,9 +35,14 @@ final class FooterRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function render(Component $component): string
|
public function render(Component $component): string
|
||||||
{
|
{
|
||||||
|
// Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||||
|
if (!PageVisibilityHelper::shouldShow('footer')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$data = $component->getData();
|
$data = $component->getData();
|
||||||
|
|
||||||
// Validar visibilidad
|
// Validar visibilidad básica
|
||||||
$visibility = $data['visibility'] ?? [];
|
$visibility = $data['visibility'] ?? [];
|
||||||
if (!($visibility['is_enabled'] ?? true)) {
|
if (!($visibility['is_enabled'] ?? true)) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class HeroRenderer
|
* Class HeroRenderer
|
||||||
@@ -33,6 +34,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class HeroRenderer implements RendererInterface
|
final class HeroRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'hero';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +51,7 @@ final class HeroRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ final class HeroRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'hero';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -76,24 +79,6 @@ final class HeroRenderer implements RendererInterface
|
|||||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
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
|
* Generar CSS usando CSSGeneratorService
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace ROITheme\Public\HeroSection\Infrastructure\Ui;
|
|||||||
|
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HeroSectionRenderer - Renderiza la sección hero con badges y título
|
* HeroSectionRenderer - Renderiza la sección hero con badges y título
|
||||||
@@ -23,6 +24,11 @@ final class HeroSectionRenderer implements RendererInterface
|
|||||||
{
|
{
|
||||||
public function render(Component $component): string
|
public function render(Component $component): string
|
||||||
{
|
{
|
||||||
|
// Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||||
|
if (!PageVisibilityHelper::shouldShow('hero-section')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$data = $component->getData();
|
$data = $component->getData();
|
||||||
|
|
||||||
if (!$this->isEnabled($data)) {
|
if (!$this->isEnabled($data)) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
use Walker_Nav_Menu;
|
use Walker_Nav_Menu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +29,8 @@ use Walker_Nav_Menu;
|
|||||||
*/
|
*/
|
||||||
final class NavbarRenderer implements RendererInterface
|
final class NavbarRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'navbar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +46,10 @@ final class NavbarRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$html = $this->buildMenu($data);
|
$html = $this->buildMenu($data);
|
||||||
|
|
||||||
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
||||||
@@ -281,7 +288,7 @@ final class NavbarRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'navbar';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RelatedPostRenderer - Renderiza seccion de posts relacionados
|
* RelatedPostRenderer - Renderiza seccion de posts relacionados
|
||||||
@@ -22,6 +23,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class RelatedPostRenderer implements RendererInterface
|
final class RelatedPostRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'related-post';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator
|
private CSSGeneratorInterface $cssGenerator
|
||||||
) {}
|
) {}
|
||||||
@@ -34,7 +37,7 @@ final class RelatedPostRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ final class RelatedPostRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'related-post';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -60,22 +63,6 @@ final class RelatedPostRenderer implements RendererInterface
|
|||||||
return $value === true || $value === '1' || $value === 1;
|
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
|
private function getVisibilityClass(array $data): ?string
|
||||||
{
|
{
|
||||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\SocialShare\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
|
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
|
||||||
@@ -27,6 +28,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class SocialShareRenderer implements RendererInterface
|
final class SocialShareRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'social-share';
|
||||||
|
|
||||||
private const NETWORKS = [
|
private const NETWORKS = [
|
||||||
'facebook' => [
|
'facebook' => [
|
||||||
'field' => 'show_facebook',
|
'field' => 'show_facebook',
|
||||||
@@ -84,7 +87,7 @@ final class SocialShareRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ final class SocialShareRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'social-share';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -105,22 +108,6 @@ final class SocialShareRenderer implements RendererInterface
|
|||||||
return $value === true || $value === '1' || $value === 1;
|
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
|
private function generateCSS(array $data): string
|
||||||
{
|
{
|
||||||
$colors = $data['colors'] ?? [];
|
$colors = $data['colors'] ?? [];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\TableOfContents\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ use DOMXPath;
|
|||||||
*/
|
*/
|
||||||
final class TableOfContentsRenderer implements RendererInterface
|
final class TableOfContentsRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'table-of-contents';
|
||||||
|
|
||||||
private array $headingCounter = [];
|
private array $headingCounter = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -44,7 +47,7 @@ final class TableOfContentsRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ final class TableOfContentsRenderer implements RendererInterface
|
|||||||
|
|
||||||
public function supports(string $componentType): bool
|
public function supports(string $componentType): bool
|
||||||
{
|
{
|
||||||
return $componentType === 'table-of-contents';
|
return $componentType === self::COMPONENT_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -71,22 +74,6 @@ final class TableOfContentsRenderer implements RendererInterface
|
|||||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
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
|
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||||
{
|
{
|
||||||
if (!$desktop && !$mobile) {
|
if (!$desktop && !$mobile) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class TopNotificationBarRenderer
|
* Class TopNotificationBarRenderer
|
||||||
@@ -34,6 +35,8 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
*/
|
*/
|
||||||
final class TopNotificationBarRenderer implements RendererInterface
|
final class TopNotificationBarRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
|
private const COMPONENT_NAME = 'top-notification-bar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +57,7 @@ final class TopNotificationBarRenderer implements RendererInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validar visibilidad por página
|
// Validar visibilidad por página
|
||||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
if (!PageVisibilityHelper::shouldShow(self::COMPONENT_NAME)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +81,7 @@ final class TopNotificationBarRenderer implements RendererInterface
|
|||||||
*/
|
*/
|
||||||
public function supports(string $componentType): bool
|
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;
|
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
|
* Verificar si el componente fue dismissed por el usuario
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -110,3 +110,14 @@
|
|||||||
transform: rotate(360deg);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": {
|
"layout": {
|
||||||
"label": "Ubicaciones Archivos/Globales",
|
"label": "Ubicaciones Archivos/Globales",
|
||||||
"priority": 80,
|
"priority": 80,
|
||||||
|
|||||||
@@ -27,14 +27,6 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,14 +27,6 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,20 +29,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el botón en pantallas móviles (<992px). Por defecto oculto para ahorrar espacio en navbar móvil"
|
"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": {
|
"is_critical": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "CSS Crítico",
|
"label": "CSS Crítico",
|
||||||
|
|||||||
@@ -27,14 +27,6 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,19 +30,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Muestra la imagen en dispositivos moviles (<768px)"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,20 +31,6 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"description": "Muestra el hero en dispositivos móviles (<768px)"
|
"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": {
|
"is_critical": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "CSS Crítico",
|
"label": "CSS Crítico",
|
||||||
|
|||||||
@@ -29,19 +29,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el menú en dispositivos de escritorio (≥768px)"
|
"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": {
|
"sticky_enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Navbar fijo (sticky)",
|
"label": "Navbar fijo (sticky)",
|
||||||
|
|||||||
@@ -27,14 +27,6 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,14 +27,6 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,14 +28,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"description": "Muestra el componente en pantallas < 992px"
|
"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": {
|
"is_critical": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "CSS Crítico",
|
"label": "CSS Crítico",
|
||||||
|
|||||||
@@ -15,20 +15,6 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"description": "Activa o desactiva la barra de notificación superior"
|
"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": {
|
"show_on_desktop": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Mostrar en desktop",
|
"label": "Mostrar en desktop",
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Application\UseCases\EvaluateComponentVisibility;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Application\UseCases\EvaluatePageVisibility\EvaluatePageVisibilityUseCase;
|
||||||
|
use ROITheme\Shared\Application\UseCases\EvaluateExclusions\EvaluateExclusionsUseCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caso de uso: Evaluar visibilidad completa de un componente
|
||||||
|
*
|
||||||
|
* Orquesta la evaluacion de:
|
||||||
|
* 1. Visibilidad por tipo de pagina (Plan 99.10)
|
||||||
|
* 2. Reglas de exclusion (Plan 99.11)
|
||||||
|
*
|
||||||
|
* El componente se muestra SOLO si:
|
||||||
|
* - Pasa la verificacion de tipo de pagina
|
||||||
|
* - NO esta excluido por ninguna regla
|
||||||
|
*
|
||||||
|
* PATRON: Facade/Orchestrator - combina dos UseCases
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Application\UseCases\EvaluateComponentVisibility
|
||||||
|
*/
|
||||||
|
final class EvaluateComponentVisibilityUseCase
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EvaluatePageVisibilityUseCase $pageVisibilityUseCase,
|
||||||
|
private readonly EvaluateExclusionsUseCase $exclusionsUseCase
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua si el componente debe mostrarse en la pagina actual
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return bool True si debe mostrarse
|
||||||
|
*/
|
||||||
|
public function execute(string $componentName): bool
|
||||||
|
{
|
||||||
|
// Paso 1: Verificar visibilidad por tipo de pagina
|
||||||
|
$visibleByPageType = $this->pageVisibilityUseCase->execute($componentName);
|
||||||
|
|
||||||
|
if (!$visibleByPageType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paso 2: Verificar exclusiones
|
||||||
|
$isExcluded = $this->exclusionsUseCase->execute($componentName);
|
||||||
|
|
||||||
|
// Mostrar si NO esta excluido
|
||||||
|
return !$isExcluded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Application\UseCases\EvaluateExclusions;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ExclusionRepositoryInterface;
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageContextProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caso de uso: Evaluar si un componente debe excluirse en la pagina actual
|
||||||
|
*
|
||||||
|
* Obtiene las reglas de exclusion del repositorio y evalua si aplican
|
||||||
|
* al contexto actual (post ID, categorias, URL).
|
||||||
|
*
|
||||||
|
* DIP: Depende de interfaces, no implementaciones.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Application\UseCases\EvaluateExclusions
|
||||||
|
*/
|
||||||
|
final class EvaluateExclusionsUseCase
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ExclusionRepositoryInterface $exclusionRepository,
|
||||||
|
private readonly PageContextProviderInterface $contextProvider
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua si el componente debe excluirse
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return bool True si debe EXCLUIRSE (NO mostrar)
|
||||||
|
*/
|
||||||
|
public function execute(string $componentName): bool
|
||||||
|
{
|
||||||
|
$exclusions = $this->exclusionRepository->getExclusions($componentName);
|
||||||
|
|
||||||
|
if (!$exclusions->isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = $this->contextProvider->getCurrentContext();
|
||||||
|
|
||||||
|
return $exclusions->shouldExclude($context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Application\UseCases\EvaluatePageVisibility;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||||
|
use ROITheme\Shared\Domain\Constants\VisibilityDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caso de uso: Evaluar si un componente debe mostrarse en la página actual
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Application\UseCases\EvaluatePageVisibility
|
||||||
|
*/
|
||||||
|
final class EvaluatePageVisibilityUseCase
|
||||||
|
{
|
||||||
|
// NOTA: Usa VisibilityDefaults::DEFAULT_VISIBILITY para cumplir DRY
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PageTypeDetectorInterface $pageTypeDetector,
|
||||||
|
private readonly PageVisibilityRepositoryInterface $visibilityRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalúa si el componente debe mostrarse en la página actual
|
||||||
|
*/
|
||||||
|
public function execute(string $componentName): bool
|
||||||
|
{
|
||||||
|
$config = $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Shared/Domain/Constants/ExclusionDefaults.php
Normal file
37
Shared/Domain/Constants/ExclusionDefaults.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constantes de exclusion por defecto para componentes
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Constants
|
||||||
|
*/
|
||||||
|
final class ExclusionDefaults
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Configuracion de exclusion por defecto (sin exclusiones)
|
||||||
|
*/
|
||||||
|
public const DEFAULT_EXCLUSIONS = [
|
||||||
|
'exclusions_enabled' => false,
|
||||||
|
'exclude_categories' => '[]',
|
||||||
|
'exclude_post_ids' => '[]',
|
||||||
|
'exclude_url_patterns' => '[]',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista de campos de exclusion validos
|
||||||
|
*/
|
||||||
|
public const EXCLUSION_FIELDS = [
|
||||||
|
'exclusions_enabled',
|
||||||
|
'exclude_categories',
|
||||||
|
'exclude_post_ids',
|
||||||
|
'exclude_url_patterns',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nombre del grupo en BD
|
||||||
|
*/
|
||||||
|
public const GROUP_NAME = '_exclusions';
|
||||||
|
}
|
||||||
45
Shared/Domain/Constants/VisibilityDefaults.php
Normal file
45
Shared/Domain/Constants/VisibilityDefaults.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constantes de visibilidad por defecto para componentes
|
||||||
|
*
|
||||||
|
* Centraliza los valores por defecto para cumplir con DRY.
|
||||||
|
* Usado por:
|
||||||
|
* - EvaluatePageVisibilityUseCase (cuando no hay config en BD)
|
||||||
|
* - MigratePageVisibilityService (para crear registros iniciales)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Constants
|
||||||
|
*/
|
||||||
|
final class VisibilityDefaults
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Configuración de visibilidad por defecto para nuevos componentes
|
||||||
|
*
|
||||||
|
* - Home: SÍ mostrar (página principal)
|
||||||
|
* - Posts: SÍ mostrar (artículos del blog)
|
||||||
|
* - Pages: SÍ mostrar (páginas estáticas)
|
||||||
|
* - Archives: NO mostrar (listados de categorías/tags)
|
||||||
|
* - Search: NO mostrar (resultados de búsqueda)
|
||||||
|
*/
|
||||||
|
public const DEFAULT_VISIBILITY = [
|
||||||
|
'show_on_home' => 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
36
Shared/Domain/Contracts/ExclusionRepositoryInterface.php
Normal file
36
Shared/Domain/Contracts/ExclusionRepositoryInterface.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Contracts;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\ExclusionRuleSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato para acceder a la configuracion de exclusiones
|
||||||
|
*
|
||||||
|
* Metodos: 3 (cumple ISP < 5 metodos)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Contracts
|
||||||
|
*/
|
||||||
|
interface ExclusionRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Obtiene las exclusiones configuradas para un componente
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return ExclusionRuleSet Configuracion de exclusiones
|
||||||
|
*/
|
||||||
|
public function getExclusions(string $componentName): ExclusionRuleSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guarda la configuracion de exclusiones de un componente
|
||||||
|
*
|
||||||
|
* @param ExclusionRuleSet $exclusions Configuracion a guardar
|
||||||
|
*/
|
||||||
|
public function saveExclusions(ExclusionRuleSet $exclusions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si existe configuracion de exclusiones para un componente
|
||||||
|
*/
|
||||||
|
public function hasExclusions(string $componentName): bool;
|
||||||
|
}
|
||||||
33
Shared/Domain/Contracts/PageContextProviderInterface.php
Normal file
33
Shared/Domain/Contracts/PageContextProviderInterface.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato para obtener el contexto de la pagina actual
|
||||||
|
*
|
||||||
|
* Abstrae la obtencion de datos del contexto actual (WordPress).
|
||||||
|
* Permite testear UseCases sin dependencia de WordPress.
|
||||||
|
*
|
||||||
|
* v1.1: Renombrado de ExclusionEvaluatorInterface (nombre semantico incorrecto)
|
||||||
|
* El nombre refleja que PROVEE contexto, no que EVALUA.
|
||||||
|
*
|
||||||
|
* Metodos: 1 (cumple ISP < 5 metodos)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Contracts
|
||||||
|
*/
|
||||||
|
interface PageContextProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Obtiene el contexto actual para evaluacion de exclusiones
|
||||||
|
*
|
||||||
|
* @return array{
|
||||||
|
* post_id: int,
|
||||||
|
* categories: array<array{term_id: int, slug: string, name: string}>,
|
||||||
|
* url: string,
|
||||||
|
* request_uri: string,
|
||||||
|
* post_type: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function getCurrentContext(): array;
|
||||||
|
}
|
||||||
25
Shared/Domain/Contracts/PageTypeDetectorInterface.php
Normal file
25
Shared/Domain/Contracts/PageTypeDetectorInterface.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Contracts;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\PageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato para detectar el tipo de página actual
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Contracts
|
||||||
|
*/
|
||||||
|
interface PageTypeDetectorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Detecta y retorna el tipo de página actual
|
||||||
|
*/
|
||||||
|
public function detect(): PageType;
|
||||||
|
|
||||||
|
public function isHome(): bool;
|
||||||
|
public function isPost(): bool;
|
||||||
|
public function isPage(): bool;
|
||||||
|
public function isArchive(): bool;
|
||||||
|
public function isSearch(): bool;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato para acceder a la configuración de visibilidad por página
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Contracts
|
||||||
|
*/
|
||||||
|
interface PageVisibilityRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Obtiene la configuración de visibilidad de un componente
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return array<string, bool> 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<string, bool> $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<string> 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<string, bool> $defaults Valores por defecto
|
||||||
|
*/
|
||||||
|
public function createDefaultVisibility(string $componentName, array $defaults): void;
|
||||||
|
}
|
||||||
27
Shared/Domain/Contracts/ServerRequestProviderInterface.php
Normal file
27
Shared/Domain/Contracts/ServerRequestProviderInterface.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrato para obtener datos del request HTTP
|
||||||
|
*
|
||||||
|
* Encapsula el acceso a $_SERVER para:
|
||||||
|
* - Evitar acceso directo a superglobales en Infrastructure
|
||||||
|
* - Permitir testear sin dependencia de $_SERVER
|
||||||
|
*
|
||||||
|
* v1.1: Nuevo - encapsular acceso a $_SERVER
|
||||||
|
*
|
||||||
|
* Metodos: 1 (cumple ISP < 5 metodos)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\Contracts
|
||||||
|
*/
|
||||||
|
interface ServerRequestProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Obtiene el Request URI actual
|
||||||
|
*
|
||||||
|
* @return string URI del request (ej: "/blog/mi-post/")
|
||||||
|
*/
|
||||||
|
public function getRequestUri(): string;
|
||||||
|
}
|
||||||
100
Shared/Domain/ValueObjects/CategoryExclusion.php
Normal file
100
Shared/Domain/ValueObjects/CategoryExclusion.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object: Exclusion por categoria
|
||||||
|
*
|
||||||
|
* Evalua si un post pertenece a alguna de las categorias excluidas.
|
||||||
|
* Soporta matching por slug o term_id.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
final class CategoryExclusion extends ExclusionRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int|string> $excludedCategories Lista de slugs o IDs de categorias
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $excludedCategories = []
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* Contexto esperado:
|
||||||
|
* - categories: array<array{term_id: int, slug: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function matches(array $context): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasValues()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postCategories = $context['categories'] ?? [];
|
||||||
|
|
||||||
|
if (empty($postCategories)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($postCategories as $category) {
|
||||||
|
// Buscar por slug
|
||||||
|
if (in_array($category['slug'], $this->excludedCategories, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar por term_id
|
||||||
|
if (in_array($category['term_id'], $this->excludedCategories, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar por term_id como string (para comparaciones flexibles)
|
||||||
|
if (in_array((string) $category['term_id'], $this->excludedCategories, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasValues(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->excludedCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serialize(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->excludedCategories, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int|string>
|
||||||
|
*/
|
||||||
|
public function getExcludedCategories(): array
|
||||||
|
{
|
||||||
|
return $this->excludedCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia desde JSON
|
||||||
|
*/
|
||||||
|
public static function fromJson(string $json): self
|
||||||
|
{
|
||||||
|
$decoded = json_decode($json, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return self::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia vacia
|
||||||
|
*/
|
||||||
|
public static function empty(): self
|
||||||
|
{
|
||||||
|
return new self([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,12 @@ final readonly class ComponentConfiguration
|
|||||||
'widget_3', // Widget 3 del footer (menú)
|
'widget_3', // Widget 3 del footer (menú)
|
||||||
'newsletter', // Sección newsletter del footer
|
'newsletter', // Sección newsletter del footer
|
||||||
'footer_bottom', // Pie del footer (copyright)
|
'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)
|
||||||
|
|
||||||
|
// Sistema de exclusiones (Plan 99.11)
|
||||||
|
'_exclusions', // Reglas de exclusión por categoría, post ID, URL pattern
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
37
Shared/Domain/ValueObjects/ExclusionRule.php
Normal file
37
Shared/Domain/ValueObjects/ExclusionRule.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clase base abstracta para reglas de exclusion
|
||||||
|
*
|
||||||
|
* Define el contrato comun para todos los tipos de exclusion.
|
||||||
|
* Cada implementacion concreta define su logica de matching.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
abstract class ExclusionRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Evalua si el contexto actual coincide con la regla
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $context Contexto de la pagina actual
|
||||||
|
* @return bool True si el contexto coincide (debe excluirse)
|
||||||
|
*/
|
||||||
|
abstract public function matches(array $context): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si la regla tiene valores configurados
|
||||||
|
*
|
||||||
|
* @return bool True si hay valores configurados
|
||||||
|
*/
|
||||||
|
abstract public function hasValues(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializa los valores para almacenamiento
|
||||||
|
*
|
||||||
|
* @return string JSON string
|
||||||
|
*/
|
||||||
|
abstract public function serialize(): string;
|
||||||
|
}
|
||||||
100
Shared/Domain/ValueObjects/ExclusionRuleSet.php
Normal file
100
Shared/Domain/ValueObjects/ExclusionRuleSet.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object Compuesto: Conjunto de reglas de exclusion
|
||||||
|
*
|
||||||
|
* Agrupa todas las reglas de exclusion para un componente.
|
||||||
|
* Evalua con logica OR (si cualquier regla coincide, se excluye).
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
final class ExclusionRuleSet
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $componentName,
|
||||||
|
private readonly bool $enabled,
|
||||||
|
private readonly CategoryExclusion $categoryExclusion,
|
||||||
|
private readonly PostIdExclusion $postIdExclusion,
|
||||||
|
private readonly UrlPatternExclusion $urlPatternExclusion
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua si el componente debe excluirse segun el contexto actual
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $context Contexto de la pagina actual
|
||||||
|
* @return bool True si debe excluirse (NO mostrar)
|
||||||
|
*/
|
||||||
|
public function shouldExclude(array $context): bool
|
||||||
|
{
|
||||||
|
if (!$this->enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluar cada tipo de exclusion (OR logico)
|
||||||
|
if ($this->categoryExclusion->matches($context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->postIdExclusion->matches($context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->urlPatternExclusion->matches($context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si tiene alguna regla configurada
|
||||||
|
*/
|
||||||
|
public function hasAnyRule(): bool
|
||||||
|
{
|
||||||
|
return $this->categoryExclusion->hasValues()
|
||||||
|
|| $this->postIdExclusion->hasValues()
|
||||||
|
|| $this->urlPatternExclusion->hasValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComponentName(): string
|
||||||
|
{
|
||||||
|
return $this->componentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategoryExclusion(): CategoryExclusion
|
||||||
|
{
|
||||||
|
return $this->categoryExclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPostIdExclusion(): PostIdExclusion
|
||||||
|
{
|
||||||
|
return $this->postIdExclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrlPatternExclusion(): UrlPatternExclusion
|
||||||
|
{
|
||||||
|
return $this->urlPatternExclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea una instancia sin exclusiones (por defecto)
|
||||||
|
*/
|
||||||
|
public static function empty(string $componentName): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$componentName,
|
||||||
|
false,
|
||||||
|
CategoryExclusion::empty(),
|
||||||
|
PostIdExclusion::empty(),
|
||||||
|
UrlPatternExclusion::empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
Shared/Domain/ValueObjects/PageType.php
Normal file
90
Shared/Domain/ValueObjects/PageType.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object que representa los tipos de página válidos
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
final class PageType
|
||||||
|
{
|
||||||
|
public const HOME = 'home';
|
||||||
|
public const POST = 'post';
|
||||||
|
public const PAGE = 'page';
|
||||||
|
public const ARCHIVE = 'archive';
|
||||||
|
public const SEARCH = 'search';
|
||||||
|
public const UNKNOWN = 'unknown';
|
||||||
|
|
||||||
|
private const VALID_TYPES = [
|
||||||
|
self::HOME,
|
||||||
|
self::POST,
|
||||||
|
self::PAGE,
|
||||||
|
self::ARCHIVE,
|
||||||
|
self::SEARCH,
|
||||||
|
self::UNKNOWN,
|
||||||
|
];
|
||||||
|
|
||||||
|
private function __construct(
|
||||||
|
private readonly string $value
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromString(string $type): self
|
||||||
|
{
|
||||||
|
if (!in_array($type, self::VALID_TYPES, true)) {
|
||||||
|
return new self(self::UNKNOWN);
|
||||||
|
}
|
||||||
|
return new self($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function home(): self
|
||||||
|
{
|
||||||
|
return new self(self::HOME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function post(): self
|
||||||
|
{
|
||||||
|
return new self(self::POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function page(): self
|
||||||
|
{
|
||||||
|
return new self(self::PAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function archive(): self
|
||||||
|
{
|
||||||
|
return new self(self::ARCHIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function search(): self
|
||||||
|
{
|
||||||
|
return new self(self::SEARCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function value(): string
|
||||||
|
{
|
||||||
|
return $this->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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Shared/Domain/ValueObjects/PostIdExclusion.php
Normal file
86
Shared/Domain/ValueObjects/PostIdExclusion.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object: Exclusion por ID de post/pagina
|
||||||
|
*
|
||||||
|
* Evalua si el post/pagina actual esta en la lista de IDs excluidos.
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
final class PostIdExclusion extends ExclusionRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int> $excludedPostIds Lista de IDs de posts/paginas
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $excludedPostIds = []
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* Contexto esperado:
|
||||||
|
* - post_id: int
|
||||||
|
*/
|
||||||
|
public function matches(array $context): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasValues()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$postId = $context['post_id'] ?? 0;
|
||||||
|
|
||||||
|
if ($postId === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($postId, $this->excludedPostIds, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasValues(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->excludedPostIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serialize(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->excludedPostIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int>
|
||||||
|
*/
|
||||||
|
public function getExcludedPostIds(): array
|
||||||
|
{
|
||||||
|
return $this->excludedPostIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia desde JSON
|
||||||
|
*/
|
||||||
|
public static function fromJson(string $json): self
|
||||||
|
{
|
||||||
|
$decoded = json_decode($json, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return self::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asegurar que son enteros
|
||||||
|
$ids = array_map('intval', $decoded);
|
||||||
|
$ids = array_filter($ids, fn(int $id): bool => $id > 0);
|
||||||
|
|
||||||
|
return new self(array_values($ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia vacia
|
||||||
|
*/
|
||||||
|
public static function empty(): self
|
||||||
|
{
|
||||||
|
return new self([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
Shared/Domain/ValueObjects/UrlPatternExclusion.php
Normal file
182
Shared/Domain/ValueObjects/UrlPatternExclusion.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object: Exclusion por patron URL
|
||||||
|
*
|
||||||
|
* Evalua si la URL actual coincide con alguno de los patrones configurados.
|
||||||
|
* Soporta:
|
||||||
|
* - Substring simple: "/privado/" coincide con cualquier URL que contenga ese texto
|
||||||
|
* - Regex: Patrones que empiezan y terminan con "/" son evaluados como regex
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Domain\ValueObjects
|
||||||
|
*/
|
||||||
|
final class UrlPatternExclusion extends ExclusionRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string> $urlPatterns Lista de patrones (substring o regex)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $urlPatterns = []
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* Contexto esperado:
|
||||||
|
* - request_uri: string (URI del request)
|
||||||
|
* - url: string (URL completa, opcional)
|
||||||
|
*/
|
||||||
|
public function matches(array $context): bool
|
||||||
|
{
|
||||||
|
if (!$this->hasValues()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestUri = $context['request_uri'] ?? '';
|
||||||
|
$url = $context['url'] ?? '';
|
||||||
|
|
||||||
|
if ($requestUri === '' && $url === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->urlPatterns as $pattern) {
|
||||||
|
if ($this->matchesPattern($pattern, $requestUri, $url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua si un patron coincide con el request_uri o url
|
||||||
|
*/
|
||||||
|
private function matchesPattern(string $pattern, string $requestUri, string $url): bool
|
||||||
|
{
|
||||||
|
// Detectar si es regex (empieza con /)
|
||||||
|
if ($this->isRegex($pattern)) {
|
||||||
|
return $this->matchesRegex($pattern, $requestUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substring matching
|
||||||
|
return $this->matchesSubstring($pattern, $requestUri, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detecta si el patron es una expresion regular
|
||||||
|
*/
|
||||||
|
private function isRegex(string $pattern): bool
|
||||||
|
{
|
||||||
|
// Un patron regex debe empezar con / y terminar con / (posiblemente con flags)
|
||||||
|
return preg_match('#^/.+/[gimsux]*$#', $pattern) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua coincidencia regex
|
||||||
|
*/
|
||||||
|
private function matchesRegex(string $pattern, string $subject): bool
|
||||||
|
{
|
||||||
|
// Suprimir warnings de regex invalidos
|
||||||
|
$result = @preg_match($pattern, $subject);
|
||||||
|
|
||||||
|
return $result === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua coincidencia por substring o wildcard
|
||||||
|
*
|
||||||
|
* Soporta wildcards simples:
|
||||||
|
* - `*sct*` coincide con URLs que contengan "sct"
|
||||||
|
* - `*` se convierte a `.*` en regex
|
||||||
|
* - Sin wildcards: busca substring literal
|
||||||
|
*/
|
||||||
|
private function matchesSubstring(string $pattern, string $requestUri, string $url): bool
|
||||||
|
{
|
||||||
|
// Detectar si tiene wildcards (*)
|
||||||
|
if (str_contains($pattern, '*')) {
|
||||||
|
return $this->matchesWildcard($pattern, $requestUri, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substring literal
|
||||||
|
if ($requestUri !== '' && str_contains($requestUri, $pattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($url !== '' && str_contains($url, $pattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua coincidencia con patron wildcard
|
||||||
|
*
|
||||||
|
* Convierte wildcards (*) a regex (.*)
|
||||||
|
*/
|
||||||
|
private function matchesWildcard(string $pattern, string $requestUri, string $url): bool
|
||||||
|
{
|
||||||
|
// Convertir wildcard a regex:
|
||||||
|
// 1. Escapar caracteres especiales de regex (excepto *)
|
||||||
|
// 2. Convertir * a .*
|
||||||
|
$regexPattern = preg_quote($pattern, '#');
|
||||||
|
$regexPattern = str_replace('\\*', '.*', $regexPattern);
|
||||||
|
$regexPattern = '#' . $regexPattern . '#i';
|
||||||
|
|
||||||
|
if ($requestUri !== '' && preg_match($regexPattern, $requestUri) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($url !== '' && preg_match($regexPattern, $url) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasValues(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->urlPatterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serialize(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->urlPatterns, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function getUrlPatterns(): array
|
||||||
|
{
|
||||||
|
return $this->urlPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia desde JSON
|
||||||
|
*/
|
||||||
|
public static function fromJson(string $json): self
|
||||||
|
{
|
||||||
|
$decoded = json_decode($json, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return self::empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar valores vacios
|
||||||
|
$patterns = array_filter($decoded, fn($p): bool => is_string($p) && $p !== '');
|
||||||
|
|
||||||
|
return new self(array_values($patterns));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea instancia vacia
|
||||||
|
*/
|
||||||
|
public static function empty(): self
|
||||||
|
{
|
||||||
|
return new self([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ROITheme\Shared\Infrastructure\Api\WordPress;
|
namespace ROITheme\Shared\Infrastructure\Api\WordPress;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WP-CLI Command para Sincronización de Schemas
|
* WP-CLI Command para Sincronización de Schemas
|
||||||
*
|
*
|
||||||
@@ -297,6 +299,298 @@ final class MigrationCommand
|
|||||||
'stats' => $stats
|
'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('/<h2[^>]*>\s*<span[^>]*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: <h2><span data-shortcode="tcb_post_title"...>...</span></h2>
|
||||||
|
$result = preg_replace('/<h2[^>]*>\s*<span[^>]*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('/<p[^>]*>.*?\[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('/<p[^>]*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
|
// Registrar comando WP-CLI
|
||||||
|
|||||||
352
Shared/Infrastructure/CLI/CleanThriveContentCommand.php
Normal file
352
Shared/Infrastructure/CLI/CleanThriveContentCommand.php
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\CLI;
|
||||||
|
|
||||||
|
use WP_CLI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comando WP-CLI para limpiar contenido Thrive congelado de páginas
|
||||||
|
*
|
||||||
|
* 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%)
|
||||||
|
*
|
||||||
|
* 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('/<h2[^>]*>.*?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(
|
||||||
|
'/<h2[^>]*>.*?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(
|
||||||
|
'/<p[^>]*>.*?\[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(
|
||||||
|
'/<p[^>]*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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,21 @@ use ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector;
|
|||||||
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
||||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
|
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;
|
||||||
|
// Exclusion System (Plan 99.11)
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ExclusionRepositoryInterface;
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageContextProviderInterface;
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ServerRequestProviderInterface;
|
||||||
|
use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressExclusionRepository;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\WordPressPageContextProvider;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\WordPressServerRequestProvider;
|
||||||
|
use ROITheme\Shared\Application\UseCases\EvaluateExclusions\EvaluateExclusionsUseCase;
|
||||||
|
use ROITheme\Shared\Application\UseCases\EvaluateComponentVisibility\EvaluateComponentVisibilityUseCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIContainer - Contenedor de Inyección de Dependencias
|
* DIContainer - Contenedor de Inyección de Dependencias
|
||||||
@@ -46,10 +61,38 @@ final class DIContainer
|
|||||||
{
|
{
|
||||||
private array $instances = [];
|
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(
|
public function __construct(
|
||||||
private \wpdb $wpdb,
|
private \wpdb $wpdb,
|
||||||
private string $schemasPath
|
private string $schemasPath
|
||||||
) {}
|
) {
|
||||||
|
// Registrar como instancia singleton
|
||||||
|
self::$instance = $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener repositorio de componentes
|
* Obtener repositorio de componentes
|
||||||
@@ -272,4 +315,132 @@ final class DIContainer
|
|||||||
|
|
||||||
return $this->instances['criticalCSSCollector'];
|
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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// Exclusion System (Plan 99.11)
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el proveedor de request HTTP
|
||||||
|
*
|
||||||
|
* Encapsula acceso a $_SERVER
|
||||||
|
*/
|
||||||
|
public function getServerRequestProvider(): ServerRequestProviderInterface
|
||||||
|
{
|
||||||
|
if (!isset($this->instances['serverRequestProvider'])) {
|
||||||
|
$this->instances['serverRequestProvider'] = new WordPressServerRequestProvider();
|
||||||
|
}
|
||||||
|
return $this->instances['serverRequestProvider'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el repositorio de exclusiones
|
||||||
|
*/
|
||||||
|
public function getExclusionRepository(): ExclusionRepositoryInterface
|
||||||
|
{
|
||||||
|
if (!isset($this->instances['exclusionRepository'])) {
|
||||||
|
$this->instances['exclusionRepository'] = new WordPressExclusionRepository($this->wpdb);
|
||||||
|
}
|
||||||
|
return $this->instances['exclusionRepository'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el proveedor de contexto de página
|
||||||
|
*/
|
||||||
|
public function getPageContextProvider(): PageContextProviderInterface
|
||||||
|
{
|
||||||
|
if (!isset($this->instances['pageContextProvider'])) {
|
||||||
|
$this->instances['pageContextProvider'] = new WordPressPageContextProvider(
|
||||||
|
$this->getServerRequestProvider()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->instances['pageContextProvider'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el caso de uso de evaluación de exclusiones
|
||||||
|
*/
|
||||||
|
public function getEvaluateExclusionsUseCase(): EvaluateExclusionsUseCase
|
||||||
|
{
|
||||||
|
if (!isset($this->instances['evaluateExclusionsUseCase'])) {
|
||||||
|
$this->instances['evaluateExclusionsUseCase'] = new EvaluateExclusionsUseCase(
|
||||||
|
$this->getExclusionRepository(),
|
||||||
|
$this->getPageContextProvider()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->instances['evaluateExclusionsUseCase'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el caso de uso orquestador de visibilidad completa
|
||||||
|
*
|
||||||
|
* Combina visibilidad por tipo de página + exclusiones
|
||||||
|
*/
|
||||||
|
public function getEvaluateComponentVisibilityUseCase(): EvaluateComponentVisibilityUseCase
|
||||||
|
{
|
||||||
|
if (!isset($this->instances['evaluateComponentVisibilityUseCase'])) {
|
||||||
|
$this->instances['evaluateComponentVisibilityUseCase'] = new EvaluateComponentVisibilityUseCase(
|
||||||
|
$this->getEvaluatePageVisibilityUseCase(),
|
||||||
|
$this->getEvaluateExclusionsUseCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->instances['evaluateComponentVisibilityUseCase'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,28 +109,67 @@ final class WordPressComponentSettingsRepository implements ComponentSettingsRep
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* Implementa UPSERT: si el registro no existe, lo crea; si existe, lo actualiza.
|
||||||
|
* Esto es necesario para grupos especiales como _page_visibility y _exclusions
|
||||||
|
* que no vienen del schema JSON.
|
||||||
*/
|
*/
|
||||||
public function saveFieldValue(string $componentName, string $groupName, string $attributeName, mixed $value): bool
|
public function saveFieldValue(string $componentName, string $groupName, string $attributeName, mixed $value): bool
|
||||||
{
|
{
|
||||||
// Serializar valor
|
// Serializar valor
|
||||||
$serializedValue = $this->serializeValue($value);
|
$serializedValue = $this->serializeValue($value);
|
||||||
|
|
||||||
// Intentar actualizar
|
// Verificar si el registro existe
|
||||||
$result = $this->wpdb->update(
|
$exists = $this->fieldExists($componentName, $groupName, $attributeName);
|
||||||
$this->tableName,
|
|
||||||
['attribute_value' => $serializedValue],
|
if ($exists) {
|
||||||
[
|
// UPDATE
|
||||||
'component_name' => $componentName,
|
$result = $this->wpdb->update(
|
||||||
'group_name' => $groupName,
|
$this->tableName,
|
||||||
'attribute_name' => $attributeName
|
['attribute_value' => $serializedValue],
|
||||||
],
|
[
|
||||||
['%s'],
|
'component_name' => $componentName,
|
||||||
['%s', '%s', '%s']
|
'group_name' => $groupName,
|
||||||
);
|
'attribute_name' => $attributeName
|
||||||
|
],
|
||||||
|
['%s'],
|
||||||
|
['%s', '%s', '%s']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// INSERT - crear nuevo registro
|
||||||
|
$result = $this->wpdb->insert(
|
||||||
|
$this->tableName,
|
||||||
|
[
|
||||||
|
'component_name' => $componentName,
|
||||||
|
'group_name' => $groupName,
|
||||||
|
'attribute_name' => $attributeName,
|
||||||
|
'attribute_value' => $serializedValue
|
||||||
|
],
|
||||||
|
['%s', '%s', '%s', '%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $result !== false;
|
return $result !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si un campo existe en la BD
|
||||||
|
*/
|
||||||
|
private function fieldExists(string $componentName, string $groupName, string $attributeName): bool
|
||||||
|
{
|
||||||
|
$sql = $this->wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->tableName}
|
||||||
|
WHERE component_name = %s
|
||||||
|
AND group_name = %s
|
||||||
|
AND attribute_name = %s",
|
||||||
|
$componentName,
|
||||||
|
$groupName,
|
||||||
|
$attributeName
|
||||||
|
);
|
||||||
|
|
||||||
|
return (int) $this->wpdb->get_var($sql) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ExclusionRepositoryInterface;
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\ExclusionRuleSet;
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\CategoryExclusion;
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\PostIdExclusion;
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\UrlPatternExclusion;
|
||||||
|
use ROITheme\Shared\Domain\Constants\ExclusionDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementacion WordPress del repositorio de exclusiones
|
||||||
|
*
|
||||||
|
* Almacena exclusiones en wp_roi_theme_component_settings
|
||||||
|
* con group_name = '_exclusions'
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
|
||||||
|
*/
|
||||||
|
final class WordPressExclusionRepository implements ExclusionRepositoryInterface
|
||||||
|
{
|
||||||
|
private const TABLE_SUFFIX = 'roi_theme_component_settings';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly \wpdb $wpdb
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getExclusions(string $componentName): ExclusionRuleSet
|
||||||
|
{
|
||||||
|
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||||
|
$groupName = ExclusionDefaults::GROUP_NAME;
|
||||||
|
|
||||||
|
$results = $this->wpdb->get_results(
|
||||||
|
$this->wpdb->prepare(
|
||||||
|
"SELECT attribute_name, attribute_value
|
||||||
|
FROM {$table}
|
||||||
|
WHERE component_name = %s
|
||||||
|
AND group_name = %s",
|
||||||
|
$componentName,
|
||||||
|
$groupName
|
||||||
|
),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($results)) {
|
||||||
|
return ExclusionRuleSet::empty($componentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach ($results as $row) {
|
||||||
|
$data[$row['attribute_name']] = $row['attribute_value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->hydrateExclusions($componentName, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveExclusions(ExclusionRuleSet $exclusions): void
|
||||||
|
{
|
||||||
|
$componentName = $exclusions->getComponentName();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'exclusions_enabled' => $exclusions->isEnabled() ? '1' : '0',
|
||||||
|
'exclude_categories' => $exclusions->getCategoryExclusion()->serialize(),
|
||||||
|
'exclude_post_ids' => $exclusions->getPostIdExclusion()->serialize(),
|
||||||
|
'exclude_url_patterns' => $exclusions->getUrlPatternExclusion()->serialize(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($data as $field => $value) {
|
||||||
|
$this->upsertField($componentName, $field, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasExclusions(string $componentName): bool
|
||||||
|
{
|
||||||
|
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||||
|
$groupName = ExclusionDefaults::GROUP_NAME;
|
||||||
|
|
||||||
|
$count = $this->wpdb->get_var($this->wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$table}
|
||||||
|
WHERE component_name = %s
|
||||||
|
AND group_name = %s",
|
||||||
|
$componentName,
|
||||||
|
$groupName
|
||||||
|
));
|
||||||
|
|
||||||
|
return (int) $count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateExclusions(string $componentName, array $data): ExclusionRuleSet
|
||||||
|
{
|
||||||
|
$enabled = ($data['exclusions_enabled'] ?? '0') === '1';
|
||||||
|
|
||||||
|
$categoryExclusion = CategoryExclusion::fromJson($data['exclude_categories'] ?? '[]');
|
||||||
|
$postIdExclusion = PostIdExclusion::fromJson($data['exclude_post_ids'] ?? '[]');
|
||||||
|
$urlPatternExclusion = UrlPatternExclusion::fromJson($data['exclude_url_patterns'] ?? '[]');
|
||||||
|
|
||||||
|
return new ExclusionRuleSet(
|
||||||
|
$componentName,
|
||||||
|
$enabled,
|
||||||
|
$categoryExclusion,
|
||||||
|
$postIdExclusion,
|
||||||
|
$urlPatternExclusion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function upsertField(string $componentName, string $field, string $value): void
|
||||||
|
{
|
||||||
|
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||||
|
$groupName = ExclusionDefaults::GROUP_NAME;
|
||||||
|
|
||||||
|
$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,
|
||||||
|
$groupName,
|
||||||
|
$field
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$this->wpdb->update(
|
||||||
|
$table,
|
||||||
|
[
|
||||||
|
'attribute_value' => $value,
|
||||||
|
'updated_at' => current_time('mysql'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'component_name' => $componentName,
|
||||||
|
'group_name' => $groupName,
|
||||||
|
'attribute_name' => $field,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->wpdb->insert($table, [
|
||||||
|
'component_name' => $componentName,
|
||||||
|
'group_name' => $groupName,
|
||||||
|
'attribute_name' => $field,
|
||||||
|
'attribute_value' => $value,
|
||||||
|
'is_editable' => 1,
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementación WordPress del repositorio de visibilidad
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
|
||||||
|
*/
|
||||||
|
final class WordPressPageVisibilityRepository implements PageVisibilityRepositoryInterface
|
||||||
|
{
|
||||||
|
private const GROUP_NAME = '_page_visibility';
|
||||||
|
private const TABLE_SUFFIX = 'roi_theme_component_settings';
|
||||||
|
|
||||||
|
private const VISIBILITY_FIELDS = [
|
||||||
|
'show_on_home',
|
||||||
|
'show_on_posts',
|
||||||
|
'show_on_pages',
|
||||||
|
'show_on_archives',
|
||||||
|
'show_on_search',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor con inyección de dependencias
|
||||||
|
*
|
||||||
|
* IMPORTANTE: Sigue el patrón existente de WordPressComponentSettingsRepository
|
||||||
|
* donde $wpdb se inyecta por constructor, no se usa global.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly \wpdb $wpdb
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getVisibilityConfig(string $componentName): array
|
||||||
|
{
|
||||||
|
$table = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||||
|
use ROITheme\Shared\Domain\Constants\VisibilityDefaults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servicio para migrar configuración de visibilidad inicial
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class MigratePageVisibilityService
|
||||||
|
{
|
||||||
|
// NOTA: Usa VisibilityDefaults::DEFAULT_VISIBILITY para cumplir DRY
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PageVisibilityRepositoryInterface $visibilityRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta la migración para todos los componentes
|
||||||
|
*
|
||||||
|
* @return array{created: int, skipped: int}
|
||||||
|
*/
|
||||||
|
public function migrate(): array
|
||||||
|
{
|
||||||
|
$created = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
$components = $this->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Shared/Infrastructure/Services/PageVisibilityHelper.php
Normal file
63
Shared/Infrastructure/Services/PageVisibilityHelper.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facade/Helper para evaluar visibilidad completa de componentes
|
||||||
|
*
|
||||||
|
* PROPOSITO:
|
||||||
|
* Permite que los Renderers existentes evaluen visibilidad sin modificar sus constructores.
|
||||||
|
* Ahora incluye tanto visibilidad por tipo de pagina como reglas de exclusion.
|
||||||
|
*
|
||||||
|
* USO EN RENDERERS:
|
||||||
|
* ```php
|
||||||
|
* if (!PageVisibilityHelper::shouldShow('cta-box-sidebar')) {
|
||||||
|
* return '';
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* FLUJO:
|
||||||
|
* 1. Verifica visibilidad por tipo de pagina (home, posts, pages, etc.)
|
||||||
|
* 2. Verifica reglas de exclusion (categorias, IDs, patrones URL)
|
||||||
|
* 3. Retorna true SOLO si pasa ambas verificaciones
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class PageVisibilityHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Evalua si un componente debe mostrarse en la pagina actual
|
||||||
|
*
|
||||||
|
* Incluye verificacion de:
|
||||||
|
* - Visibilidad por tipo de pagina (Plan 99.10)
|
||||||
|
* - Reglas de exclusion (Plan 99.11)
|
||||||
|
*
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return bool True si debe mostrarse
|
||||||
|
*/
|
||||||
|
public static function shouldShow(string $componentName): bool
|
||||||
|
{
|
||||||
|
$container = DIContainer::getInstance();
|
||||||
|
$useCase = $container->getEvaluateComponentVisibilityUseCase();
|
||||||
|
|
||||||
|
return $useCase->execute($componentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evalua SOLO visibilidad por tipo de pagina (sin exclusiones)
|
||||||
|
*
|
||||||
|
* @deprecated Usar shouldShow() que incluye exclusiones
|
||||||
|
* @param string $componentName Nombre del componente (kebab-case)
|
||||||
|
* @return bool True si debe mostrarse segun tipo de pagina
|
||||||
|
*/
|
||||||
|
public static function shouldShowByPageType(string $componentName): bool
|
||||||
|
{
|
||||||
|
$container = DIContainer::getInstance();
|
||||||
|
$useCase = $container->getEvaluatePageVisibilityUseCase();
|
||||||
|
|
||||||
|
return $useCase->execute($componentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageContextProviderInterface;
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ServerRequestProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementacion WordPress del proveedor de contexto de pagina
|
||||||
|
*
|
||||||
|
* Obtiene informacion del post/pagina actual usando funciones de WordPress.
|
||||||
|
*
|
||||||
|
* v1.1: Renombrado de WordPressExclusionEvaluator
|
||||||
|
* Inyecta ServerRequestProviderInterface (no accede a $_SERVER directamente)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class WordPressPageContextProvider implements PageContextProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ServerRequestProviderInterface $requestProvider
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getCurrentContext(): array
|
||||||
|
{
|
||||||
|
$postId = $this->getCurrentPostId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'post_id' => $postId,
|
||||||
|
'categories' => $this->getPostCategories($postId),
|
||||||
|
'url' => $this->getCurrentUrl(),
|
||||||
|
'request_uri' => $this->requestProvider->getRequestUri(),
|
||||||
|
'post_type' => $this->getCurrentPostType($postId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentPostId(): int
|
||||||
|
{
|
||||||
|
if (is_singular()) {
|
||||||
|
return get_the_ID() ?: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<array{term_id: int, slug: string, name: string}>
|
||||||
|
*/
|
||||||
|
private function getPostCategories(int $postId): array
|
||||||
|
{
|
||||||
|
if ($postId === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = get_the_category($postId);
|
||||||
|
|
||||||
|
if (empty($categories)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(function (\WP_Term $term): array {
|
||||||
|
return [
|
||||||
|
'term_id' => $term->term_id,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'name' => $term->name,
|
||||||
|
];
|
||||||
|
}, $categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentUrl(): string
|
||||||
|
{
|
||||||
|
global $wp;
|
||||||
|
|
||||||
|
if (isset($wp->request)) {
|
||||||
|
return home_url($wp->request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return home_url(add_query_arg([], false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentPostType(int $postId): string
|
||||||
|
{
|
||||||
|
if ($postId === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_post_type($postId) ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Shared/Infrastructure/Services/WordPressPageTypeDetector.php
Normal file
65
Shared/Infrastructure/Services/WordPressPageTypeDetector.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
|
||||||
|
use ROITheme\Shared\Domain\ValueObjects\PageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementación WordPress del detector de tipo de página
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class WordPressPageTypeDetector implements PageTypeDetectorInterface
|
||||||
|
{
|
||||||
|
public function detect(): PageType
|
||||||
|
{
|
||||||
|
if ($this->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\ServerRequestProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementacion WordPress del proveedor de request HTTP
|
||||||
|
*
|
||||||
|
* Encapsula el acceso a $_SERVER.
|
||||||
|
*
|
||||||
|
* v1.1: Nuevo - extrae logica de acceso a superglobales
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class WordPressServerRequestProvider implements ServerRequestProviderInterface
|
||||||
|
{
|
||||||
|
public function getRequestUri(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
324
build-bootstrap-subset.js
Normal file
324
build-bootstrap-subset.js
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/**
|
||||||
|
* Build Bootstrap Subset Script
|
||||||
|
*
|
||||||
|
* Genera un subset de Bootstrap con SOLO las clases usadas en el tema.
|
||||||
|
*
|
||||||
|
* USO:
|
||||||
|
* node build-bootstrap-subset.js
|
||||||
|
*
|
||||||
|
* OUTPUT:
|
||||||
|
* Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { PurgeCSS } = require('purgecss');
|
||||||
|
const { globSync } = require('glob');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function buildBootstrapSubset() {
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('Building Bootstrap Subset for ROI Theme');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
|
||||||
|
const themeDir = __dirname;
|
||||||
|
const inputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap.min.css');
|
||||||
|
const outputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css');
|
||||||
|
|
||||||
|
// Verificar que existe el archivo de entrada
|
||||||
|
if (!fs.existsSync(inputFile)) {
|
||||||
|
console.error('ERROR: bootstrap.min.css not found at:', inputFile);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputSize = fs.statSync(inputFile).size;
|
||||||
|
console.log(`Input: bootstrap.min.css (${(inputSize / 1024).toFixed(2)} KB)`);
|
||||||
|
|
||||||
|
// Encontrar archivos PHP y JS manualmente
|
||||||
|
console.log('\nScanning for PHP and JS files...');
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
'*.php',
|
||||||
|
'Public/**/*.php',
|
||||||
|
'Admin/**/*.php',
|
||||||
|
'Inc/**/*.php',
|
||||||
|
'Shared/**/*.php',
|
||||||
|
'template-parts/**/*.php',
|
||||||
|
'Assets/js/**/*.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
let contentFiles = [];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const files = globSync(pattern, { cwd: themeDir, absolute: true });
|
||||||
|
contentFiles = contentFiles.concat(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${contentFiles.length} files to analyze`);
|
||||||
|
|
||||||
|
if (contentFiles.length === 0) {
|
||||||
|
console.error('ERROR: No content files found. Check glob patterns.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar algunos archivos encontrados
|
||||||
|
console.log('\nSample files:');
|
||||||
|
contentFiles.slice(0, 5).forEach(f => console.log(' -', path.relative(themeDir, f)));
|
||||||
|
if (contentFiles.length > 5) {
|
||||||
|
console.log(` ... and ${contentFiles.length - 5} more`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const purgeCSSResult = await new PurgeCSS().purge({
|
||||||
|
css: [inputFile],
|
||||||
|
content: contentFiles,
|
||||||
|
|
||||||
|
// Safelist: Clases que SIEMPRE deben incluirse
|
||||||
|
safelist: {
|
||||||
|
standard: [
|
||||||
|
// Estados de navbar scroll (JavaScript)
|
||||||
|
'scrolled',
|
||||||
|
'navbar-scrolled',
|
||||||
|
|
||||||
|
// Bootstrap Collapse (JavaScript)
|
||||||
|
'show',
|
||||||
|
'showing',
|
||||||
|
'hiding',
|
||||||
|
'collapse',
|
||||||
|
'collapsing',
|
||||||
|
|
||||||
|
// Estados de dropdown
|
||||||
|
'dropdown-menu',
|
||||||
|
'dropdown-item',
|
||||||
|
'dropdown-toggle',
|
||||||
|
|
||||||
|
// Estados de form
|
||||||
|
'is-valid',
|
||||||
|
'is-invalid',
|
||||||
|
'was-validated',
|
||||||
|
|
||||||
|
// Visually hidden (accesibilidad)
|
||||||
|
'visually-hidden',
|
||||||
|
'visually-hidden-focusable',
|
||||||
|
|
||||||
|
// Screen reader
|
||||||
|
'sr-only',
|
||||||
|
|
||||||
|
// Container
|
||||||
|
'container',
|
||||||
|
'container-fluid',
|
||||||
|
|
||||||
|
// Row
|
||||||
|
'row',
|
||||||
|
|
||||||
|
// Display
|
||||||
|
'd-flex',
|
||||||
|
'd-none',
|
||||||
|
'd-block',
|
||||||
|
'd-inline-block',
|
||||||
|
'd-inline',
|
||||||
|
'd-grid',
|
||||||
|
|
||||||
|
// Common spacing
|
||||||
|
'mb-0', 'mb-1', 'mb-2', 'mb-3', 'mb-4', 'mb-5',
|
||||||
|
'mt-0', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mt-5',
|
||||||
|
'me-0', 'me-1', 'me-2', 'me-3', 'me-4', 'me-5',
|
||||||
|
'ms-0', 'ms-1', 'ms-2', 'ms-3', 'ms-4', 'ms-5',
|
||||||
|
'mx-auto',
|
||||||
|
'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5',
|
||||||
|
'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5',
|
||||||
|
'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5',
|
||||||
|
'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5',
|
||||||
|
'g-0', 'g-1', 'g-2', 'g-3', 'g-4', 'g-5',
|
||||||
|
|
||||||
|
// Flex
|
||||||
|
'flex-wrap',
|
||||||
|
'flex-nowrap',
|
||||||
|
'flex-column',
|
||||||
|
'flex-row',
|
||||||
|
'justify-content-center',
|
||||||
|
'justify-content-between',
|
||||||
|
'justify-content-start',
|
||||||
|
'justify-content-end',
|
||||||
|
'align-items-center',
|
||||||
|
'align-items-start',
|
||||||
|
'align-items-end',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
'text-center',
|
||||||
|
'text-start',
|
||||||
|
'text-end',
|
||||||
|
'text-white',
|
||||||
|
'text-muted',
|
||||||
|
'fw-bold',
|
||||||
|
'fw-normal',
|
||||||
|
'small',
|
||||||
|
|
||||||
|
// Images
|
||||||
|
'img-fluid',
|
||||||
|
|
||||||
|
// Border/rounded
|
||||||
|
'rounded',
|
||||||
|
'rounded-circle',
|
||||||
|
'border',
|
||||||
|
'border-0',
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
'shadow',
|
||||||
|
'shadow-sm',
|
||||||
|
'shadow-lg',
|
||||||
|
|
||||||
|
// Width
|
||||||
|
'w-100',
|
||||||
|
'w-auto',
|
||||||
|
'h-100',
|
||||||
|
'h-auto',
|
||||||
|
|
||||||
|
// Toast classes (plugin IP View Limit)
|
||||||
|
'toast-container',
|
||||||
|
'toast',
|
||||||
|
'toast-body',
|
||||||
|
'position-fixed',
|
||||||
|
'bottom-0',
|
||||||
|
'end-0',
|
||||||
|
'start-50',
|
||||||
|
'translate-middle-x',
|
||||||
|
'text-dark',
|
||||||
|
'bg-warning',
|
||||||
|
'btn-close',
|
||||||
|
'm-auto',
|
||||||
|
],
|
||||||
|
|
||||||
|
deep: [
|
||||||
|
// Grid responsive
|
||||||
|
/^col-/,
|
||||||
|
/^col$/,
|
||||||
|
|
||||||
|
// Display responsive
|
||||||
|
/^d-[a-z]+-/,
|
||||||
|
|
||||||
|
// Navbar responsive
|
||||||
|
/^navbar-expand/,
|
||||||
|
/^navbar-/,
|
||||||
|
|
||||||
|
// Responsive margins/padding
|
||||||
|
/^m[tbsexy]?-[a-z]+-/,
|
||||||
|
/^p[tbsexy]?-[a-z]+-/,
|
||||||
|
|
||||||
|
// Text responsive
|
||||||
|
/^text-[a-z]+-/,
|
||||||
|
|
||||||
|
// Flex responsive
|
||||||
|
/^flex-[a-z]+-/,
|
||||||
|
/^justify-content-[a-z]+-/,
|
||||||
|
/^align-items-[a-z]+-/,
|
||||||
|
],
|
||||||
|
|
||||||
|
greedy: [
|
||||||
|
// Form controls
|
||||||
|
/form-/,
|
||||||
|
/input-/,
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
/btn/,
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
/card/,
|
||||||
|
|
||||||
|
// Navbar
|
||||||
|
/navbar/,
|
||||||
|
/nav-/,
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
/table/,
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
/alert/,
|
||||||
|
|
||||||
|
// Toast
|
||||||
|
/toast/,
|
||||||
|
|
||||||
|
// Badges
|
||||||
|
/badge/,
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
/list-/,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mantener variables CSS de Bootstrap
|
||||||
|
variables: true,
|
||||||
|
|
||||||
|
// Mantener keyframes
|
||||||
|
keyframes: true,
|
||||||
|
|
||||||
|
// Mantener font-face
|
||||||
|
fontFace: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (purgeCSSResult.length === 0 || !purgeCSSResult[0].css) {
|
||||||
|
console.error('ERROR: PurgeCSS returned empty result');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST-PROCESAMIENTO: Remover propiedades que son manejadas por CSS crítico
|
||||||
|
// Esto evita que bootstrap-subset.min.css sobrescriba los estilos críticos
|
||||||
|
let processedCSS = purgeCSSResult[0].css;
|
||||||
|
|
||||||
|
// Remover position:relative de .navbar (manejado por CriticalCSSService con sticky)
|
||||||
|
// Regex: encuentra .navbar{...position:relative...} y remueve solo position:relative
|
||||||
|
processedCSS = processedCSS.replace(
|
||||||
|
/\.navbar\{([^}]*?)position:relative;?([^}]*)\}/g,
|
||||||
|
'.navbar{$1$2}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limpiar posibles dobles punto y coma o punto y coma antes de }
|
||||||
|
processedCSS = processedCSS.replace(/;;+/g, ';');
|
||||||
|
processedCSS = processedCSS.replace(/;\}/g, '}');
|
||||||
|
|
||||||
|
console.log('\nPost-processing: Removed position:relative from .navbar (handled by CriticalCSSService)');
|
||||||
|
|
||||||
|
// Agregar header al CSS generado
|
||||||
|
const header = `/**
|
||||||
|
* Bootstrap 5.3.2 Subset - ROI Theme
|
||||||
|
*
|
||||||
|
* Generado automáticamente con PurgeCSS
|
||||||
|
* Contiene SOLO las clases Bootstrap usadas en el tema.
|
||||||
|
*
|
||||||
|
* Original: ${(inputSize / 1024).toFixed(2)} KB
|
||||||
|
* Subset: ${(processedCSS.length / 1024).toFixed(2)} KB
|
||||||
|
* Reduccion: ${(100 - (processedCSS.length / inputSize * 100)).toFixed(1)}%
|
||||||
|
*
|
||||||
|
* Generado: ${new Date().toISOString()}
|
||||||
|
*
|
||||||
|
* Para regenerar:
|
||||||
|
* node build-bootstrap-subset.js
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const outputCSS = header + processedCSS;
|
||||||
|
|
||||||
|
// Escribir archivo
|
||||||
|
fs.writeFileSync(outputFile, outputCSS);
|
||||||
|
|
||||||
|
const outputSize = fs.statSync(outputFile).size;
|
||||||
|
const reduction = ((1 - outputSize / inputSize) * 100).toFixed(1);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('SUCCESS!');
|
||||||
|
console.log('-'.repeat(60));
|
||||||
|
console.log(`Output: bootstrap-subset.min.css (${(outputSize / 1024).toFixed(2)} KB)`);
|
||||||
|
console.log(`Reduction: ${reduction}% smaller`);
|
||||||
|
console.log('-'.repeat(60));
|
||||||
|
console.log('');
|
||||||
|
console.log('Next steps:');
|
||||||
|
console.log('1. Update Inc/enqueue-scripts.php to use bootstrap-subset.min.css');
|
||||||
|
console.log('2. Test the theme thoroughly');
|
||||||
|
console.log('3. Run PageSpeed Insights to verify improvement');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildBootstrapSubset();
|
||||||
206
front-page.php
206
front-page.php
@@ -2,7 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying the static front page
|
* 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
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#front-page
|
||||||
*
|
*
|
||||||
@@ -13,118 +14,105 @@
|
|||||||
get_header();
|
get_header();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Hero Title Section (Template líneas 322-345) -->
|
<?php while (have_posts()) : the_post(); ?>
|
||||||
<div class="container-fluid py-5 mb-4 hero-title">
|
|
||||||
<div class="container">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
|
<!-- Hero Section - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Main Content Column (col-lg-9) -->
|
||||||
|
<div class="col-lg-9">
|
||||||
|
|
||||||
|
<!-- Featured Image - Componente dinámico -->
|
||||||
<?php
|
<?php
|
||||||
while ( have_posts() ) :
|
if (function_exists('roi_render_component')) {
|
||||||
the_post();
|
echo roi_render_component('featured-image');
|
||||||
|
}
|
||||||
// Categories Section
|
|
||||||
$categories = get_the_category();
|
|
||||||
if ( ! empty( $categories ) && count( $categories ) > 0 ) :
|
|
||||||
?>
|
|
||||||
<div class="mb-3 d-flex justify-content-center">
|
|
||||||
<div class="d-flex gap-2 flex-wrap justify-content-center">
|
|
||||||
<?php
|
|
||||||
// Limit to 3 categories max
|
|
||||||
$cat_count = 0;
|
|
||||||
foreach ( $categories as $category ) :
|
|
||||||
if ( $cat_count >= 3 ) break;
|
|
||||||
if ( $category->slug === 'uncategorized' ) continue;
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url( get_category_link( $category->term_id ) ); ?>" class="category-badge category-badge-hero">
|
|
||||||
<i class="bi bi-folder-fill me-1"></i>
|
|
||||||
<?php echo esc_html( $category->name ); ?>
|
|
||||||
</a>
|
|
||||||
<?php
|
|
||||||
$cat_count++;
|
|
||||||
endforeach;
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Page Title -->
|
|
||||||
<h1 class="display-5 fw-bold text-center">
|
|
||||||
<?php the_title(); ?>
|
|
||||||
</h1>
|
|
||||||
<?php endwhile; ?>
|
|
||||||
</div><!-- .container -->
|
|
||||||
</div><!-- .hero-title -->
|
|
||||||
|
|
||||||
<main id="main-content" class="site-main front-page" role="main">
|
|
||||||
|
|
||||||
<!-- Container Bootstrap (Template línea 347) -->
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<?php
|
|
||||||
while ( have_posts() ) :
|
|
||||||
the_post();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
|
||||||
|
|
||||||
<!-- Front Page Content -->
|
|
||||||
<div class="entry-content">
|
|
||||||
<?php
|
|
||||||
the_content();
|
|
||||||
|
|
||||||
// Display page links for paginated pages
|
|
||||||
wp_link_pages(
|
|
||||||
array(
|
|
||||||
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'roi-theme' ),
|
|
||||||
'after' => '</div>',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</div><!-- .entry-content -->
|
|
||||||
|
|
||||||
<!-- Front Page Footer -->
|
|
||||||
<?php if ( get_edit_post_link() ) : ?>
|
|
||||||
<footer class="entry-footer">
|
|
||||||
<?php
|
|
||||||
// Edit post link for logged-in users with permission
|
|
||||||
edit_post_link(
|
|
||||||
sprintf(
|
|
||||||
wp_kses(
|
|
||||||
/* translators: %s: Page title. Only visible to screen readers. */
|
|
||||||
__( 'Edit<span class="screen-reader-text"> "%s"</span>', 'roi-theme' ),
|
|
||||||
array(
|
|
||||||
'span' => array(
|
|
||||||
'class' => array(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
get_the_title()
|
|
||||||
),
|
|
||||||
'<span class="edit-link">',
|
|
||||||
'</span>'
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</footer><!-- .entry-footer -->
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</article><!-- #post-<?php the_ID(); ?> -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Display comments section if enabled
|
|
||||||
if ( comments_open() || get_comments_number() ) :
|
|
||||||
comments_template();
|
|
||||||
endif;
|
|
||||||
|
|
||||||
endwhile; // End of the loop.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to display additional content on front page
|
|
||||||
* This can be used to add featured posts, testimonials, etc.
|
|
||||||
*/
|
|
||||||
do_action( 'roi_front_page_content' );
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
</div><!-- .container -->
|
<!-- Page Content -->
|
||||||
|
<article id="post-<?php the_ID(); ?>" <?php post_class('post-content'); ?>>
|
||||||
|
<?php
|
||||||
|
the_content();
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
wp_link_pages(array(
|
||||||
|
'before' => '<div class="page-links">' . esc_html__('Pages:', 'roi-theme'),
|
||||||
|
'after' => '</div>',
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Share Buttons - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('social-share');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Post - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-post');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Related Posts - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('related-post');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Ad After Related Posts -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_ad_slot')) {
|
||||||
|
echo roi_render_ad_slot('after-related');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .col-lg-9 -->
|
||||||
|
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- .row -->
|
||||||
|
</div><!-- .container -->
|
||||||
|
|
||||||
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<?php endwhile; ?>
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
@@ -380,3 +380,127 @@ add_action('after_setup_theme', function() {
|
|||||||
// desde la base de datos a través de sus respectivos Renderers.
|
// desde la base de datos a través de sus respectivos Renderers.
|
||||||
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
|
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPER FUNCTION: roi_get_adsense_search_config()
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la configuracion de AdSense para resultados de busqueda
|
||||||
|
*
|
||||||
|
* Esta funcion es la API publica que el plugin roi-apu-search consume.
|
||||||
|
* El plugin NO debe acceder directamente a la tabla del tema.
|
||||||
|
*
|
||||||
|
* OPTIMIZACION: Una sola query carga todos los settings del componente.
|
||||||
|
*
|
||||||
|
* @return array Configuracion para JavaScript
|
||||||
|
*/
|
||||||
|
function roi_get_adsense_search_config(): array {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// CARGAR TODOS LOS SETTINGS EN UNA SOLA QUERY
|
||||||
|
// =========================================================================
|
||||||
|
$table = $wpdb->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'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
60
minify-css.php
Normal file
60
minify-css.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simple CSS Minifier Script
|
||||||
|
* Run from command line: php minify-css.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
function minify_css($css) {
|
||||||
|
// Remove comments
|
||||||
|
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
||||||
|
|
||||||
|
// Remove space after colons
|
||||||
|
$css = str_replace(': ', ':', $css);
|
||||||
|
|
||||||
|
// Remove whitespace
|
||||||
|
$css = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ', ' '), '', $css);
|
||||||
|
|
||||||
|
// Remove space before and after specific characters
|
||||||
|
$css = preg_replace('/\s*([{};,>+~])\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";
|
||||||
192
page.php
192
page.php
@@ -2,9 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* The template for displaying all pages
|
* The template for displaying all pages
|
||||||
*
|
*
|
||||||
* This is the template that displays all pages by default.
|
* Replica la estructura de single.php para consistencia visual.
|
||||||
* Please note that this is the WordPress construct of pages and that
|
* Grid layout: col-lg-9 (contenido) + col-lg-3 (sidebar)
|
||||||
* other 'pages' on your WordPress site will use a different template.
|
|
||||||
*
|
*
|
||||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-page
|
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-page
|
||||||
*
|
*
|
||||||
@@ -15,112 +14,105 @@
|
|||||||
get_header();
|
get_header();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<main id="main-content" class="site-main" role="main">
|
<?php while (have_posts()) : the_post(); ?>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<main id="main-content" class="site-main" role="main">
|
||||||
|
|
||||||
<!-- Primary Content Area -->
|
<!-- Hero Section - Componente dinámico -->
|
||||||
<div id="primary" class="content-area">
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('hero');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<!-- Main Content Grid -->
|
||||||
while ( have_posts() ) :
|
<div class="container">
|
||||||
the_post();
|
<div class="row">
|
||||||
?>
|
|
||||||
|
|
||||||
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
<!-- Main Content Column (col-lg-9) -->
|
||||||
|
<div class="col-lg-9">
|
||||||
<!-- Featured Image -->
|
|
||||||
<?php if ( has_post_thumbnail() ) : ?>
|
|
||||||
<div class="post-thumbnail">
|
|
||||||
<?php
|
|
||||||
the_post_thumbnail(
|
|
||||||
'roi-featured-large',
|
|
||||||
array(
|
|
||||||
'alt' => the_title_attribute(
|
|
||||||
array(
|
|
||||||
'echo' => false,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'loading' => 'eager',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Page Header -->
|
|
||||||
<header class="entry-header">
|
|
||||||
<h1 class="entry-title">
|
|
||||||
<?php the_title(); ?>
|
|
||||||
</h1>
|
|
||||||
</header><!-- .entry-header -->
|
|
||||||
|
|
||||||
<!-- Page Content -->
|
|
||||||
<div class="entry-content">
|
|
||||||
<?php
|
|
||||||
the_content();
|
|
||||||
|
|
||||||
// Display page links for paginated pages
|
|
||||||
wp_link_pages(
|
|
||||||
array(
|
|
||||||
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'roi-theme' ),
|
|
||||||
'after' => '</div>',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</div><!-- .entry-content -->
|
|
||||||
|
|
||||||
<!-- Page Footer -->
|
|
||||||
<?php if ( get_edit_post_link() ) : ?>
|
|
||||||
<footer class="entry-footer">
|
|
||||||
<?php
|
|
||||||
// Edit post link for logged-in users with permission
|
|
||||||
edit_post_link(
|
|
||||||
sprintf(
|
|
||||||
wp_kses(
|
|
||||||
/* translators: %s: Page title. Only visible to screen readers. */
|
|
||||||
__( 'Edit<span class="screen-reader-text"> "%s"</span>', 'roi-theme' ),
|
|
||||||
array(
|
|
||||||
'span' => array(
|
|
||||||
'class' => array(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
get_the_title()
|
|
||||||
),
|
|
||||||
'<span class="edit-link">',
|
|
||||||
'</span>'
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</footer><!-- .entry-footer -->
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</article><!-- #post-<?php the_ID(); ?> -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Display comments section if enabled
|
|
||||||
if ( comments_open() || get_comments_number() ) :
|
|
||||||
comments_template();
|
|
||||||
endif;
|
|
||||||
|
|
||||||
endwhile; // End of the loop.
|
|
||||||
?>
|
|
||||||
|
|
||||||
</div><!-- #primary -->
|
|
||||||
|
|
||||||
|
<!-- Featured Image - Componente dinámico -->
|
||||||
<?php
|
<?php
|
||||||
/**
|
if (function_exists('roi_render_component')) {
|
||||||
* Sidebar
|
echo roi_render_component('featured-image');
|
||||||
* Display the sidebar if it's active.
|
}
|
||||||
*/
|
|
||||||
if ( is_active_sidebar( 'sidebar-1' ) ) :
|
|
||||||
get_sidebar();
|
|
||||||
endif;
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
</div><!-- .content-wrapper -->
|
<!-- Page Content -->
|
||||||
|
<article id="post-<?php the_ID(); ?>" <?php post_class('post-content'); ?>>
|
||||||
|
<?php
|
||||||
|
the_content();
|
||||||
|
|
||||||
</main><!-- #main-content -->
|
wp_link_pages(array(
|
||||||
|
'before' => '<div class="page-links">' . esc_html__('Pages:', 'roi-theme'),
|
||||||
|
'after' => '</div>',
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Share Buttons - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('social-share');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Post - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-post');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Related Posts - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('related-post');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Ad After Related Posts -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_ad_slot')) {
|
||||||
|
echo roi_render_ad_slot('after-related');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
</div><!-- .col-lg-9 -->
|
||||||
|
|
||||||
|
<!-- Sidebar Column (col-lg-3) -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<!-- Table of Contents - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('table-of-contents');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- CTA Box Sidebar - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('cta-box-sidebar');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- .row -->
|
||||||
|
</div><!-- .container -->
|
||||||
|
|
||||||
|
</main><!-- #main-content -->
|
||||||
|
|
||||||
|
<?php endwhile; ?>
|
||||||
|
|
||||||
|
<!-- Contact Form Section - Componente dinámico -->
|
||||||
|
<?php
|
||||||
|
if (function_exists('roi_render_component')) {
|
||||||
|
echo roi_render_component('contact-form');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
get_footer();
|
get_footer();
|
||||||
|
|||||||
Reference in New Issue
Block a user