diff --git a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
index 270400a9..a5846e36 100644
--- a/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
+++ b/Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php
@@ -73,6 +73,28 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
'adsense-placementMinContentLength' => ['group' => 'forms', 'attribute' => 'min_content_length'],
'adsense-placementDelayEnabled' => ['group' => 'forms', 'attribute' => 'delay_enabled'],
'adsense-placementDelayTimeout' => ['group' => 'forms', 'attribute' => 'delay_timeout'],
+
+ // ANCHOR ADS
+ 'adsense-placementAnchorEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_enabled'],
+ 'adsense-placementAnchorPosition' => ['group' => 'anchor_ads', 'attribute' => 'anchor_position'],
+ 'adsense-placementAnchorHeight' => ['group' => 'anchor_ads', 'attribute' => 'anchor_height'],
+ 'adsense-placementAnchorCollapsibleEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_collapsible_enabled'],
+ 'adsense-placementAnchorShowOnMobile' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_mobile'],
+ 'adsense-placementAnchorShowOnWideScreens' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_wide_screens'],
+ 'adsense-placementAnchorRememberState' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_state'],
+ 'adsense-placementAnchorRememberDuration' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_duration'],
+
+ // VIGNETTE ADS
+ 'adsense-placementVignetteEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_enabled'],
+ 'adsense-placementVignetteTrigger' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger'],
+ 'adsense-placementVignetteTriggerDelay' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger_delay'],
+ 'adsense-placementVignetteSize' => ['group' => 'vignette_ads', 'attribute' => 'vignette_size'],
+ 'adsense-placementVignetteOverlayOpacity' => ['group' => 'vignette_ads', 'attribute' => 'vignette_overlay_opacity'],
+ 'adsense-placementVignetteShowOnMobile' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_mobile'],
+ 'adsense-placementVignetteShowOnDesktop' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_desktop'],
+ 'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
+ 'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
+ 'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
];
}
}
diff --git a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
index 71ca7b81..0e13d3f1 100644
--- a/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
+++ b/Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php
@@ -54,6 +54,8 @@ final class AdsensePlacementFormBuilder
$html .= $this->buildCredentialsGroup($componentId);
$html .= $this->buildAnalyticsGroup($componentId);
$html .= $this->buildRailAdsGroup($componentId);
+ $html .= $this->buildAnchorAdsGroup($componentId);
+ $html .= $this->buildVignetteAdsGroup($componentId);
$html .= $this->buildExclusionsGroup($componentId);
$html .= ' ';
@@ -114,6 +116,11 @@ final class AdsensePlacementFormBuilder
// Diagrama visual del layout
$html .= '
';
diff --git a/Inc/adsense-placement.php b/Inc/adsense-placement.php
index 4cdfe4b9..ee687357 100644
--- a/Inc/adsense-placement.php
+++ b/Inc/adsense-placement.php
@@ -5,10 +5,20 @@
* Funciones para usar en templates:
* - roi_render_ad_slot('post-top')
* - roi_render_rail_ads() - Para los margenes laterales del viewport
+ * - roi_render_anchor_ads() - Anuncios fijos top/bottom
+ * - roi_render_vignette_ad() - Anuncio pantalla completa
*
* Funciones auto-registradas en wp_head:
* - roi_enqueue_adsense_script() - Script principal de AdSense
* - roi_enqueue_analytics_script() - Script de Google Analytics
+ *
+ * Funciones auto-registradas en wp_footer:
+ * - roi_render_rail_ads() (priority 50)
+ * - roi_render_anchor_ads() (priority 51)
+ * - roi_render_vignette_ad() (priority 52)
+ *
+ * Funciones auto-registradas en wp_enqueue_scripts:
+ * - roi_enqueue_anchor_vignette_scripts() - Script JS para Anchor/Vignette
*/
if (!defined('ABSPATH')) {
@@ -374,3 +384,156 @@ function roi_render_universal_analytics_script(string $trackingId, bool $anonymi
echo 'ga("send", "pageview");' . "\n";
echo '' . "\n";
}
+
+/**
+ * Renderiza Anchor Ads (anuncios fijos top/bottom)
+ *
+ * NOTA DIP: El renderer se obtiene del DIContainer, NO se instancia directamente.
+ */
+function roi_render_anchor_ads(): string
+{
+ global $container;
+
+ if ($container === null) {
+ return '';
+ }
+
+ try {
+ $repository = $container->getComponentSettingsRepository();
+ $settings = $repository->getComponentSettings('adsense-placement');
+
+ if (empty($settings)) {
+ return '';
+ }
+
+ // Verificar si ocultar para usuarios logueados
+ if (roi_should_hide_for_logged_in($settings)) {
+ return '';
+ }
+
+ // Verificar exclusiones
+ if (roi_is_ad_excluded($settings)) {
+ return '';
+ }
+
+ // Obtener renderer desde DIContainer (DIP compliant)
+ $renderer = $container->getAdsensePlacementRenderer();
+
+ return $renderer->renderAnchorAds($settings);
+
+ } catch (\Throwable $e) {
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ error_log('ROI Anchor Ads: ' . $e->getMessage());
+ }
+ return '';
+ }
+}
+
+/**
+ * Renderiza Vignette Ad (pantalla completa)
+ *
+ * NOTA DIP: El renderer se obtiene del DIContainer, NO se instancia directamente.
+ */
+function roi_render_vignette_ad(): string
+{
+ global $container;
+
+ if ($container === null) {
+ return '';
+ }
+
+ try {
+ $repository = $container->getComponentSettingsRepository();
+ $settings = $repository->getComponentSettings('adsense-placement');
+
+ if (empty($settings)) {
+ return '';
+ }
+
+ // Verificar si ocultar para usuarios logueados
+ if (roi_should_hide_for_logged_in($settings)) {
+ return '';
+ }
+
+ // Verificar exclusiones
+ if (roi_is_ad_excluded($settings)) {
+ return '';
+ }
+
+ // Obtener renderer desde DIContainer (DIP compliant)
+ $renderer = $container->getAdsensePlacementRenderer();
+
+ return $renderer->renderVignetteAd($settings);
+
+ } catch (\Throwable $e) {
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ error_log('ROI Vignette Ad: ' . $e->getMessage());
+ }
+ return '';
+ }
+}
+
+/**
+ * Hook para inyectar Anchor Ads en el footer
+ */
+add_action('wp_footer', function() {
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo roi_render_anchor_ads();
+}, 51);
+
+/**
+ * Hook para inyectar Vignette Ad en el footer
+ */
+add_action('wp_footer', function() {
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo roi_render_vignette_ad();
+}, 52);
+
+/**
+ * Encola el script JavaScript para Anchor y Vignette Ads
+ */
+function roi_enqueue_anchor_vignette_scripts(): void
+{
+ global $container;
+
+ if ($container === null || is_admin()) {
+ return;
+ }
+
+ try {
+ $repository = $container->getComponentSettingsRepository();
+ $settings = $repository->getComponentSettings('adsense-placement');
+
+ if (!($settings['visibility']['is_enabled'] ?? false)) {
+ return;
+ }
+
+ // Verificar si Anchor o Vignette estan habilitados
+ $anchorEnabled = $settings['anchor_ads']['anchor_enabled'] ?? false;
+ $vignetteEnabled = $settings['vignette_ads']['vignette_enabled'] ?? false;
+
+ if (!$anchorEnabled && !$vignetteEnabled) {
+ return;
+ }
+
+ // Verificar si ocultar para usuarios logueados
+ if (roi_should_hide_for_logged_in($settings)) {
+ return;
+ }
+
+ // Encolar script
+ wp_enqueue_script(
+ 'roi-anchor-vignette',
+ get_template_directory_uri() . '/Public/AdsensePlacement/Infrastructure/Ui/Assets/anchor-vignette.js',
+ [],
+ '1.0.0',
+ true // En footer
+ );
+
+ } catch (\Throwable $e) {
+ if (defined('WP_DEBUG') && WP_DEBUG) {
+ error_log('ROI Anchor/Vignette Scripts: ' . $e->getMessage());
+ }
+ }
+}
+add_action('wp_enqueue_scripts', 'roi_enqueue_anchor_vignette_scripts');
diff --git a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php
index ee56e8f2..55ce284a 100644
--- a/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php
+++ b/Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php
@@ -549,4 +549,395 @@ final class AdsensePlacementRenderer
return $html;
}
+
+ /**
+ * Renderiza Anchor Ads (anuncios fijos en top/bottom)
+ *
+ * @param array $settings Configuracion desde BD
+ * @return string HTML de los anchor ads
+ */
+ public function renderAnchorAds(array $settings): string
+ {
+ // Verificar si Anchor Ads estan habilitados
+ if (!($settings['visibility']['is_enabled'] ?? false)) {
+ return '';
+ }
+ if (!($settings['anchor_ads']['anchor_enabled'] ?? false)) {
+ return '';
+ }
+
+ $publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
+ $slotId = $settings['content']['slot_auto'] ?? '';
+
+ if (empty($publisherId) || empty($slotId)) {
+ return '';
+ }
+
+ // Configuracion de anchor
+ $anchorConfig = $settings['anchor_ads'] ?? [];
+ $position = $anchorConfig['anchor_position'] ?? 'bottom';
+ $height = (int)($anchorConfig['anchor_height'] ?? 90);
+ $showMobile = ($anchorConfig['anchor_show_on_mobile'] ?? true) === true;
+ $showWide = ($anchorConfig['anchor_show_on_wide_screens'] ?? false) === true;
+ $collapsible = ($anchorConfig['anchor_collapsible_enabled'] ?? true) === true;
+ $collapsedHeight = (int)($anchorConfig['anchor_collapsed_height'] ?? 24);
+ $collapseText = esc_html($anchorConfig['anchor_collapse_button_text'] ?? 'Ver anuncio');
+ $closePosition = $anchorConfig['anchor_close_position'] ?? 'right';
+ $rememberState = ($anchorConfig['anchor_remember_state'] ?? true) === true;
+ $rememberDuration = $anchorConfig['anchor_remember_duration'] ?? 'session';
+
+ $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
+ $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
+ $dataAttr = $delayEnabled ? ' data-adsense-push' : '';
+
+ // === CSS via CSSGenerator ===
+ $cssRules = [];
+
+ // Base anchor styles
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad', [
+ 'position' => 'fixed',
+ 'left' => '0',
+ 'right' => '0',
+ 'z-index' => '9999',
+ 'background' => '#f8f9fa',
+ 'border-color' => '#dee2e6',
+ 'box-shadow' => '0 -2px 10px rgba(0,0,0,0.1)',
+ 'transition' => 'height 0.3s ease, transform 0.3s ease, opacity 0.3s ease',
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad-top', [
+ 'top' => '0',
+ 'border-bottom-width' => '1px',
+ 'border-bottom-style' => 'solid',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad-bottom', [
+ 'bottom' => '0',
+ 'border-top-width' => '1px',
+ 'border-top-style' => 'solid',
+ ]);
+
+ // Controls container
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls', [
+ 'position' => 'absolute',
+ 'top' => '4px',
+ 'display' => 'flex',
+ 'gap' => '4px',
+ 'z-index' => '10',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-left', ['left' => '8px']);
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-right', ['right' => '8px']);
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-controls-center', ['left' => '50%', 'transform' => 'translateX(-50%)']);
+
+ // Buttons
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-btn', [
+ 'width' => '28px',
+ 'height' => '28px',
+ 'border' => 'none',
+ 'border-radius' => '4px',
+ 'background' => 'rgba(0,0,0,0.1)',
+ 'color' => '#333',
+ 'cursor' => 'pointer',
+ 'font-size' => '14px',
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ 'transition' => 'background 0.2s',
+ ]);
+
+ // Collapsed state
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed', [
+ 'height' => $collapsedHeight . 'px !important',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed .roi-anchor-content', [
+ 'display' => 'none',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-expand-bar', [
+ 'display' => 'none',
+ 'width' => '100%',
+ 'height' => '100%',
+ 'background' => '#e9ecef',
+ 'border' => 'none',
+ 'cursor' => 'pointer',
+ 'font-size' => '12px',
+ 'color' => '#495057',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.collapsed .roi-anchor-expand-bar', [
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-anchor-ad.hidden', [
+ 'display' => 'none !important',
+ ]);
+
+ // Media query para visibilidad
+ if (!$showMobile && $showWide) {
+ $cssRules[] = "@media (max-width: 999px) { .roi-anchor-ad { display: none !important; } }";
+ } elseif ($showMobile && !$showWide) {
+ $cssRules[] = "@media (min-width: 1000px) { .roi-anchor-ad { display: none !important; } }";
+ } elseif (!$showMobile && !$showWide) {
+ $cssRules[] = ".roi-anchor-ad { display: none !important; }";
+ }
+
+ $css = implode("\n", $cssRules);
+ $html = "\n";
+
+ // Renderizar anchor(s)
+ $controlsClass = 'roi-anchor-controls roi-anchor-controls-' . esc_attr($closePosition);
+
+ if ($position === 'top' || $position === 'both') {
+ $html .= $this->buildAnchorHTML('top', $height, $publisherId, $slotId, $scriptType, $dataAttr, $controlsClass, $collapsible, $collapseText);
+ }
+
+ if ($position === 'bottom' || $position === 'both') {
+ $html .= $this->buildAnchorHTML('bottom', $height, $publisherId, $slotId, $scriptType, $dataAttr, $controlsClass, $collapsible, $collapseText);
+ }
+
+ // Config para JavaScript (data attributes en lugar de inline JS)
+ $jsConfig = json_encode([
+ 'rememberState' => $rememberState,
+ 'rememberDuration' => $rememberDuration,
+ 'collapsible' => $collapsible,
+ ]);
+
+ $html .= '';
+
+ return $html;
+ }
+
+ /**
+ * Genera HTML para un anchor individual
+ */
+ private function buildAnchorHTML(
+ string $pos,
+ int $height,
+ string $client,
+ string $slot,
+ string $scriptType,
+ string $dataAttr,
+ string $controlsClass,
+ bool $collapsible,
+ string $collapseText
+ ): string {
+ $posClass = 'roi-anchor-ad-' . $pos;
+
+ $collapseBtn = $collapsible
+ ? '
'
+ : '';
+
+ return sprintf(
+ '
+
+ %s
+
+
+
+
+
+
+
+
',
+ esc_attr($posClass),
+ esc_attr($pos),
+ $height,
+ esc_attr($controlsClass),
+ $collapseBtn,
+ $height - 10,
+ esc_attr($client),
+ esc_attr($slot),
+ $scriptType,
+ $dataAttr,
+ esc_html($collapseText)
+ );
+ }
+
+ /**
+ * Renderiza Vignette Ad (pantalla completa)
+ *
+ * @param array $settings Configuracion desde BD
+ * @return string HTML del vignette ad
+ */
+ public function renderVignetteAd(array $settings): string
+ {
+ // Verificar si Vignette Ads estan habilitados
+ if (!($settings['visibility']['is_enabled'] ?? false)) {
+ return '';
+ }
+ if (!($settings['vignette_ads']['vignette_enabled'] ?? false)) {
+ return '';
+ }
+
+ $publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
+ $slotId = $settings['content']['slot_display'] ?? $settings['content']['slot_auto'] ?? '';
+
+ if (empty($publisherId) || empty($slotId)) {
+ return '';
+ }
+
+ // Configuracion de vignette
+ $vignetteConfig = $settings['vignette_ads'] ?? [];
+ $trigger = $vignetteConfig['vignette_trigger'] ?? 'pageview';
+ $triggerDelay = (int)($vignetteConfig['vignette_trigger_delay'] ?? 5);
+ $showMobile = ($vignetteConfig['vignette_show_on_mobile'] ?? true) === true;
+ $showDesktop = ($vignetteConfig['vignette_show_on_desktop'] ?? true) === true;
+ $size = $vignetteConfig['vignette_size'] ?? '300x250';
+ $overlayOpacity = (float)($vignetteConfig['vignette_overlay_opacity'] ?? 0.7);
+ $closeDelay = (int)($vignetteConfig['vignette_close_button_delay'] ?? 0);
+ $reshowEnabled = ($vignetteConfig['vignette_reshow_enabled'] ?? true) === true;
+ $reshowTime = (int)($vignetteConfig['vignette_reshow_time'] ?? 5);
+ $maxPerSession = $vignetteConfig['vignette_max_per_session'] ?? '3';
+ $maxPerPage = $vignetteConfig['vignette_max_per_page'] ?? '1';
+
+ // Calcular dimensiones
+ list($adWidth, $adHeight) = $this->parseVignetteSize($size);
+
+ $delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
+ $scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
+ $dataAttr = $delayEnabled ? ' data-adsense-push' : '';
+
+ // === CSS via CSSGenerator ===
+ $cssRules = [];
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-overlay', [
+ 'position' => 'fixed',
+ 'top' => '0',
+ 'left' => '0',
+ 'right' => '0',
+ 'bottom' => '0',
+ 'background' => 'rgba(0,0,0,' . $overlayOpacity . ')',
+ 'z-index' => '99999',
+ 'display' => 'none',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ 'opacity' => '0',
+ 'transition' => 'opacity 0.3s ease',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-overlay.active', [
+ 'display' => 'flex',
+ 'opacity' => '1',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-modal', [
+ 'position' => 'relative',
+ 'background' => '#fff',
+ 'border-radius' => '8px',
+ 'padding' => '20px',
+ 'box-shadow' => '0 10px 40px rgba(0,0,0,0.3)',
+ 'max-width' => '95vw',
+ 'max-height' => '95vh',
+ 'overflow' => 'auto',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close', [
+ 'position' => 'absolute',
+ 'top' => '-12px',
+ 'right' => '-12px',
+ 'width' => '32px',
+ 'height' => '32px',
+ 'border' => 'none',
+ 'border-radius' => '50%',
+ 'background' => '#dc3545',
+ 'color' => '#fff',
+ 'cursor' => 'pointer',
+ 'font-size' => '18px',
+ 'display' => 'flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ 'box-shadow' => '0 2px 8px rgba(0,0,0,0.2)',
+ 'transition' => 'transform 0.2s, opacity 0.3s',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close.delayed', [
+ 'opacity' => '0',
+ 'pointer-events' => 'none',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-vignette-close:not(.delayed)', [
+ 'opacity' => '1',
+ 'pointer-events' => 'auto',
+ ]);
+
+ // Media query para visibilidad
+ if (!$showMobile && $showDesktop) {
+ $cssRules[] = "@media (max-width: 991px) { .roi-vignette-overlay { display: none !important; } }";
+ } elseif ($showMobile && !$showDesktop) {
+ $cssRules[] = "@media (min-width: 992px) { .roi-vignette-overlay { display: none !important; } }";
+ } elseif (!$showMobile && !$showDesktop) {
+ $cssRules[] = ".roi-vignette-overlay { display: none !important; }";
+ }
+
+ $css = implode("\n", $cssRules);
+ $html = "\n";
+
+ // Determinar estilo de anuncio segun tamano
+ $adStyle = $size === 'responsive'
+ ? 'display:block;min-width:300px;min-height:250px'
+ : sprintf('display:inline-block;width:%dpx;height:%dpx', $adWidth, $adHeight);
+
+ $adFormat = $size === 'responsive' ? ' data-ad-format="auto" data-full-width-responsive="true"' : '';
+
+ $closeDelayClass = $closeDelay > 0 ? ' delayed' : '';
+
+ $html .= sprintf(
+ '
',
+ $closeDelayClass,
+ $closeDelay,
+ $adStyle,
+ esc_attr($publisherId),
+ esc_attr($slotId),
+ $adFormat,
+ $scriptType,
+ $dataAttr
+ );
+
+ // Config para JavaScript
+ $jsConfig = json_encode([
+ 'trigger' => $trigger,
+ 'triggerDelay' => $triggerDelay,
+ 'closeDelay' => $closeDelay,
+ 'reshowEnabled' => $reshowEnabled,
+ 'reshowTime' => $reshowTime,
+ 'maxPerSession' => $maxPerSession,
+ 'maxPerPage' => $maxPerPage,
+ ]);
+
+ $html .= '';
+
+ return $html;
+ }
+
+ /**
+ * Parsea el tamano del vignette
+ */
+ private function parseVignetteSize(string $size): array
+ {
+ return match($size) {
+ '300x250' => [300, 250],
+ '336x280' => [336, 280],
+ default => [300, 250],
+ };
+ }
}
diff --git a/Public/AdsensePlacement/Infrastructure/Ui/Assets/anchor-vignette.js b/Public/AdsensePlacement/Infrastructure/Ui/Assets/anchor-vignette.js
new file mode 100644
index 00000000..1e3baaaa
--- /dev/null
+++ b/Public/AdsensePlacement/Infrastructure/Ui/Assets/anchor-vignette.js
@@ -0,0 +1,365 @@
+/**
+ * ROI Theme - Anchor & Vignette Ads JavaScript
+ *
+ * Logica para:
+ * - Mostrar/ocultar/colapsar anchors
+ * - Triggers de vignette (pageview, scroll, exit_intent, time_delay)
+ * - Persistencia con localStorage
+ * - Tiempo de reaparicion configurable
+ *
+ * NO usa onclick inline - usa addEventListener
+ */
+(function() {
+ 'use strict';
+
+ // =====================================================
+ // UTILIDADES
+ // =====================================================
+
+ /**
+ * Obtiene configuracion desde script type="application/json"
+ */
+ function getConfig(id) {
+ var el = document.getElementById(id);
+ if (!el) return null;
+ try {
+ return JSON.parse(el.textContent);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ /**
+ * Calcula duracion en milisegundos
+ */
+ function getDurationMs(duration) {
+ switch (duration) {
+ case 'session': return 0; // sessionStorage
+ case '1hour': return 60 * 60 * 1000;
+ case '1day': return 24 * 60 * 60 * 1000;
+ case '1week': return 7 * 24 * 60 * 60 * 1000;
+ default: return 0;
+ }
+ }
+
+ /**
+ * Guarda estado en storage
+ */
+ function saveState(key, value, duration) {
+ var data = {
+ value: value,
+ expires: duration === 'session' ? 0 : Date.now() + getDurationMs(duration)
+ };
+
+ if (duration === 'session') {
+ sessionStorage.setItem(key, JSON.stringify(data));
+ } else {
+ localStorage.setItem(key, JSON.stringify(data));
+ }
+ }
+
+ /**
+ * Recupera estado desde storage
+ */
+ function getState(key, duration) {
+ var storage = duration === 'session' ? sessionStorage : localStorage;
+ var raw = storage.getItem(key);
+
+ if (!raw) return null;
+
+ try {
+ var data = JSON.parse(raw);
+
+ // Verificar expiracion
+ if (data.expires && data.expires < Date.now()) {
+ storage.removeItem(key);
+ return null;
+ }
+
+ return data.value;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ // =====================================================
+ // ANCHOR ADS
+ // =====================================================
+
+ function initAnchorAds() {
+ var config = getConfig('roi-anchor-config');
+ if (!config) return;
+
+ var anchors = document.querySelectorAll('.roi-anchor-ad');
+ if (!anchors.length) return;
+
+ // Restaurar estados guardados
+ anchors.forEach(function(anchor) {
+ var pos = anchor.dataset.position;
+ var stateKey = 'roi_anchor_' + pos + '_state';
+
+ if (config.rememberState) {
+ var savedState = getState(stateKey, config.rememberDuration);
+
+ if (savedState === 'closed') {
+ anchor.classList.add('hidden');
+ } else if (savedState === 'collapsed' && config.collapsible) {
+ anchor.classList.add('collapsed');
+ }
+ }
+ });
+
+ // Event delegation para botones
+ document.addEventListener('click', function(e) {
+ var btn = e.target.closest('[data-action]');
+ if (!btn) return;
+
+ var action = btn.dataset.action;
+ var anchor = btn.closest('.roi-anchor-ad');
+
+ if (!anchor) return;
+
+ var pos = anchor.dataset.position;
+ var stateKey = 'roi_anchor_' + pos + '_state';
+
+ switch (action) {
+ case 'close':
+ anchor.classList.add('hidden');
+ if (config.rememberState) {
+ saveState(stateKey, 'closed', config.rememberDuration);
+ }
+ break;
+
+ case 'collapse':
+ if (config.collapsible) {
+ anchor.classList.add('collapsed');
+ if (config.rememberState) {
+ saveState(stateKey, 'collapsed', config.rememberDuration);
+ }
+ }
+ break;
+
+ case 'expand':
+ anchor.classList.remove('collapsed');
+ if (config.rememberState) {
+ saveState(stateKey, 'expanded', config.rememberDuration);
+ }
+ break;
+ }
+ });
+ }
+
+ // =====================================================
+ // VIGNETTE ADS
+ // =====================================================
+
+ function initVignetteAds() {
+ var config = getConfig('roi-vignette-config');
+ if (!config) return;
+
+ var overlay = document.getElementById('roi-vignette-overlay');
+ if (!overlay) return;
+
+ var STORAGE_KEYS = {
+ lastClosed: 'roi_vignette_last_closed',
+ sessionCount: 'roi_vignette_session_count',
+ pageCount: 'roi_vignette_page_count_' + window.location.pathname
+ };
+
+ // Estado local
+ var pageShowCount = 0;
+ var triggered = false;
+
+ /**
+ * Verifica si se puede mostrar el vignette
+ */
+ function canShow() {
+ // Verificar max por pagina
+ var maxPage = config.maxPerPage === 'unlimited' ? 999 : parseInt(config.maxPerPage);
+ if (pageShowCount >= maxPage) return false;
+
+ // Verificar max por sesion
+ var maxSession = config.maxPerSession === 'unlimited' ? 999 : parseInt(config.maxPerSession);
+ var sessionCount = parseInt(sessionStorage.getItem(STORAGE_KEYS.sessionCount) || '0');
+ if (sessionCount >= maxSession) return false;
+
+ // Verificar tiempo de reaparicion
+ if (config.reshowEnabled) {
+ var lastClosed = parseInt(localStorage.getItem(STORAGE_KEYS.lastClosed) || '0');
+ var minWait = config.reshowTime * 60 * 1000; // minutos a ms
+
+ if (lastClosed && (Date.now() - lastClosed) < minWait) {
+ return false;
+ }
+ } else {
+ // Si no se permite reaparicion, verificar si ya se cerro
+ if (sessionStorage.getItem(STORAGE_KEYS.lastClosed)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Muestra el vignette
+ */
+ function showVignette() {
+ if (!canShow() || triggered) return;
+
+ triggered = true;
+
+ // Mostrar overlay
+ overlay.classList.add('active');
+ document.body.style.overflow = 'hidden';
+
+ // Incrementar contadores
+ pageShowCount++;
+ var sessionCount = parseInt(sessionStorage.getItem(STORAGE_KEYS.sessionCount) || '0');
+ sessionStorage.setItem(STORAGE_KEYS.sessionCount, (sessionCount + 1).toString());
+
+ // Manejar delay del boton cerrar
+ var closeBtn = overlay.querySelector('.roi-vignette-close');
+ if (closeBtn && closeBtn.classList.contains('delayed')) {
+ var delay = parseInt(closeBtn.dataset.delay || '0') * 1000;
+
+ setTimeout(function() {
+ closeBtn.classList.remove('delayed');
+ }, delay);
+ }
+ }
+
+ /**
+ * Cierra el vignette
+ */
+ function closeVignette() {
+ overlay.classList.remove('active');
+ document.body.style.overflow = '';
+
+ // Guardar tiempo de cierre
+ localStorage.setItem(STORAGE_KEYS.lastClosed, Date.now().toString());
+
+ if (!config.reshowEnabled) {
+ sessionStorage.setItem(STORAGE_KEYS.lastClosed, '1');
+ }
+
+ // Permitir nueva trigger si se permite reaparicion
+ if (config.reshowEnabled) {
+ triggered = false;
+ }
+ }
+
+ // Event listeners para cerrar
+ document.addEventListener('click', function(e) {
+ var btn = e.target.closest('[data-action="close-vignette"]');
+ if (btn) {
+ closeVignette();
+ return;
+ }
+
+ // Click fuera del modal
+ if (e.target === overlay) {
+ closeVignette();
+ }
+ });
+
+ // Escape key
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && overlay.classList.contains('active')) {
+ closeVignette();
+ }
+ });
+
+ // === TRIGGERS ===
+
+ /**
+ * Trigger: Al cargar pagina
+ */
+ function setupPageviewTrigger() {
+ setTimeout(function() {
+ showVignette();
+ }, config.triggerDelay * 1000);
+ }
+
+ /**
+ * Trigger: Al scrollear X%
+ */
+ function setupScrollTrigger(percent) {
+ var scrollHandler = function() {
+ if (triggered) return;
+
+ var scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
+ var scrolled = window.scrollY / scrollHeight;
+
+ if (scrolled >= percent / 100) {
+ showVignette();
+ window.removeEventListener('scroll', scrollHandler);
+ }
+ };
+
+ window.addEventListener('scroll', scrollHandler, { passive: true });
+ }
+
+ /**
+ * Trigger: Exit intent (mouse sale del viewport)
+ */
+ function setupExitIntentTrigger() {
+ var exitHandler = function(e) {
+ if (triggered) return;
+
+ // Solo activar si el mouse sale por arriba
+ if (e.clientY <= 0) {
+ showVignette();
+ document.removeEventListener('mouseout', exitHandler);
+ }
+ };
+
+ document.addEventListener('mouseout', exitHandler);
+ }
+
+ /**
+ * Trigger: Despues de X segundos
+ */
+ function setupTimeDelayTrigger() {
+ setTimeout(function() {
+ showVignette();
+ }, config.triggerDelay * 1000);
+ }
+
+ // Configurar trigger segun config
+ switch (config.trigger) {
+ case 'pageview':
+ setupPageviewTrigger();
+ break;
+ case 'scroll_50':
+ setupScrollTrigger(50);
+ break;
+ case 'scroll_75':
+ setupScrollTrigger(75);
+ break;
+ case 'exit_intent':
+ setupExitIntentTrigger();
+ break;
+ case 'time_delay':
+ setupTimeDelayTrigger();
+ break;
+ }
+ }
+
+ // =====================================================
+ // INICIALIZACION
+ // =====================================================
+
+ function init() {
+ initAnchorAds();
+ initVignetteAds();
+ }
+
+ // Ejecutar cuando el DOM este listo
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+})();
diff --git a/Schemas/adsense-placement.json b/Schemas/adsense-placement.json
index e9f563bf..c22386a7 100644
--- a/Schemas/adsense-placement.json
+++ b/Schemas/adsense-placement.json
@@ -1,7 +1,7 @@
{
"component_name": "adsense-placement",
- "version": "1.2.0",
- "description": "Control de AdSense y Google Analytics - Panel reorganizado",
+ "version": "1.3.0",
+ "description": "Control de AdSense y Google Analytics - Con Anchor y Vignette Ads",
"groups": {
"visibility": {
"label": "Activacion",
@@ -261,6 +261,182 @@
}
}
},
+ "anchor_ads": {
+ "label": "Anuncios Fijos (Anchor)",
+ "priority": 71,
+ "fields": {
+ "anchor_enabled": {
+ "type": "boolean",
+ "label": "Activar Anchor Ads",
+ "default": false,
+ "editable": true,
+ "description": "Anuncios fijos en el borde de la pantalla"
+ },
+ "anchor_position": {
+ "type": "select",
+ "label": "Posicion del anuncio fijo",
+ "default": "bottom",
+ "editable": true,
+ "options": ["top", "bottom", "both"],
+ "description": "Solo superior, solo inferior, o ambos"
+ },
+ "anchor_height": {
+ "type": "select",
+ "label": "Altura del anchor",
+ "default": "90",
+ "editable": true,
+ "options": ["50", "90", "100", "120"],
+ "description": "Altura en pixeles"
+ },
+ "anchor_show_on_mobile": {
+ "type": "boolean",
+ "label": "Mostrar en movil",
+ "default": true,
+ "editable": true,
+ "description": "Pantallas menores a 1000px"
+ },
+ "anchor_show_on_wide_screens": {
+ "type": "boolean",
+ "label": "Permitir en pantallas anchas (>1000px)",
+ "default": false,
+ "editable": true,
+ "description": "Como las de computadora"
+ },
+ "anchor_collapsible_enabled": {
+ "type": "boolean",
+ "label": "Permitir anchors contraibles",
+ "default": true,
+ "editable": true,
+ "description": "Usuario puede minimizar en lugar de cerrar"
+ },
+ "anchor_collapsed_height": {
+ "type": "select",
+ "label": "Altura contraido",
+ "default": "24",
+ "editable": true,
+ "options": ["20", "24", "28", "32"],
+ "description": "Altura cuando esta minimizado"
+ },
+ "anchor_collapse_button_text": {
+ "type": "text",
+ "label": "Texto boton expandir",
+ "default": "Ver anuncio",
+ "editable": true
+ },
+ "anchor_close_position": {
+ "type": "select",
+ "label": "Posicion boton cerrar",
+ "default": "right",
+ "editable": true,
+ "options": ["left", "right", "center"]
+ },
+ "anchor_remember_state": {
+ "type": "boolean",
+ "label": "Recordar cierre/colapso",
+ "default": true,
+ "editable": true,
+ "description": "Usa localStorage para recordar estado"
+ },
+ "anchor_remember_duration": {
+ "type": "select",
+ "label": "Duracion del recuerdo",
+ "default": "session",
+ "editable": true,
+ "options": ["session", "1hour", "1day", "1week"]
+ }
+ }
+ },
+ "vignette_ads": {
+ "label": "Anuncios de Vineta (Pantalla Completa)",
+ "priority": 72,
+ "fields": {
+ "vignette_enabled": {
+ "type": "boolean",
+ "label": "Activar Vignette Ads",
+ "default": false,
+ "editable": true,
+ "description": "Anuncios pantalla completa entre cargas de pagina"
+ },
+ "vignette_trigger": {
+ "type": "select",
+ "label": "Cuando mostrar",
+ "default": "pageview",
+ "editable": true,
+ "options": ["pageview", "scroll_50", "scroll_75", "exit_intent", "time_delay"],
+ "description": "Disparador del vignette"
+ },
+ "vignette_trigger_delay": {
+ "type": "text",
+ "label": "Delay inicial (segundos)",
+ "default": "5",
+ "editable": true,
+ "description": "Segundos antes de mostrar"
+ },
+ "vignette_show_on_mobile": {
+ "type": "boolean",
+ "label": "Mostrar en movil",
+ "default": true,
+ "editable": true
+ },
+ "vignette_show_on_desktop": {
+ "type": "boolean",
+ "label": "Mostrar en desktop",
+ "default": true,
+ "editable": true
+ },
+ "vignette_size": {
+ "type": "select",
+ "label": "Tamano del anuncio",
+ "default": "300x250",
+ "editable": true,
+ "options": ["300x250", "336x280", "responsive"]
+ },
+ "vignette_overlay_opacity": {
+ "type": "select",
+ "label": "Opacidad del fondo",
+ "default": "0.7",
+ "editable": true,
+ "options": ["0.5", "0.6", "0.7", "0.8", "0.9"]
+ },
+ "vignette_close_button_delay": {
+ "type": "select",
+ "label": "Delay boton cerrar (segundos)",
+ "default": "0",
+ "editable": true,
+ "options": ["0", "1", "2", "3", "5"],
+ "description": "Segundos antes de mostrar el boton X"
+ },
+ "vignette_reshow_enabled": {
+ "type": "boolean",
+ "label": "Permitir reaparicion",
+ "default": true,
+ "editable": true,
+ "description": "Puede volver a aparecer despues de cerrarlo"
+ },
+ "vignette_reshow_time": {
+ "type": "select",
+ "label": "Tiempo para reaparecer (minutos)",
+ "default": "5",
+ "editable": true,
+ "options": ["1", "2", "3", "4", "5", "10", "15", "30"],
+ "description": "Minutos antes de poder volver a mostrar"
+ },
+ "vignette_max_per_session": {
+ "type": "select",
+ "label": "Maximo por sesion",
+ "default": "3",
+ "editable": true,
+ "options": ["1", "2", "3", "5", "unlimited"]
+ },
+ "vignette_max_per_page": {
+ "type": "select",
+ "label": "Maximo por pagina",
+ "default": "1",
+ "editable": true,
+ "options": ["1", "2", "unlimited"]
+ }
+ }
+ },
"layout": {
"label": "Ubicaciones Archivos/Globales",
"priority": 80,