Migración completa a Clean Architecture con componentes funcionales
- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/ - 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero, FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost, RelatedPost, ContactForm, Footer - Panel de administración con tabs Bootstrap 5 funcionales - Schemas JSON para configuración de componentes - Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado) - FormBuilders para UI admin con Design System consistente - Fix: Bootstrap JS cargado en header para tabs funcionales - Fix: buildTextInput maneja valores mixed (bool/string) - Eliminación de estructura legacy (src/, admin/, assets/css/componente-*) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
137
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
137
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Estilos para el Dashboard del Panel de Administración ROI Theme
|
||||
* Siguiendo especificaciones del Design System
|
||||
*/
|
||||
|
||||
/* Sobrescribir max-width de .card de WordPress */
|
||||
.wrap.roi-admin-panel .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix para switches de Bootstrap - resetear completamente estilos de WordPress */
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input {
|
||||
all: unset !important;
|
||||
/* Restaurar estilos necesarios de Bootstrap */
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
margin-left: -2.5em !important;
|
||||
margin-right: 0.5em !important;
|
||||
background-color: #dee2e6 !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: left center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: contain !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.25) !important;
|
||||
border-radius: 2em !important;
|
||||
transition: background-position 0.15s ease-in-out !important;
|
||||
cursor: pointer !important;
|
||||
flex-shrink: 0 !important;
|
||||
appearance: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:checked {
|
||||
background-color: #0d6efd !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: right center !important;
|
||||
border-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::before,
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:focus {
|
||||
outline: 0 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Alinear verticalmente los labels con los switches */
|
||||
.wrap.roi-admin-panel .form-check {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-check-label {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Tabs Navigation */
|
||||
.nav-tabs-admin {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.4rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link i.bi {
|
||||
margin-right: 0.2rem !important;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link:hover {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FFB800;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link.active {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FF8600;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 991px) {
|
||||
.nav-tabs-admin {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.nav-tabs-admin {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
407
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
407
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* JavaScript para el Dashboard del Panel de Administración ROI Theme
|
||||
* Vanilla JavaScript - No frameworks
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Inicializa el dashboard cuando el DOM está listo
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeTabs();
|
||||
initializeFormValidation();
|
||||
initializeButtons();
|
||||
initializeColorPickers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Inicializa el sistema de tabs
|
||||
*/
|
||||
function initializeTabs() {
|
||||
const tabs = document.querySelectorAll('.nav-tab');
|
||||
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function(e) {
|
||||
// Prevenir comportamiento por defecto si es necesario
|
||||
// (En este caso dejamos que funcione la navegación normal)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa validación de formularios
|
||||
*/
|
||||
function initializeFormValidation() {
|
||||
const forms = document.querySelectorAll('.roi-component-config form');
|
||||
|
||||
forms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(form)) {
|
||||
e.preventDefault();
|
||||
showError('Por favor, corrige los errores en el formulario.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida un formulario
|
||||
*
|
||||
* @param {HTMLFormElement} form Formulario a validar
|
||||
* @returns {boolean} True si es válido
|
||||
*/
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
|
||||
requiredFields.forEach(function(field) {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('error');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de error
|
||||
*
|
||||
* @param {string} message Mensaje a mostrar
|
||||
*/
|
||||
function showError(message) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'notice notice-error is-dismissible';
|
||||
notice.innerHTML = '<p>' + escapeHtml(message) + '</p>';
|
||||
|
||||
const h1 = document.querySelector('.roi-admin-dashboard h1');
|
||||
if (h1 && h1.nextElementSibling) {
|
||||
h1.nextElementSibling.after(notice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapa HTML para prevenir XSS
|
||||
*
|
||||
* @param {string} text Texto a escapar
|
||||
* @returns {string} Texto escapado
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los botones del panel
|
||||
*/
|
||||
function initializeButtons() {
|
||||
// Botón Guardar Cambios
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', handleSaveSettings);
|
||||
}
|
||||
|
||||
// Botón Cancelar
|
||||
const cancelButton = document.getElementById('cancelChanges');
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', handleCancelChanges);
|
||||
}
|
||||
|
||||
// Botones Restaurar valores por defecto (dinámico para todos los componentes)
|
||||
const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]');
|
||||
resetButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
const componentId = this.getAttribute('data-component');
|
||||
handleResetDefaults(e, componentId, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda los cambios del formulario
|
||||
*/
|
||||
function handleSaveSettings(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Obtener el tab activo
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (!activeTab) {
|
||||
showNotice('error', 'No hay ningún componente seleccionado.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el ID del componente desde el tab
|
||||
const componentId = activeTab.id.replace('Tab', '');
|
||||
|
||||
// Recopilar todos los campos del formulario activo
|
||||
const formData = collectFormData(activeTab);
|
||||
|
||||
// Mostrar loading en el botón
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
const originalText = saveButton.innerHTML;
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Guardando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_save_component_settings',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId,
|
||||
settings: JSON.stringify(formData)
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Cambios guardados correctamente.');
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al guardar los cambios.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al guardar los cambios.');
|
||||
})
|
||||
.finally(() => {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancela los cambios y recarga la página
|
||||
*/
|
||||
function handleCancelChanges(e) {
|
||||
e.preventDefault();
|
||||
showConfirmModal(
|
||||
'Cancelar cambios',
|
||||
'¿Descartar todos los cambios no guardados?',
|
||||
function() {
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura los valores por defecto de un componente
|
||||
*
|
||||
* @param {Event} e Evento del click
|
||||
* @param {string} componentId ID del componente a resetear
|
||||
* @param {HTMLElement} resetButton Elemento del botón que disparó el evento
|
||||
*/
|
||||
function handleResetDefaults(e, componentId, resetButton) {
|
||||
e.preventDefault();
|
||||
|
||||
showConfirmModal(
|
||||
'Restaurar valores por defecto',
|
||||
'¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.',
|
||||
function() {
|
||||
// Mostrar loading en el botón
|
||||
const originalText = resetButton.innerHTML;
|
||||
resetButton.disabled = true;
|
||||
resetButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Restaurando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_reset_component_defaults',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Valores restaurados correctamente.');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al restaurar los valores.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al restaurar los valores.');
|
||||
})
|
||||
.finally(() => {
|
||||
resetButton.disabled = false;
|
||||
resetButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recopila los datos del formulario del tab activo
|
||||
*/
|
||||
function collectFormData(container) {
|
||||
const formData = {};
|
||||
|
||||
// Inputs de texto, textarea, select, color, number, email, password
|
||||
const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select');
|
||||
textInputs.forEach(input => {
|
||||
if (input.id) {
|
||||
formData[input.id] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Checkboxes (switches)
|
||||
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.id) {
|
||||
formData[checkbox.id] = checkbox.checked;
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un toast de Bootstrap
|
||||
*/
|
||||
function showNotice(type, message) {
|
||||
// Mapear tipos
|
||||
const typeMap = {
|
||||
'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' },
|
||||
'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' },
|
||||
'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' },
|
||||
'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' }
|
||||
};
|
||||
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
// Crear container de toasts si no existe
|
||||
let toastContainer = document.getElementById('roiToastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'roiToastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||
toastContainer.style.top = '60px';
|
||||
toastContainer.style.zIndex = '999999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Crear toast
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi ${config.icon} me-2"></i>
|
||||
<strong>${escapeHtml(message)}</strong>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
// Mostrar toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Eliminar del DOM después de ocultarse
|
||||
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un modal de confirmación de Bootstrap
|
||||
*/
|
||||
function showConfirmModal(title, message, onConfirm) {
|
||||
// Crear modal si no existe
|
||||
let modal = document.getElementById('roiConfirmModal');
|
||||
if (!modal) {
|
||||
const modalHTML = `
|
||||
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
||||
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
||||
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
||||
<span id="roiConfirmModalTitle">Confirmar</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
||||
Mensaje de confirmación
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
modal = document.getElementById('roiConfirmModal');
|
||||
}
|
||||
|
||||
// Actualizar contenido
|
||||
document.getElementById('roiConfirmModalTitle').textContent = title;
|
||||
document.getElementById('roiConfirmModalBody').textContent = message;
|
||||
|
||||
// Configurar callback
|
||||
const confirmButton = document.getElementById('roiConfirmModalConfirm');
|
||||
const newConfirmButton = confirmButton.cloneNode(true);
|
||||
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
|
||||
|
||||
newConfirmButton.addEventListener('click', function() {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
bsModal.hide();
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
// Mostrar modal
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los color pickers para mostrar el valor HEX
|
||||
*/
|
||||
function initializeColorPickers() {
|
||||
const colorPickers = document.querySelectorAll('input[type="color"]');
|
||||
|
||||
colorPickers.forEach(picker => {
|
||||
// Elemento donde se muestra el valor HEX
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user