feat(admin): Add theme-settings component for global configurations
- Add Schemas/theme-settings.json with analytics and custom_code groups - Add ThemeSettingsFormBuilder for Admin Panel UI - Add ThemeSettingsFieldMapper for AJAX field mapping - Add ThemeSettingsRenderer for injecting GA/CSS/JS - Add ThemeSettingsInjector for wp_head/wp_footer hooks - Register component in AdminDashboardRenderer::getComponents() - Register FieldMapper in FieldMapperProvider 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,11 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
'label' => 'Footer',
|
||||
'icon' => 'bi-layout-text-window-reverse',
|
||||
],
|
||||
'theme-settings' => [
|
||||
'id' => 'theme-settings',
|
||||
'label' => 'Theme Settings',
|
||||
'icon' => 'bi-gear',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.4rem;
|
||||
padding: 0.3rem 0.3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@@ -31,6 +31,7 @@ final class FieldMapperProvider
|
||||
'RelatedPost',
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*/
|
||||
final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'theme-settings';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Analytics
|
||||
'themeSettingsGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'themeSettingsGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'],
|
||||
'themeSettingsCustomJsFooter' => ['group' => 'custom_code', 'attribute' => 'custom_js_footer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (analytics, codigo personalizado)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*
|
||||
* @package ROITheme\Admin\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda - Analytics
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha - Custom Code
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildCustomCodeGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Analytics y Codigo Personalizado';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="theme-settings">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-graph-up me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Analytics';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$gaTrackingId = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsGaTrackingId', 'Google Analytics ID', 'bi-bar-chart', $gaTrackingId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: G-XXXXXXXXXX o UA-XXXXXXXX-X</div>';
|
||||
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch('themeSettingsGaAnonymizeIp', 'Anonimizar IP (GDPR)', 'bi-shield-check', $gaAnonymizeIp);
|
||||
|
||||
$html .= ' <div class="alert alert-warning small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' Recomendado activar para cumplir con GDPR/RGPD';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCustomCodeGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-code-slash me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Codigo Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customCss = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_css', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomCss', 'CSS Personalizado', 'bi-filetype-css', $customCss, 'Se inyecta en wp_head. No incluir etiquetas <style>');
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsHeader', 'JavaScript en Header', 'bi-filetype-js', $customJsHeader, 'Se inyecta en wp_head. No incluir etiquetas <script>');
|
||||
|
||||
$customJsFooter = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_footer', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsFooter', 'JavaScript en Footer', 'bi-filetype-js', $customJsFooter, 'Se inyecta en wp_footer. No incluir etiquetas <script>');
|
||||
|
||||
$html .= ' <div class="alert alert-danger small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-octagon me-1"></i>';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo personalizado puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private function buildSwitch(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = ' <div class="form-check form-switch mb-2">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextareaCode(string $id, string $label, string $icon, mixed $value, string $helpText = ''): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="4" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . $helpText . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ThemeSettings\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
use ROITheme\Public\ThemeSettings\Infrastructure\Ui\ThemeSettingsRenderer;
|
||||
|
||||
/**
|
||||
* ThemeSettingsInjector
|
||||
*
|
||||
* Servicio que inyecta las configuraciones globales del tema
|
||||
* en los hooks de WordPress (wp_head y wp_footer).
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Registrar hooks de WordPress
|
||||
* - Obtener configuracion de theme-settings desde BD
|
||||
* - Delegar renderizado a ThemeSettingsRenderer
|
||||
* - Inyectar contenido en los hooks correspondientes
|
||||
*
|
||||
* @package ROITheme\Public\ThemeSettings\Infrastructure\Services
|
||||
*/
|
||||
final class ThemeSettingsInjector
|
||||
{
|
||||
private const COMPONENT_NAME = 'theme-settings';
|
||||
|
||||
/**
|
||||
* @param ComponentSettingsRepositoryInterface $repository Repositorio para leer configuraciones
|
||||
* @param ThemeSettingsRenderer $renderer Renderer para generar contenido
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ComponentSettingsRepositoryInterface $repository,
|
||||
private readonly ThemeSettingsRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress para inyeccion
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Inyectar en wp_head con prioridad alta para GA y CSS
|
||||
add_action('wp_head', [$this, 'injectHeadContent'], 5);
|
||||
|
||||
// Inyectar en wp_footer con prioridad baja (al final)
|
||||
add_action('wp_footer', [$this, 'injectFooterContent'], 99);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta contenido en wp_head
|
||||
*
|
||||
* Callback para el hook wp_head.
|
||||
* Genera y muestra: Google Analytics, Custom CSS, Custom JS Header
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function injectHeadContent(): void
|
||||
{
|
||||
try {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if (empty($settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->renderer->renderHeadContent($settings);
|
||||
|
||||
if (!empty($content)) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $content;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logError('Error injecting head content', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta contenido en wp_footer
|
||||
*
|
||||
* Callback para el hook wp_footer.
|
||||
* Genera y muestra: Custom JS Footer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function injectFooterContent(): void
|
||||
{
|
||||
try {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if (empty($settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->renderer->renderFooterContent($settings);
|
||||
|
||||
if (!empty($content)) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $content;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logError('Error injecting footer content', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones del componente theme-settings
|
||||
*
|
||||
* @return array Configuraciones agrupadas o array vacio si no hay
|
||||
*/
|
||||
private function getSettings(): array
|
||||
{
|
||||
return $this->repository->getComponentSettings(self::COMPONENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra errores en el log de WordPress
|
||||
*
|
||||
* @param string $message Mensaje de error
|
||||
* @param \Throwable $e Excepcion
|
||||
* @return void
|
||||
*/
|
||||
private function logError(string $message, \Throwable $e): void
|
||||
{
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf(
|
||||
'ROI Theme - ThemeSettingsInjector: %s - %s in %s:%d',
|
||||
$message,
|
||||
$e->getMessage(),
|
||||
$e->getFile(),
|
||||
$e->getLine()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
308
Public/ThemeSettings/Infrastructure/Ui/ThemeSettingsRenderer.php
Normal file
308
Public/ThemeSettings/Infrastructure/Ui/ThemeSettingsRenderer.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* ThemeSettingsRenderer
|
||||
*
|
||||
* Renderizador del componente Theme Settings.
|
||||
* A diferencia de otros componentes, no renderiza HTML visual
|
||||
* sino que genera codigo para inyectar en wp_head y wp_footer.
|
||||
*
|
||||
* NOTA: Este es un componente especial que NO requiere:
|
||||
* - CSSGeneratorInterface (no genera CSS, solo inyecta CSS del usuario)
|
||||
* - Grupo visibility (siempre esta activo, configuraciones globales)
|
||||
* - Metodo getVisibilityClasses (no es un componente visual)
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Generar script de Google Analytics
|
||||
* - Generar CSS personalizado
|
||||
* - Generar JavaScript para header
|
||||
* - Generar JavaScript para footer
|
||||
*
|
||||
* @package ROITheme\Public\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Para este componente, render() no se usa directamente.
|
||||
* Se usan los metodos especificos: renderHeadContent() y renderFooterContent()
|
||||
*/
|
||||
public function render(Component $component): string
|
||||
{
|
||||
// Este componente no renderiza HTML visual
|
||||
// Los contenidos se inyectan via hooks wp_head y wp_footer
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'theme-settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera contenido para wp_head
|
||||
*
|
||||
* Incluye:
|
||||
* - Google Analytics script (si configurado)
|
||||
* - Custom CSS (si configurado)
|
||||
* - Custom JS Header (si configurado)
|
||||
*
|
||||
* @param array $data Datos del componente desde BD
|
||||
* @return string Contenido para wp_head
|
||||
*/
|
||||
public function renderHeadContent(array $data): string
|
||||
{
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
// Google Analytics
|
||||
$gaOutput = $this->renderGoogleAnalytics($data);
|
||||
if (!empty($gaOutput)) {
|
||||
$output .= $gaOutput . "\n";
|
||||
}
|
||||
|
||||
// Custom CSS
|
||||
$cssOutput = $this->renderCustomCSS($data);
|
||||
if (!empty($cssOutput)) {
|
||||
$output .= $cssOutput . "\n";
|
||||
}
|
||||
|
||||
// Custom JS Header
|
||||
$jsHeaderOutput = $this->renderCustomJSHeader($data);
|
||||
if (!empty($jsHeaderOutput)) {
|
||||
$output .= $jsHeaderOutput . "\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera contenido para wp_footer
|
||||
*
|
||||
* Incluye:
|
||||
* - Custom JS Footer (si configurado)
|
||||
*
|
||||
* @param array $data Datos del componente desde BD
|
||||
* @return string Contenido para wp_footer
|
||||
*/
|
||||
public function renderFooterContent(array $data): string
|
||||
{
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->renderCustomJSFooter($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el componente esta habilitado
|
||||
*
|
||||
* NOTA: Theme Settings es un componente de configuracion global
|
||||
* que siempre esta activo. No tiene grupo visibility.
|
||||
* Si el usuario no quiere GA o CSS custom, simplemente deja
|
||||
* los campos vacios.
|
||||
*
|
||||
* @param array $data Datos del componente (no usado)
|
||||
* @return bool Siempre true
|
||||
*/
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
// Theme Settings siempre esta activo (configuraciones globales)
|
||||
// Los campos individuales se validan en sus metodos respectivos
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el script de Google Analytics
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Script de GA o vacio si no configurado
|
||||
*/
|
||||
private function renderGoogleAnalytics(array $data): string
|
||||
{
|
||||
$trackingId = trim($data['analytics']['ga_tracking_id'] ?? '');
|
||||
|
||||
if (empty($trackingId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si GA ya esta cargado por otro plugin
|
||||
if ($this->isGoogleAnalyticsLoaded()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$anonymizeIp = ($data['analytics']['ga_anonymize_ip'] ?? true) === true;
|
||||
|
||||
// Detectar tipo de ID (GA4 vs Universal Analytics)
|
||||
if (strpos($trackingId, 'G-') === 0) {
|
||||
// Google Analytics 4
|
||||
return $this->renderGA4Script($trackingId, $anonymizeIp);
|
||||
} elseif (strpos($trackingId, 'UA-') === 0) {
|
||||
// Universal Analytics (legacy)
|
||||
return $this->renderUniversalAnalyticsScript($trackingId, $anonymizeIp);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script de Google Analytics 4
|
||||
*
|
||||
* @param string $trackingId ID de GA4 (G-XXXXXXXXXX)
|
||||
* @param bool $anonymizeIp Si anonimizar IP
|
||||
* @return string Script HTML
|
||||
*/
|
||||
private function renderGA4Script(string $trackingId, bool $anonymizeIp): string
|
||||
{
|
||||
$config = $anonymizeIp ? "{ 'anonymize_ip': true }" : '{}';
|
||||
|
||||
return sprintf(
|
||||
'<!-- Google Analytics 4 (ROI Theme) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=%1$s"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "%1$s", %2$s);
|
||||
</script>',
|
||||
esc_attr($trackingId),
|
||||
$config
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script de Universal Analytics (legacy)
|
||||
*
|
||||
* @param string $trackingId ID de UA (UA-XXXXXXXX-X)
|
||||
* @param bool $anonymizeIp Si anonimizar IP
|
||||
* @return string Script HTML
|
||||
*/
|
||||
private function renderUniversalAnalyticsScript(string $trackingId, bool $anonymizeIp): string
|
||||
{
|
||||
$anonymizeConfig = $anonymizeIp ? "ga('set', 'anonymizeIp', true);" : '';
|
||||
|
||||
return sprintf(
|
||||
'<!-- Universal Analytics (ROI Theme) -->
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
|
||||
ga("create", "%s", "auto");
|
||||
%s
|
||||
ga("send", "pageview");
|
||||
</script>',
|
||||
esc_attr($trackingId),
|
||||
$anonymizeConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si Google Analytics ya esta cargado
|
||||
*
|
||||
* @return bool True si ya esta cargado por otro plugin
|
||||
*/
|
||||
private function isGoogleAnalyticsLoaded(): bool
|
||||
{
|
||||
// Verificar plugins comunes de GA
|
||||
if (function_exists('gtag')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si MonsterInsights esta activo
|
||||
if (class_exists('MonsterInsights_Lite') || class_exists('MonsterInsights')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si Site Kit de Google esta activo
|
||||
if (class_exists('Google\Site_Kit\Plugin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el CSS personalizado
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque style o vacio si no hay CSS
|
||||
*/
|
||||
private function renderCustomCSS(array $data): string
|
||||
{
|
||||
$css = trim($data['custom_code']['custom_css'] ?? '');
|
||||
|
||||
if (empty($css)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom CSS (ROI Theme) -->
|
||||
<style id="roi-theme-custom-css">
|
||||
%s
|
||||
</style>',
|
||||
$css // No escapar CSS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el JavaScript personalizado para header
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque script o vacio si no hay JS
|
||||
*/
|
||||
private function renderCustomJSHeader(array $data): string
|
||||
{
|
||||
$js = trim($data['custom_code']['custom_js_header'] ?? '');
|
||||
|
||||
if (empty($js)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom JS Header (ROI Theme) -->
|
||||
<script id="roi-theme-custom-js-header">
|
||||
%s
|
||||
</script>',
|
||||
$js // No escapar JS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el JavaScript personalizado para footer
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque script o vacio si no hay JS
|
||||
*/
|
||||
private function renderCustomJSFooter(array $data): string
|
||||
{
|
||||
$js = trim($data['custom_code']['custom_js_footer'] ?? '');
|
||||
|
||||
if (empty($js)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom JS Footer (ROI Theme) -->
|
||||
<script id="roi-theme-custom-js-footer">
|
||||
%s
|
||||
</script>',
|
||||
$js // No escapar JS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
}
|
||||
54
schemas/theme-settings.json
Normal file
54
schemas/theme-settings.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"component_name": "theme-settings",
|
||||
"version": "1.1.0",
|
||||
"description": "Configuraciones globales del tema: analytics y codigo personalizado",
|
||||
"groups": {
|
||||
"analytics": {
|
||||
"label": "Analytics",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"ga_tracking_id": {
|
||||
"type": "text",
|
||||
"label": "Google Analytics ID",
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"description": "ID de seguimiento de Google Analytics (ej: G-XXXXXXXXXX o UA-XXXXXXXX-X)"
|
||||
},
|
||||
"ga_anonymize_ip": {
|
||||
"type": "boolean",
|
||||
"label": "Anonimizar IP",
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Anonimiza las direcciones IP de los visitantes (recomendado para GDPR)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom_code": {
|
||||
"label": "Codigo Personalizado",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"custom_css": {
|
||||
"type": "textarea",
|
||||
"label": "CSS Personalizado",
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"description": "CSS personalizado que se inyecta en wp_head. No incluir etiquetas <style>"
|
||||
},
|
||||
"custom_js_header": {
|
||||
"type": "textarea",
|
||||
"label": "JavaScript en Header",
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"description": "JavaScript que se inyecta en wp_head. No incluir etiquetas <script>"
|
||||
},
|
||||
"custom_js_footer": {
|
||||
"type": "textarea",
|
||||
"label": "JavaScript en Footer",
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"description": "JavaScript que se inyecta en wp_footer. No incluir etiquetas <script>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user