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 .= '
'; + // Anchor Top + $html .= '
'; + $html .= ' ANCHOR TOP (fijo, collapsible)'; + $html .= '
'; + // Header $html .= '
'; $html .= ' HEADER'; @@ -169,10 +176,15 @@ final class AdsensePlacementFormBuilder $html .= '
'; // Footer - $html .= '
'; + $html .= '
'; $html .= ' FOOTER'; $html .= '
'; + // Anchor Bottom + $html .= '
'; + $html .= ' ANCHOR BOTTOM (fijo, collapsible)'; + $html .= '
'; + // Rail Ads (laterales) $html .= '
'; $html .= '
'; @@ -183,11 +195,19 @@ final class AdsensePlacementFormBuilder $html .= '
'; $html .= '
'; + // Vignette Ad (modal) + $html .= '
'; + $html .= ' VIGNETTE (modal pantalla completa)'; + $html .= '
Aparece segun trigger configurado'; + $html .= '
'; + $html .= '
'; $html .= '
'; - $html .= ' Los anuncios amarillos son configurables abajo.'; - $html .= ' Los rojos solo aparecen en pantallas >1600px.'; + $html .= ' Amarillo = Posts, '; + $html .= ' Rojo = Rails >1600px, '; + $html .= ' Cyan = Anchors, '; + $html .= ' Morado = Vignette'; $html .= '
'; $html .= '
'; @@ -506,6 +526,177 @@ final class AdsensePlacementFormBuilder return $html; } + /** + * Seccion para Anchor Ads (anuncios fijos top/bottom) + */ + private function buildAnchorAdsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Anuncios Fijos (Anchor)'; + $html .= '
'; + $html .= '

Anuncios fijos en el borde superior o inferior de la pantalla.

'; + + // Master switch + $anchorEnabled = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_enabled', false); + $html .= $this->buildSwitch($cid . 'AnchorEnabled', 'Activar Anchor Ads', $anchorEnabled, 'bi-power'); + + // Posicion + $anchorPosition = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_position', 'bottom'); + $html .= $this->buildSelect($cid . 'AnchorPosition', 'Posicion del anuncio', + $anchorPosition, + [ + 'top' => 'Solo en la parte superior', + 'bottom' => 'Solo en la parte inferior', + 'both' => 'Superior e inferior' + ] + ); + + // Altura + $anchorHeight = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_height', '90'); + $html .= $this->buildSelect($cid . 'AnchorHeight', 'Altura del anchor', + $anchorHeight, + ['50' => '50px', '90' => '90px', '100' => '100px', '120' => '120px'] + ); + + // Collapsible toggle + $collapsible = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_collapsible_enabled', true); + $html .= $this->buildSwitch($cid . 'AnchorCollapsibleEnabled', 'Permitir minimizar', $collapsible, 'bi-arrows-collapse'); + $html .= 'Usuario puede minimizar en lugar de cerrar'; + + // Pantallas + $html .= '
'; + $html .= '
'; + $showMobile = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_mobile', true); + $html .= $this->buildSwitch($cid . 'AnchorShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone'); + $html .= '
'; + $html .= '
'; + $showWide = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_wide_screens', false); + $html .= $this->buildSwitch($cid . 'AnchorShowOnWideScreens', 'Pantallas anchas', $showWide, 'bi-display'); + $html .= '
'; + $html .= '
'; + + // Recordar estado + $html .= '
'; + $rememberState = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_state', true); + $html .= $this->buildSwitch($cid . 'AnchorRememberState', 'Recordar cierre/colapso', $rememberState, 'bi-clock-history'); + + $rememberDuration = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_duration', 'session'); + $html .= $this->buildSelect($cid . 'AnchorRememberDuration', 'Duracion', + $rememberDuration, + [ + 'session' => 'Solo esta sesion', + '1hour' => '1 hora', + '1day' => '1 dia', + '1week' => '1 semana' + ] + ); + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Seccion para Vignette Ads (pantalla completa) + */ + private function buildVignetteAdsGroup(string $cid): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= ' '; + $html .= ' Anuncios de Vineta'; + $html .= ' Pantalla Completa'; + $html .= '
'; + $html .= '

Anuncios que ocupan toda la pantalla, aparecen segun el trigger configurado.

'; + + // Master switch + $vignetteEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_enabled', false); + $html .= $this->buildSwitch($cid . 'VignetteEnabled', 'Activar Vignette Ads', $vignetteEnabled, 'bi-power'); + + // Trigger + $vignetteTrigger = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger', 'pageview'); + $html .= $this->buildSelect($cid . 'VignetteTrigger', 'Cuando mostrar', + $vignetteTrigger, + [ + 'pageview' => 'Al cargar la pagina', + 'scroll_50' => 'Al scrollear 50%', + 'scroll_75' => 'Al scrollear 75%', + 'exit_intent' => 'Al intentar salir', + 'time_delay' => 'Despues de X segundos' + ] + ); + + // Delay inicial + $triggerDelay = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger_delay', '5'); + $html .= $this->buildTextInput($cid . 'VignetteTriggerDelay', 'Delay inicial (segundos)', (string)$triggerDelay, '5'); + + // Tamano y opacidad + $html .= '
'; + $html .= '
'; + $size = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_size', '300x250'); + $html .= $this->buildSelect($cid . 'VignetteSize', 'Tamano', + $size, + ['300x250' => '300x250', '336x280' => '336x280', 'responsive' => 'Responsive'] + ); + $html .= '
'; + $html .= '
'; + $opacity = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_overlay_opacity', '0.7'); + $html .= $this->buildSelect($cid . 'VignetteOverlayOpacity', 'Opacidad fondo', + $opacity, + ['0.5' => '50%', '0.6' => '60%', '0.7' => '70%', '0.8' => '80%', '0.9' => '90%'] + ); + $html .= '
'; + $html .= '
'; + + // Pantallas + $html .= '
'; + $html .= '
'; + $showMobile = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_mobile', true); + $html .= $this->buildSwitch($cid . 'VignetteShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone'); + $html .= '
'; + $html .= '
'; + $showDesktop = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_desktop', true); + $html .= $this->buildSwitch($cid . 'VignetteShowOnDesktop', 'Mostrar en desktop', $showDesktop, 'bi-display'); + $html .= '
'; + $html .= '
'; + + // Reaparicion + $html .= '
'; + $html .= '

Reaparicion

'; + + $reshowEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_enabled', true); + $html .= $this->buildSwitch($cid . 'VignetteReshowEnabled', 'Permitir reaparicion', $reshowEnabled); + + $html .= '
'; + $html .= '
'; + $reshowTime = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_time', '5'); + $html .= $this->buildSelect($cid . 'VignetteReshowTime', 'Tiempo (min)', + $reshowTime, + ['1' => '1 min', '2' => '2 min', '3' => '3 min', '4' => '4 min', '5' => '5 min', '10' => '10 min', '15' => '15 min', '30' => '30 min'] + ); + $html .= '
'; + $html .= '
'; + $maxSession = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_max_per_session', '3'); + $html .= $this->buildSelect($cid . 'VignetteMaxPerSession', 'Max/sesion', + $maxSession, + ['1' => '1', '2' => '2', '3' => '3', '5' => '5', 'unlimited' => 'Sin limite'] + ); + $html .= '
'; + $html .= '
'; + $html .= '
'; + + $html .= '
'; + $html .= '
'; + + return $html; + } + private function buildExclusionsGroup(string $cid): string { $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,