Feat: Implementar Top Bar configurable - Issue #143

Implementación completa del componente Top Bar con 15 campos configurables desde el admin panel, siguiendo el algoritmo universal v2.0.

## Cambios Realizados

### Backend (PHP)
- Agregados defaults del Top Bar a class-settings-manager.php
- Implementada validación completa en class-validator.php (15 campos)
- Implementada sanitización con sanitize_text_field, esc_url_raw y sanitize_hex_color
- Modificado header.php con código 100% configurable usando wp_parse_args()

### Frontend (Admin Panel)
- Creado tab HTML completo con 3 secciones: Activación, Contenido y Estilos
- Implementado JavaScript para renderizado y recolección de datos
- 15 campos configurables: enabled, visibility, icon, content, link, custom styles

### Infraestructura
- Creado admin-panel/init.php para carga del módulo
- Creada class-admin-menu.php con enqueue de Bootstrap 5 y assets
- Creada estructura base CSS y JavaScript del admin
- Ya cargado en functions.php línea 276

## Características
- Responsive: Control independiente mobile/desktop
- Estilos personalizables: 4 colores + tamaño de fuente
- Validación robusta: Límites de caracteres, URLs, colores hex
- Defaults inteligentes: Valores seguros si no hay configuración

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-09 15:13:55 -06:00
parent bbc6ed2c98
commit 465b879135
8 changed files with 1136 additions and 5 deletions

View File

@@ -0,0 +1,169 @@
/**
* Admin Panel Styles
*
* Estilos base para el panel de administración
*
* @package Apus_Theme
* @since 2.0.0
*/
/* ========================================
Container
======================================== */
.apus-admin-panel {
max-width: 1400px;
margin: 20px auto;
}
/* ========================================
Header
======================================== */
.apus-admin-panel h1 {
margin-bottom: 10px;
}
.apus-admin-panel .description {
color: #666;
margin-bottom: 20px;
}
/* ========================================
Tabs
======================================== */
.nav-tabs {
border-bottom: 2px solid #dee2e6;
}
.nav-tabs .nav-link {
color: #666;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.nav-tabs .nav-link:hover {
color: #0073aa;
border-bottom-color: #0073aa;
}
.nav-tabs .nav-link.active {
color: #0073aa;
font-weight: 600;
border-bottom-color: #0073aa;
background-color: transparent;
}
/* ========================================
Tab Content
======================================== */
.tab-content {
background: #fff;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-pane h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.tab-pane h4 {
margin-top: 25px;
margin-bottom: 10px;
font-size: 16px;
color: #333;
}
/* ========================================
Form Sections
======================================== */
.form-section {
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.form-section:last-child {
border-bottom: none;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group input[type="email"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
max-width: 600px;
}
.form-group .form-text {
margin-top: 5px;
font-size: 13px;
}
.form-group .form-text code {
background: #f5f5f5;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
}
/* ========================================
Action Buttons
======================================== */
.admin-actions {
padding: 20px;
background: #f9f9f9;
border-top: 1px solid #ddd;
border-radius: 4px;
}
.admin-actions .button-primary {
font-size: 14px;
padding: 8px 20px;
height: auto;
}
.admin-actions .button-primary i {
vertical-align: middle;
}
/* ========================================
Responsive
======================================== */
@media (max-width: 782px) {
.apus-admin-panel {
margin: 10px;
}
.tab-content {
padding: 15px;
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group input[type="email"],
.form-group select,
.form-group textarea {
max-width: 100%;
}
}

View File

@@ -0,0 +1,276 @@
/**
* Admin Panel Application
*
* Gestión de configuraciones de componentes del tema
*
* @package Apus_Theme
* @since 2.0.0
*/
const AdminPanel = {
/**
* Estado de la aplicación
*/
STATE: {
settings: {},
hasChanges: false,
isLoading: false
},
/**
* Inicializar aplicación
*/
init() {
this.bindEvents();
this.loadSettings();
},
/**
* Vincular eventos
*/
bindEvents() {
// Botón guardar
document.getElementById('saveSettings').addEventListener('click', () => {
this.saveSettings();
});
// Detectar cambios en formularios
document.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('change', () => {
this.STATE.hasChanges = true;
document.getElementById('saveSettings').disabled = false;
});
});
// Tabs
const tabs = document.querySelectorAll('.nav-tabs .nav-link');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
e.preventDefault();
this.switchTab(tab);
});
});
},
/**
* Cambiar tab
*/
switchTab(tab) {
// Remover active de todos
document.querySelectorAll('.nav-tabs .nav-link').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
// Activar seleccionado
tab.classList.add('active');
const targetId = tab.getAttribute('data-bs-target').substring(1);
const targetPane = document.getElementById(targetId);
if (targetPane) {
targetPane.classList.add('show', 'active');
}
},
/**
* Cargar configuraciones desde servidor
*/
async loadSettings() {
this.STATE.isLoading = true;
this.showSpinner(true);
try {
const response = await axios({
method: 'POST',
url: apusAdminData.ajaxUrl,
data: new URLSearchParams({
action: 'apus_get_settings',
nonce: apusAdminData.nonce
})
});
if (response.data.success) {
this.STATE.settings = response.data.data;
this.renderAllComponents();
} else {
this.showNotice('Error al cargar configuraciones', 'error');
}
} catch (error) {
console.error('Error loading settings:', error);
this.showNotice('Error de conexión', 'error');
} finally {
this.STATE.isLoading = false;
this.showSpinner(false);
}
},
/**
* Guardar configuraciones al servidor
*/
async saveSettings() {
if (!this.STATE.hasChanges) {
this.showNotice('No hay cambios para guardar', 'info');
return;
}
this.showSpinner(true);
try {
const formData = this.collectFormData();
const response = await axios({
method: 'POST',
url: apusAdminData.ajaxUrl,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
action: 'apus_save_settings',
nonce: apusAdminData.nonce,
...formData
})
});
if (response.data.success) {
this.STATE.hasChanges = false;
document.getElementById('saveSettings').disabled = true;
this.showNotice('Configuración guardada correctamente', 'success');
} else {
this.showNotice(response.data.data.message || 'Error al guardar', 'error');
}
} catch (error) {
console.error('Error saving settings:', error);
this.showNotice('Error de conexión', 'error');
} finally {
this.showSpinner(false);
}
},
/**
* Recolectar datos del formulario
* Cada componente agregará su sección aquí
*/
collectFormData() {
return {
components: {
top_bar: {
enabled: document.getElementById('topBarEnabled').checked,
show_on_mobile: document.getElementById('topBarShowOnMobile').checked,
show_on_desktop: document.getElementById('topBarShowOnDesktop').checked,
icon_class: document.getElementById('topBarIconClass').value.trim(),
show_icon: document.getElementById('topBarShowIcon').checked,
highlight_text: document.getElementById('topBarHighlightText').value.trim(),
message_text: document.getElementById('topBarMessageText').value.trim(),
link_text: document.getElementById('topBarLinkText').value.trim(),
link_url: document.getElementById('topBarLinkUrl').value.trim(),
link_target: document.getElementById('topBarLinkTarget').value,
show_link: document.getElementById('topBarShowLink').checked,
custom_styles: {
background_color: this.getColorValue('topBarBgColor', ''),
text_color: this.getColorValue('topBarTextColor', ''),
highlight_color: this.getColorValue('topBarHighlightColor', ''),
link_hover_color: this.getColorValue('topBarLinkHoverColor', ''),
font_size: document.getElementById('topBarFontSize').value
}
}
// Navbar - Pendiente
// Hero - Pendiente
// Footer - Pendiente
}
};
},
/**
* Renderizar todos los componentes
*/
renderAllComponents() {
const components = this.STATE.settings.components || {};
// Top Bar
if (components.top_bar) {
this.renderTopBar(components.top_bar);
}
// Navbar - Pendiente
// Hero - Pendiente
// Footer - Pendiente
},
/**
* Renderizar Top Bar
*/
renderTopBar(topBar) {
document.getElementById('topBarEnabled').checked = topBar.enabled !== undefined ? topBar.enabled : true;
document.getElementById('topBarShowOnMobile').checked = topBar.show_on_mobile !== undefined ? topBar.show_on_mobile : true;
document.getElementById('topBarShowOnDesktop').checked = topBar.show_on_desktop !== undefined ? topBar.show_on_desktop : true;
document.getElementById('topBarIconClass').value = topBar.icon_class || 'bi bi-megaphone-fill';
document.getElementById('topBarShowIcon').checked = topBar.show_icon !== undefined ? topBar.show_icon : true;
document.getElementById('topBarHighlightText').value = topBar.highlight_text || 'Nuevo:';
document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
document.getElementById('topBarLinkText').value = topBar.link_text || 'Ver Catálogo';
document.getElementById('topBarLinkUrl').value = topBar.link_url || '/catalogo';
document.getElementById('topBarLinkTarget').value = topBar.link_target || '_self';
document.getElementById('topBarShowLink').checked = topBar.show_link !== undefined ? topBar.show_link : true;
// Estilos personalizados
if (topBar.custom_styles) {
if (topBar.custom_styles.background_color) {
document.getElementById('topBarBgColor').value = topBar.custom_styles.background_color;
}
if (topBar.custom_styles.text_color) {
document.getElementById('topBarTextColor').value = topBar.custom_styles.text_color;
}
if (topBar.custom_styles.highlight_color) {
document.getElementById('topBarHighlightColor').value = topBar.custom_styles.highlight_color;
}
if (topBar.custom_styles.link_hover_color) {
document.getElementById('topBarLinkHoverColor').value = topBar.custom_styles.link_hover_color;
}
document.getElementById('topBarFontSize').value = topBar.custom_styles.font_size || 'normal';
}
},
/**
* Utilidad: Obtener valor de color con fallback
*/
getColorValue(inputId, defaultValue) {
const input = document.getElementById(inputId);
const value = input ? input.value.trim() : '';
return value || defaultValue;
},
/**
* Utilidad: Mostrar spinner
*/
showSpinner(show) {
const spinner = document.querySelector('.spinner');
if (spinner) {
spinner.style.display = show ? 'inline-block' : 'none';
}
},
/**
* Utilidad: Mostrar notificación
*/
showNotice(message, type = 'info') {
// WordPress admin notices
const noticeDiv = document.createElement('div');
noticeDiv.className = `notice notice-${type} is-dismissible`;
noticeDiv.innerHTML = `<p>${message}</p>`;
const container = document.querySelector('.apus-admin-panel');
if (container) {
container.insertBefore(noticeDiv, container.firstChild);
// Auto-dismiss después de 5 segundos
setTimeout(() => {
noticeDiv.remove();
}, 5000);
}
}
};
// Inicializar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
AdminPanel.init();
});

View File

@@ -0,0 +1,119 @@
<?php
/**
* Admin Menu Class
*
* Registra menú en WordPress admin y carga assets
*
* @package Apus_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class APUS_Admin_Menu {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', array($this, 'add_menu_page'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
}
/**
* Registrar página de admin
*/
public function add_menu_page() {
add_theme_page(
'APUs Theme Settings', // Page title
'Tema APUs', // Menu title
'manage_options', // Capability
'apus-theme-settings', // Menu slug
array($this, 'render_admin_page'), // Callback
59 // Position
);
}
/**
* Renderizar página de admin
*/
public function render_admin_page() {
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permisos para acceder a esta página.'));
}
require_once APUS_ADMIN_PANEL_PATH . 'admin/pages/main.php';
}
/**
* Encolar assets (CSS/JS)
*/
public function enqueue_assets($hook) {
// Solo cargar en nuestra página
if ($hook !== 'appearance_page_apus-theme-settings') {
return;
}
// Bootstrap 5.3.2 CSS
wp_enqueue_style(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
array(),
'5.3.2'
);
// Bootstrap Icons
wp_enqueue_style(
'bootstrap-icons',
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css',
array(),
'1.11.1'
);
// Admin Panel CSS
wp_enqueue_style(
'apus-admin-panel-css',
APUS_ADMIN_PANEL_URL . 'admin/assets/css/admin-panel.css',
array('bootstrap'),
APUS_ADMIN_PANEL_VERSION
);
// Bootstrap 5.3.2 JS
wp_enqueue_script(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
array(),
'5.3.2',
true
);
// Axios (para AJAX)
wp_enqueue_script(
'axios',
'https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js',
array(),
'1.6.0',
true
);
// Admin Panel JS
wp_enqueue_script(
'apus-admin-panel-js',
APUS_ADMIN_PANEL_URL . 'admin/assets/js/admin-app.js',
array('jquery', 'axios'),
APUS_ADMIN_PANEL_VERSION,
true
);
// Pasar datos a JavaScript
wp_localize_script('apus-admin-panel-js', 'apusAdminData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('apus_admin_nonce')
));
}
}
// Instanciar clase
new APUS_Admin_Menu();

View File

@@ -0,0 +1,177 @@
<?php
/**
* Admin Panel - Main Page
*
* Interfaz de administración de componentes del tema
*
* @package Apus_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap apus-admin-panel">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<p class="description">Configure los componentes del tema Apus</p>
<!-- Navigation Tabs -->
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" data-bs-target="#topBar" href="#topBar">Top Bar</a>
</li>
<!-- Más tabs aquí: Navbar, Hero, Footer, etc. -->
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3">
<!-- Top Bar Tab -->
<div id="topBar" class="tab-pane fade show active">
<h3>Configuración Top Bar</h3>
<p class="text-muted">Personaliza la barra de notificación superior del sitio.</p>
<!-- Activación y Visibilidad -->
<div class="form-section mb-4">
<h4>Activación y Visibilidad</h4>
<div class="form-group mb-3">
<label class="form-check-label">
<input type="checkbox" id="topBarEnabled" class="form-check-input">
Activar Top Bar
</label>
<p class="form-text text-muted">Si está desactivado, el Top Bar no se mostrará en el sitio.</p>
</div>
<div class="form-group mb-3">
<label class="form-check-label">
<input type="checkbox" id="topBarShowOnMobile" class="form-check-input">
Mostrar en móvil
</label>
</div>
<div class="form-group mb-3">
<label class="form-check-label">
<input type="checkbox" id="topBarShowOnDesktop" class="form-check-input">
Mostrar en desktop
</label>
</div>
</div>
<!-- Contenido -->
<div class="form-section mb-4">
<h4>Contenido</h4>
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label for="topBarIconClass">Clase del icono</label>
<input type="text" id="topBarIconClass" class="form-control" placeholder="bi bi-megaphone-fill">
<p class="form-text text-muted">Usa clases de <a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icons</a></p>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-check-label">
<input type="checkbox" id="topBarShowIcon" class="form-check-input">
Mostrar icono
</label>
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="topBarHighlightText">Texto destacado</label>
<input type="text" id="topBarHighlightText" class="form-control" placeholder="Nuevo:" maxlength="30">
<p class="form-text text-muted">Máximo 30 caracteres. Aparece en negritas.</p>
</div>
<div class="form-group mb-3">
<label for="topBarMessageText">Mensaje principal *</label>
<input type="text" id="topBarMessageText" class="form-control" placeholder="Accede a más de 200,000 APUs..." maxlength="250" required>
<p class="form-text text-muted">Máximo 250 caracteres. Campo obligatorio.</p>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group mb-3">
<label for="topBarLinkText">Texto del enlace</label>
<input type="text" id="topBarLinkText" class="form-control" placeholder="Ver Catálogo" maxlength="50">
</div>
</div>
<div class="col-md-4">
<div class="form-group mb-3">
<label for="topBarLinkUrl">URL del enlace</label>
<input type="url" id="topBarLinkUrl" class="form-control" placeholder="/catalogo">
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="topBarLinkTarget">Target</label>
<select id="topBarLinkTarget" class="form-select">
<option value="_self">Misma pestaña</option>
<option value="_blank">Nueva pestaña</option>
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label class="form-check-label">
<input type="checkbox" id="topBarShowLink" class="form-check-input">
Mostrar enlace
</label>
</div>
</div>
</div>
</div>
<!-- Estilos Personalizados -->
<div class="form-section">
<h4>Estilos Personalizados</h4>
<p class="form-text text-muted mb-3">Deja en blanco para usar los colores del tema por defecto.</p>
<div class="row">
<div class="col-md-3">
<div class="form-group mb-3">
<label for="topBarBgColor">Color de fondo</label>
<input type="color" id="topBarBgColor" class="form-control form-control-color">
</div>
</div>
<div class="col-md-3">
<div class="form-group mb-3">
<label for="topBarTextColor">Color de texto</label>
<input type="color" id="topBarTextColor" class="form-control form-control-color">
</div>
</div>
<div class="col-md-3">
<div class="form-group mb-3">
<label for="topBarHighlightColor">Color del highlight</label>
<input type="color" id="topBarHighlightColor" class="form-control form-control-color">
</div>
</div>
<div class="col-md-3">
<div class="form-group mb-3">
<label for="topBarLinkHoverColor">Color hover enlace</label>
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color">
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="topBarFontSize">Tamaño de fuente</label>
<select id="topBarFontSize" class="form-select" style="max-width: 200px;">
<option value="small">Pequeña</option>
<option value="normal" selected>Normal</option>
<option value="large">Grande</option>
</select>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="admin-actions mt-4">
<button type="button" id="saveSettings" class="button button-primary" disabled>
<i class="bi bi-save me-2"></i>Guardar Cambios
</button>
<span class="spinner" style="display: none; float: none; margin-left: 10px;"></span>
</div>
</div>

View File

@@ -0,0 +1,183 @@
<?php
/**
* Settings Manager Class
*
* CRUD de configuraciones por componentes
*
* @package Apus_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class APUS_Settings_Manager {
const OPTION_NAME = 'apus_theme_settings';
/**
* Constructor
*/
public function __construct() {
add_action('wp_ajax_apus_get_settings', array($this, 'ajax_get_settings'));
add_action('wp_ajax_apus_save_settings', array($this, 'ajax_save_settings'));
}
/**
* Obtener configuraciones
*/
public function get_settings() {
$settings = get_option(self::OPTION_NAME, array());
$defaults = $this->get_defaults();
return wp_parse_args($settings, $defaults);
}
/**
* Guardar configuraciones
*/
public function save_settings($data) {
// Validar
$validator = new APUS_Validator();
$validation = $validator->validate($data);
if (!$validation['valid']) {
return array(
'success' => false,
'message' => 'Error de validación',
'errors' => $validation['errors']
);
}
// Sanitizar
$sanitized = $this->sanitize_settings($data);
// Agregar metadata
$sanitized['version'] = APUS_ADMIN_PANEL_VERSION;
$sanitized['updated_at'] = current_time('mysql');
// Guardar
update_option(self::OPTION_NAME, $sanitized, false);
return array(
'success' => true,
'message' => 'Configuración guardada correctamente'
);
}
/**
* Valores por defecto
* NOTA: Aquí se agregan los defaults de cada componente
*/
public function get_defaults() {
return array(
'version' => APUS_ADMIN_PANEL_VERSION,
'components' => array(
'top_bar' => array(
'enabled' => true,
'show_on_mobile' => true,
'show_on_desktop' => true,
'icon_class' => 'bi bi-megaphone-fill',
'show_icon' => true,
'highlight_text' => 'Nuevo:',
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
'link_text' => 'Ver Catálogo',
'link_url' => '/catalogo',
'link_target' => '_self',
'show_link' => true,
'custom_styles' => array(
'background_color' => '',
'text_color' => '',
'highlight_color' => '',
'link_hover_color' => '',
'font_size' => 'normal'
)
)
// Navbar - Pendiente
// Hero - Pendiente
// Footer - Pendiente
)
);
}
/**
* Sanitizar configuraciones
* NOTA: Aquí se agrega sanitización de cada componente
*/
public function sanitize_settings($data) {
$sanitized = array(
'components' => array()
);
// Sanitizar Top Bar
if (isset($data['components']['top_bar'])) {
$top_bar = $data['components']['top_bar'];
$sanitized['components']['top_bar'] = array(
'enabled' => !empty($top_bar['enabled']),
'show_on_mobile' => !empty($top_bar['show_on_mobile']),
'show_on_desktop' => !empty($top_bar['show_on_desktop']),
'icon_class' => sanitize_text_field($top_bar['icon_class'] ?? ''),
'show_icon' => !empty($top_bar['show_icon']),
'highlight_text' => sanitize_text_field($top_bar['highlight_text'] ?? ''),
'message_text' => sanitize_text_field($top_bar['message_text'] ?? ''),
'link_text' => sanitize_text_field($top_bar['link_text'] ?? ''),
'link_url' => esc_url_raw($top_bar['link_url'] ?? ''),
'link_target' => in_array($top_bar['link_target'] ?? '', array('_self', '_blank')) ? $top_bar['link_target'] : '_self',
'show_link' => !empty($top_bar['show_link']),
'custom_styles' => array(
'background_color' => sanitize_hex_color($top_bar['custom_styles']['background_color'] ?? ''),
'text_color' => sanitize_hex_color($top_bar['custom_styles']['text_color'] ?? ''),
'highlight_color' => sanitize_hex_color($top_bar['custom_styles']['highlight_color'] ?? ''),
'link_hover_color' => sanitize_hex_color($top_bar['custom_styles']['link_hover_color'] ?? ''),
'font_size' => in_array($top_bar['custom_styles']['font_size'] ?? '', array('small', 'normal', 'large')) ? $top_bar['custom_styles']['font_size'] : 'normal'
)
);
}
return $sanitized;
}
/**
* AJAX: Obtener configuraciones
*/
public function ajax_get_settings() {
check_ajax_referer('apus_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$settings = $this->get_settings();
wp_send_json_success($settings);
}
/**
* AJAX: Guardar configuraciones
*/
public function ajax_save_settings() {
check_ajax_referer('apus_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
wp_send_json_error('Datos inválidos');
}
$result = $this->save_settings($data);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
}
// Instanciar clase
new APUS_Settings_Manager();

View File

@@ -0,0 +1,100 @@
<?php
/**
* Validator Class
*
* Validación de datos por componentes
*
* @package Apus_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class APUS_Validator {
/**
* Validar todas las configuraciones
*/
public function validate($data) {
$errors = array();
// Validar estructura base
if (!isset($data['components']) || !is_array($data['components'])) {
$errors[] = 'Estructura de datos inválida';
return array('valid' => false, 'errors' => $errors);
}
// Validar Top Bar
if (isset($data['components']['top_bar'])) {
$top_bar_errors = $this->validate_top_bar($data['components']['top_bar']);
$errors = array_merge($errors, $top_bar_errors);
}
return array(
'valid' => empty($errors),
'errors' => $errors
);
}
/**
* Validar Top Bar
*/
public function validate_top_bar($top_bar) {
$errors = array();
// Validar icon_class
if (!empty($top_bar['icon_class']) && strlen($top_bar['icon_class']) > 50) {
$errors[] = 'La clase del icono no puede exceder 50 caracteres';
}
// Validar highlight_text
if (!empty($top_bar['highlight_text']) && strlen($top_bar['highlight_text']) > 30) {
$errors[] = 'El texto destacado no puede exceder 30 caracteres';
}
// Validar message_text
if (empty($top_bar['message_text'])) {
$errors[] = 'El mensaje principal es obligatorio';
} elseif (strlen($top_bar['message_text']) > 250) {
$errors[] = 'El mensaje principal no puede exceder 250 caracteres';
}
// Validar link_text
if (!empty($top_bar['link_text']) && strlen($top_bar['link_text']) > 50) {
$errors[] = 'El texto del enlace no puede exceder 50 caracteres';
}
// Validar link_url
if (!empty($top_bar['link_url']) && !filter_var($top_bar['link_url'], FILTER_VALIDATE_URL)) {
$errors[] = 'La URL del enlace no es válida';
}
// Validar link_target
if (!in_array($top_bar['link_target'] ?? '', array('_self', '_blank'))) {
$errors[] = 'El target del enlace debe ser _self o _blank';
}
// Validar colores
if (!empty($top_bar['custom_styles']['background_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['background_color'])) {
$errors[] = 'El color de fondo debe ser un color hexadecimal válido';
}
if (!empty($top_bar['custom_styles']['text_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['text_color'])) {
$errors[] = 'El color de texto debe ser un color hexadecimal válido';
}
if (!empty($top_bar['custom_styles']['highlight_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['highlight_color'])) {
$errors[] = 'El color del highlight debe ser un color hexadecimal válido';
}
if (!empty($top_bar['custom_styles']['link_hover_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['link_hover_color'])) {
$errors[] = 'El color hover del enlace debe ser un color hexadecimal válido';
}
// Validar font_size
if (!in_array($top_bar['custom_styles']['font_size'] ?? '', array('small', 'normal', 'large'))) {
$errors[] = 'El tamaño de fuente debe ser small, normal o large';
}
return $errors;
}
}

25
admin-panel/init.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
/**
* Admin Panel Module - Initialization
*
* Sistema de configuración por componentes
* Cada componente del tema es configurable desde el admin panel
*
* @package Apus_Theme
* @since 2.0.0
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Module constants
define('APUS_ADMIN_PANEL_VERSION', '2.0.0');
define('APUS_ADMIN_PANEL_PATH', get_template_directory() . '/admin-panel/');
define('APUS_ADMIN_PANEL_URL', get_template_directory_uri() . '/admin-panel/');
// Load classes
require_once APUS_ADMIN_PANEL_PATH . 'admin/includes/class-admin-menu.php';
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-settings-manager.php';
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-validator.php';

View File

@@ -20,16 +20,98 @@
<body <?php body_class(); ?> data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100"> <body <?php body_class(); ?> data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100">
<?php wp_body_open(); ?> <?php wp_body_open(); ?>
<!-- Top Notification Bar (Template líneas 55-63) --> <!-- Top Notification Bar (Configurable desde Admin Panel) -->
<div class="top-notification-bar"> <?php
// Obtener configuración del Top Bar
$settings = get_option('apus_theme_settings', array());
$top_bar_defaults = array(
'enabled' => true,
'show_on_mobile' => true,
'show_on_desktop' => true,
'icon_class' => 'bi bi-megaphone-fill',
'show_icon' => true,
'highlight_text' => 'Nuevo:',
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
'link_text' => 'Ver Catálogo',
'link_url' => '/catalogo',
'link_target' => '_self',
'show_link' => true,
'custom_styles' => array(
'background_color' => '',
'text_color' => '',
'highlight_color' => '',
'link_hover_color' => '',
'font_size' => 'normal'
)
);
$top_bar_config = isset($settings['components']['top_bar']) ? $settings['components']['top_bar'] : array();
$top_bar = wp_parse_args($top_bar_config, $top_bar_defaults);
// Verificar si está habilitado
if ($top_bar['enabled']):
// Construir clases CSS
$classes = array('top-notification-bar');
if (!$top_bar['show_on_mobile']) {
$classes[] = 'd-none';
$classes[] = 'd-md-block';
}
if (!$top_bar['show_on_desktop']) {
$classes[] = 'd-md-none';
}
$class_attr = implode(' ', $classes);
// Construir estilos inline
$inline_styles = array();
if (!empty($top_bar['custom_styles']['background_color'])) {
$inline_styles[] = 'background-color: ' . esc_attr($top_bar['custom_styles']['background_color']);
}
if (!empty($top_bar['custom_styles']['text_color'])) {
$inline_styles[] = 'color: ' . esc_attr($top_bar['custom_styles']['text_color']);
}
// Font size
$font_size_map = array(
'small' => '0.8rem',
'normal' => '0.9rem',
'large' => '1rem'
);
if (isset($font_size_map[$top_bar['custom_styles']['font_size']])) {
$inline_styles[] = 'font-size: ' . $font_size_map[$top_bar['custom_styles']['font_size']];
}
$style_attr = !empty($inline_styles) ? ' style="' . implode('; ', $inline_styles) . '"' : '';
?>
<div class="<?php echo esc_attr($class_attr); ?>"<?php echo $style_attr; ?>>
<div class="container"> <div class="container">
<div class="d-flex align-items-center justify-content-center"> <div class="d-flex align-items-center justify-content-center">
<i class="bi bi-megaphone-fill me-2"></i> <?php if ($top_bar['show_icon'] && !empty($top_bar['icon_class'])): ?>
<span><strong>Nuevo:</strong> Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</span> <i class="<?php echo esc_attr($top_bar['icon_class']); ?> me-2"></i>
<a href="<?php echo esc_url(home_url('/catalogo')); ?>" class="ms-2 text-white text-decoration-underline">Ver Catálogo</a> <?php endif; ?>
<span>
<?php if (!empty($top_bar['highlight_text'])): ?>
<strong<?php if (!empty($top_bar['custom_styles']['highlight_color'])): ?> style="color: <?php echo esc_attr($top_bar['custom_styles']['highlight_color']); ?>"<?php endif; ?>>
<?php echo esc_html($top_bar['highlight_text']); ?>
</strong>
<?php endif; ?>
<?php echo esc_html($top_bar['message_text']); ?>
</span>
<?php if ($top_bar['show_link'] && !empty($top_bar['link_text']) && !empty($top_bar['link_url'])): ?>
<a href="<?php echo esc_url(home_url($top_bar['link_url'])); ?>"
class="ms-2 text-white text-decoration-underline top-bar-link"
target="<?php echo esc_attr($top_bar['link_target']); ?>"
<?php if (!empty($top_bar['custom_styles']['link_hover_color'])): ?>
data-hover-color="<?php echo esc_attr($top_bar['custom_styles']['link_hover_color']); ?>"
<?php endif; ?>>
<?php echo esc_html($top_bar['link_text']); ?>
</a>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>
<?php endif; ?>
<!-- Navbar (Template líneas 264-320) --> <!-- Navbar (Template líneas 264-320) -->
<nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'apus-theme'); ?>"> <nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'apus-theme'); ?>">