diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php index b3c677db..c4abf4ec 100644 --- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php +++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php @@ -37,8 +37,11 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface 'adsense-placementPostTopEnabled' => ['group' => 'behavior', 'attribute' => 'post_top_enabled'], 'adsense-placementPostTopFormat' => ['group' => 'behavior', 'attribute' => 'post_top_format'], 'adsense-placementPostContentEnabled' => ['group' => 'behavior', 'attribute' => 'post_content_enabled'], - 'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'], + 'adsense-placementPostContentRandomMode' => ['group' => 'behavior', 'attribute' => 'post_content_random_mode'], + 'adsense-placementPostContentMinAds' => ['group' => 'behavior', 'attribute' => 'post_content_min_ads'], 'adsense-placementPostContentMaxAds' => ['group' => 'behavior', 'attribute' => 'post_content_max_ads'], + 'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'], + 'adsense-placementPostContentMinParagraphsBetween' => ['group' => 'behavior', 'attribute' => 'post_content_min_paragraphs_between'], 'adsense-placementPostContentFormat' => ['group' => 'behavior', 'attribute' => 'post_content_format'], 'adsense-placementPostBottomEnabled' => ['group' => 'behavior', 'attribute' => 'post_bottom_enabled'], 'adsense-placementPostBottomFormat' => ['group' => 'behavior', 'attribute' => 'post_bottom_format'], diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php index a7786657..ba21ba02 100644 --- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php +++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php @@ -7,6 +7,11 @@ use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer; /** * FormBuilder para AdSense Placement y Google Analytics + * + * Panel reorganizado con: + * - Diagrama visual de ubicaciones + * - Secciones colapsables + * - In-content ads configurables (1-8 random) */ final class AdsensePlacementFormBuilder { @@ -27,7 +32,7 @@ final class AdsensePlacementFormBuilder $html .= ' AdSense y Analytics'; $html .= ' '; $html .= '

'; - $html .= ' Configura Google AdSense y Google Analytics'; + $html .= ' Configura Google AdSense y Analytics con ubicaciones visuales'; $html .= '

'; $html .= ' '; $html .= ' '; @@ -36,18 +41,19 @@ final class AdsensePlacementFormBuilder // LAYOUT 2 COLUMNAS $html .= '
'; - // COLUMNA IZQUIERDA - $html .= '
'; + // COLUMNA IZQUIERDA (7 cols) + $html .= '
'; $html .= $this->buildVisibilityGroup($componentId); - $html .= $this->buildAnalyticsGroup($componentId); - $html .= $this->buildCredentialsGroup($componentId); + $html .= $this->buildDiagramSection(); $html .= $this->buildPostLocationsGroup($componentId); + $html .= $this->buildInContentAdsGroup($componentId); $html .= '
'; - // COLUMNA DERECHA - $html .= '
'; + // COLUMNA DERECHA (5 cols) + $html .= '
'; + $html .= $this->buildCredentialsGroup($componentId); + $html .= $this->buildAnalyticsGroup($componentId); $html .= $this->buildRailAdsGroup($componentId); - $html .= $this->buildArchiveLocationsGroup($componentId); $html .= $this->buildExclusionsGroup($componentId); $html .= '
'; @@ -58,24 +64,27 @@ final class AdsensePlacementFormBuilder private function buildVisibilityGroup(string $cid): string { - $html = '
'; + $html = '
'; $html .= '
'; $html .= '
'; - $html .= ' '; - $html .= ' Activacion AdSense'; + $html .= ' '; + $html .= ' Activacion Global'; $html .= '
'; - // Switch: Enabled + $html .= '
'; + $html .= '
'; $enabled = $this->renderer->getFieldValue($cid, 'visibility', 'is_enabled', false); $html .= $this->buildSwitch($cid . 'Enabled', 'Activar AdSense', $enabled, 'bi-power'); - - // Switch: Show on Mobile + $html .= '
'; + $html .= '
'; $showMobile = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_mobile', true); $html .= $this->buildSwitch($cid . 'ShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone'); - - // Switch: Show on Desktop + $html .= '
'; + $html .= '
'; $showDesktop = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_desktop', true); $html .= $this->buildSwitch($cid . 'ShowOnDesktop', 'Mostrar en escritorio', $showDesktop, 'bi-display'); + $html .= '
'; + $html .= '
'; $html .= '
'; $html .= '
'; @@ -83,32 +92,261 @@ final class AdsensePlacementFormBuilder return $html; } - private function buildAnalyticsGroup(string $cid): string + /** + * Diagrama visual de ubicaciones de anuncios + */ + private function buildDiagramSection(): string { - $html = '
'; + $html = '
'; $html .= '
'; $html .= '
'; - $html .= ' '; - $html .= ' Google Analytics'; + $html .= ' '; + $html .= ' Mapa de Ubicaciones'; $html .= '
'; - // Switch: Analytics Enabled - $analyticsEnabled = $this->renderer->getFieldValue($cid, 'analytics', 'analytics_enabled', false); - $html .= $this->buildSwitch($cid . 'AnalyticsEnabled', 'Activar Analytics', $analyticsEnabled, 'bi-power'); + // Diagrama visual del layout + $html .= '
'; - // Tracking ID - $gaTrackingId = $this->renderer->getFieldValue($cid, 'analytics', 'ga_tracking_id', ''); - $html .= $this->buildTextInput($cid . 'GaTrackingId', 'Google Analytics ID', $gaTrackingId, 'G-XXXXXXXXXX'); - $html .= '
Formato: G-XXXXXXXXXX o UA-XXXXXXXX-X
'; + // Header + $html .= '
'; + $html .= ' HEADER'; + $html .= '
'; - // Anonymize IP - $gaAnonymizeIp = $this->renderer->getFieldValue($cid, 'analytics', 'ga_anonymize_ip', true); - $html .= $this->buildSwitch($cid . 'GaAnonymizeIp', 'Anonimizar IP (GDPR)', $gaAnonymizeIp, 'bi-shield-check'); + // Hero / Featured Image + $html .= '
'; + $html .= ' Featured Image / Hero'; + $html .= '
'; - $html .= '
'; - $html .= ' '; - $html .= ' Recomendado activar para cumplir con GDPR/RGPD'; - $html .= '
'; + // Ad: Post Top + $html .= '
'; + $html .= ' 📍 POST-TOP (Despues de imagen)'; + $html .= '
'; + + // Content container + $html .= '
'; + $html .= '
📝 CONTENIDO DEL POST
'; + $html .= '
Parrafo 1...
'; + $html .= '
Parrafo 2...
'; + $html .= '
Parrafo 3...
'; + + // In-content ad + $html .= '
'; + $html .= ' 📍 IN-CONTENT #1'; + $html .= '
'; + + $html .= '
Parrafo 4...
'; + $html .= '
Parrafo 5...
'; + $html .= '
Parrafo 6...
'; + + // In-content ad 2 + $html .= '
'; + $html .= ' 📍 IN-CONTENT #2 (random)'; + $html .= '
'; + + $html .= '
Mas parrafos...
'; + $html .= '
'; + + // Ad: Post Bottom + $html .= '
'; + $html .= ' 📍 POST-BOTTOM (Despues del contenido)'; + $html .= '
'; + + // Related Posts + $html .= '
'; + $html .= ' Related Posts'; + $html .= '
'; + + // Ad: After Related + $html .= '
'; + $html .= ' 📍 AFTER-RELATED'; + $html .= '
'; + + // Footer + $html .= '
'; + $html .= ' FOOTER'; + $html .= '
'; + + // Rail Ads (laterales) + $html .= '
'; + $html .= '
'; + $html .= ' 📍 RAIL IZQ
(160x600)'; + $html .= '
'; + $html .= '
'; + $html .= ' 📍 RAIL DER
(160x600)'; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + + $html .= '
'; + $html .= ' Los anuncios amarillos son configurables abajo.'; + $html .= ' Los rojos solo aparecen en pantallas >1600px.'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + private function buildPostLocationsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Ubicaciones en Posts'; + $html .= '
'; + + // === POST-TOP === + $html .= '
'; + $html .= '
'; + $html .= ' POST-TOP'; + $html .= ' Despues de la imagen destacada'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $postTopEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_top_enabled', true); + $html .= $this->buildSwitch($cid . 'PostTopEnabled', 'Activar', $postTopEnabled); + $html .= '
'; + $html .= '
'; + $html .= $this->buildSelect($cid . 'PostTopFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'post_top_format', 'auto'), + [ + 'auto' => 'Auto (responsive)', + 'in-article' => 'In-Article (fluid)', + 'display' => 'Display (728x90)', + 'display-large' => 'Display Large (970x250)' + ] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + // === POST-BOTTOM === + $html .= '
'; + $html .= '
'; + $html .= ' POST-BOTTOM'; + $html .= ' Despues del contenido, antes de Related'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $postBottomEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_enabled', true); + $html .= $this->buildSwitch($cid . 'PostBottomEnabled', 'Activar', $postBottomEnabled); + $html .= '
'; + $html .= '
'; + $html .= $this->buildSelect($cid . 'PostBottomFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_format', 'auto'), + ['auto' => 'Auto', 'in-article' => 'In-Article', 'display' => 'Display'] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + // === AFTER-RELATED === + $html .= '
'; + $html .= '
'; + $html .= ' AFTER-RELATED'; + $html .= ' Despues de Related Posts'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + $afterRelatedEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'after_related_enabled', false); + $html .= $this->buildSwitch($cid . 'AfterRelatedEnabled', 'Activar', $afterRelatedEnabled); + $html .= '
'; + $html .= '
'; + $html .= $this->buildSelect($cid . 'AfterRelatedFormat', 'Formato', + $this->renderer->getFieldValue($cid, 'behavior', 'after_related_format', 'autorelaxed'), + ['autorelaxed' => 'Autorelaxed (feed)', 'auto' => 'Auto'] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Seccion especial para in-content ads con configuracion de 1-8 random + */ + private function buildInContentAdsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Anuncios Dentro del Contenido'; + $html .= ' 1-8 ads'; + $html .= '
'; + + $html .= '
'; + $html .= ' '; + $html .= ' Modo Random: Inserta entre 1 y 8 anuncios en posiciones aleatorias entre parrafos.'; + $html .= ' Mejor UX al variar la posicion en cada visita.'; + $html .= '
'; + + // Master switch + $postContentEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_enabled', false); + $html .= '
'; + $html .= $this->buildSwitch($cid . 'PostContentEnabled', 'Activar In-Content Ads', $postContentEnabled, 'bi-power'); + $html .= '
'; + + // Configuracion de cantidad + $html .= '
'; + $html .= '
'; + $minAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_ads', '1'); + $html .= $this->buildSelect($cid . 'PostContentMinAds', 'Minimo de anuncios', + is_string($minAdsValue) ? $minAdsValue : '1', + ['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios'] + ); + $html .= '
'; + $html .= '
'; + $maxAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_max_ads', '3'); + $html .= $this->buildSelect($cid . 'PostContentMaxAds', 'Maximo de anuncios', + is_string($maxAdsValue) ? $maxAdsValue : '3', + [ + '1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios', + '5' => '5 anuncios', '6' => '6 anuncios', '7' => '7 anuncios', '8' => '8 anuncios' + ] + ); + $html .= '
'; + $html .= '
'; + + // Configuracion de posicionamiento + $html .= '
'; + $html .= '
'; + $afterPara = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_after_paragraphs', '3'); + $html .= $this->buildTextInput($cid . 'PostContentAfterParagraphs', 'Primer ad despues del parrafo #', (string)$afterPara, '3'); + $html .= '
'; + $html .= '
'; + $minBetweenValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_paragraphs_between', '4'); + $html .= $this->buildSelect($cid . 'PostContentMinParagraphsBetween', 'Parrafos entre ads', + is_string($minBetweenValue) ? $minBetweenValue : '4', + ['2' => '2 parrafos', '3' => '3 parrafos', '4' => '4 parrafos', '5' => '5 parrafos', '6' => '6 parrafos'] + ); + $html .= '
'; + $html .= '
'; + + // Modo y formato + $html .= '
'; + $html .= '
'; + $randomMode = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_random_mode', true); + $html .= $this->buildSwitch($cid . 'PostContentRandomMode', 'Posiciones aleatorias', $randomMode, 'bi-shuffle'); + $html .= '
'; + $html .= '
'; + $formatValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_format', 'in-article'); + $html .= $this->buildSelect($cid . 'PostContentFormat', 'Formato de ads', + is_string($formatValue) ? $formatValue : 'in-article', + ['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)'] + ); + $html .= '
'; + $html .= '
'; $html .= '
'; $html .= '
'; @@ -129,28 +367,38 @@ final class AdsensePlacementFormBuilder $pubId = $this->renderer->getFieldValue($cid, 'content', 'publisher_id', 'ca-pub-8476420265998726'); $html .= $this->buildTextInput($cid . 'PublisherId', 'Publisher ID', $pubId, 'ca-pub-XXXXX'); - // Slots (grid 2 columnas) - $html .= '
'; - $html .= '
'; - $slotDisplay = $this->renderer->getFieldValue($cid, 'content', 'slot_display', '2873062302'); - $html .= $this->buildTextInput($cid . 'SlotDisplay', 'Slot Display', $slotDisplay); - $html .= '
'; - $html .= '
'; + $html .= '
'; + $html .= '

Slots por tipo de anuncio:

'; + + // Slots con descripciones claras + $html .= '
'; $slotAuto = $this->renderer->getFieldValue($cid, 'content', 'slot_auto', '8471732096'); - $html .= $this->buildTextInput($cid . 'SlotAuto', 'Slot Auto', $slotAuto); - $html .= '
'; - $html .= '
'; - $slotRelaxed = $this->renderer->getFieldValue($cid, 'content', 'slot_autorelaxed', '9205569855'); - $html .= $this->buildTextInput($cid . 'SlotAutorelaxed', 'Slot Autorelaxed', $slotRelaxed); - $html .= '
'; - $html .= '
'; + $html .= $this->buildTextInput($cid . 'SlotAuto', '📱 Auto (responsive)', $slotAuto); + $html .= '
Para: Post-Top, Post-Bottom, globales
'; + $html .= '
'; + + $html .= '
'; $slotInArticle = $this->renderer->getFieldValue($cid, 'content', 'slot_inarticle', '7285187368'); - $html .= $this->buildTextInput($cid . 'SlotInarticle', 'Slot In-Article', $slotInArticle); - $html .= '
'; - $html .= '
'; + $html .= $this->buildTextInput($cid . 'SlotInarticle', '📝 In-Article (fluid)', $slotInArticle); + $html .= '
Para: In-Content (dentro del texto)
'; + $html .= '
'; + + $html .= '
'; + $slotDisplay = $this->renderer->getFieldValue($cid, 'content', 'slot_display', '2873062302'); + $html .= $this->buildTextInput($cid . 'SlotDisplay', '🖥️ Display (fijo)', $slotDisplay); + $html .= '
Para: 728x90, 970x250 (opcional)
'; + $html .= '
'; + + $html .= '
'; + $slotRelaxed = $this->renderer->getFieldValue($cid, 'content', 'slot_autorelaxed', '9205569855'); + $html .= $this->buildTextInput($cid . 'SlotAutorelaxed', '📋 Autorelaxed (feed)', $slotRelaxed); + $html .= '
Para: After-Related, archives
'; + $html .= '
'; + + $html .= '
'; $slotSkyscraper = $this->renderer->getFieldValue($cid, 'content', 'slot_skyscraper', ''); - $html .= $this->buildTextInput($cid . 'SlotSkyscraper', 'Slot Skyscraper (Rail Ads)', $slotSkyscraper); - $html .= '
'; + $html .= $this->buildTextInput($cid . 'SlotSkyscraper', '🏢 Skyscraper (tall)', $slotSkyscraper); + $html .= '
Para: Rail Ads laterales (160x600)
'; $html .= '
'; $html .= '
'; @@ -159,59 +407,27 @@ final class AdsensePlacementFormBuilder return $html; } - private function buildPostLocationsGroup(string $cid): string + private function buildAnalyticsGroup(string $cid): string { - $html = '
'; + $html = '
'; $html .= '
'; $html .= '
'; - $html .= ' '; - $html .= ' Ubicaciones en Posts'; + $html .= ' '; + $html .= ' Google Analytics'; $html .= '
'; - // Post Top - $postTopEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_top_enabled', true); - $html .= $this->buildSwitch($cid . 'PostTopEnabled', 'Despues de Featured Image', $postTopEnabled); - $html .= $this->buildSelect($cid . 'PostTopFormat', 'Formato', - $this->renderer->getFieldValue($cid, 'behavior', 'post_top_format', 'auto'), - ['auto' => 'Auto (responsive)', 'in-article' => 'In-Article', 'display' => 'Display (728x90)', 'display-large' => 'Display Large (970x250)'] - ); + // Switch: Analytics Enabled + $analyticsEnabled = $this->renderer->getFieldValue($cid, 'analytics', 'analytics_enabled', false); + $html .= $this->buildSwitch($cid . 'AnalyticsEnabled', 'Activar Analytics', $analyticsEnabled, 'bi-power'); - // Post Content - $postContentEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_enabled', false); - $html .= $this->buildSwitch($cid . 'PostContentEnabled', 'Insertar dentro del contenido', $postContentEnabled); + // Tracking ID + $gaTrackingId = $this->renderer->getFieldValue($cid, 'analytics', 'ga_tracking_id', ''); + $html .= $this->buildTextInput($cid . 'GaTrackingId', 'Google Analytics ID', $gaTrackingId, 'G-XXXXXXXXXX'); + $html .= '
Formato: G-XXXXXXXXXX (GA4) o UA-XXXXXXXX-X
'; - $html .= '
'; - $html .= '
'; - $afterPara = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_after_paragraphs', '3'); - $html .= $this->buildTextInput($cid . 'PostContentAfterParagraphs', 'Despues parrafo #', $afterPara); - $html .= '
'; - $html .= '
'; - $maxAds = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_max_ads', '2'); - $html .= $this->buildTextInput($cid . 'PostContentMaxAds', 'Max ads', $maxAds); - $html .= '
'; - $html .= '
'; - $html .= $this->buildSelect($cid . 'PostContentFormat', 'Formato', - $this->renderer->getFieldValue($cid, 'behavior', 'post_content_format', 'in-article'), - ['in-article' => 'In-Article', 'auto' => 'Auto'] - ); - $html .= '
'; - $html .= '
'; - - // Post Bottom - $postBottomEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_enabled', true); - $html .= $this->buildSwitch($cid . 'PostBottomEnabled', 'Despues del contenido', $postBottomEnabled); - $html .= $this->buildSelect($cid . 'PostBottomFormat', 'Formato', - $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_format', 'auto'), - ['auto' => 'Auto', 'in-article' => 'In-Article', 'display' => 'Display'] - ); - - // After Related - $afterRelatedEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'after_related_enabled', false); - $html .= $this->buildSwitch($cid . 'AfterRelatedEnabled', 'Despues de Related Posts', $afterRelatedEnabled); - $html .= $this->buildSelect($cid . 'AfterRelatedFormat', 'Formato', - $this->renderer->getFieldValue($cid, 'behavior', 'after_related_format', 'autorelaxed'), - ['autorelaxed' => 'Autorelaxed', 'auto' => 'Auto'] - ); + // Anonymize IP + $gaAnonymizeIp = $this->renderer->getFieldValue($cid, 'analytics', 'ga_anonymize_ip', true); + $html .= $this->buildSwitch($cid . 'GaAnonymizeIp', 'Anonimizar IP (GDPR)', $gaAnonymizeIp, 'bi-shield-check'); $html .= '
'; $html .= '
'; @@ -221,13 +437,14 @@ final class AdsensePlacementFormBuilder private function buildRailAdsGroup(string $cid): string { - $html = '
'; + $html = '
'; $html .= '
'; $html .= '
'; - $html .= ' '; - $html .= ' Rail Ads (Margenes Laterales)'; + $html .= ' '; + $html .= ' Rail Ads (Laterales)'; + $html .= ' >1600px'; $html .= '
'; - $html .= '

Anuncios fijos en los espacios laterales del viewport. Solo visibles en pantallas >= 1600px.

'; + $html .= '

Anuncios fijos en los margenes del viewport. Solo en pantallas muy anchas.

'; // Master switch $railEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_ads_enabled', false); @@ -262,66 +479,28 @@ final class AdsensePlacementFormBuilder return $html; } - private function buildArchiveLocationsGroup(string $cid): string - { - $html = '
'; - $html .= '
'; - $html .= '
'; - $html .= ' '; - $html .= ' Ubicaciones Archives/Globales'; - $html .= '
'; - - // Archive locations - $archiveTopEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_top_enabled', false); - $html .= $this->buildSwitch($cid . 'ArchiveTopEnabled', 'Arriba del listado', $archiveTopEnabled); - - $archiveBetweenEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_between_enabled', false); - $html .= $this->buildSwitch($cid . 'ArchiveBetweenEnabled', 'Entre posts del listado', $archiveBetweenEnabled); - - $archiveEvery = $this->renderer->getFieldValue($cid, 'layout', 'archive_between_every', '4'); - $html .= $this->buildTextInput($cid . 'ArchiveBetweenEvery', 'Mostrar cada X posts', $archiveEvery); - - $archiveBottomEnabled = $this->renderer->getFieldValue($cid, 'layout', 'archive_bottom_enabled', false); - $html .= $this->buildSwitch($cid . 'ArchiveBottomEnabled', 'Abajo del listado', $archiveBottomEnabled); - - // Archive format (aplica a todas las ubicaciones archive) - $html .= $this->buildSelect($cid . 'ArchiveFormat', 'Formato para archives', - $this->renderer->getFieldValue($cid, 'layout', 'archive_format', 'autorelaxed'), - ['autorelaxed' => 'Autorelaxed', 'auto' => 'Auto'] - ); - - $html .= '
'; - $html .= '

Ubicaciones Globales

'; - - // Global locations - $headerBelowEnabled = $this->renderer->getFieldValue($cid, 'layout', 'header_below_enabled', false); - $html .= $this->buildSwitch($cid . 'HeaderBelowEnabled', 'Debajo del header (global)', $headerBelowEnabled); - - $footerAboveEnabled = $this->renderer->getFieldValue($cid, 'layout', 'footer_above_enabled', false); - $html .= $this->buildSwitch($cid . 'FooterAboveEnabled', 'Arriba del footer (global)', $footerAboveEnabled); - - // Global format - $html .= $this->buildSelect($cid . 'GlobalFormat', 'Formato para globales', - $this->renderer->getFieldValue($cid, 'layout', 'global_format', 'auto'), - ['auto' => 'Auto', 'display-large' => 'Display Large (970x250)'] - ); - - $html .= '
'; - $html .= '
'; - - return $html; - } - private function buildExclusionsGroup(string $cid): string { - $html = '
'; + $html = '
'; $html .= '
'; $html .= '
'; - $html .= ' '; + $html .= ' '; $html .= ' Exclusiones y Rendimiento'; $html .= '
'; - // Exclusions + // Accordion para exclusiones + $html .= '
'; + + // Exclusiones + $html .= '
'; + $html .= '

'; + $html .= ' '; + $html .= '

'; + $html .= '
'; + $html .= '
'; + $excludeCats = $this->renderer->getFieldValue($cid, 'forms', 'exclude_categories', ''); $html .= $this->buildTextarea($cid . 'ExcludeCategories', 'Excluir categorias (IDs)', $excludeCats, 'Ej: 5,12,23'); @@ -334,9 +513,18 @@ final class AdsensePlacementFormBuilder $minLength = $this->renderer->getFieldValue($cid, 'forms', 'min_content_length', '500'); $html .= $this->buildTextInput($cid . 'MinContentLength', 'Longitud minima de contenido', $minLength); - // Delay settings + $html .= '
'; + $html .= '
'; + $html .= '
'; + + $html .= '
'; // end accordion + + // Delay settings (siempre visibles) + $html .= '
'; + $html .= '

Rendimiento:

'; + $delayEnabled = $this->renderer->getFieldValue($cid, 'forms', 'delay_enabled', true); - $html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga de anuncios', $delayEnabled, 'bi-hourglass-split'); + $html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga (mejor PageSpeed)', $delayEnabled, 'bi-hourglass-split'); $delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000'); $html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout); diff --git a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php index b1f2ec3c..109f21a9 100644 --- a/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php +++ b/Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php @@ -8,6 +8,11 @@ use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer; /** * Inyecta anuncios dentro del contenido del post * via filtro the_content + * + * Soporta: + * - Modo aleatorio (random) con posiciones variables + * - Configuracion de 1-8 ads maximo + * - Espacio minimo entre anuncios */ final class ContentAdInjector { @@ -31,40 +36,166 @@ final class ContentAdInjector return $content; } + // Obtener configuracion + $minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1); + $maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3); $afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3); - $maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 2); + $minBetween = (int)($this->settings['behavior']['post_content_min_paragraphs_between'] ?? 4); + $randomMode = ($this->settings['behavior']['post_content_random_mode'] ?? true) === true; + + // Validar min <= max + if ($minAds > $maxAds) { + $minAds = $maxAds; + } // Dividir contenido en parrafos - $paragraphs = explode('

', $content); + $paragraphs = $this->splitIntoParagraphs($content); $totalParagraphs = count($paragraphs); + // Necesitamos al menos afterParagraphs + 1 parrafos if ($totalParagraphs < $afterParagraphs + 1) { return $content; } - $adsInserted = 0; + // Calcular posiciones de insercion + $adPositions = $this->calculateAdPositions( + $totalParagraphs, + $afterParagraphs, + $minBetween, + $minAds, + $maxAds, + $randomMode + ); + + if (empty($adPositions)) { + return $content; + } + + // Reconstruir contenido con anuncios insertados + return $this->buildContentWithAds($paragraphs, $adPositions); + } + + /** + * Divide el contenido en parrafos preservando el HTML + */ + private function splitIntoParagraphs(string $content): array + { + // Dividir por

, pero mantener el tag + $parts = preg_split('/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + + $paragraphs = []; + $current = ''; + + foreach ($parts as $part) { + $current .= $part; + if (strtolower($part) === '

') { + $paragraphs[] = $current; + $current = ''; + } + } + + // Si hay contenido restante (sin cerrar), agregarlo + if (!empty(trim($current))) { + $paragraphs[] = $current; + } + + return $paragraphs; + } + + /** + * Calcula las posiciones donde insertar anuncios + * + * @return int[] Indices de parrafos despues de los cuales insertar ads + */ + private function calculateAdPositions( + int $totalParagraphs, + int $afterFirst, + int $minBetween, + int $minAds, + int $maxAds, + bool $randomMode + ): array { + // Calcular posiciones disponibles respetando el espacio minimo + $availablePositions = []; + $lastPosition = $afterFirst; // Primera posicion fija + + // La primera posicion siempre es despues del parrafo indicado + if ($afterFirst < $totalParagraphs) { + $availablePositions[] = $afterFirst; + } + + // Calcular posiciones adicionales respetando minBetween + $nextPosition = $afterFirst + $minBetween; + while ($nextPosition < $totalParagraphs - 1) { // -1 para no insertar al final + $availablePositions[] = $nextPosition; + $nextPosition += $minBetween; + } + + // Determinar cuantos ads insertar + $maxPossible = count($availablePositions); + if ($maxPossible === 0) { + return []; + } + + // Limitar por maxAds y lo que el contenido permite + $actualMax = min($maxAds, $maxPossible); + $actualMin = min($minAds, $actualMax); + + // Determinar cantidad final de ads + if ($randomMode) { + // En modo random, elegir cantidad aleatoria entre min y max + $numAds = rand($actualMin, $actualMax); + } else { + // En modo fijo, usar el maximo posible + $numAds = $actualMax; + } + + if ($numAds === 0) { + return []; + } + + // Seleccionar posiciones + if ($randomMode && $numAds < $maxPossible) { + // Modo random: elegir posiciones aleatorias + // Siempre incluir la primera posicion + $selectedPositions = [$availablePositions[0]]; + + if ($numAds > 1) { + // Elegir aleatoriamente del resto + $remainingPositions = array_slice($availablePositions, 1); + shuffle($remainingPositions); + $additionalPositions = array_slice($remainingPositions, 0, $numAds - 1); + $selectedPositions = array_merge($selectedPositions, $additionalPositions); + } + + // Ordenar para insertar en orden correcto + sort($selectedPositions); + return $selectedPositions; + } else { + // Modo fijo o todas las posiciones necesarias + return array_slice($availablePositions, 0, $numAds); + } + } + + /** + * Reconstruye el contenido insertando anuncios en las posiciones indicadas + */ + private function buildContentWithAds(array $paragraphs, array $adPositions): string + { $newContent = ''; + $adsInserted = 0; foreach ($paragraphs as $index => $paragraph) { $newContent .= $paragraph; - if ($index < $totalParagraphs - 1) { - $newContent .= '

'; - } - + // Verificar si debemos insertar un ad despues de este parrafo + // El indice es 0-based, las posiciones son 1-based (parrafo #3 = index 2) $paragraphNumber = $index + 1; - // Primer anuncio despues del parrafo indicado - if ($paragraphNumber === $afterParagraphs && $adsInserted < $maxAds) { - $newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1)); - $adsInserted++; - } - - // Segundo anuncio a mitad del contenido restante - $midPoint = $afterParagraphs + (int)(($totalParagraphs - $afterParagraphs) / 2); - if ($paragraphNumber === $midPoint && $adsInserted < $maxAds && $maxAds > 1) { - $newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1)); + if (in_array($paragraphNumber, $adPositions, true)) { $adsInserted++; + $adHtml = $this->renderer->renderSlot($this->settings, 'post-content-' . $adsInserted); + $newContent .= $adHtml; } } diff --git a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php index fb6c2c57..ea61cff7 100644 --- a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php +++ b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php @@ -105,6 +105,15 @@ final class AdsensePlacementRenderer { $locationKey = str_replace('-', '_', $location); + // Manejar ubicaciones de in-content (post_content_1, post_content_2, etc.) + if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) { + // In-content ads heredan la configuracion de post_content + return [ + 'enabled' => $settings['behavior']['post_content_enabled'] ?? false, + 'format' => $settings['behavior']['post_content_format'] ?? 'in-article', + ]; + } + // Mapeo de ubicaciones a grupos y campos $locationMap = [ 'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'], diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json index da1189d2..d06987d8 100644 --- a/Schemas/adsense-placement.json +++ b/Schemas/adsense-placement.json @@ -1,10 +1,10 @@ { "component_name": "adsense-placement", - "version": "1.1.0", - "description": "Control de AdSense y Google Analytics", + "version": "1.2.0", + "description": "Control de AdSense y Google Analytics - Panel reorganizado", "groups": { "visibility": { - "label": "Visibilidad AdSense", + "label": "Activacion", "priority": 10, "fields": { "is_enabled": { @@ -130,18 +130,43 @@ "editable": true, "description": "Inserta anuncios automaticamente entre parrafos" }, - "post_content_after_paragraphs": { - "type": "text", - "label": "Despues del parrafo #", - "default": "3", + "post_content_random_mode": { + "type": "boolean", + "label": "Modo aleatorio", + "default": true, "editable": true, - "description": "Numero de parrafo despues del cual insertar" + "description": "Inserta ads en posiciones aleatorias (mejor UX)" + }, + "post_content_min_ads": { + "type": "select", + "label": "Minimo de ads", + "default": "1", + "editable": true, + "options": ["1", "2", "3", "4"], + "description": "Cantidad minima de anuncios a mostrar" }, "post_content_max_ads": { + "type": "select", + "label": "Maximo de ads", + "default": "3", + "editable": true, + "options": ["1", "2", "3", "4", "5", "6", "7", "8"], + "description": "Cantidad maxima de anuncios a mostrar" + }, + "post_content_after_paragraphs": { "type": "text", - "label": "Maximo ads en contenido", - "default": "2", - "editable": true + "label": "Primer ad despues del parrafo #", + "default": "3", + "editable": true, + "description": "Numero de parrafo despues del cual insertar el primer ad" + }, + "post_content_min_paragraphs_between": { + "type": "select", + "label": "Parrafos minimos entre ads", + "default": "4", + "editable": true, + "options": ["2", "3", "4", "5", "6"], + "description": "Espacio minimo entre anuncios consecutivos" }, "post_content_format": { "type": "select", @@ -270,7 +295,7 @@ } }, "forms": { - "label": "Exclusiones", + "label": "Exclusiones y Rendimiento", "priority": 90, "fields": { "exclude_categories": {