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:
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Application - Orquestación sin lógica de negocio ni WordPress
|
||||
*/
|
||||
final class RenderDashboardUseCase
|
||||
{
|
||||
/**
|
||||
* @param DashboardRendererInterface $renderer Renderizador del dashboard
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly DashboardRendererInterface $renderer
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta el caso de uso
|
||||
*
|
||||
* @param string $viewType Tipo de vista a renderizar
|
||||
* @return string HTML renderizado
|
||||
* @throws \RuntimeException Si el renderer no soporta el tipo de vista
|
||||
*/
|
||||
public function execute(string $viewType = 'dashboard'): string
|
||||
{
|
||||
if (!$this->renderer->supports($viewType)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Renderer does not support view type: %s', $viewType)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->renderer->render();
|
||||
}
|
||||
}
|
||||
601
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
601
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
@@ -0,0 +1,601 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Contact Form
|
||||
*
|
||||
* SEGURIDAD: El webhook_url se muestra como input type="password" para evitar
|
||||
* que sea visible accidentalmente en pantalla compartida.
|
||||
*
|
||||
* @package ROITheme\Admin\ContactForm\Infrastructure\Ui
|
||||
*/
|
||||
final class ContactFormFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildContactInfoGroup($componentId);
|
||||
$html .= $this->buildFormLabelsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildIntegrationGroup($componentId);
|
||||
$html .= $this->buildMessagesGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Formulario de Contacto';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de contacto antes del footer con envio a webhook';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="contact_form">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('contactFormEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', '¿Tienes alguna pregunta?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionDescription = $this->renderer->getFieldValue($componentId, 'content', 'section_description', 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="contactFormSectionDescription" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($sectionDescription);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonText = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_text', 'Enviar Mensaje');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSubmitButtonText" class="form-label small mb-1 fw-semibold">Texto boton enviar</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonIcon = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_icon', 'bi-send-fill');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormSubmitButtonIcon" class="form-label small mb-1 fw-semibold">Icono boton (Bootstrap Icons)</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonIcon) . '" placeholder="bi-send-fill">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContactInfoGroup(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-person-lines-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Info de Contacto';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showContactInfo = $this->renderer->getFieldValue($componentId, 'contact_info', 'show_contact_info', true);
|
||||
$html .= $this->buildSwitch('contactFormShowContactInfo', 'Mostrar info contacto', 'bi-eye', $showContactInfo);
|
||||
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">Telefono</p>';
|
||||
|
||||
$phoneLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_label', 'Teléfono');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$phoneValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_value', '+52 55 1234 5678');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneValue) . '" placeholder="Numero">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Email</p>';
|
||||
|
||||
$emailLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_label', 'Email');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormEmailLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_value', 'contacto@apumexico.com');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="email" id="contactFormEmailValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Ubicacion</p>';
|
||||
|
||||
$locationLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_label', 'Ubicación');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormLocationLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$locationValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_value', 'Ciudad de México, México');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <input type="text" id="contactFormLocationValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFormLabelsGroup(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-input-cursor-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Labels del Formulario';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$fullnamePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'fullname_placeholder', 'Nombre completo *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormFullnamePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder nombre</label>';
|
||||
$html .= ' <input type="text" id="contactFormFullnamePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fullnamePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$companyPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'company_placeholder', 'Empresa');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormCompanyPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder empresa</label>';
|
||||
$html .= ' <input type="text" id="contactFormCompanyPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($companyPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$whatsappPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'whatsapp_placeholder', 'WhatsApp *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormWhatsappPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder WhatsApp</label>';
|
||||
$html .= ' <input type="text" id="contactFormWhatsappPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($whatsappPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'email_placeholder', 'Correo electrónico *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormEmailPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder email</label>';
|
||||
$html .= ' <input type="text" id="contactFormEmailPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$messagePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'message_placeholder', '¿En qué podemos ayudarte?');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormMessagePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder mensaje</label>';
|
||||
$html .= ' <input type="text" id="contactFormMessagePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($messagePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildIntegrationGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Integracion Webhook';
|
||||
$html .= ' <span class="badge bg-warning text-dark ms-2">Privado</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="alert alert-info py-2 small mb-3">';
|
||||
$html .= ' <i class="bi bi-shield-lock me-1"></i>';
|
||||
$html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL del Webhook';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormWebhookUrl" class="form-control form-control-sm" rows="2" ';
|
||||
$html .= ' placeholder="https://tu-webhook.com/endpoint">';
|
||||
$html .= esc_textarea($webhookUrl);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' <small class="text-muted">Deja vacio si no deseas enviar a un webhook externo.</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookMethod = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_method', 'POST');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookMethod" class="form-label small mb-1 fw-semibold">Metodo HTTP</label>';
|
||||
$html .= ' <select id="contactFormWebhookMethod" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="POST"' . ($webhookMethod === 'POST' ? ' selected' : '') . '>POST</option>';
|
||||
$html .= ' <option value="GET"' . ($webhookMethod === 'GET' ? ' selected' : '') . '>GET</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$includePageUrl = $this->renderer->getFieldValue($componentId, 'integration', 'include_page_url', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludePageUrl', 'Incluir URL de pagina', 'bi-link', $includePageUrl);
|
||||
|
||||
$includeTimestamp = $this->renderer->getFieldValue($componentId, 'integration', 'include_timestamp', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludeTimestamp', 'Incluir timestamp', 'bi-clock', $includeTimestamp);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMessagesGroup(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-chat-quote me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensajes';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$successMessage = $this->renderer->getFieldValue($componentId, 'messages', 'success_message', '¡Gracias por contactarnos! Te responderemos pronto.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSuccessMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-check-circle me-1 text-success"></i>';
|
||||
$html .= ' Mensaje de exito';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormSuccessMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($successMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormErrorMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-x-circle me-1 text-danger"></i>';
|
||||
$html .= ' Mensaje de error';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormErrorMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($errorMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSendingMessage" class="form-label small mb-1 fw-semibold">Mensaje enviando</label>';
|
||||
$html .= ' <input type="text" id="contactFormSendingMessage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sendingMessage) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormValidationRequired" class="form-label small mb-1 fw-semibold">Error campo requerido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationRequired" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationRequired) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormValidationEmail" class="form-label small mb-1 fw-semibold">Error email invalido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationEmail" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationEmail) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_bg_color', 'rgba(108, 117, 125, 0.25)');
|
||||
$html .= $this->buildColorPicker('contactFormSectionBgColor', 'Fondo seccion', $sectionBgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('contactFormTitleColor', 'Color titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('contactFormButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#e67a00');
|
||||
$html .= $this->buildColorPicker('contactFormButtonHoverBg', 'Hover boton', $buttonHoverBg);
|
||||
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormIconColor', 'Iconos', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mensajes
|
||||
$html .= ' <p class="small fw-semibold mb-2">Mensajes</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$successBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'success_bg_color', '#d1e7dd');
|
||||
$html .= $this->buildColorPicker('contactFormSuccessBgColor', 'Fondo exito', $successBgColor);
|
||||
|
||||
$errorBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'error_bg_color', '#f8d7da');
|
||||
$html .= $this->buildColorPicker('contactFormErrorBgColor', 'Fondo error', $errorBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionPaddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'section_padding_y', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionPaddingY" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionPaddingY" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionPaddingY) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="contactFormTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$formGap = $this->renderer->getFieldValue($componentId, 'spacing', 'form_gap', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormFormGap" class="form-label small mb-1 fw-semibold">Espacio campos</label>';
|
||||
$html .= ' <input type="text" id="contactFormFormGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($formGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$inputBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormInputBorderRadius" class="form-label small mb-1 fw-semibold">Radio inputs</label>';
|
||||
$html .= ' <input type="text" id="contactFormInputBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($inputBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.75rem 2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="contactFormTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormTextareaRows" class="form-label small mb-1 fw-semibold">Filas textarea</label>';
|
||||
$html .= ' <input type="number" id="contactFormTextareaRows" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($textareaRows) . '" min="2" max="10">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
// Manejar colores rgba
|
||||
$colorValue = $value;
|
||||
if (strpos($value, 'rgba') === 0 || strpos($value, 'rgb') === 0) {
|
||||
// Para rgba usamos un color aproximado en el picker
|
||||
$colorValue = '#6c757d';
|
||||
}
|
||||
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($colorValue)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <input type="text" class="form-control" id="%sText" value="%s" style="font-size: 0.75rem;">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para el CTA Box Sidebar
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaBoxSidebarFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-megaphone me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Box Sidebar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Caja de llamada a la accion en el sidebar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta_box_sidebar">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaEnabled', 'Activar CTA box', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaDescription" class="form-control form-control-sm" rows="2">' . esc_textarea($description) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonIcon" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-stars me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Icono del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="ej: bi bi-calendar-check">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_action
|
||||
$buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonAction" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-cursor me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Accion del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaButtonAction" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="modal"' . ($buttonAction === 'modal' ? ' selected' : '') . '>Abrir modal</option>';
|
||||
$html .= ' <option value="link"' . ($buttonAction === 'link' ? ' selected' : '') . '>Ir a URL</option>';
|
||||
$html .= ' <option value="scroll"' . ($buttonAction === 'scroll' ? ' selected' : '') . '>Scroll a seccion</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_link
|
||||
$buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaButtonLink" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL/ID destino';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonLink" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonLink) . '" placeholder="ej: #contactModal o https://...">';
|
||||
$html .= ' <small class="text-muted">Para modal usa #nombreModal, para scroll usa #idSeccion</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(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-sliders me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// text_align
|
||||
$textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaTextAlign" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-text-center me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Alineacion del texto';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaTextAlign" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="left"' . ($textAlign === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||
$html .= ' <option value="center"' . ($textAlign === 'center' ? ' selected' : '') . '>Centro</option>';
|
||||
$html .= ' <option value="right"' . ($textAlign === 'right' ? ' selected' : '') . '>Derecha</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.25rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_font_size
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_font_size
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_font_weight
|
||||
$buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontWeight" class="form-label small mb-1 fw-semibold">Peso boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)');
|
||||
$html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonBgColor', 'Fondo', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores hover
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton Hover</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_background', '#0E2337');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverBg', 'Fondo hover', $buttonHoverBg);
|
||||
|
||||
$buttonHoverText = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverText', 'Texto hover', $buttonHoverText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_margin_bottom
|
||||
$descMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'description_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescMarginBottom" class="form-label small mb-1 fw-semibold">Margen descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.75rem 1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// icon_margin_right
|
||||
$iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaIconMarginRight" class="form-label small mb-1 fw-semibold">Margen icono</label>';
|
||||
$html .= ' <input type="text" id="ctaIconMarginRight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconMarginRight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="ctaBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mt-3 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
450
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
450
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkFormBuilder
|
||||
*
|
||||
* Genera el formulario de administración para el componente CTA "Let's Talk".
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar formulario de configuración del botón CTA
|
||||
* - Organizar campos en grupos según el schema JSON
|
||||
* - Aplicar Design System (gradiente navy, borde orange)
|
||||
* - Usar Bootstrap 5 form controls
|
||||
*
|
||||
* @package ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaLetsTalkFormBuilder
|
||||
{
|
||||
private const COMPONENT_ID = 'cta-lets-talk';
|
||||
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($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-lightning-charge-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Botón "Let\'s Talk"';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el botón CTA principal del navbar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-lets-talk">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnabled">';
|
||||
$html .= ' <strong>Mostrar botón Let\'s Talk</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en escritorio</strong> <span class="text-muted">(≥992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowMobile">';
|
||||
$html .= ' <strong>Mostrar en móvil</strong> <span class="text-muted">(<992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk");
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkButtonText" class="form-label small mb-1 fw-semibold">Texto del botón</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkButtonText" name="content[button_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '" maxlength="30" placeholder="Let\'s Talk">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show Icon
|
||||
$showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowIcon" name="content[show_icon]" ';
|
||||
$html .= checked($showIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowIcon">';
|
||||
$html .= ' <strong>Mostrar ícono</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Class
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Clase del ícono <a href="https://icons.getbootstrap.com/" target="_blank" class="text-decoration-none"><i class="bi bi-box-arrow-up-right"></i></a>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconClass" name="content[icon_class]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-lightning-charge-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons (ej: bi-chat-dots)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Modal Target
|
||||
$modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkModalTarget" class="form-label small mb-1 fw-semibold">ID del modal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkModalTarget" name="content[modal_target]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($modalTarget) . '" placeholder="#contactModal">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: ARIA Label
|
||||
$ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkAriaLabel" class="form-label small mb-1 fw-semibold">Etiqueta ARIA (accesibilidad)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkAriaLabel" name="content[aria_label]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($ariaLabel) . '" maxlength="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(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-mouse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enable Modal
|
||||
$enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnableModal" name="behavior[enable_modal]" ';
|
||||
$html .= checked($enableModal, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnableModal">';
|
||||
$html .= ' <strong>Abrir modal al hacer clic</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Si está desactivado, usará la URL personalizada</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL: Custom URL
|
||||
$customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkCustomUrl" class="form-label small mb-1 fw-semibold">URL personalizada</label>';
|
||||
$html .= ' <input type="url" id="ctaLetsTalkCustomUrl" name="behavior[custom_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customUrl) . '" placeholder="https://ejemplo.com/contacto">';
|
||||
$html .= ' <small class="text-muted">Solo se usa si "Abrir modal" está desactivado</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Open in New Tab
|
||||
$openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkOpenNewTab" name="behavior[open_in_new_tab]" ';
|
||||
$html .= checked($openNewTab, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkOpenNewTab">';
|
||||
$html .= ' <strong>Abrir en nueva pestaña</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkFontSize" name="typography[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontWeight" class="form-label small mb-1 fw-semibold">Peso de fuente</label>';
|
||||
$html .= ' <select id="ctaLetsTalkFontWeight" name="typography[font_weight]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($fontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($fontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($fontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($fontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Text Transform
|
||||
$textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTextTransform" class="form-label small mb-1 fw-semibold">Transformación de texto</label>';
|
||||
$html .= ' <select id="ctaLetsTalkTextTransform" name="typography[text_transform]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="none" ' . selected($textTransform, 'none', false) . '>Normal</option>';
|
||||
$html .= ' <option value="uppercase" ' . selected($textTransform, 'uppercase', false) . '>MAYÚSCULAS</option>';
|
||||
$html .= ' <option value="lowercase" ' . selected($textTransform, 'lowercase', false) . '>minúsculas</option>';
|
||||
$html .= ' <option value="capitalize" ' . selected($textTransform, 'capitalize', false) . '>Capitalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Color: Background
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Background Hover
|
||||
$bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgHoverColor" class="form-label small mb-1 fw-semibold">Color de fondo (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgHoverColor" name="colors[background_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextColor" name="colors[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text Hover
|
||||
$textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextHoverColor" class="form-label small mb-1 fw-semibold">Color del texto (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextHoverColor" name="colors[text_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Color (permite transparent)
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderColor" class="form-label small mb-1 fw-semibold">Color del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderColor" name="colors[border_color]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderColor) . '" placeholder="transparent o #RRGGBB">';
|
||||
$html .= ' <small class="text-muted">Usa "transparent" para sin borde visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-angle-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Padding Top/Bottom
|
||||
$paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingTB" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingTB" name="spacing[padding_top_bottom]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingTB) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Padding Left/Right
|
||||
$paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingLR" class="form-label small mb-1 fw-semibold">Padding horizontal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingLR" name="spacing[padding_left_right]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingLR) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Margin Left
|
||||
$marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkMarginLeft" class="form-label small mb-1 fw-semibold">Margen izquierdo (desktop)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkMarginLeft" name="spacing[margin_left]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginLeft) . '" placeholder="1rem">';
|
||||
$html .= ' <small class="text-muted">Separación del menú en pantallas ≥992px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Spacing
|
||||
$iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkIconSpacing" class="form-label small mb-1 fw-semibold">Espaciado del ícono</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconSpacing" name="spacing[icon_spacing]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconSpacing) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(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-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderRadius" class="form-label small mb-1 fw-semibold">Radio de bordes</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="6px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderWidth" class="form-label small mb-1 fw-semibold">Grosor del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderWidth" name="visual_effects[border_width]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBoxShadow" name="visual_effects[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '" placeholder="none">';
|
||||
$html .= ' <small class="text-muted">Ej: 0 2px 4px rgba(0,0,0,0.1)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Transition Duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTransition" class="form-label small mb-1 fw-semibold">Duración de transición</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkTransition" name="visual_effects[transition_duration]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
440
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
440
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para CTA Post
|
||||
*
|
||||
* @package ROITheme\Admin\CtaPost\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaPostFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Post';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' CTA promocional debajo del contenido del post';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta_post">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaPostDescription" class="form-control form-control-sm" rows="3">';
|
||||
$html .= esc_textarea($description);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button URL
|
||||
$buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonUrl" class="form-label small mb-1 fw-semibold">URL del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonUrl) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaPostButtonIcon" class="form-label small mb-1 fw-semibold">Icono del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="bi-arrow-right">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1.125rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Gradiente
|
||||
$html .= ' <p class="small fw-semibold mb-2">Gradiente de fondo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientStart', 'Inicio', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#FFB800');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientEnd', 'Fin', $gradientEnd);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Textos
|
||||
$html .= ' <p class="small fw-semibold mb-2">Textos</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostDescColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonBg', 'Fondo', $buttonBg);
|
||||
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonText', 'Texto', $buttonText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostPadding" class="form-label small mb-1 fw-semibold">Padding interno</label>';
|
||||
$html .= ' <input type="text" id="ctaPostPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleMargin = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleMargin" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleMargin" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMargin) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gradientAngle = $this->renderer->getFieldValue($componentId, 'visual_effects', 'gradient_angle', '135deg');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostGradientAngle" class="form-label small mb-1 fw-semibold">Angulo gradiente</label>';
|
||||
$html .= ' <input type="text" id="ctaPostGradientAngle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gradientAngle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.5rem 1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para tabs de componentes en el dashboard
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface ComponentTabInterface
|
||||
{
|
||||
/**
|
||||
* Obtiene el ID del componente
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre visible del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el ícono del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string;
|
||||
|
||||
/**
|
||||
* Renderiza el contenido del tab
|
||||
*
|
||||
* @return string HTML del contenido
|
||||
*/
|
||||
public function renderContent(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el tab está activo
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive(): bool;
|
||||
}
|
||||
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface DashboardRendererInterface
|
||||
{
|
||||
/**
|
||||
* Renderiza el dashboard completo
|
||||
*
|
||||
* @return string HTML del dashboard
|
||||
*/
|
||||
public function render(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el renderizador soporta un tipo de vista
|
||||
*
|
||||
* @param string $viewType Tipo de vista (dashboard, settings, etc.)
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(string $viewType): bool;
|
||||
}
|
||||
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para registrar el menú de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface MenuRegistrarInterface
|
||||
{
|
||||
/**
|
||||
* Registra el menú en el sistema de administración
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void;
|
||||
|
||||
/**
|
||||
* Obtiene la capacidad requerida para acceder al menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCapability(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el slug del menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSlug(): string;
|
||||
}
|
||||
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object para representar un item del menú
|
||||
*
|
||||
* Domain - Objeto inmutable sin WordPress
|
||||
*/
|
||||
final class MenuItem
|
||||
{
|
||||
/**
|
||||
* @param string $pageTitle Título de la página
|
||||
* @param string $menuTitle Título del menú
|
||||
* @param string $capability Capacidad requerida
|
||||
* @param string $menuSlug Slug del menú
|
||||
* @param string $icon Ícono del menú
|
||||
* @param int $position Posición en el menú
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $pageTitle,
|
||||
private readonly string $menuTitle,
|
||||
private readonly string $capability,
|
||||
private readonly string $menuSlug,
|
||||
private readonly string $icon,
|
||||
private readonly int $position
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->pageTitle)) {
|
||||
throw new \InvalidArgumentException('Page title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuTitle)) {
|
||||
throw new \InvalidArgumentException('Menu title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->capability)) {
|
||||
throw new \InvalidArgumentException('Capability cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuSlug)) {
|
||||
throw new \InvalidArgumentException('Menu slug cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->position < 0) {
|
||||
throw new \InvalidArgumentException('Position must be >= 0');
|
||||
}
|
||||
}
|
||||
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
return $this->pageTitle;
|
||||
}
|
||||
|
||||
public function getMenuTitle(): string
|
||||
{
|
||||
return $this->menuTitle;
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->capability;
|
||||
}
|
||||
|
||||
public function getMenuSlug(): string
|
||||
{
|
||||
return $this->menuSlug;
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
final class FeaturedImageFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Imagen Destacada';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la imagen destacada de los posts';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="featured_image">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar imagen destacada</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamano de imagen';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageSize" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="roi-featured-large" ' . selected($imageSize, 'roi-featured-large', false) . '>Grande (1200x600)</option>';
|
||||
$html .= ' <option value="roi-featured-medium" ' . selected($imageSize, 'roi-featured-medium', false) . '>Mediano (800x400)</option>';
|
||||
$html .= ' <option value="full" ' . selected($imageSize, 'full', false) . '>Original (tamano completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLazyLoading" ';
|
||||
$html .= checked($lazyLoading, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLazyLoading">';
|
||||
$html .= ' <i class="bi bi-lightning me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Carga diferida (lazy loading)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Mejora rendimiento cargando imagen cuando es visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLinkToMedia" ';
|
||||
$html .= checked($linkToMedia, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLinkToMedia">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Enlazar a imagen completa</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Abre la imagen en tamano completo al hacer clic</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginTop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen superior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '" placeholder="2rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="featuredImageBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Radio de bordes';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="12px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageHoverEffect" ';
|
||||
$html .= checked($hoverEffect, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageHoverEffect">';
|
||||
$html .= ' <i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Efecto hover</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Aplica efecto de escala sutil al pasar el mouse</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$hoverScale = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_scale', '1.02');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageHoverScale" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Escala en hover';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageHoverScale" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverScale) . '" placeholder="1.02">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageTransitionDuration" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Duracion transicion';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
413
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
413
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Footer
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Footer
|
||||
*
|
||||
* @package ROITheme\Admin\Footer\Infrastructure\Ui
|
||||
*/
|
||||
final class FooterFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildWidget1Group($componentId);
|
||||
$html .= $this->buildWidget2Group($componentId);
|
||||
$html .= $this->buildWidget3Group($componentId);
|
||||
$html .= $this->buildNewsletterGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildFooterBottomGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-layout-text-window-reverse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Footer';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Footer con menus de navegacion y newsletter';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="footer">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('footerEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('footerShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget1Group(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-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 1 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget1Visible', 'Mostrar Widget 1', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_title', 'Recursos');
|
||||
$html .= $this->buildTextInput('footerWidget1Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 1</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget2Group(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-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 2 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget2Visible', 'Mostrar Widget 2', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_title', 'Soporte');
|
||||
$html .= $this->buildTextInput('footerWidget2Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 2</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget3Group(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-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 3 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget3Visible', 'Mostrar Widget 3', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_title', 'Empresa');
|
||||
$html .= $this->buildTextInput('footerWidget3Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 3</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNewsletterGroup(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-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Newsletter';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_visible', true);
|
||||
$html .= $this->buildSwitch('footerNewsletterVisible', 'Mostrar Newsletter', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_title', 'Suscribete al Newsletter');
|
||||
$html .= $this->buildTextInput('footerNewsletterTitle', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$description = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_description', 'Recibe las ultimas actualizaciones.');
|
||||
$html .= $this->buildTextarea('footerNewsletterDescription', 'Descripcion', 'bi-text-paragraph', $description);
|
||||
|
||||
$placeholder = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_placeholder', 'Email');
|
||||
$html .= $this->buildTextInput('footerNewsletterPlaceholder', 'Placeholder email', 'bi-input-cursor', $placeholder);
|
||||
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_button_text', 'Suscribirse');
|
||||
$html .= $this->buildTextInput('footerNewsletterButtonText', 'Texto boton', 'bi-cursor', $buttonText);
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_webhook_url', '');
|
||||
$html .= $this->buildPasswordInput('footerNewsletterWebhookUrl', 'URL del Webhook', 'bi-link-45deg', $webhookUrl);
|
||||
|
||||
$successMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_success_message', 'Gracias por suscribirte!');
|
||||
$html .= $this->buildTextInput('footerNewsletterSuccessMessage', 'Mensaje exito', 'bi-check-circle', $successMsg);
|
||||
|
||||
$errorMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_error_message', 'Error al suscribirse.');
|
||||
$html .= $this->buildTextInput('footerNewsletterErrorMessage', 'Mensaje error', 'bi-x-circle', $errorMsg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFooterBottomGroup(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-c-circle me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Pie de Footer';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$copyright = $this->renderer->getFieldValue($componentId, 'footer_bottom', 'copyright_text', date('Y') . ' Todos los derechos reservados.');
|
||||
$html .= $this->buildTextInput('footerCopyrightText', 'Texto copyright', 'bi-c-circle', $copyright);
|
||||
|
||||
$html .= ' <div class="alert alert-secondary small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El simbolo © se agrega automaticamente';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'bg_color', '#212529');
|
||||
$html .= $this->buildColorInput('footerBgColor', 'Fondo footer', $bgColor);
|
||||
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTextColor', 'Color texto', $textColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTitleColor', 'Color titulos', $titleColor);
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerLinkColor', 'Color links', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorInput('footerLinkHoverColor', 'Color links hover', $linkHoverColor);
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#0d6efd');
|
||||
$html .= $this->buildColorInput('footerButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#0b5ed7');
|
||||
$html .= $this->buildColorInput('footerButtonHoverBg', 'Fondo boton hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$paddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_y', '3rem');
|
||||
$html .= $this->buildTextInput('footerPaddingY', 'Padding vertical', 'bi-arrows-vertical', $paddingY);
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '0');
|
||||
$html .= $this->buildTextInput('footerMarginTop', 'Margen superior', 'bi-arrow-up', $marginTop);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$inputRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerInputBorderRadius', 'Radio input', 'bi-square', $inputRadius);
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerButtonBorderRadius', 'Radio boton', 'bi-square', $buttonRadius);
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= $this->buildTextInput('footerTransitionDuration', 'Duracion transicion', 'bi-hourglass', $transition);
|
||||
|
||||
$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 buildPasswordInput(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="password" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' <div class="form-text small">URL oculta por seguridad</div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextarea(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 .= ' <textarea class="form-control form-control-sm" id="' . esc_attr($id) . '" rows="2">' . esc_textarea($value) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorInput(string $id, string $label, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-2 d-flex align-items-center gap-2">';
|
||||
$html .= ' <input type="color" class="form-control form-control-color" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '" style="width: 40px; height: 30px;">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label mb-0 small">' . esc_html($label) . '</label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*
|
||||
* El repositorio convierte '0' a false y '1' a true automáticamente,
|
||||
* pero para campos de texto necesitamos el valor original como string.
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
416
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
416
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
final class HeroFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($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-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Hero Section';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la sección hero con título y badges de categorías';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="hero">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar Hero Section</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowCategories" ';
|
||||
$html .= checked($showCategories, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowCategories">';
|
||||
$html .= ' <i class="bi bi-tags me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar badges de categorías</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true);
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowBadgeIcon" ';
|
||||
$html .= checked($showBadgeIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowBadgeIcon">';
|
||||
$html .= ' <i class="bi bi-star me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar ícono en badges</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="heroBadgeIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bootstrap me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono de badge';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeIconClass) . '" placeholder="bi-folder-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroTitleTag" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-code me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta HTML del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleTag" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="h1" ' . selected($titleTag, 'h1', false) . '>H1 (recomendado para SEO)</option>';
|
||||
$html .= ' <option value="h2" ' . selected($titleTag, 'h2', false) . '>H2</option>';
|
||||
$html .= ' <option value="div" ' . selected($titleTag, 'div', false) . '>DIV (sin semántica)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#1e3a5f');
|
||||
$html .= $this->buildColorPicker('heroGradientStart', 'Degradado (inicio)', 'circle-half', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#2c5282');
|
||||
$html .= $this->buildColorPicker('heroGradientEnd', 'Degradado (fin)', 'circle-fill', $gradientEnd);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroTitleColor', 'Color título', 'fonts', $titleColor);
|
||||
|
||||
$badgeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_bg_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeBgColor', 'Fondo badges', 'badge', $badgeBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeTextColor', 'Texto badges', 'card-text', $badgeTextColor);
|
||||
|
||||
$badgeIconColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_icon_color', '#FFB800');
|
||||
$html .= $this->buildColorPicker('heroBadgeIconColor', 'Ícono badges', 'star-fill', $badgeIconColor);
|
||||
|
||||
$badgeHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'badge_hover_bg', '#FF8600');
|
||||
$html .= $this->buildColorPicker('heroBadgeHoverBg', 'Badges (hover)', 'hand-index', $badgeHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '2.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño desktop';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontSizeMobile = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size_mobile', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSizeMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño mobile';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSizeMobile" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSizeMobile) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontWeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Peso del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleFontWeight" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($titleFontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($titleFontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($titleFontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($titleFontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleLineHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Altura de línea';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleLineHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño fuente badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_vertical', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroPaddingVertical" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroPaddingVertical" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgePadding = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_padding', '0.375rem 0.875rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgePadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgePadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgePadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBorderRadius = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_border_radius', '20px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgeBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Border radius badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del hero';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroTitleTextShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleTextShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleTextShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeBackdropBlur" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Blur de fondo badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBackdropBlur" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBackdropBlur) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
596
Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal file
596
Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal file
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\API\WordPress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administración
|
||||
*
|
||||
* Infrastructure - WordPress specific
|
||||
*/
|
||||
final class AdminAjaxHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress para AJAX
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Guardar configuración de componente
|
||||
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
|
||||
|
||||
// Restaurar valores por defecto
|
||||
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda la configuración de un componente
|
||||
*/
|
||||
public function saveComponentSettings(): void
|
||||
{
|
||||
// Verificar nonce
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error([
|
||||
'message' => 'No tienes permisos para realizar esta acción.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener datos
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
$settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true);
|
||||
|
||||
if (empty($component) || empty($settings)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Datos incompletos.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Mapear IDs de campos a nombres de atributos del schema
|
||||
$fieldMapping = $this->getFieldMapping();
|
||||
$mappedSettings = [];
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
// Convertir ID del campo a nombre del atributo
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
continue; // Campo no mapeado, ignorar
|
||||
}
|
||||
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
// Usar Use Case para guardar
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
$updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings);
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se guardaron %d campos correctamente.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => 'Error: Use Case no disponible.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura los valores por defecto de un componente
|
||||
*/
|
||||
public function resetComponentDefaults(): void
|
||||
{
|
||||
// Verificar nonce
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error([
|
||||
'message' => 'No tienes permisos para realizar esta acción.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener componente
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
|
||||
if (empty($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Componente no especificado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Ruta al schema JSON
|
||||
$schemaPath = get_template_directory() . '/Schemas/' . $component . '.json';
|
||||
|
||||
if (!file_exists($schemaPath)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Schema del componente no encontrado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Usar repositorio para restaurar valores
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
global $wpdb;
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
|
||||
$updated = $repository->resetToDefaults($component, $schemaPath);
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => 'Error: Repositorio no disponible.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapeo de IDs de campos HTML a nombres de atributos del schema
|
||||
*
|
||||
* @return array<string, array{group: string, attribute: string}>
|
||||
*/
|
||||
private function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// =====================================================
|
||||
// TOP NOTIFICATION BAR
|
||||
// =====================================================
|
||||
|
||||
// Activación y Visibilidad
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Contenido
|
||||
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
'topBarMessageText' => ['group' => 'content', 'attribute' => 'message_text'],
|
||||
'topBarLinkText' => ['group' => 'content', 'attribute' => 'link_text'],
|
||||
'topBarLinkUrl' => ['group' => 'content', 'attribute' => 'link_url'],
|
||||
|
||||
// Colores
|
||||
'topBarBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'topBarTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'topBarLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'topBarIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'topBarLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'topBarLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
|
||||
// Espaciado
|
||||
'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'],
|
||||
'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
|
||||
// =====================================================
|
||||
// NAVBAR
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||
|
||||
// Layout
|
||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
||||
'navbarZIndex' => ['group' => 'layout', 'attribute' => 'z_index'],
|
||||
|
||||
// Behavior
|
||||
'navbarMenuLocation' => ['group' => 'behavior', 'attribute' => 'menu_location'],
|
||||
'navbarCustomMenuId' => ['group' => 'behavior', 'attribute' => 'custom_menu_id'],
|
||||
'navbarEnableDropdowns' => ['group' => 'behavior', 'attribute' => 'enable_dropdowns'],
|
||||
'navbarMobileBreakpoint' => ['group' => 'behavior', 'attribute' => 'mobile_breakpoint'],
|
||||
|
||||
// Media (Logo/Marca)
|
||||
'navbarShowBrand' => ['group' => 'media', 'attribute' => 'show_brand'],
|
||||
'navbarUseLogo' => ['group' => 'media', 'attribute' => 'use_logo'],
|
||||
'navbarLogoUrl' => ['group' => 'media', 'attribute' => 'logo_url'],
|
||||
'navbarLogoHeight' => ['group' => 'media', 'attribute' => 'logo_height'],
|
||||
'navbarBrandText' => ['group' => 'media', 'attribute' => 'brand_text'],
|
||||
'navbarBrandFontSize' => ['group' => 'media', 'attribute' => 'brand_font_size'],
|
||||
'navbarBrandColor' => ['group' => 'media', 'attribute' => 'brand_color'],
|
||||
'navbarBrandHoverColor' => ['group' => 'media', 'attribute' => 'brand_hover_color'],
|
||||
|
||||
// Links
|
||||
'linksTextColor' => ['group' => 'links', 'attribute' => 'text_color'],
|
||||
'linksHoverColor' => ['group' => 'links', 'attribute' => 'hover_color'],
|
||||
'linksActiveColor' => ['group' => 'links', 'attribute' => 'active_color'],
|
||||
'linksFontSize' => ['group' => 'links', 'attribute' => 'font_size'],
|
||||
'linksFontWeight' => ['group' => 'links', 'attribute' => 'font_weight'],
|
||||
'linksPadding' => ['group' => 'links', 'attribute' => 'padding'],
|
||||
'linksBorderRadius' => ['group' => 'links', 'attribute' => 'border_radius'],
|
||||
'linksShowUnderline' => ['group' => 'links', 'attribute' => 'show_underline_effect'],
|
||||
'linksUnderlineColor' => ['group' => 'links', 'attribute' => 'underline_color'],
|
||||
|
||||
// Visual Effects (Dropdown)
|
||||
'dropdownBgColor' => ['group' => 'visual_effects', 'attribute' => 'background_color'],
|
||||
'dropdownBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'dropdownShadow' => ['group' => 'visual_effects', 'attribute' => 'shadow'],
|
||||
'dropdownItemColor' => ['group' => 'visual_effects', 'attribute' => 'item_color'],
|
||||
'dropdownItemHoverBg' => ['group' => 'visual_effects', 'attribute' => 'item_hover_background'],
|
||||
'dropdownItemPadding' => ['group' => 'visual_effects', 'attribute' => 'item_padding'],
|
||||
'dropdownMaxHeight' => ['group' => 'visual_effects', 'attribute' => 'dropdown_max_height'],
|
||||
|
||||
// Colors (Navbar styles)
|
||||
'navbarBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'navbarBoxShadow' => ['group' => 'colors', 'attribute' => 'box_shadow'],
|
||||
|
||||
// =====================================================
|
||||
// CTA LETS TALK
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'],
|
||||
'ctaLetsTalkIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'ctaLetsTalkModalTarget' => ['group' => 'content', 'attribute' => 'modal_target'],
|
||||
'ctaLetsTalkAriaLabel' => ['group' => 'content', 'attribute' => 'aria_label'],
|
||||
|
||||
// Behavior
|
||||
'ctaLetsTalkEnableModal' => ['group' => 'behavior', 'attribute' => 'enable_modal'],
|
||||
'ctaLetsTalkCustomUrl' => ['group' => 'behavior', 'attribute' => 'custom_url'],
|
||||
'ctaLetsTalkOpenNewTab' => ['group' => 'behavior', 'attribute' => 'open_in_new_tab'],
|
||||
|
||||
// Typography
|
||||
'ctaLetsTalkFontSize' => ['group' => 'typography', 'attribute' => 'font_size'],
|
||||
'ctaLetsTalkFontWeight' => ['group' => 'typography', 'attribute' => 'font_weight'],
|
||||
'ctaLetsTalkTextTransform' => ['group' => 'typography', 'attribute' => 'text_transform'],
|
||||
|
||||
// Colors
|
||||
'ctaLetsTalkBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaLetsTalkBgHoverColor' => ['group' => 'colors', 'attribute' => 'background_hover_color'],
|
||||
'ctaLetsTalkTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'ctaLetsTalkTextHoverColor' => ['group' => 'colors', 'attribute' => 'text_hover_color'],
|
||||
'ctaLetsTalkBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaLetsTalkPaddingTB' => ['group' => 'spacing', 'attribute' => 'padding_top_bottom'],
|
||||
'ctaLetsTalkPaddingLR' => ['group' => 'spacing', 'attribute' => 'padding_left_right'],
|
||||
'ctaLetsTalkMarginLeft' => ['group' => 'spacing', 'attribute' => 'margin_left'],
|
||||
'ctaLetsTalkIconSpacing' => ['group' => 'spacing', 'attribute' => 'icon_spacing'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaLetsTalkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaLetsTalkBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'ctaLetsTalkBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaLetsTalkTransition' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
|
||||
// =====================================================
|
||||
// HERO SECTION
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
||||
'heroBadgeIconClass' => ['group' => 'content', 'attribute' => 'badge_icon_class'],
|
||||
'heroTitleTag' => ['group' => 'content', 'attribute' => 'title_tag'],
|
||||
|
||||
// Colors
|
||||
'heroGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'heroGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'heroTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'heroBadgeBgColor' => ['group' => 'colors', 'attribute' => 'badge_bg_color'],
|
||||
'heroBadgeTextColor' => ['group' => 'colors', 'attribute' => 'badge_text_color'],
|
||||
'heroBadgeIconColor' => ['group' => 'colors', 'attribute' => 'badge_icon_color'],
|
||||
'heroBadgeHoverBg' => ['group' => 'colors', 'attribute' => 'badge_hover_bg'],
|
||||
|
||||
// Typography
|
||||
'heroTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'heroTitleFontSizeMobile' => ['group' => 'typography', 'attribute' => 'title_font_size_mobile'],
|
||||
'heroTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'heroTitleLineHeight' => ['group' => 'typography', 'attribute' => 'title_line_height'],
|
||||
'heroBadgeFontSize' => ['group' => 'typography', 'attribute' => 'badge_font_size'],
|
||||
|
||||
// Spacing
|
||||
'heroPaddingVertical' => ['group' => 'spacing', 'attribute' => 'padding_vertical'],
|
||||
'heroMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'heroBadgePadding' => ['group' => 'spacing', 'attribute' => 'badge_padding'],
|
||||
'heroBadgeBorderRadius' => ['group' => 'spacing', 'attribute' => 'badge_border_radius'],
|
||||
|
||||
// Visual Effects
|
||||
'heroBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'heroTitleTextShadow' => ['group' => 'visual_effects', 'attribute' => 'title_text_shadow'],
|
||||
'heroBadgeBackdropBlur' => ['group' => 'visual_effects', 'attribute' => 'badge_backdrop_blur'],
|
||||
|
||||
// =====================================================
|
||||
// FEATURED IMAGE
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||
'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'],
|
||||
'featuredImageLinkToMedia' => ['group' => 'content', 'attribute' => 'link_to_media'],
|
||||
|
||||
// Spacing
|
||||
'featuredImageMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
'featuredImageMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
|
||||
// Visual Effects
|
||||
'featuredImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'featuredImageBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'featuredImageHoverEffect' => ['group' => 'visual_effects', 'attribute' => 'hover_effect'],
|
||||
'featuredImageHoverScale' => ['group' => 'visual_effects', 'attribute' => 'hover_scale'],
|
||||
'featuredImageTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
|
||||
// =====================================================
|
||||
// TABLE OF CONTENTS
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'],
|
||||
'tocHeadingLevels' => ['group' => 'content', 'attribute' => 'heading_levels'],
|
||||
'tocSmoothScroll' => ['group' => 'content', 'attribute' => 'smooth_scroll'],
|
||||
|
||||
// Typography
|
||||
'tocTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'tocTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'tocLinkFontSize' => ['group' => 'typography', 'attribute' => 'link_font_size'],
|
||||
'tocLinkLineHeight' => ['group' => 'typography', 'attribute' => 'link_line_height'],
|
||||
'tocLevelThreeFontSize' => ['group' => 'typography', 'attribute' => 'level_three_font_size'],
|
||||
'tocLevelFourFontSize' => ['group' => 'typography', 'attribute' => 'level_four_font_size'],
|
||||
|
||||
// Colors
|
||||
'tocBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'tocBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
'tocTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'tocTitleBorderColor' => ['group' => 'colors', 'attribute' => 'title_border_color'],
|
||||
'tocLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'tocLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'tocLinkHoverBackground' => ['group' => 'colors', 'attribute' => 'link_hover_background'],
|
||||
'tocActiveBorderColor' => ['group' => 'colors', 'attribute' => 'active_border_color'],
|
||||
'tocActiveBackgroundColor' => ['group' => 'colors', 'attribute' => 'active_background_color'],
|
||||
'tocActiveTextColor' => ['group' => 'colors', 'attribute' => 'active_text_color'],
|
||||
'tocScrollbarTrackColor' => ['group' => 'colors', 'attribute' => 'scrollbar_track_color'],
|
||||
'tocScrollbarThumbColor' => ['group' => 'colors', 'attribute' => 'scrollbar_thumb_color'],
|
||||
|
||||
// Spacing
|
||||
'tocContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'tocMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'tocTitlePaddingBottom' => ['group' => 'spacing', 'attribute' => 'title_padding_bottom'],
|
||||
'tocTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'tocItemMarginBottom' => ['group' => 'spacing', 'attribute' => 'item_margin_bottom'],
|
||||
'tocLinkPadding' => ['group' => 'spacing', 'attribute' => 'link_padding'],
|
||||
'tocLevelThreePaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_three_padding_left'],
|
||||
'tocLevelFourPaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_four_padding_left'],
|
||||
'tocScrollbarWidth' => ['group' => 'spacing', 'attribute' => 'scrollbar_width'],
|
||||
|
||||
// Visual Effects
|
||||
'tocBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'tocBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'tocBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'tocLinkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'link_border_radius'],
|
||||
'tocActiveBorderLeftWidth' => ['group' => 'visual_effects', 'attribute' => 'active_border_left_width'],
|
||||
'tocTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'tocScrollbarBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'scrollbar_border_radius'],
|
||||
|
||||
// Behavior
|
||||
'tocIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||
'tocScrollOffset' => ['group' => 'behavior', 'attribute' => 'scroll_offset'],
|
||||
'tocMaxHeight' => ['group' => 'behavior', 'attribute' => 'max_height'],
|
||||
|
||||
// =====================================================
|
||||
// SOCIAL SHARE
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||
'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
|
||||
// Networks
|
||||
'socialShareFacebook' => ['group' => 'networks', 'attribute' => 'show_facebook'],
|
||||
'socialShareFacebookUrl' => ['group' => 'networks', 'attribute' => 'facebook_url'],
|
||||
'socialShareInstagram' => ['group' => 'networks', 'attribute' => 'show_instagram'],
|
||||
'socialShareInstagramUrl' => ['group' => 'networks', 'attribute' => 'instagram_url'],
|
||||
'socialShareLinkedin' => ['group' => 'networks', 'attribute' => 'show_linkedin'],
|
||||
'socialShareLinkedinUrl' => ['group' => 'networks', 'attribute' => 'linkedin_url'],
|
||||
'socialShareWhatsapp' => ['group' => 'networks', 'attribute' => 'show_whatsapp'],
|
||||
'socialShareWhatsappNumber' => ['group' => 'networks', 'attribute' => 'whatsapp_number'],
|
||||
'socialShareTwitter' => ['group' => 'networks', 'attribute' => 'show_twitter'],
|
||||
'socialShareTwitterUrl' => ['group' => 'networks', 'attribute' => 'twitter_url'],
|
||||
'socialShareEmail' => ['group' => 'networks', 'attribute' => 'show_email'],
|
||||
'socialShareEmailAddress' => ['group' => 'networks', 'attribute' => 'email_address'],
|
||||
|
||||
// Colors
|
||||
'socialShareLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'socialShareBorderTopColor' => ['group' => 'colors', 'attribute' => 'border_top_color'],
|
||||
'socialShareButtonBg' => ['group' => 'colors', 'attribute' => 'button_background'],
|
||||
'socialShareFacebookColor' => ['group' => 'colors', 'attribute' => 'facebook_color'],
|
||||
'socialShareInstagramColor' => ['group' => 'colors', 'attribute' => 'instagram_color'],
|
||||
'socialShareLinkedinColor' => ['group' => 'colors', 'attribute' => 'linkedin_color'],
|
||||
'socialShareWhatsappColor' => ['group' => 'colors', 'attribute' => 'whatsapp_color'],
|
||||
'socialShareTwitterColor' => ['group' => 'colors', 'attribute' => 'twitter_color'],
|
||||
'socialShareEmailColor' => ['group' => 'colors', 'attribute' => 'email_color'],
|
||||
|
||||
// Typography
|
||||
'socialShareLabelFontSize' => ['group' => 'typography', 'attribute' => 'label_font_size'],
|
||||
'socialShareIconFontSize' => ['group' => 'typography', 'attribute' => 'icon_font_size'],
|
||||
|
||||
// Spacing
|
||||
'socialShareMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'socialShareMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'socialSharePaddingTop' => ['group' => 'spacing', 'attribute' => 'container_padding_top'],
|
||||
'socialSharePaddingBottom' => ['group' => 'spacing', 'attribute' => 'container_padding_bottom'],
|
||||
'socialShareLabelMarginBottom' => ['group' => 'spacing', 'attribute' => 'label_margin_bottom'],
|
||||
'socialShareButtonsGap' => ['group' => 'spacing', 'attribute' => 'buttons_gap'],
|
||||
'socialShareButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
|
||||
// Visual Effects
|
||||
'socialShareBorderTopWidth' => ['group' => 'visual_effects', 'attribute' => 'border_top_width'],
|
||||
'socialShareButtonBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'button_border_width'],
|
||||
'socialShareButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'socialShareTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'socialShareHoverBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'hover_box_shadow'],
|
||||
|
||||
// =====================================================
|
||||
// CTA POST
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaPostButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaPostButtonUrl' => ['group' => 'content', 'attribute' => 'button_url'],
|
||||
'ctaPostButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
|
||||
// Typography
|
||||
'ctaPostTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaPostTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaPostDescriptionFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaPostButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
|
||||
// Colors
|
||||
'ctaPostGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'ctaPostGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'ctaPostTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaPostDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaPostButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'ctaPostButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaPostButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'ctaPostContainerMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'ctaPostContainerMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'ctaPostContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaPostButtonIconMargin' => ['group' => 'spacing', 'attribute' => 'button_icon_margin'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaPostBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaPostGradientAngle' => ['group' => 'visual_effects', 'attribute' => 'gradient_angle'],
|
||||
'ctaPostButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaPostButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'ctaPostTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'ctaPostBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
|
||||
// =====================================================
|
||||
// CONTACT FORM
|
||||
// =====================================================
|
||||
|
||||
// Visibility
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'],
|
||||
'contactFormSubmitButtonText' => ['group' => 'content', 'attribute' => 'submit_button_text'],
|
||||
'contactFormSubmitButtonIcon' => ['group' => 'content', 'attribute' => 'submit_button_icon'],
|
||||
|
||||
// Contact Info
|
||||
'contactFormShowContactInfo' => ['group' => 'contact_info', 'attribute' => 'show_contact_info'],
|
||||
'contactFormPhoneLabel' => ['group' => 'contact_info', 'attribute' => 'phone_label'],
|
||||
'contactFormPhoneValue' => ['group' => 'contact_info', 'attribute' => 'phone_value'],
|
||||
'contactFormEmailLabel' => ['group' => 'contact_info', 'attribute' => 'email_label'],
|
||||
'contactFormEmailValue' => ['group' => 'contact_info', 'attribute' => 'email_value'],
|
||||
'contactFormLocationLabel' => ['group' => 'contact_info', 'attribute' => 'location_label'],
|
||||
'contactFormLocationValue' => ['group' => 'contact_info', 'attribute' => 'location_value'],
|
||||
|
||||
// Form Labels
|
||||
'contactFormFullnamePlaceholder' => ['group' => 'form_labels', 'attribute' => 'fullname_placeholder'],
|
||||
'contactFormCompanyPlaceholder' => ['group' => 'form_labels', 'attribute' => 'company_placeholder'],
|
||||
'contactFormWhatsappPlaceholder' => ['group' => 'form_labels', 'attribute' => 'whatsapp_placeholder'],
|
||||
'contactFormEmailPlaceholder' => ['group' => 'form_labels', 'attribute' => 'email_placeholder'],
|
||||
'contactFormMessagePlaceholder' => ['group' => 'form_labels', 'attribute' => 'message_placeholder'],
|
||||
|
||||
// Integration
|
||||
'contactFormWebhookUrl' => ['group' => 'integration', 'attribute' => 'webhook_url'],
|
||||
'contactFormWebhookMethod' => ['group' => 'integration', 'attribute' => 'webhook_method'],
|
||||
'contactFormIncludePageUrl' => ['group' => 'integration', 'attribute' => 'include_page_url'],
|
||||
'contactFormIncludeTimestamp' => ['group' => 'integration', 'attribute' => 'include_timestamp'],
|
||||
|
||||
// Messages
|
||||
'contactFormSuccessMessage' => ['group' => 'messages', 'attribute' => 'success_message'],
|
||||
'contactFormErrorMessage' => ['group' => 'messages', 'attribute' => 'error_message'],
|
||||
'contactFormSendingMessage' => ['group' => 'messages', 'attribute' => 'sending_message'],
|
||||
'contactFormValidationRequired' => ['group' => 'messages', 'attribute' => 'validation_required'],
|
||||
'contactFormValidationEmail' => ['group' => 'messages', 'attribute' => 'validation_email'],
|
||||
|
||||
// Colors
|
||||
'contactFormSectionBgColor' => ['group' => 'colors', 'attribute' => 'section_bg_color'],
|
||||
'contactFormTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'contactFormDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'contactFormIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'contactFormInfoLabelColor' => ['group' => 'colors', 'attribute' => 'info_label_color'],
|
||||
'contactFormInfoValueColor' => ['group' => 'colors', 'attribute' => 'info_value_color'],
|
||||
'contactFormInputBorderColor' => ['group' => 'colors', 'attribute' => 'input_border_color'],
|
||||
'contactFormInputFocusBorder' => ['group' => 'colors', 'attribute' => 'input_focus_border'],
|
||||
'contactFormButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'contactFormButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'contactFormButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
'contactFormSuccessBgColor' => ['group' => 'colors', 'attribute' => 'success_bg_color'],
|
||||
'contactFormSuccessTextColor' => ['group' => 'colors', 'attribute' => 'success_text_color'],
|
||||
'contactFormErrorBgColor' => ['group' => 'colors', 'attribute' => 'error_bg_color'],
|
||||
'contactFormErrorTextColor' => ['group' => 'colors', 'attribute' => 'error_text_color'],
|
||||
|
||||
// Spacing
|
||||
'contactFormSectionPaddingY' => ['group' => 'spacing', 'attribute' => 'section_padding_y'],
|
||||
'contactFormSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'contactFormTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'contactFormDescriptionMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'contactFormFormGap' => ['group' => 'spacing', 'attribute' => 'form_gap'],
|
||||
|
||||
// Visual Effects
|
||||
'contactFormInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'contactFormButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'contactFormButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'contactFormTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'contactFormTextareaRows' => ['group' => 'visual_effects', 'attribute' => 'textarea_rows'],
|
||||
];
|
||||
}
|
||||
}
|
||||
76
Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php
Normal file
76
Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\API\WordPress;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
|
||||
|
||||
/**
|
||||
* Registra el menú de administración en WordPress
|
||||
*
|
||||
* Infrastructure - Implementación específica de WordPress
|
||||
*/
|
||||
final class AdminMenuRegistrar implements MenuRegistrarInterface
|
||||
{
|
||||
private MenuItem $menuItem;
|
||||
|
||||
/**
|
||||
* @param MenuItem $menuItem Configuración del menú
|
||||
* @param RenderDashboardUseCase $renderUseCase Caso de uso para renderizar
|
||||
*/
|
||||
public function __construct(
|
||||
MenuItem $menuItem,
|
||||
private readonly RenderDashboardUseCase $renderUseCase
|
||||
) {
|
||||
$this->menuItem = $menuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra el menú en WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_menu', [$this, 'addMenuPage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para agregar la página al menú de WordPress
|
||||
*/
|
||||
public function addMenuPage(): void
|
||||
{
|
||||
add_menu_page(
|
||||
$this->menuItem->getPageTitle(),
|
||||
$this->menuItem->getMenuTitle(),
|
||||
$this->menuItem->getCapability(),
|
||||
$this->menuItem->getMenuSlug(),
|
||||
[$this, 'renderPage'],
|
||||
$this->menuItem->getIcon(),
|
||||
$this->menuItem->getPosition()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para renderizar la página
|
||||
*/
|
||||
public function renderPage(): void
|
||||
{
|
||||
try {
|
||||
echo $this->renderUseCase->execute('dashboard');
|
||||
} catch (\Exception $e) {
|
||||
echo '<div class="error"><p>Error rendering dashboard: ' . esc_html($e->getMessage()) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->menuItem->getCapability();
|
||||
}
|
||||
|
||||
public function getSlug(): string
|
||||
{
|
||||
return $this->menuItem->getMenuSlug();
|
||||
}
|
||||
}
|
||||
120
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
120
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para enqueue de assets del panel de administración
|
||||
*
|
||||
* Infrastructure - WordPress specific
|
||||
*/
|
||||
final class AdminAssetEnqueuer
|
||||
{
|
||||
private const ADMIN_PAGE_SLUG = 'roi-theme-admin';
|
||||
|
||||
public function __construct(
|
||||
private readonly string $themeUri
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de assets solo en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name de WordPress
|
||||
*/
|
||||
public function enqueueAssets(string $hook): void
|
||||
{
|
||||
// Solo cargar en nuestra página de admin
|
||||
if (!$this->isAdminPage($hook)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueueStyles();
|
||||
$this->enqueueScripts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si estamos en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdminPage(string $hook): bool
|
||||
{
|
||||
return strpos($hook, self::ADMIN_PAGE_SLUG) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de estilos CSS
|
||||
*/
|
||||
private function enqueueStyles(): void
|
||||
{
|
||||
// Bootstrap 5 CSS
|
||||
wp_enqueue_style(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
|
||||
[],
|
||||
'5.3.2'
|
||||
);
|
||||
|
||||
// Bootstrap Icons
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css',
|
||||
[],
|
||||
'1.11.3'
|
||||
);
|
||||
|
||||
// Estilos del dashboard
|
||||
wp_enqueue_style(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css',
|
||||
['bootstrap', 'bootstrap-icons'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de scripts JavaScript
|
||||
*/
|
||||
private function enqueueScripts(): void
|
||||
{
|
||||
// Bootstrap 5 JS Bundle (incluye Popper)
|
||||
// IMPORTANTE: Cargar en header (false) para que esté disponible antes del contenido
|
||||
wp_enqueue_script(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
|
||||
[],
|
||||
'5.3.2',
|
||||
false // Load in header, not footer - required for Bootstrap tabs to work
|
||||
);
|
||||
|
||||
// Script del dashboard
|
||||
wp_enqueue_script(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js',
|
||||
['bootstrap'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar variables al JavaScript
|
||||
wp_localize_script(
|
||||
'roi-admin-dashboard',
|
||||
'roiAdminDashboard',
|
||||
[
|
||||
'nonce' => wp_create_nonce('roi_admin_dashboard'),
|
||||
'ajaxurl' => admin_url('admin-ajax.php')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
160
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
160
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
||||
|
||||
/**
|
||||
* Renderiza el dashboard del panel de administración
|
||||
*
|
||||
* Infrastructure - Implementación con WordPress
|
||||
*/
|
||||
final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
{
|
||||
private const SUPPORTED_VIEWS = ['dashboard'];
|
||||
|
||||
/**
|
||||
* @param GetComponentSettingsUseCase|null $getComponentSettingsUseCase
|
||||
* @param array<string, mixed> $components Componentes disponibles
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null,
|
||||
private readonly array $components = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
ob_start();
|
||||
require __DIR__ . '/Views/dashboard.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public function supports(string $viewType): bool
|
||||
{
|
||||
return in_array($viewType, self::SUPPORTED_VIEWS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los componentes disponibles
|
||||
*
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
public function getComponents(): array
|
||||
{
|
||||
return [
|
||||
'top-notification-bar' => [
|
||||
'id' => 'top-notification-bar',
|
||||
'label' => 'TopBar',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'navbar' => [
|
||||
'id' => 'navbar',
|
||||
'label' => 'Navbar',
|
||||
'icon' => 'bi-list',
|
||||
],
|
||||
'cta-lets-talk' => [
|
||||
'id' => 'cta-lets-talk',
|
||||
'label' => "Let's Talk",
|
||||
'icon' => 'bi-lightning-charge-fill',
|
||||
],
|
||||
'hero' => [
|
||||
'id' => 'hero',
|
||||
'label' => 'Hero Section',
|
||||
'icon' => 'bi-image',
|
||||
],
|
||||
'featured-image' => [
|
||||
'id' => 'featured-image',
|
||||
'label' => 'Featured Image',
|
||||
'icon' => 'bi-card-image',
|
||||
],
|
||||
'table-of-contents' => [
|
||||
'id' => 'table-of-contents',
|
||||
'label' => 'Table of Contents',
|
||||
'icon' => 'bi-list-nested',
|
||||
],
|
||||
'cta-box-sidebar' => [
|
||||
'id' => 'cta-box-sidebar',
|
||||
'label' => 'CTA Sidebar',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'social-share' => [
|
||||
'id' => 'social-share',
|
||||
'label' => 'Social Share',
|
||||
'icon' => 'bi-share',
|
||||
],
|
||||
'cta-post' => [
|
||||
'id' => 'cta-post',
|
||||
'label' => 'CTA Post',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'related-post' => [
|
||||
'id' => 'related-post',
|
||||
'label' => 'Related Posts',
|
||||
'icon' => 'bi-grid-3x3-gap',
|
||||
],
|
||||
'contact-form' => [
|
||||
'id' => 'contact-form',
|
||||
'label' => 'Contact Form',
|
||||
'icon' => 'bi-envelope-paper',
|
||||
],
|
||||
'footer' => [
|
||||
'id' => 'footer',
|
||||
'label' => 'Footer',
|
||||
'icon' => 'bi-layout-text-window-reverse',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones de un componente
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @return array<string, array<string, mixed>> Configuraciones agrupadas por grupo
|
||||
*/
|
||||
public function getComponentSettings(string $componentName): array
|
||||
{
|
||||
if ($this->getComponentSettingsUseCase === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getComponentSettingsUseCase->execute($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el valor de un campo de configuración
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $groupName Nombre del grupo
|
||||
* @param string $attributeName Nombre del atributo
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue(string $componentName, string $groupName, string $attributeName, mixed $default = null): mixed
|
||||
{
|
||||
$settings = $this->getComponentSettings($componentName);
|
||||
|
||||
return $settings[$groupName][$attributeName] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la clase del FormBuilder para un componente
|
||||
*
|
||||
* @param string $componentId ID del componente en kebab-case (ej: 'top-notification-bar')
|
||||
* @return string Namespace completo del FormBuilder
|
||||
*/
|
||||
public function getFormBuilderClass(string $componentId): string
|
||||
{
|
||||
// Convertir kebab-case a PascalCase
|
||||
// 'top-notification-bar' → 'TopNotificationBar'
|
||||
$className = str_replace('-', '', ucwords($componentId, '-'));
|
||||
|
||||
// Construir namespace completo
|
||||
// ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder
|
||||
return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder";
|
||||
}
|
||||
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
76
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
76
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* ROI Theme - Panel de Administración Principal
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Prevenir acceso directo
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$components = $this->getComponents();
|
||||
$firstComponentId = array_key_first($components);
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs nav-tabs-admin mb-0" role="tablist">
|
||||
<?php foreach ($components as $componentId => $component): ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link <?php echo $componentId === $firstComponentId ? 'active' : ''; ?>"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#<?php echo esc_attr($componentId); ?>Tab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="<?php echo esc_attr($componentId); ?>Tab"
|
||||
aria-selected="<?php echo $componentId === $firstComponentId ? 'true' : 'false'; ?>">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<?php foreach ($components as $componentId => $component):
|
||||
$isFirst = ($componentId === $firstComponentId);
|
||||
$componentSettings = $this->getComponentSettings($componentId);
|
||||
?>
|
||||
<!-- Tab: <?php echo esc_html($component['label']); ?> -->
|
||||
<div class="tab-pane fade <?php echo $isFirst ? 'show active' : ''; ?>"
|
||||
id="<?php echo esc_attr($componentId); ?>Tab"
|
||||
role="tabpanel">
|
||||
|
||||
<?php
|
||||
// Renderizar FormBuilder del componente
|
||||
$formBuilderClass = $this->getFormBuilderClass($componentId);
|
||||
if (class_exists($formBuilderClass)) {
|
||||
$formBuilder = new $formBuilderClass($this);
|
||||
echo $formBuilder->buildForm($componentId);
|
||||
} else {
|
||||
echo '<p class="text-danger">FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Botones Globales Save/Cancel -->
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" id="saveSettings" class="btn fw-semibold text-white" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
517
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
517
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
@@ -0,0 +1,517 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
final class NavbarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildMediaGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildLinksGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($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-menu-button-wide me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Navbar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el menú de navegación principal del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="navbar">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnabled">';
|
||||
$html .= ' <strong>Activar Navbar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowMobile">';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Sticky
|
||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarSticky" name="visibility[sticky_enabled]" ';
|
||||
$html .= checked($sticky, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarSticky">';
|
||||
$html .= ' <strong>Navbar fijo (sticky)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(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-layout-sidebar me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Estructura';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Container Type
|
||||
$containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarContainerType" class="form-label small mb-1 fw-semibold">Tipo de contenedor</label>';
|
||||
$html .= ' <select id="navbarContainerType" name="layout[container_type]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="container" ' . selected($containerType, 'container', false) . '>Container (ancho fijo)</option>';
|
||||
$html .= ' <option value="container-fluid" ' . selected($containerType, 'container-fluid', false) . '>Container Fluid (ancho completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding Vertical
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="navbarPaddingVertical" name="layout[padding_vertical]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '" placeholder="0.75rem 0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Z-index
|
||||
$zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarZIndex" class="form-label small mb-1 fw-semibold">Z-index</label>';
|
||||
$html .= ' <input type="text" id="navbarZIndex" name="layout[z_index]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($zIndex) . '" placeholder="1030">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(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-list me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Menú';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Menu Location
|
||||
$menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold">Ubicación del menú</label>';
|
||||
$html .= ' <select id="navbarMenuLocation" name="behavior[menu_location]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="primary" ' . selected($menuLocation, 'primary', false) . '>Menú Principal</option>';
|
||||
$html .= ' <option value="secondary" ' . selected($menuLocation, 'secondary', false) . '>Menú Secundario</option>';
|
||||
$html .= ' <option value="custom" ' . selected($menuLocation, 'custom', false) . '>Menú personalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Custom Menu ID
|
||||
$customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold">ID del menú personalizado</label>';
|
||||
$html .= ' <input type="text" id="navbarCustomMenuId" name="behavior[custom_menu_id]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customMenuId) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Enable Dropdowns
|
||||
$enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" name="behavior[enable_dropdowns]" ';
|
||||
$html .= checked($enableDropdowns, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnableDropdowns">';
|
||||
$html .= ' <strong>Habilitar submenús desplegables</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mobile Breakpoint
|
||||
$mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold">Breakpoint para menú móvil</label>';
|
||||
$html .= ' <select id="navbarMobileBreakpoint" name="behavior[mobile_breakpoint]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="sm" ' . selected($mobileBreakpoint, 'sm', false) . '>Small (576px)</option>';
|
||||
$html .= ' <option value="md" ' . selected($mobileBreakpoint, 'md', false) . '>Medium (768px)</option>';
|
||||
$html .= ' <option value="lg" ' . selected($mobileBreakpoint, 'lg', false) . '>Large (992px)</option>';
|
||||
$html .= ' <option value="xl" ' . selected($mobileBreakpoint, 'xl', false) . '>Extra Large (1200px)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMediaGroup(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-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Logo/Marca';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Show Brand
|
||||
$showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowBrand" name="media[show_brand]" ';
|
||||
$html .= checked($showBrand, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowBrand">';
|
||||
$html .= ' <strong>Mostrar logo/marca</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Use Logo
|
||||
$useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarUseLogo" name="media[use_logo]" ';
|
||||
$html .= checked($useLogo, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarUseLogo">';
|
||||
$html .= ' <strong>Usar logo (imagen)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo URL
|
||||
$logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold">URL del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoUrl" name="media[logo_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo Height
|
||||
$logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold">Altura del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoHeight" name="media[logo_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoHeight) . '" placeholder="40px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Text
|
||||
$brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandText" class="form-label small mb-1 fw-semibold">Texto de la marca</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandText" name="media[brand_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Font Size
|
||||
$brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandFontSize" name="media[brand_font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandFontSize) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Color
|
||||
$brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandColor" class="form-label small mb-1 fw-semibold">Color de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandColor" name="media[brand_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Hover Color
|
||||
$brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold">Color hover de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandHoverColor" name="media[brand_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLinksGroup(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-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Enlaces';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="linksTextColor" name="links[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Hover Color
|
||||
$hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksHoverColor" class="form-label small mb-1 fw-semibold">Color hover</label>';
|
||||
$html .= ' <input type="color" id="linksHoverColor" name="links[hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($hoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Active Color
|
||||
$activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksActiveColor" class="form-label small mb-1 fw-semibold">Color del item activo</label>';
|
||||
$html .= ' <input type="color" id="linksActiveColor" name="links[active_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($activeColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontSize" name="links[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontWeight" class="form-label small mb-1 fw-semibold">Grosor de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontWeight" name="links[font_weight]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontWeight) . '" placeholder="500">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksPadding" class="form-label small mb-1 fw-semibold">Padding de enlaces</label>';
|
||||
$html .= ' <input type="text" id="linksPadding" name="links[padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '" placeholder="0.5rem 0.65rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksBorderRadius" class="form-label small mb-1 fw-semibold">Border radius hover</label>';
|
||||
$html .= ' <input type="text" id="linksBorderRadius" name="links[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="4px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show Underline Effect
|
||||
$showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="linksShowUnderline" name="links[show_underline_effect]" ';
|
||||
$html .= checked($showUnderline, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="linksShowUnderline">';
|
||||
$html .= ' <strong>Mostrar efecto de subrayado</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Underline Color
|
||||
$underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="linksUnderlineColor" class="form-label small mb-1 fw-semibold">Color del subrayado</label>';
|
||||
$html .= ' <input type="color" id="linksUnderlineColor" name="links[underline_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($underlineColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(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-chevron-down me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Dropdown';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBgColor" class="form-label small mb-1 fw-semibold">Fondo de dropdown</label>';
|
||||
$html .= ' <input type="color" id="dropdownBgColor" name="visual_effects[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBorderRadius" class="form-label small mb-1 fw-semibold">Border radius</label>';
|
||||
$html .= ' <input type="text" id="dropdownBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="8px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Shadow
|
||||
$shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownShadow" class="form-label small mb-1 fw-semibold">Sombra del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownShadow" name="visual_effects[shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($shadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Color
|
||||
$itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemColor" class="form-label small mb-1 fw-semibold">Color de items</label>';
|
||||
$html .= ' <input type="color" id="dropdownItemColor" name="visual_effects[item_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($itemColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Hover Background
|
||||
$itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemHoverBg" class="form-label small mb-1 fw-semibold">Fondo hover de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemHoverBg" name="visual_effects[item_hover_background]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemHoverBg) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Padding
|
||||
$itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemPadding" class="form-label small mb-1 fw-semibold">Padding de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemPadding" name="visual_effects[item_padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemPadding) . '" placeholder="0.625rem 1.25rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Dropdown Max Height
|
||||
$dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="dropdownMaxHeight" class="form-label small mb-1 fw-semibold">Altura máxima del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownMaxHeight" name="visual_effects[dropdown_max_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($dropdownMaxHeight) . '" placeholder="300px">';
|
||||
$html .= ' <small class="text-muted">Si se excede, aparece scroll vertical</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos del Navbar';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="navbarBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($navbarBgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold">Sombra del navbar</label>';
|
||||
$html .= ' <input type="text" id="navbarBoxShadow" name="colors[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
@@ -0,0 +1,544 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Navbar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: NAVBAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="navbarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>
|
||||
Configuración de Navbar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el menú de navegación principal del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Navbar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-2 mt-3">
|
||||
<label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="navbarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Switch 5: Sticky Enabled -->
|
||||
<div class="mb-0 mt-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarStickyEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarStickyEnabled" style="color: #495057;">
|
||||
<i class="bi bi-pin-angle me-1" style="color: #FF8600;"></i>
|
||||
<strong>Navbar fijo (sticky)</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: LAYOUT Y ESTRUCTURA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-columns-gap me-2" style="color: #FF8600;"></i>
|
||||
Layout y Estructura
|
||||
</h5>
|
||||
|
||||
<!-- container_type (select) -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarContainerType" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box me-1" style="color: #FF8600;"></i>
|
||||
Tipo de contenedor
|
||||
</label>
|
||||
<select id="navbarContainerType" class="form-select form-select-sm">
|
||||
<option value="container" selected>Container (ancho fijo)</option>
|
||||
<option value="container-fluid">Container Fluid (ancho completo)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- padding_vertical + z_index (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="navbarPaddingVertical" class="form-control form-control-sm" value="0.75rem 0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarZIndex" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-layers me-1" style="color: #FF8600;"></i>
|
||||
Z-index
|
||||
</label>
|
||||
<input type="number" id="navbarZIndex" class="form-control form-control-sm" value="1030" min="1" max="9999">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: CONFIGURACIÓN DEL MENÚ
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Menú
|
||||
</h5>
|
||||
|
||||
<!-- menu_location + custom_menu_id (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-pin-map me-1" style="color: #FF8600;"></i>
|
||||
Ubicación del menú
|
||||
</label>
|
||||
<select id="navbarMenuLocation" class="form-select form-select-sm">
|
||||
<option value="primary" selected>Menú Principal</option>
|
||||
<option value="secondary">Menú Secundario</option>
|
||||
<option value="custom">Menú personalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hash me-1" style="color: #FF8600;"></i>
|
||||
ID del menú
|
||||
</label>
|
||||
<input type="number" id="navbarCustomMenuId" class="form-control form-control-sm" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- enable_dropdowns (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" checked>
|
||||
<label class="form-check-label small" for="navbarEnableDropdowns" style="color: #495057;">
|
||||
<i class="bi bi-chevron-down me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar submenús desplegables</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobile_breakpoint (select) -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-phone-landscape me-1" style="color: #FF8600;"></i>
|
||||
Breakpoint para menú móvil
|
||||
</label>
|
||||
<select id="navbarMobileBreakpoint" class="form-select form-select-sm">
|
||||
<option value="sm">Small (576px)</option>
|
||||
<option value="md">Medium (768px)</option>
|
||||
<option value="lg" selected>Large (992px)</option>
|
||||
<option value="xl">Extra Large (1200px)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: LOGO/MARCA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-award me-2" style="color: #FF8600;"></i>
|
||||
Logo/Marca
|
||||
</h5>
|
||||
|
||||
<!-- show_brand (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowBrand">
|
||||
<label class="form-check-label small" for="navbarShowBrand" style="color: #495057;">
|
||||
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar logo/marca</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- use_logo (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarUseLogo">
|
||||
<label class="form-check-label small" for="navbarUseLogo" style="color: #495057;">
|
||||
<i class="bi bi-image me-1" style="color: #FF8600;"></i>
|
||||
<strong>Usar logo (imagen)</strong>
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block ms-4 mt-1">Usa una imagen en lugar de texto</small>
|
||||
</div>
|
||||
|
||||
<!-- logo_url + logo_height (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-8">
|
||||
<label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
URL del logo
|
||||
</label>
|
||||
<input type="url" id="navbarLogoUrl" class="form-control form-control-sm" placeholder="https://...">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Altura
|
||||
</label>
|
||||
<input type="text" id="navbarLogoHeight" class="form-control form-control-sm" value="40px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_text -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBrandText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Texto de la marca
|
||||
</label>
|
||||
<input type="text" id="navbarBrandText" class="form-control form-control-sm" value="Mi Sitio" maxlength="50">
|
||||
<small class="text-muted">Se muestra si no hay logo</small>
|
||||
</div>
|
||||
|
||||
<!-- brand_font_size + brand_color (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarBrandFontSize" class="form-control form-control-sm" value="1.5rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color
|
||||
</label>
|
||||
<input type="color" id="navbarBrandColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_hover_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarBrandHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 5: ESTILOS DEL NAVBAR
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-paint-bucket me-2" style="color: #FF8600;"></i>
|
||||
Estilos del Navbar
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="navbarBackgroundColor" class="form-control form-control-color w-100" value="#1e3a5f">
|
||||
<small class="text-muted d-block mt-1" id="navbarBackgroundColorValue">#1E3A5F</small>
|
||||
</div>
|
||||
|
||||
<!-- box_shadow -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra del navbar
|
||||
</label>
|
||||
<input type="text" id="navbarBoxShadow" class="form-control form-control-sm" value="0 4px 12px rgba(30, 58, 95, 0.15)">
|
||||
<small class="text-muted">Sombra CSS (ej: 0 4px 12px rgba(0,0,0,0.15))</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 6: ESTILOS DE ENLACES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Enlaces
|
||||
</h5>
|
||||
|
||||
<!-- COLOR PICKERS EN GRID 3 COLORES -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-4">
|
||||
<label for="navbarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color texto
|
||||
</label>
|
||||
<input type="color" id="navbarTextColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarActiveColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-check-circle me-1" style="color: #FF8600;"></i>
|
||||
Color activo
|
||||
</label>
|
||||
<input type="color" id="navbarActiveColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarActiveColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- font_size + font_weight (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Grosor fuente
|
||||
</label>
|
||||
<input type="number" id="navbarFontWeight" class="form-control form-control-sm" value="500" min="100" max="900" step="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- padding + border_radius (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarLinkPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding
|
||||
</label>
|
||||
<input type="text" id="navbarLinkPadding" class="form-control form-control-sm" value="0.5rem 0.65rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarBorderRadius" class="form-control form-control-sm" value="4px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- show_underline_effect (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowUnderlineEffect" checked>
|
||||
<label class="form-check-label small" for="navbarShowUnderlineEffect" style="color: #495057;">
|
||||
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar efecto de subrayado</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- underline_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarUnderlineColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color del subrayado
|
||||
</label>
|
||||
<input type="color" id="navbarUnderlineColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarUnderlineColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: ESTILOS DE DROPDOWN
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Dropdown
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarDropdownBackground" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo dropdown
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownBackground" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownBackgroundValue">#FFFFFF</small>
|
||||
</div>
|
||||
|
||||
<!-- border_radius + shadow (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownBorderRadius" class="form-control form-control-sm" value="8px">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownShadow" class="form-control form-control-sm" value="0 8px 24px rgba(0,0,0,0.12)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_color + item_hover_background -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color items
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemColor" class="form-control form-control-color w-100" value="#495057">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemHoverBg" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo hover
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemHoverBg" class="form-control form-control-color w-100" value="#FFF5EB">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_padding -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarDropdownItemPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding de items
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownItemPadding" class="form-control form-control-sm" value="0.625rem 1.25rem">
|
||||
<small class="text-muted">Espaciado interno de items (ej: 0.625rem 1.25rem)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Actualizar valores HEX de color pickers
|
||||
document.querySelectorAll('input[type="color"]').forEach(picker => {
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Simular reset button
|
||||
document.getElementById('resetNavbarDefaults').addEventListener('click', function() {
|
||||
if (confirm('¿Restaurar todos los valores a los valores por defecto?')) {
|
||||
alert('En producción, esto restauraría los valores del schema JSON');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
501
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
501
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
@@ -0,0 +1,501 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Related Posts
|
||||
*
|
||||
* @package ROITheme\Admin\RelatedPost\Infrastructure\Ui
|
||||
*/
|
||||
final class RelatedPostFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($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-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Posts Relacionados';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de posts relacionados con grid de cards';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="related_post">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('relatedPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Section Title
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Posts per page
|
||||
$postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostPerPage" class="form-label small mb-1 fw-semibold">Posts por pagina</label>';
|
||||
$html .= ' <input type="number" id="relatedPostPerPage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($postsPerPage) . '" min="1" max="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order by
|
||||
$orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrderby" class="form-label small mb-1 fw-semibold">Ordenar por</label>';
|
||||
$html .= ' <select id="relatedPostOrderby" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="rand"' . ($orderby === 'rand' ? ' selected' : '') . '>Aleatorio</option>';
|
||||
$html .= ' <option value="date"' . ($orderby === 'date' ? ' selected' : '') . '>Fecha</option>';
|
||||
$html .= ' <option value="title"' . ($orderby === 'title' ? ' selected' : '') . '>Titulo</option>';
|
||||
$html .= ' <option value="comment_count"' . ($orderby === 'comment_count' ? ' selected' : '') . '>Comentarios</option>';
|
||||
$html .= ' <option value="menu_order"' . ($orderby === 'menu_order' ? ' selected' : '') . '>Orden de menu</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order direction
|
||||
$order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrder" class="form-label small mb-1 fw-semibold">Direccion</label>';
|
||||
$html .= ' <select id="relatedPostOrder" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="DESC"' . ($order === 'DESC' ? ' selected' : '') . '>Descendente</option>';
|
||||
$html .= ' <option value="ASC"' . ($order === 'ASC' ? ' selected' : '') . '>Ascendente</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show pagination
|
||||
$showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(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-grid me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Disposicion';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Columns desktop
|
||||
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas escritorio';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsDesktop" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns tablet
|
||||
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsTablet" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas tablet';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsTablet" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns mobile
|
||||
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="relatedPostColsMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas movil';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsMobile" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_size', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Cards
|
||||
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostCardBgColor', 'Fondo card', $cardBgColor);
|
||||
|
||||
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostCardTitleColor', 'Titulo card', $cardTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$paginationBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationBgColor', 'Fondo', $paginationBgColor);
|
||||
|
||||
$paginationTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_text_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationTextColor', 'Texto', $paginationTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||
|
||||
$paginationActiveText = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_text', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveText', 'Activo texto', $paginationActiveText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="relatedPostTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostGridGap" class="form-label small mb-1 fw-semibold">Espacio cards</label>';
|
||||
$html .= ' <input type="text" id="relatedPostGridGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gridGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardPadding" class="form-label small mb-1 fw-semibold">Padding card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostPaginationMarginTop" class="form-label small mb-1 fw-semibold">Margen paginacion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostPaginationMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paginationMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', '0.3s ease');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)');
|
||||
$html .= ' <label for="relatedPostCardShadow" class="form-label small mb-1 fw-semibold">Sombra card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)');
|
||||
$html .= ' <label for="relatedPostCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardHoverShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
529
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
529
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Social Share
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\SocialShare\Infrastructure\Ui
|
||||
*/
|
||||
final class SocialShareFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildNetworksGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($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-share me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Compartir en Redes';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Botones para compartir contenido en redes sociales';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="social_share">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('socialShareEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// show_label
|
||||
$showLabel = $this->renderer->getFieldValue($componentId, 'content', 'show_label', true);
|
||||
$html .= $this->buildSwitch('socialShareShowLabel', 'Mostrar etiqueta', 'bi-tag', $showLabel);
|
||||
|
||||
// label_text
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Compartir:');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareLabelText" class="form-label small mb-1 fw-semibold">Texto etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworksGroup(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-globe me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Redes Sociales';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Configura las redes sociales y sus URLs</p>';
|
||||
|
||||
// Facebook
|
||||
$showFacebook = $this->renderer->getFieldValue($componentId, 'networks', 'show_facebook', true);
|
||||
$facebookUrl = $this->renderer->getFieldValue($componentId, 'networks', 'facebook_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareFacebook', 'socialShareFacebookUrl', 'Facebook', 'bi-facebook', $showFacebook, $facebookUrl, 'https://facebook.com/tu-pagina');
|
||||
|
||||
// Instagram
|
||||
$showInstagram = $this->renderer->getFieldValue($componentId, 'networks', 'show_instagram', true);
|
||||
$instagramUrl = $this->renderer->getFieldValue($componentId, 'networks', 'instagram_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareInstagram', 'socialShareInstagramUrl', 'Instagram', 'bi-instagram', $showInstagram, $instagramUrl, 'https://instagram.com/tu-perfil');
|
||||
|
||||
// LinkedIn
|
||||
$showLinkedin = $this->renderer->getFieldValue($componentId, 'networks', 'show_linkedin', true);
|
||||
$linkedinUrl = $this->renderer->getFieldValue($componentId, 'networks', 'linkedin_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareLinkedin', 'socialShareLinkedinUrl', 'LinkedIn', 'bi-linkedin', $showLinkedin, $linkedinUrl, 'https://linkedin.com/in/tu-perfil');
|
||||
|
||||
// WhatsApp
|
||||
$showWhatsapp = $this->renderer->getFieldValue($componentId, 'networks', 'show_whatsapp', true);
|
||||
$whatsappNumber = $this->renderer->getFieldValue($componentId, 'networks', 'whatsapp_number', '');
|
||||
$html .= $this->buildNetworkField('socialShareWhatsapp', 'socialShareWhatsappNumber', 'WhatsApp', 'bi-whatsapp', $showWhatsapp, $whatsappNumber, '521234567890');
|
||||
|
||||
// X (Twitter)
|
||||
$showTwitter = $this->renderer->getFieldValue($componentId, 'networks', 'show_twitter', true);
|
||||
$twitterUrl = $this->renderer->getFieldValue($componentId, 'networks', 'twitter_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareTwitter', 'socialShareTwitterUrl', 'X (Twitter)', 'bi-twitter-x', $showTwitter, $twitterUrl, 'https://x.com/tu-perfil');
|
||||
|
||||
// Email
|
||||
$showEmail = $this->renderer->getFieldValue($componentId, 'networks', 'show_email', true);
|
||||
$emailAddress = $this->renderer->getFieldValue($componentId, 'networks', 'email_address', '');
|
||||
$html .= $this->buildNetworkField('socialShareEmail', 'socialShareEmailAddress', 'Email', 'bi-envelope', $showEmail, $emailAddress, 'contacto@tudominio.com');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworkField(string $switchId, string $urlId, string $label, string $icon, mixed $checked, string $urlValue, string $placeholder): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-3 p-2 rounded" style="background-color: #f8f9fa;">';
|
||||
|
||||
// Switch
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($switchId),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small fw-semibold" for="%s">',
|
||||
esc_attr($switchId)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL Input
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm mt-2" value="%s" placeholder="%s">',
|
||||
esc_attr($urlId),
|
||||
esc_attr($urlValue),
|
||||
esc_attr($placeholder)
|
||||
);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores generales
|
||||
$html .= ' <p class="small fw-semibold mb-2">General</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareLabelColor', 'Etiqueta', $labelColor);
|
||||
|
||||
$borderTopColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_top_color', '#dee2e6');
|
||||
$html .= $this->buildColorPicker('socialShareBorderTopColor', 'Borde superior', $borderTopColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff');
|
||||
$html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores por red social
|
||||
$html .= ' <p class="small fw-semibold mb-2">Redes Sociales</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$facebookColor = $this->renderer->getFieldValue($componentId, 'colors', 'facebook_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('socialShareFacebookColor', 'Facebook', $facebookColor);
|
||||
|
||||
$instagramColor = $this->renderer->getFieldValue($componentId, 'colors', 'instagram_color', '#dc3545');
|
||||
$html .= $this->buildColorPicker('socialShareInstagramColor', 'Instagram', $instagramColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkedinColor = $this->renderer->getFieldValue($componentId, 'colors', 'linkedin_color', '#0dcaf0');
|
||||
$html .= $this->buildColorPicker('socialShareLinkedinColor', 'LinkedIn', $linkedinColor);
|
||||
|
||||
$whatsappColor = $this->renderer->getFieldValue($componentId, 'colors', 'whatsapp_color', '#198754');
|
||||
$html .= $this->buildColorPicker('socialShareWhatsappColor', 'WhatsApp', $whatsappColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$twitterColor = $this->renderer->getFieldValue($componentId, 'colors', 'twitter_color', '#212529');
|
||||
$html .= $this->buildColorPicker('socialShareTwitterColor', 'X (Twitter)', $twitterColor);
|
||||
|
||||
$emailColor = $this->renderer->getFieldValue($componentId, 'colors', 'email_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareEmailColor', 'Email', $emailColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// label_font_size
|
||||
$labelFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'label_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelFontSize" class="form-label small mb-1 fw-semibold">Tamano etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// icon_font_size
|
||||
$iconFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'icon_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareIconFontSize" class="form-label small mb-1 fw-semibold">Tamano iconos</label>';
|
||||
$html .= ' <input type="text" id="socialShareIconFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_margin_top
|
||||
$containerMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_margin_bottom
|
||||
$containerMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding_top
|
||||
$containerPaddingTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_top', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingTop" class="form-label small mb-1 fw-semibold">Padding superior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_padding_bottom
|
||||
$containerPaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingBottom" class="form-label small mb-1 fw-semibold">Padding inferior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// label_margin_bottom
|
||||
$labelMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'label_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelMarginBottom" class="form-label small mb-1 fw-semibold">Margen etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// buttons_gap
|
||||
$buttonsGap = $this->renderer->getFieldValue($componentId, 'spacing', 'buttons_gap', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonsGap" class="form-label small mb-1 fw-semibold">Espacio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonsGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonsGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonPadding" class="form-label small mb-1 fw-semibold">Padding botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_top_width
|
||||
$borderTopWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_top_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareBorderTopWidth" class="form-label small mb-1 fw-semibold">Grosor borde sup.</label>';
|
||||
$html .= ' <input type="text" id="socialShareBorderTopWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderTopWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_width
|
||||
$buttonBorderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_width', '2px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde btn</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="socialShareTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// hover_box_shadow
|
||||
$hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="socialShareHoverBoxShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="socialShareHoverBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverBoxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para la Tabla de Contenido
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\TableOfContents\Infrastructure\Ui
|
||||
*/
|
||||
final class TableOfContentsFormBuilder
|
||||
{
|
||||
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
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($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-list-nested me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Tabla de Contenido';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Navegacion automatica con ScrollSpy';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="table_of_contents">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('tocEnabled', 'Activar tabla de contenido', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('tocShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocTitle" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Titulo';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '" placeholder="Tabla de Contenido">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// auto_generate
|
||||
$autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true);
|
||||
$html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate);
|
||||
$html .= ' <small class="text-muted d-block mb-3">Genera TOC desde los encabezados del contenido</small>';
|
||||
|
||||
// heading_levels
|
||||
$headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocHeadingLevels" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-list-ol me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Niveles de encabezados';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocHeadingLevels" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($headingLevels) . '" placeholder="h2,h3">';
|
||||
$html .= ' <small class="text-muted">Separados por coma: h2,h3,h4</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// smooth_scroll
|
||||
$smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true);
|
||||
$html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(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-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_sticky
|
||||
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', true);
|
||||
$html .= $this->buildSwitch('tocIsSticky', 'Sticky (fijo al scroll)', 'bi-pin', $isSticky);
|
||||
|
||||
// scroll_offset
|
||||
$scrollOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'scroll_offset', '100');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocScrollOffset" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Offset de scroll (px)';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocScrollOffset" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollOffset) . '" placeholder="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// max_height
|
||||
$maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="tocMaxHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Altura maxima';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocMaxHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($maxHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(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-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '600');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '" placeholder="600">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_font_size
|
||||
$linkFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'link_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkFontSize" class="form-label small mb-1 fw-semibold">Tamano enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkFontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_line_height
|
||||
$linkLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'link_line_height', '1.3');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkLineHeight" class="form-label small mb-1 fw-semibold">Altura linea</label>';
|
||||
$html .= ' <input type="text" id="tocLinkLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkLineHeight) . '" placeholder="1.3">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_font_size
|
||||
$level3FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_three_font_size', '0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreeFontSize" class="form-label small mb-1 fw-semibold">Tamano H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3FontSize) . '" placeholder="0.85rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_font_size
|
||||
$level4FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_four_font_size', '0.8rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourFontSize" class="form-label small mb-1 fw-semibold">Tamano H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4FontSize) . '" placeholder="0.8rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('tocBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocBorderColor', 'Borde', $borderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del titulo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Titulo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocTitleColor', 'Color', $titleColor);
|
||||
|
||||
$titleBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocTitleBorderColor', 'Borde', $titleBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de enlaces
|
||||
$html .= ' <p class="small fw-semibold mb-2">Enlaces</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocLinkColor', 'Normal', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverColor', 'Hover', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_background', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverBackground', 'Fondo hover', $linkHoverBg);
|
||||
|
||||
$activeBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_border_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveBorderColor', 'Borde activo', $activeBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de activo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Estado Activo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$activeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_background_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocActiveBackgroundColor', 'Fondo', $activeBgColor);
|
||||
|
||||
$activeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_text_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveTextColor', 'Texto', $activeTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de scrollbar
|
||||
$html .= ' <p class="small fw-semibold mb-2">Scrollbar</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$scrollbarTrack = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_track_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocScrollbarTrackColor', 'Pista', $scrollbarTrack);
|
||||
|
||||
$scrollbarThumb = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_thumb_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocScrollbarThumbColor', 'Thumb', $scrollbarThumb);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(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-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '12px 16px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="tocContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// margin_bottom
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '13px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="tocMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_padding_bottom
|
||||
$titlePaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_padding_bottom', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitlePaddingBottom" class="form-label small mb-1 fw-semibold">Padding titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitlePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titlePaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// item_margin_bottom
|
||||
$itemMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'item_margin_bottom', '0.15rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocItemMarginBottom" class="form-label small mb-1 fw-semibold">Margen items</label>';
|
||||
$html .= ' <input type="text" id="tocItemMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_padding
|
||||
$linkPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'link_padding', '0.3rem 0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkPadding" class="form-label small mb-1 fw-semibold">Padding enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_padding_left
|
||||
$level3PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_three_padding_left', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreePaddingLeft" class="form-label small mb-1 fw-semibold">Padding H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreePaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_padding_left
|
||||
$level4PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_four_padding_left', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourPaddingLeft" class="form-label small mb-1 fw-semibold">Padding H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourPaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(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-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// border_width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="tocBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_border_radius
|
||||
$linkBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'link_border_radius', '4px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkBorderRadius" class="form-label small mb-1 fw-semibold">Radio enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// active_border_left_width
|
||||
$activeBorderLeftWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'active_border_left_width', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocActiveBorderLeftWidth" class="form-label small mb-1 fw-semibold">Borde activo</label>';
|
||||
$html .= ' <input type="text" id="tocActiveBorderLeftWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($activeBorderLeftWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTransitionDuration" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="tocTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// scrollbar_border_radius
|
||||
$scrollbarBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'scrollbar_border_radius', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocScrollbarBorderRadius" class="form-label small mb-1 fw-semibold">Radio scrollbar</label>';
|
||||
$html .= ' <input type="text" id="tocScrollbarBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollbarBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
final class TopNotificationBarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyAndSpacingGroup($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-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de TopBar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la barra de notificación superior del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="top-notification-bar">';
|
||||
$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 buildVisibilityGroup(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-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarEnabled" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar TopBar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(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-chat-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// icon_class + label_text (row)
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono';
|
||||
$html .= ' </label>';
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-megaphone-fill');
|
||||
$html .= ' <input type="text" id="topBarIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLabelText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta';
|
||||
$html .= ' </label>';
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Nuevo:');
|
||||
$html .= ' <input type="text" id="topBarLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '" maxlength="30">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// message_text (textarea)
|
||||
$messageText = $this->renderer->getFieldValue($componentId, 'content', 'message_text',
|
||||
'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="topBarMessageText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensaje';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">';
|
||||
$html .= esc_textarea($messageText);
|
||||
$html .= ' </textarea>';
|
||||
$html .= ' <small class="text-muted">Máximo 200 caracteres</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_text + link_url (row)
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Texto del enlace';
|
||||
$html .= ' </label>';
|
||||
$linkText = $this->renderer->getFieldValue($componentId, 'content', 'link_text', 'Ver Catálogo');
|
||||
$html .= ' <input type="text" id="topBarLinkText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL';
|
||||
$html .= ' </label>';
|
||||
$linkUrl = $this->renderer->getFieldValue($componentId, 'content', 'link_url', '#');
|
||||
$html .= ' <input type="url" id="topBarLinkUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_url($linkUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(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-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Grid 2x3 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('topBarBackgroundColor', 'Color de fondo', 'paint-bucket', $bgColor);
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarTextColor', 'Color de texto', 'fonts', $textColor);
|
||||
|
||||
// Label Color
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLabelColor', 'Color etiqueta', 'tag-fill', $labelColor);
|
||||
|
||||
// Icon Color
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarIconColor', 'Color ícono', 'star', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Row 2 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Link Color
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarLinkColor', 'Color enlace', 'link', $linkColor);
|
||||
|
||||
// Link Hover Color
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLinkHoverColor', 'Color enlace (hover)', 'hand-index', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyAndSpacingGroup(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-arrows-fullscreen me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía y Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Font Size
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamaño de fuente';
|
||||
$html .= ' </label>';
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'spacing', 'font_size', '0.9rem');
|
||||
$html .= ' <input type="text" id="topBarFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.9rem, 14px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarPadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '0.5rem 0');
|
||||
$html .= ' <input type="text" id="topBarPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.5rem 0</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TopBar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: TOP NOTIFICATION BAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="topBarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración de TopBar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza la barra de notificación superior del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetTopBarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarEnabled" checked>
|
||||
<label class="form-check-label small" for="topBarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar TopBar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-0 mt-3">
|
||||
<label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="topBarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: CONTENIDO
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido
|
||||
</h5>
|
||||
|
||||
<!-- icon_class + label_text (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>
|
||||
Clase del ícono
|
||||
</label>
|
||||
<input type="text" id="topBarIconClass" class="form-control form-control-sm" value="bi-megaphone-fill" placeholder="bi-...">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag me-1" style="color: #FF8600;"></i>
|
||||
Etiqueta
|
||||
</label>
|
||||
<input type="text" id="topBarLabelText" class="form-control form-control-sm" value="Nuevo:" maxlength="30">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- message_text (textarea full width) -->
|
||||
<div class="mb-2">
|
||||
<label for="topBarMessageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
<small class="text-muted">Máximo 200 caracteres</small>
|
||||
</div>
|
||||
|
||||
<!-- link_text + link_url (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
Texto del enlace
|
||||
</label>
|
||||
<input type="text" id="topBarLinkText" class="form-control form-control-sm" value="Ver Catálogo" maxlength="50">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
|
||||
URL
|
||||
</label>
|
||||
<input type="url" id="topBarLinkUrl" class="form-control form-control-sm" value="#" placeholder="https://...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 3: ESTILOS - COLORES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Colores
|
||||
</h5>
|
||||
|
||||
<!-- PATRÓN 5: COLOR PICKERS EN GRID 2X2 -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="topBarBackgroundColor" class="form-control form-control-color w-100" value="#0E2337" title="Color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="topBarBackgroundColorValue">#0E2337</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color de texto
|
||||
</label>
|
||||
<input type="color" id="topBarTextColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color de texto">
|
||||
<small class="text-muted d-block mt-1" id="topBarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag-fill me-1" style="color: #FF8600;"></i>
|
||||
Color etiqueta
|
||||
</label>
|
||||
<input type="color" id="topBarLabelColor" class="form-control form-control-color w-100" value="#FF8600" title="Color etiqueta">
|
||||
<small class="text-muted d-block mt-1" id="topBarLabelColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
|
||||
Color ícono
|
||||
</label>
|
||||
<input type="color" id="topBarIconColor" class="form-control form-control-color w-100" value="#FF8600" title="Color ícono">
|
||||
<small class="text-muted d-block mt-1" id="topBarIconColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
|
||||
Color enlace
|
||||
</label>
|
||||
<input type="color" id="topBarLinkColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color enlace">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color enlace (hover)
|
||||
</label>
|
||||
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Color enlace hover">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: ESTILOS - TAMAÑOS
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Tamaños
|
||||
</h5>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño de fuente
|
||||
</label>
|
||||
<input type="text" id="topBarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
<small class="text-muted">Ej: 0.9rem, 14px</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="topBarPadding" class="form-control form-control-sm" value="0.5rem 0">
|
||||
<small class="text-muted">Ej: 0.5rem 0</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
|
||||
/**
|
||||
* ContactFormAjaxHandler - Procesa envios del formulario de contacto
|
||||
*
|
||||
* RESPONSABILIDAD: Recibir datos del formulario y enviarlos al webhook configurado
|
||||
*
|
||||
* SEGURIDAD:
|
||||
* - Verifica nonce
|
||||
* - Webhook URL NUNCA se expone al cliente
|
||||
* - Webhook URL se obtiene de BD server-side
|
||||
* - Rate limiting basico
|
||||
* - Sanitizacion de inputs
|
||||
*
|
||||
* @package ROITheme\Public\ContactForm\Infrastructure\Api\WordPress
|
||||
*/
|
||||
final class ContactFormAjaxHandler
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_contact_form_nonce';
|
||||
private const COMPONENT_NAME = 'contact-form';
|
||||
|
||||
public function __construct(
|
||||
private ComponentSettingsRepositoryInterface $settingsRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registrar hooks AJAX
|
||||
* Usa wp_ajax_nopriv para usuarios no logueados
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_contact_form_submit', [$this, 'handleSubmit']);
|
||||
add_action('wp_ajax_nopriv_roi_contact_form_submit', [$this, 'handleSubmit']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar envio del formulario
|
||||
*/
|
||||
public function handleSubmit(): void
|
||||
{
|
||||
// 1. Verificar nonce
|
||||
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
|
||||
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Rate limiting basico (1 envio por IP cada 30 segundos)
|
||||
if (!$this->checkRateLimit()) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme')
|
||||
], 429);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Sanitizar y validar inputs
|
||||
$formData = $this->sanitizeFormData($_POST);
|
||||
$validation = $this->validateFormData($formData);
|
||||
|
||||
if (!$validation['valid']) {
|
||||
wp_send_json_error([
|
||||
'message' => $validation['message'],
|
||||
'errors' => $validation['errors']
|
||||
], 422);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Obtener configuracion del componente (incluye webhook URL)
|
||||
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
|
||||
|
||||
if (empty($settings)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
$integration = $settings['integration'] ?? [];
|
||||
$webhookUrl = $integration['webhook_url'] ?? '';
|
||||
$webhookMethod = $integration['webhook_method'] ?? 'POST';
|
||||
$includePageUrl = $this->toBool($integration['include_page_url'] ?? true);
|
||||
$includeTimestamp = $this->toBool($integration['include_timestamp'] ?? true);
|
||||
|
||||
if (empty($webhookUrl)) {
|
||||
// Si no hay webhook configurado, simular exito para UX
|
||||
// pero loguear warning para admin
|
||||
error_log('ROI Theme Contact Form: No webhook URL configured');
|
||||
wp_send_json_success([
|
||||
'message' => $this->getSuccessMessage($settings)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Preparar payload para webhook
|
||||
$payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp);
|
||||
|
||||
// 6. Enviar a webhook
|
||||
$result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success([
|
||||
'message' => $this->getSuccessMessage($settings)
|
||||
]);
|
||||
} else {
|
||||
error_log('ROI Theme Contact Form webhook error: ' . $result['error']);
|
||||
wp_send_json_error([
|
||||
'message' => $this->getErrorMessage($settings)
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizar datos del formulario
|
||||
*/
|
||||
private function sanitizeFormData(array $post): array
|
||||
{
|
||||
return [
|
||||
'fullName' => sanitize_text_field($post['fullName'] ?? ''),
|
||||
'company' => sanitize_text_field($post['company'] ?? ''),
|
||||
'whatsapp' => sanitize_text_field($post['whatsapp'] ?? ''),
|
||||
'email' => sanitize_email($post['email'] ?? ''),
|
||||
'message' => sanitize_textarea_field($post['message'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar datos del formulario
|
||||
*/
|
||||
private function validateFormData(array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Nombre requerido
|
||||
if (empty($data['fullName'])) {
|
||||
$errors['fullName'] = __('El nombre es obligatorio', 'roi-theme');
|
||||
}
|
||||
|
||||
// WhatsApp requerido
|
||||
if (empty($data['whatsapp'])) {
|
||||
$errors['whatsapp'] = __('El WhatsApp es obligatorio', 'roi-theme');
|
||||
}
|
||||
|
||||
// Email requerido y valido
|
||||
if (empty($data['email'])) {
|
||||
$errors['email'] = __('El email es obligatorio', 'roi-theme');
|
||||
} elseif (!is_email($data['email'])) {
|
||||
$errors['email'] = __('Por favor ingresa un email valido', 'roi-theme');
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'message' => __('Por favor corrige los errores del formulario', 'roi-theme'),
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'message' => '', 'errors' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparar payload para webhook
|
||||
*/
|
||||
private function preparePayload(array $formData, bool $includePageUrl, bool $includeTimestamp): array
|
||||
{
|
||||
$payload = [
|
||||
'fullName' => $formData['fullName'],
|
||||
'company' => $formData['company'],
|
||||
'whatsapp' => $formData['whatsapp'],
|
||||
'email' => $formData['email'],
|
||||
'message' => $formData['message'],
|
||||
];
|
||||
|
||||
if ($includePageUrl) {
|
||||
$payload['pageUrl'] = sanitize_url($_POST['pageUrl'] ?? '');
|
||||
$payload['pageTitle'] = sanitize_text_field($_POST['pageTitle'] ?? '');
|
||||
}
|
||||
|
||||
if ($includeTimestamp) {
|
||||
$payload['timestamp'] = current_time('c');
|
||||
$payload['timezone'] = wp_timezone_string();
|
||||
}
|
||||
|
||||
// Metadata adicional util para el webhook
|
||||
$payload['source'] = 'contact-form';
|
||||
$payload['siteName'] = get_bloginfo('name');
|
||||
$payload['siteUrl'] = home_url();
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar datos al webhook
|
||||
*/
|
||||
private function sendToWebhook(string $url, string $method, array $payload): array
|
||||
{
|
||||
$args = [
|
||||
'method' => strtoupper($method),
|
||||
'timeout' => 30,
|
||||
'redirection' => 5,
|
||||
'httpversion' => '1.1',
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
];
|
||||
|
||||
if ($method === 'POST') {
|
||||
$args['body'] = wp_json_encode($payload);
|
||||
} else {
|
||||
$url = add_query_arg($payload, $url);
|
||||
}
|
||||
|
||||
$response = wp_remote_request($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
];
|
||||
}
|
||||
|
||||
$statusCode = wp_remote_retrieve_response_code($response);
|
||||
|
||||
// Considerar 2xx como exito
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
return ['success' => true, 'error' => ''];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting basico por IP
|
||||
*/
|
||||
private function checkRateLimit(): bool
|
||||
{
|
||||
$ip = $this->getClientIP();
|
||||
$transientKey = 'roi_contact_form_' . md5($ip);
|
||||
$lastSubmit = get_transient($transientKey);
|
||||
|
||||
if ($lastSubmit !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_transient($transientKey, time(), 30);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener IP del cliente
|
||||
*/
|
||||
private function getClientIP(): string
|
||||
{
|
||||
$ip = '';
|
||||
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener mensaje de exito desde configuracion
|
||||
*/
|
||||
private function getSuccessMessage(array $data): string
|
||||
{
|
||||
$messages = $data['messages'] ?? [];
|
||||
return $messages['success_message'] ?? __('¡Gracias por contactarnos! Te responderemos pronto.', 'roi-theme');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener mensaje de error desde configuracion
|
||||
*/
|
||||
private function getErrorMessage(array $data): string
|
||||
{
|
||||
$messages = $data['messages'] ?? [];
|
||||
return $messages['error_message'] ?? __('Hubo un error al enviar el mensaje. Por favor intenta de nuevo.', 'roi-theme');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertir valor a boolean
|
||||
*/
|
||||
private function toBool($value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
461
Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
Normal file
461
Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
Normal file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* ContactFormRenderer - Renderiza formulario de contacto con webhook
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del formulario de contacto
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Formulario responsive Bootstrap 5
|
||||
* - Envio a webhook configurable (no expuesto en frontend)
|
||||
* - Info de contacto configurable
|
||||
* - Mensajes de exito/error personalizables
|
||||
*
|
||||
* @package ROITheme\Public\ContactForm\Infrastructure\Ui
|
||||
*/
|
||||
final class ContactFormRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$visibilityClass = $this->getVisibilityClass($data);
|
||||
if ($visibilityClass === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data, $visibilityClass);
|
||||
$js = $this->buildJS($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s\n<script>%s</script>", $css, $html, $js);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'contact-form';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
|
||||
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
|
||||
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
|
||||
|
||||
if (!$showDesktop && !$showMobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$showDesktop && $showMobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($showDesktop && !$showMobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Section background
|
||||
$sectionBgColor = $colors['section_bg_color'] ?? 'rgba(108, 117, 125, 0.25)';
|
||||
$sectionPaddingY = $spacing['section_padding_y'] ?? '3rem';
|
||||
$sectionMarginTop = $spacing['section_margin_top'] ?? '3rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section', [
|
||||
'background-color' => $sectionBgColor,
|
||||
'padding-top' => $sectionPaddingY,
|
||||
'padding-bottom' => $sectionPaddingY,
|
||||
'margin-top' => $sectionMarginTop,
|
||||
]);
|
||||
|
||||
// Title
|
||||
$titleColor = $colors['title_color'] ?? '#212529';
|
||||
$titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.75rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-title', [
|
||||
'color' => $titleColor,
|
||||
'margin-bottom' => $titleMarginBottom,
|
||||
]);
|
||||
|
||||
// Description
|
||||
$descColor = $colors['description_color'] ?? '#212529';
|
||||
$descMarginBottom = $spacing['description_margin_bottom'] ?? '1.5rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-description', [
|
||||
'color' => $descColor,
|
||||
'margin-bottom' => $descMarginBottom,
|
||||
]);
|
||||
|
||||
// Icons
|
||||
$iconColor = $colors['icon_color'] ?? '#FF8600';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-icon', [
|
||||
'color' => $iconColor,
|
||||
]);
|
||||
|
||||
// Info labels and values
|
||||
$infoLabelColor = $colors['info_label_color'] ?? '#212529';
|
||||
$infoValueColor = $colors['info_value_color'] ?? '#6c757d';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-label', [
|
||||
'color' => $infoLabelColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-value', [
|
||||
'color' => $infoValueColor,
|
||||
]);
|
||||
|
||||
// Form inputs
|
||||
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
|
||||
$inputFocusBorder = $colors['input_focus_border'] ?? '#FF8600';
|
||||
$inputBorderRadius = $effects['input_border_radius'] ?? '6px';
|
||||
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control', [
|
||||
'border-color' => $inputBorderColor,
|
||||
'border-radius' => $inputBorderRadius,
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control:focus', [
|
||||
'border-color' => $inputFocusBorder,
|
||||
'box-shadow' => "0 0 0 0.2rem rgba(255, 134, 0, 0.25)",
|
||||
'outline' => 'none',
|
||||
]);
|
||||
|
||||
// Submit button
|
||||
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
|
||||
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
|
||||
$buttonHoverBg = $colors['button_hover_bg'] ?? '#e67a00';
|
||||
$buttonBorderRadius = $effects['button_border_radius'] ?? '6px';
|
||||
$buttonPadding = $effects['button_padding'] ?? '0.75rem 2rem';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit', [
|
||||
'background-color' => $buttonBgColor,
|
||||
'color' => $buttonTextColor,
|
||||
'font-weight' => '600',
|
||||
'padding' => $buttonPadding,
|
||||
'border' => 'none',
|
||||
'border-radius' => $buttonBorderRadius,
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:hover', [
|
||||
'background-color' => $buttonHoverBg,
|
||||
'color' => $buttonTextColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:disabled', [
|
||||
'opacity' => '0.7',
|
||||
'cursor' => 'not-allowed',
|
||||
]);
|
||||
|
||||
// Success/Error messages
|
||||
$successBgColor = $colors['success_bg_color'] ?? '#d1e7dd';
|
||||
$successTextColor = $colors['success_text_color'] ?? '#0f5132';
|
||||
$errorBgColor = $colors['error_bg_color'] ?? '#f8d7da';
|
||||
$errorTextColor = $colors['error_text_color'] ?? '#842029';
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-success', [
|
||||
'background-color' => $successBgColor,
|
||||
'color' => $successTextColor,
|
||||
'border-color' => $successBgColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-danger', [
|
||||
'background-color' => $errorBgColor,
|
||||
'color' => $errorTextColor,
|
||||
'border-color' => $errorBgColor,
|
||||
]);
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data, string $visibilityClass): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$contactInfo = $data['contact_info'] ?? [];
|
||||
$formLabels = $data['form_labels'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
|
||||
// Content
|
||||
$sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?';
|
||||
$sectionDesc = $content['section_description'] ?? 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.';
|
||||
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
|
||||
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
|
||||
|
||||
// Contact info
|
||||
$showContactInfo = $contactInfo['show_contact_info'] ?? true;
|
||||
$showContactInfo = $showContactInfo === true || $showContactInfo === '1' || $showContactInfo === 1;
|
||||
|
||||
// Form labels/placeholders
|
||||
$fullnamePlaceholder = $formLabels['fullname_placeholder'] ?? 'Nombre completo *';
|
||||
$companyPlaceholder = $formLabels['company_placeholder'] ?? 'Empresa';
|
||||
$whatsappPlaceholder = $formLabels['whatsapp_placeholder'] ?? 'WhatsApp *';
|
||||
$emailPlaceholder = $formLabels['email_placeholder'] ?? 'Correo electrónico *';
|
||||
$messagePlaceholder = $formLabels['message_placeholder'] ?? '¿En qué podemos ayudarte?';
|
||||
|
||||
$textareaRows = $effects['textarea_rows'] ?? '4';
|
||||
|
||||
// Container class
|
||||
$containerClass = 'roi-contact-form-section';
|
||||
if (!empty($visibilityClass)) {
|
||||
$containerClass .= ' ' . $visibilityClass;
|
||||
}
|
||||
|
||||
// Nonce for AJAX security
|
||||
$nonce = wp_create_nonce('roi_contact_form_nonce');
|
||||
|
||||
$html = sprintf('<section class="%s">', esc_attr($containerClass));
|
||||
$html .= '<div class="container">';
|
||||
$html .= '<div class="row justify-content-center">';
|
||||
$html .= '<div class="col-lg-10">';
|
||||
$html .= '<div class="row">';
|
||||
|
||||
// Left column - Contact info
|
||||
$html .= '<div class="col-lg-5 mb-4 mb-lg-0">';
|
||||
$html .= sprintf('<h2 class="h3 contact-title">%s</h2>', esc_html($sectionTitle));
|
||||
$html .= sprintf('<p class="contact-description">%s</p>', esc_html($sectionDesc));
|
||||
|
||||
if ($showContactInfo) {
|
||||
$html .= $this->buildContactInfoHTML($contactInfo);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
// Right column - Form
|
||||
$html .= '<div class="col-lg-7">';
|
||||
$html .= sprintf('<form id="roiContactForm" data-nonce="%s">', esc_attr($nonce));
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Full name field
|
||||
$html .= '<div class="col-md-6">';
|
||||
$html .= sprintf(
|
||||
'<input type="text" class="form-control" id="roiContactFullName" name="fullName" placeholder="%s" required>',
|
||||
esc_attr($fullnamePlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// Company field
|
||||
$html .= '<div class="col-md-6">';
|
||||
$html .= sprintf(
|
||||
'<input type="text" class="form-control" id="roiContactCompany" name="company" placeholder="%s">',
|
||||
esc_attr($companyPlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// WhatsApp field
|
||||
$html .= '<div class="col-md-6">';
|
||||
$html .= sprintf(
|
||||
'<input type="tel" class="form-control" id="roiContactWhatsapp" name="whatsapp" placeholder="%s" required>',
|
||||
esc_attr($whatsappPlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// Email field
|
||||
$html .= '<div class="col-md-6">';
|
||||
$html .= sprintf(
|
||||
'<input type="email" class="form-control" id="roiContactEmail" name="email" placeholder="%s" required>',
|
||||
esc_attr($emailPlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// Message field
|
||||
$html .= '<div class="col-12">';
|
||||
$html .= sprintf(
|
||||
'<textarea class="form-control" id="roiContactMessage" name="message" rows="%s" placeholder="%s"></textarea>',
|
||||
esc_attr($textareaRows),
|
||||
esc_attr($messagePlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// Submit button
|
||||
$html .= '<div class="col-12">';
|
||||
$html .= '<button type="submit" class="btn btn-contact-submit w-100">';
|
||||
$html .= sprintf('<i class="%s me-2"></i>', esc_attr($submitIcon));
|
||||
$html .= esc_html($submitText);
|
||||
$html .= '</button>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Message container
|
||||
$html .= '<div id="roiContactFormMessage" class="col-12 mt-2 alert" style="display: none;"></div>';
|
||||
|
||||
$html .= '</div>'; // .row g-3
|
||||
$html .= '</form>';
|
||||
$html .= '</div>'; // .col-lg-7
|
||||
|
||||
$html .= '</div>'; // .row
|
||||
$html .= '</div>'; // .col-lg-10
|
||||
$html .= '</div>'; // .row justify-content-center
|
||||
$html .= '</div>'; // .container
|
||||
$html .= '</section>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContactInfoHTML(array $contactInfo): string
|
||||
{
|
||||
$phoneLabel = $contactInfo['phone_label'] ?? 'Teléfono';
|
||||
$phoneValue = $contactInfo['phone_value'] ?? '+52 55 1234 5678';
|
||||
$emailLabel = $contactInfo['email_label'] ?? 'Email';
|
||||
$emailValue = $contactInfo['email_value'] ?? 'contacto@apumexico.com';
|
||||
$locationLabel = $contactInfo['location_label'] ?? 'Ubicación';
|
||||
$locationValue = $contactInfo['location_value'] ?? 'Ciudad de México, México';
|
||||
|
||||
$html = '<div class="contact-info">';
|
||||
|
||||
// Phone
|
||||
$html .= '<div class="d-flex align-items-start mb-3">';
|
||||
$html .= '<i class="bi bi-telephone-fill me-3 fs-5 contact-icon"></i>';
|
||||
$html .= '<div>';
|
||||
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($phoneLabel));
|
||||
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($phoneValue));
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Email
|
||||
$html .= '<div class="d-flex align-items-start mb-3">';
|
||||
$html .= '<i class="bi bi-envelope-fill me-3 fs-5 contact-icon"></i>';
|
||||
$html .= '<div>';
|
||||
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($emailLabel));
|
||||
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($emailValue));
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Location
|
||||
$html .= '<div class="d-flex align-items-start">';
|
||||
$html .= '<i class="bi bi-geo-alt-fill me-3 fs-5 contact-icon"></i>';
|
||||
$html .= '<div>';
|
||||
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($locationLabel));
|
||||
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($locationValue));
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildJS(array $data): string
|
||||
{
|
||||
$messages = $data['messages'] ?? [];
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$successMessage = $messages['success_message'] ?? '¡Gracias por contactarnos! Te responderemos pronto.';
|
||||
$errorMessage = $messages['error_message'] ?? 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.';
|
||||
$sendingMessage = $messages['sending_message'] ?? 'Enviando...';
|
||||
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
|
||||
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
|
||||
|
||||
// AJAX URL for WordPress
|
||||
$ajaxUrl = admin_url('admin-ajax.php');
|
||||
|
||||
$js = <<<JS
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('roiContactForm');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const messageDiv = document.getElementById('roiContactFormMessage');
|
||||
const originalBtnHtml = submitBtn.innerHTML;
|
||||
const nonce = form.dataset.nonce;
|
||||
|
||||
// Disable button and show sending state
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>' + '{$sendingMessage}';
|
||||
messageDiv.style.display = 'none';
|
||||
|
||||
// Collect form data
|
||||
const formData = new FormData(form);
|
||||
formData.append('action', 'roi_contact_form_submit');
|
||||
formData.append('nonce', nonce);
|
||||
formData.append('pageUrl', window.location.href);
|
||||
formData.append('pageTitle', document.title);
|
||||
|
||||
try {
|
||||
const response = await fetch('{$ajaxUrl}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
messageDiv.className = 'col-12 mt-2 alert alert-success';
|
||||
messageDiv.textContent = '{$successMessage}';
|
||||
messageDiv.style.display = 'block';
|
||||
form.reset();
|
||||
} else {
|
||||
messageDiv.className = 'col-12 mt-2 alert alert-danger';
|
||||
messageDiv.textContent = result.data?.message || '{$errorMessage}';
|
||||
messageDiv.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
messageDiv.className = 'col-12 mt-2 alert alert-danger';
|
||||
messageDiv.textContent = '{$errorMessage}';
|
||||
messageDiv.style.display = 'block';
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
JS;
|
||||
|
||||
return $js;
|
||||
}
|
||||
}
|
||||
280
Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
Normal file
280
Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del CTA Box Sidebar
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Titulo configurable
|
||||
* - Descripcion configurable
|
||||
* - Boton con icono y multiples acciones (modal, link, scroll)
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar CTA box)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
$script = $this->buildScript();
|
||||
|
||||
return sprintf("<style>%s</style>\n%s\n%s", $css, $html, $script);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-box-sidebar';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$behavior = $data['behavior'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
||||
|
||||
// Container styles - Match template exactly (height: 250px, flexbox centering)
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar', [
|
||||
'background' => $colors['background_color'] ?? '#FF8600',
|
||||
'border-radius' => $effects['border_radius'] ?? '8px',
|
||||
'padding' => $spacing['container_padding'] ?? '24px',
|
||||
'text-align' => $behavior['text_align'] ?? 'center',
|
||||
'box-shadow' => $effects['box_shadow'] ?? '0 4px 12px rgba(255, 133, 0, 0.2)',
|
||||
'margin-top' => '0',
|
||||
'margin-bottom' => '15px',
|
||||
'height' => '250px',
|
||||
'display' => 'flex',
|
||||
'flex-direction' => 'column',
|
||||
'justify-content' => 'center',
|
||||
]);
|
||||
|
||||
// Title styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-title', [
|
||||
'color' => $colors['title_color'] ?? '#ffffff',
|
||||
'font-weight' => $typography['title_font_weight'] ?? '700',
|
||||
'font-size' => $typography['title_font_size'] ?? '1.25rem',
|
||||
'margin-bottom' => $spacing['title_margin_bottom'] ?? '1rem',
|
||||
'margin-top' => '0',
|
||||
]);
|
||||
|
||||
// Description styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-text', [
|
||||
'color' => $colors['description_color'] ?? 'rgba(255, 255, 255, 0.95)',
|
||||
'font-size' => $typography['description_font_size'] ?? '0.9rem',
|
||||
'margin-bottom' => $spacing['description_margin_bottom'] ?? '1rem',
|
||||
]);
|
||||
|
||||
// Button styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box', [
|
||||
'background-color' => $colors['button_background_color'] ?? '#ffffff',
|
||||
'color' => $colors['button_text_color'] ?? '#FF8600',
|
||||
'font-weight' => $typography['button_font_weight'] ?? '700',
|
||||
'font-size' => $typography['button_font_size'] ?? '1rem',
|
||||
'border' => 'none',
|
||||
'padding' => $spacing['button_padding'] ?? '0.75rem 1.5rem',
|
||||
'border-radius' => $effects['button_border_radius'] ?? '8px',
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
'cursor' => 'pointer',
|
||||
'display' => 'inline-flex',
|
||||
'align-items' => 'center',
|
||||
'justify-content' => 'center',
|
||||
'width' => '100%',
|
||||
]);
|
||||
|
||||
// Button hover styles (template uses --color-navy-primary = #1e3a5f)
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box:hover', [
|
||||
'background-color' => $colors['button_hover_background'] ?? '#1e3a5f',
|
||||
'color' => $colors['button_hover_text_color'] ?? '#ffffff',
|
||||
]);
|
||||
|
||||
// Button icon spacing
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box i', [
|
||||
'margin-right' => $spacing['icon_margin_right'] ?? '0.5rem',
|
||||
]);
|
||||
|
||||
// Responsive visibility
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? false;
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 991.98px) {
|
||||
.cta-box-sidebar { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.cta-box-sidebar { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$title = $content['title'] ?? '¿Listo para potenciar tus proyectos?';
|
||||
$description = $content['description'] ?? 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.';
|
||||
$buttonText = $content['button_text'] ?? 'Solicitar Demo';
|
||||
$buttonIcon = $content['button_icon'] ?? 'bi bi-calendar-check';
|
||||
$buttonAction = $content['button_action'] ?? 'modal';
|
||||
$buttonLink = $content['button_link'] ?? '#contactModal';
|
||||
|
||||
// Build button attributes based on action type
|
||||
$buttonAttributes = $this->getButtonAttributes($buttonAction, $buttonLink);
|
||||
|
||||
$html = '<div class="cta-box-sidebar">';
|
||||
|
||||
// Title
|
||||
$html .= sprintf(
|
||||
'<h5 class="cta-box-title">%s</h5>',
|
||||
esc_html($title)
|
||||
);
|
||||
|
||||
// Description
|
||||
$html .= sprintf(
|
||||
'<p class="cta-box-text">%s</p>',
|
||||
esc_html($description)
|
||||
);
|
||||
|
||||
// Button
|
||||
$iconHtml = !empty($buttonIcon)
|
||||
? sprintf('<i class="%s"></i>', esc_attr($buttonIcon))
|
||||
: '';
|
||||
|
||||
$html .= sprintf(
|
||||
'<button class="btn btn-cta-box" %s>%s%s</button>',
|
||||
$buttonAttributes,
|
||||
$iconHtml,
|
||||
esc_html($buttonText)
|
||||
);
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getButtonAttributes(string $action, string $link): string
|
||||
{
|
||||
switch ($action) {
|
||||
case 'modal':
|
||||
// Extract modal ID from link (e.g., #contactModal -> contactModal)
|
||||
$modalId = ltrim($link, '#');
|
||||
return sprintf(
|
||||
'type="button" data-bs-toggle="modal" data-bs-target="#%s"',
|
||||
esc_attr($modalId)
|
||||
);
|
||||
|
||||
case 'link':
|
||||
return sprintf(
|
||||
'type="button" data-cta-action="link" data-cta-href="%s"',
|
||||
esc_url($link)
|
||||
);
|
||||
|
||||
case 'scroll':
|
||||
$targetId = ltrim($link, '#');
|
||||
return sprintf(
|
||||
'type="button" data-cta-action="scroll" data-cta-target="%s"',
|
||||
esc_attr($targetId)
|
||||
);
|
||||
|
||||
default:
|
||||
return 'type="button"';
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if (!$desktop && !$mobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function buildScript(): string
|
||||
{
|
||||
return <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var ctaButtons = document.querySelectorAll('.btn-cta-box[data-cta-action]');
|
||||
ctaButtons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var action = this.getAttribute('data-cta-action');
|
||||
if (action === 'link') {
|
||||
var href = this.getAttribute('data-cta-href');
|
||||
if (href) window.location.href = href;
|
||||
} else if (action === 'scroll') {
|
||||
var target = this.getAttribute('data-cta-target');
|
||||
var el = document.getElementById(target);
|
||||
if (el) el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
}
|
||||
360
Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
Normal file
360
Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkRenderer
|
||||
*
|
||||
* Renderizador del componente CTA "Let's Talk" para el frontend.
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar botón CTA "Let's Talk" en el navbar
|
||||
* - Delegar generación de CSS a CSSGeneratorInterface
|
||||
* - Validar visibilidad (is_enabled, show_on_pages, show_on_desktop, show_on_mobile)
|
||||
* - Manejar visibilidad responsive con clases Bootstrap
|
||||
* - Generar atributos para modal o URL personalizada
|
||||
* - Sanitizar todos los outputs
|
||||
*
|
||||
* NO responsable de:
|
||||
* - Generar string CSS (delega a CSSGeneratorService)
|
||||
* - Persistir datos (ya están en Component)
|
||||
* - Lógica de negocio (está en Domain)
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar este componente)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\CtaLetsTalk\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaLetsTalkRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por página
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generar CSS usando CSSGeneratorService
|
||||
$css = $this->generateCSS($data);
|
||||
|
||||
// Generar HTML
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
// Combinar todo
|
||||
return sprintf(
|
||||
"<style>%s</style>\n%s",
|
||||
$css,
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-lets-talk';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente está habilitado
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si debe mostrarse en la página actual
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
return match ($showOn) {
|
||||
'all' => true,
|
||||
'home' => is_front_page(),
|
||||
'posts' => is_single(),
|
||||
'pages' => is_page(),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular clases de visibilidad responsive
|
||||
*
|
||||
* @param bool $desktop Mostrar en desktop
|
||||
* @param bool $mobile Mostrar en mobile
|
||||
* @return string|null Clases CSS o null si no debe mostrarse
|
||||
*/
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if (!$desktop && !$mobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar CSS usando CSSGeneratorService
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string CSS generado
|
||||
*/
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$css = '';
|
||||
|
||||
// Estilos base del botón
|
||||
$baseStyles = [
|
||||
'background_color' => $data['colors']['background_color'] ?? '#FF8600',
|
||||
'color' => $data['colors']['text_color'] ?? '#FFFFFF',
|
||||
'font_size' => $data['typography']['font_size'] ?? '1rem',
|
||||
'font_weight' => $data['typography']['font_weight'] ?? '600',
|
||||
'text_transform' => $data['typography']['text_transform'] ?? 'none',
|
||||
'padding' => sprintf(
|
||||
'%s %s',
|
||||
$data['spacing']['padding_top_bottom'] ?? '0.5rem',
|
||||
$data['spacing']['padding_left_right'] ?? '1.5rem'
|
||||
),
|
||||
'border' => sprintf(
|
||||
'%s solid %s',
|
||||
$data['visual_effects']['border_width'] ?? '0',
|
||||
$data['colors']['border_color'] ?? 'transparent'
|
||||
),
|
||||
'border_radius' => $data['visual_effects']['border_radius'] ?? '6px',
|
||||
'box_shadow' => $data['visual_effects']['box_shadow'] ?? 'none',
|
||||
'transition' => sprintf(
|
||||
'all %s ease',
|
||||
$data['visual_effects']['transition_duration'] ?? '0.3s'
|
||||
),
|
||||
'cursor' => 'pointer',
|
||||
];
|
||||
$css .= $this->cssGenerator->generate('.btn-lets-talk', $baseStyles);
|
||||
|
||||
// Estilos hover del botón
|
||||
$hoverStyles = [
|
||||
'background_color' => $data['colors']['background_hover_color'] ?? '#FF6B35',
|
||||
'color' => $data['colors']['text_hover_color'] ?? '#FFFFFF',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk:hover', $hoverStyles);
|
||||
|
||||
// Estilos del ícono dentro del botón
|
||||
$iconStyles = [
|
||||
'color' => $data['colors']['text_color'] ?? '#FFFFFF',
|
||||
'margin_right' => $data['spacing']['icon_spacing'] ?? '0.5rem',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk i', $iconStyles);
|
||||
|
||||
// Estilos responsive - ocultar en móvil si show_on_mobile = false
|
||||
$showOnMobile = ($data['visibility']['show_on_mobile'] ?? false) === true;
|
||||
if (!$showOnMobile) {
|
||||
$responsiveStyles = [
|
||||
'display' => 'none !important',
|
||||
];
|
||||
$css .= "\n@media (max-width: 991px) {\n";
|
||||
$css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
|
||||
$css .= "\n}";
|
||||
}
|
||||
|
||||
// Estilos responsive - ocultar en desktop si show_on_desktop = false
|
||||
$showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true;
|
||||
if (!$showOnDesktop) {
|
||||
$responsiveStyles = [
|
||||
'display' => 'none !important',
|
||||
];
|
||||
$css .= "\n@media (min-width: 992px) {\n";
|
||||
$css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
|
||||
$css .= "\n}";
|
||||
}
|
||||
|
||||
// Margen izquierdo para separar del menú (solo desktop)
|
||||
$marginLeft = $data['spacing']['margin_left'] ?? '1rem';
|
||||
if (!empty($marginLeft) && $marginLeft !== '0') {
|
||||
$css .= "\n@media (min-width: 992px) {\n";
|
||||
$css .= $this->cssGenerator->generate('.btn-lets-talk', ['margin_left' => $marginLeft]);
|
||||
$css .= "\n}";
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar HTML del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML generado
|
||||
*/
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$classes = $this->buildClasses($data);
|
||||
$attributes = $this->buildAttributes($data);
|
||||
$content = $this->buildContent($data);
|
||||
|
||||
$tag = $this->useModal($data) ? 'button' : 'a';
|
||||
|
||||
return sprintf(
|
||||
'<%s class="%s"%s>%s</%s>',
|
||||
$tag,
|
||||
esc_attr($classes),
|
||||
$attributes,
|
||||
$content,
|
||||
$tag
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir clases CSS del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Clases CSS
|
||||
*/
|
||||
private function buildClasses(array $data): string
|
||||
{
|
||||
$classes = ['btn', 'btn-lets-talk'];
|
||||
|
||||
// Agregar clase ms-lg-3 para margen en desktop (Bootstrap)
|
||||
// Esto solo aplica en pantallas >= lg (992px)
|
||||
$classes[] = 'ms-lg-3';
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determinar si debe usar modal o URL
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function useModal(array $data): bool
|
||||
{
|
||||
return ($data['behavior']['enable_modal'] ?? true) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir atributos HTML del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Atributos HTML
|
||||
*/
|
||||
private function buildAttributes(array $data): string
|
||||
{
|
||||
$attributes = [];
|
||||
|
||||
if ($this->useModal($data)) {
|
||||
// Atributos para modal de Bootstrap
|
||||
$attributes[] = 'type="button"';
|
||||
$attributes[] = 'data-bs-toggle="modal"';
|
||||
|
||||
$modalTarget = $data['content']['modal_target'] ?? '#contactModal';
|
||||
$attributes[] = sprintf('data-bs-target="%s"', esc_attr($modalTarget));
|
||||
} else {
|
||||
// Atributos para enlace
|
||||
$customUrl = $data['behavior']['custom_url'] ?? '';
|
||||
$attributes[] = sprintf('href="%s"', esc_url($customUrl ?: '#'));
|
||||
|
||||
if (($data['behavior']['open_in_new_tab'] ?? false) === true) {
|
||||
$attributes[] = 'target="_blank"';
|
||||
$attributes[] = 'rel="noopener noreferrer"';
|
||||
}
|
||||
}
|
||||
|
||||
// Atributo ARIA para accesibilidad
|
||||
$ariaLabel = $data['content']['aria_label'] ?? 'Abrir formulario de contacto';
|
||||
if (!empty($ariaLabel)) {
|
||||
$attributes[] = sprintf('aria-label="%s"', esc_attr($ariaLabel));
|
||||
}
|
||||
|
||||
return !empty($attributes) ? ' ' . implode(' ', $attributes) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir contenido del botón
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del contenido
|
||||
*/
|
||||
private function buildContent(array $data): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Ícono (si está habilitado)
|
||||
if ($this->shouldShowIcon($data)) {
|
||||
$html .= $this->buildIcon($data);
|
||||
}
|
||||
|
||||
// Texto del botón
|
||||
$buttonText = $data['content']['button_text'] ?? "Let's Talk";
|
||||
$html .= esc_html($buttonText);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si debe mostrar el ícono
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldShowIcon(array $data): bool
|
||||
{
|
||||
return ($data['content']['show_icon'] ?? true) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir ícono del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del ícono
|
||||
*/
|
||||
private function buildIcon(array $data): string
|
||||
{
|
||||
$iconClass = $data['content']['icon_class'] ?? 'bi-lightning-charge-fill';
|
||||
|
||||
// Asegurar prefijo 'bi-'
|
||||
if (strpos($iconClass, 'bi-') !== 0) {
|
||||
$iconClass = 'bi-' . $iconClass;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<i class="bi %s"></i>',
|
||||
esc_attr($iconClass)
|
||||
);
|
||||
}
|
||||
}
|
||||
187
Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
Normal file
187
Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del componente CTA Post
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Gradiente configurable
|
||||
* - Layout responsive (2 columnas en desktop)
|
||||
* - Boton CTA con icono
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* @package ROITheme\Public\CtaPost\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaPostRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'cta-post';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
$gradientStart = $colors['gradient_start'] ?? '#FF8600';
|
||||
$gradientEnd = $colors['gradient_end'] ?? '#FFB800';
|
||||
$gradientAngle = $effects['gradient_angle'] ?? '135deg';
|
||||
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
|
||||
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
|
||||
$buttonHoverBgColor = $colors['button_hover_bg_color'] ?? '#e67a00';
|
||||
|
||||
// Container - gradient background
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-post-container', [
|
||||
'background' => "linear-gradient({$gradientAngle}, {$gradientStart} 0%, {$gradientEnd} 100%)",
|
||||
]);
|
||||
|
||||
// Button styles (matching template .cta-button) - Using !important to override Bootstrap btn-light
|
||||
$cssRules[] = ".cta-post-container .cta-button {
|
||||
background-color: {$buttonBgColor} !important;
|
||||
color: {$buttonTextColor} !important;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 2rem;
|
||||
border: none !important;
|
||||
border-radius: 8px;
|
||||
transition: 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}";
|
||||
|
||||
// Button hover state
|
||||
$cssRules[] = ".cta-post-container .cta-button:hover {
|
||||
background-color: {$buttonHoverBgColor};
|
||||
color: {$buttonTextColor};
|
||||
}";
|
||||
|
||||
// Responsive: button full width on mobile
|
||||
$cssRules[] = "@media (max-width: 768px) {
|
||||
.cta-post-container .cta-button {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}";
|
||||
|
||||
// Responsive visibility
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
||||
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 991.98px) {
|
||||
.cta-post-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.cta-post-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$title = $content['title'] ?? 'Accede a 200,000+ Análisis de Precios Unitarios';
|
||||
$description = $content['description'] ?? '';
|
||||
$buttonText = $content['button_text'] ?? 'Ver Catálogo Completo';
|
||||
$buttonUrl = $content['button_url'] ?? '#';
|
||||
$buttonIcon = $content['button_icon'] ?? 'bi-arrow-right';
|
||||
|
||||
$html = '<div class="my-5 p-4 rounded cta-post-container">';
|
||||
$html .= ' <div class="row align-items-center">';
|
||||
|
||||
// Left column - Content
|
||||
$html .= ' <div class="col-md-8">';
|
||||
$html .= sprintf(
|
||||
' <h3 class="h4 fw-bold text-white mb-2">%s</h3>',
|
||||
esc_html($title)
|
||||
);
|
||||
if (!empty($description)) {
|
||||
$html .= sprintf(
|
||||
' <p class="text-white mb-md-0">%s</p>',
|
||||
esc_html($description)
|
||||
);
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
// Right column - Button
|
||||
$html .= ' <div class="col-md-4 text-md-end mt-3 mt-md-0">';
|
||||
$html .= sprintf(
|
||||
' <a href="%s" class="btn btn-light btn-lg cta-button">%s',
|
||||
esc_url($buttonUrl),
|
||||
esc_html($buttonText)
|
||||
);
|
||||
if (!empty($buttonIcon)) {
|
||||
$html .= sprintf(' <i class="bi %s ms-2"></i>', esc_attr($buttonIcon));
|
||||
}
|
||||
$html .= '</a>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
202
Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
Normal file
202
Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* FeaturedImageRenderer - Renderiza la imagen destacada del post
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS de la imagen destacada
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Integracion con get_the_post_thumbnail()
|
||||
* - Estilos configurables desde BD
|
||||
* - Efecto hover opcional
|
||||
* - Soporte responsive
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar featured image)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\FeaturedImage\Infrastructure\Ui
|
||||
*/
|
||||
final class FeaturedImageRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->hasPostThumbnail()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'featured-image';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function hasPostThumbnail(): bool
|
||||
{
|
||||
return is_singular() && has_post_thumbnail();
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$marginTop = $spacing['margin_top'] ?? '1rem';
|
||||
$marginBottom = $spacing['margin_bottom'] ?? '2rem';
|
||||
|
||||
$borderRadius = $effects['border_radius'] ?? '12px';
|
||||
$boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)';
|
||||
$hoverEffect = $effects['hover_effect'] ?? true;
|
||||
$hoverScale = $effects['hover_scale'] ?? '1.02';
|
||||
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
||||
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Container styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.featured-image-container', [
|
||||
'border-radius' => $borderRadius,
|
||||
'overflow' => 'hidden',
|
||||
'box-shadow' => $boxShadow,
|
||||
'margin-top' => $marginTop,
|
||||
'margin-bottom' => $marginBottom,
|
||||
'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
// Image styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [
|
||||
'width' => '100%',
|
||||
'height' => 'auto',
|
||||
'display' => 'block',
|
||||
'transition' => "transform {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
// Hover effect
|
||||
if ($hoverEffect) {
|
||||
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover', [
|
||||
'box-shadow' => '0 12px 32px rgba(0, 0, 0, 0.15)',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover img', [
|
||||
'transform' => "scale({$hoverScale})",
|
||||
]);
|
||||
}
|
||||
|
||||
// Link styles (remove default link styling)
|
||||
$cssRules[] = $this->cssGenerator->generate('.featured-image-container a', [
|
||||
'display' => 'block',
|
||||
'line-height' => '0',
|
||||
]);
|
||||
|
||||
// Responsive visibility
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 767.98px) {
|
||||
.featured-image-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 768px) {
|
||||
.featured-image-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$imageSize = $content['image_size'] ?? 'roi-featured-large';
|
||||
$lazyLoading = $content['lazy_loading'] ?? true;
|
||||
$linkToMedia = $content['link_to_media'] ?? false;
|
||||
|
||||
$imgAttr = [
|
||||
'class' => 'img-fluid featured-image',
|
||||
'alt' => get_the_title(),
|
||||
];
|
||||
|
||||
if ($lazyLoading) {
|
||||
$imgAttr['loading'] = 'lazy';
|
||||
}
|
||||
|
||||
$thumbnail = get_the_post_thumbnail(null, $imageSize, $imgAttr);
|
||||
|
||||
if (empty($thumbnail)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<div class="featured-image-container">';
|
||||
|
||||
if ($linkToMedia) {
|
||||
$fullImageUrl = get_the_post_thumbnail_url(null, 'full');
|
||||
$html .= sprintf(
|
||||
'<a href="%s" target="_blank" rel="noopener" aria-label="%s">',
|
||||
esc_url($fullImageUrl),
|
||||
esc_attr__('Ver imagen en tamano completo', 'roi-theme')
|
||||
);
|
||||
}
|
||||
|
||||
$html .= $thumbnail;
|
||||
|
||||
if ($linkToMedia) {
|
||||
$html .= '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
|
||||
/**
|
||||
* NewsletterAjaxHandler - Procesa suscripciones al newsletter
|
||||
*
|
||||
* RESPONSABILIDAD: Recibir email y enviarlo al webhook configurado
|
||||
*
|
||||
* SEGURIDAD:
|
||||
* - Verifica nonce
|
||||
* - Webhook URL nunca se expone al cliente
|
||||
* - Rate limiting basico
|
||||
* - Sanitizacion de inputs
|
||||
*
|
||||
* @package ROITheme\Public\Footer\Infrastructure\Api\WordPress
|
||||
*/
|
||||
final class NewsletterAjaxHandler
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_newsletter_nonce';
|
||||
private const COMPONENT_NAME = 'footer';
|
||||
|
||||
public function __construct(
|
||||
private ComponentSettingsRepositoryInterface $settingsRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registrar hooks AJAX
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
|
||||
add_action('wp_ajax_nopriv_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar suscripcion
|
||||
*/
|
||||
public function handleSubscribe(): void
|
||||
{
|
||||
// 1. Verificar nonce
|
||||
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
|
||||
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Rate limiting (1 suscripcion por IP cada 60 segundos)
|
||||
if (!$this->checkRateLimit()) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Por favor espera un momento antes de intentar de nuevo.', 'roi-theme')
|
||||
], 429);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Validar email
|
||||
$email = sanitize_email($_POST['email'] ?? '');
|
||||
if (empty($email) || !is_email($email)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Por favor ingresa un email valido.', 'roi-theme')
|
||||
], 422);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Obtener configuracion del componente
|
||||
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
|
||||
|
||||
if (empty($settings)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
$newsletter = $settings['newsletter'] ?? [];
|
||||
$webhookUrl = $newsletter['newsletter_webhook_url'] ?? '';
|
||||
$successMsg = $newsletter['newsletter_success_message'] ?? __('Gracias por suscribirte!', 'roi-theme');
|
||||
$errorMsg = $newsletter['newsletter_error_message'] ?? __('Error al suscribirse. Intenta de nuevo.', 'roi-theme');
|
||||
|
||||
if (empty($webhookUrl)) {
|
||||
// Si no hay webhook, simular exito para UX pero loguear warning
|
||||
error_log('ROI Theme Newsletter: No webhook URL configured');
|
||||
wp_send_json_success([
|
||||
'message' => $successMsg
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Preparar payload
|
||||
$payload = [
|
||||
'email' => $email,
|
||||
'source' => 'newsletter-footer',
|
||||
'timestamp' => current_time('c'),
|
||||
'siteName' => get_bloginfo('name'),
|
||||
'siteUrl' => home_url(),
|
||||
];
|
||||
|
||||
// 6. Enviar a webhook
|
||||
$result = $this->sendToWebhook($webhookUrl, $payload);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success([
|
||||
'message' => $successMsg
|
||||
]);
|
||||
} else {
|
||||
error_log('ROI Theme Newsletter webhook error: ' . $result['error']);
|
||||
wp_send_json_error([
|
||||
'message' => $errorMsg
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar datos al webhook
|
||||
*/
|
||||
private function sendToWebhook(string $url, array $payload): array
|
||||
{
|
||||
$response = wp_remote_post($url, [
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode($payload),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
];
|
||||
}
|
||||
|
||||
$statusCode = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
return ['success' => true, 'error' => ''];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting por IP
|
||||
*/
|
||||
private function checkRateLimit(): bool
|
||||
{
|
||||
$ip = $this->getClientIP();
|
||||
$transientKey = 'roi_newsletter_' . md5($ip);
|
||||
$lastSubmit = get_transient($transientKey);
|
||||
|
||||
if ($lastSubmit !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_transient($transientKey, time(), 60);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener IP del cliente
|
||||
*/
|
||||
private function getClientIP(): string
|
||||
{
|
||||
$ip = '';
|
||||
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
423
Public/Footer/Infrastructure/Ui/FooterRenderer.php
Normal file
423
Public/Footer/Infrastructure/Ui/FooterRenderer.php
Normal file
@@ -0,0 +1,423 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* FooterRenderer - Renderiza el footer del sitio
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del footer con menus WP y newsletter
|
||||
*
|
||||
* SEGURIDAD:
|
||||
* - Webhook URL nunca se expone al cliente
|
||||
* - Escaping de todos los outputs
|
||||
* - Nonce para formulario newsletter
|
||||
*
|
||||
* @package ROITheme\Public\Footer\Infrastructure\Ui
|
||||
*/
|
||||
final class FooterRenderer implements RendererInterface
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_newsletter_nonce';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'footer';
|
||||
}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
// Validar visibilidad
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
if (!($visibility['is_enabled'] ?? true)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar visibilidad responsive
|
||||
$showDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showMobile = $visibility['show_on_mobile'] ?? true;
|
||||
|
||||
if (!$showDesktop && !$showMobile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generar CSS
|
||||
$css = $this->generateCSS($data, $showDesktop, $showMobile);
|
||||
|
||||
// Generar HTML
|
||||
$html = $this->generateHTML($data);
|
||||
|
||||
// Generar JavaScript
|
||||
$js = $this->generateJS($data);
|
||||
|
||||
return $css . $html . $js;
|
||||
}
|
||||
|
||||
private function generateCSS(array $data, bool $showDesktop, bool $showMobile): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
|
||||
// Valores con fallbacks
|
||||
$bgColor = $colors['bg_color'] ?? '#212529';
|
||||
$textColor = $colors['text_color'] ?? '#ffffff';
|
||||
$titleColor = $colors['title_color'] ?? '#ffffff';
|
||||
$linkColor = $colors['link_color'] ?? '#ffffff';
|
||||
$linkHoverColor = $colors['link_hover_color'] ?? '#FF8600';
|
||||
$inputBgColor = $colors['input_bg_color'] ?? '#ffffff';
|
||||
$inputTextColor = $colors['input_text_color'] ?? '#212529';
|
||||
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
|
||||
$buttonBgColor = $colors['button_bg_color'] ?? '#0d6efd';
|
||||
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
|
||||
$buttonHoverBg = $colors['button_hover_bg'] ?? '#0b5ed7';
|
||||
$borderTopColor = $colors['border_top_color'] ?? 'rgba(255, 255, 255, 0.2)';
|
||||
|
||||
$paddingY = $spacing['padding_y'] ?? '3rem';
|
||||
$marginTop = $spacing['margin_top'] ?? '0';
|
||||
$widgetTitleMb = $spacing['widget_title_margin_bottom'] ?? '1rem';
|
||||
$linkMb = $spacing['link_margin_bottom'] ?? '0.5rem';
|
||||
$copyrightPy = $spacing['copyright_padding_y'] ?? '1.5rem';
|
||||
|
||||
$inputRadius = $effects['input_border_radius'] ?? '6px';
|
||||
$buttonRadius = $effects['button_border_radius'] ?? '6px';
|
||||
$transition = $effects['transition_duration'] ?? '0.3s';
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Footer principal
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer', [
|
||||
'background-color' => $bgColor,
|
||||
'color' => $textColor,
|
||||
'padding-top' => $paddingY,
|
||||
'padding-bottom' => $paddingY,
|
||||
'margin-top' => $marginTop,
|
||||
]);
|
||||
|
||||
// Grid custom para 3+3+3+4 = 13 columnas
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-grid', [
|
||||
'display' => 'grid',
|
||||
'grid-template-columns' => 'repeat(4, 1fr)',
|
||||
'gap' => '2rem',
|
||||
]);
|
||||
|
||||
// En desktop: distribucion 3+3+3+4
|
||||
$cssRules[] = "@media (min-width: 768px) {
|
||||
.roi-footer .footer-grid {
|
||||
grid-template-columns: 23% 23% 23% 31%;
|
||||
}
|
||||
}";
|
||||
|
||||
// En mobile: 2 columnas
|
||||
$cssRules[] = "@media (max-width: 767px) {
|
||||
.roi-footer .footer-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.roi-footer .footer-widget-newsletter {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}";
|
||||
|
||||
// Titulos de widgets
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .widget-title', [
|
||||
'color' => $titleColor,
|
||||
'font-size' => '1.25rem',
|
||||
'font-weight' => '500',
|
||||
'margin-bottom' => $widgetTitleMb,
|
||||
]);
|
||||
|
||||
// Links de navegacion
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav', [
|
||||
'list-style' => 'none',
|
||||
'padding' => '0',
|
||||
'margin' => '0',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav li', [
|
||||
'margin-bottom' => $linkMb,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a', [
|
||||
'color' => $linkColor,
|
||||
'text-decoration' => 'none',
|
||||
'transition' => "color {$transition}",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a:hover', [
|
||||
'color' => $linkHoverColor,
|
||||
]);
|
||||
|
||||
// Newsletter description
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-description', [
|
||||
'color' => $textColor,
|
||||
'margin-bottom' => '1rem',
|
||||
'opacity' => '0.9',
|
||||
]);
|
||||
|
||||
// Input newsletter
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input', [
|
||||
'width' => '100%',
|
||||
'padding' => '0.75rem 1rem',
|
||||
'background-color' => $inputBgColor,
|
||||
'color' => $inputTextColor,
|
||||
'border' => "1px solid {$inputBorderColor}",
|
||||
'border-radius' => $inputRadius,
|
||||
'margin-bottom' => '0.75rem',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input:focus', [
|
||||
'outline' => 'none',
|
||||
'border-color' => $buttonBgColor,
|
||||
'box-shadow' => "0 0 0 0.2rem rgba(13, 110, 253, 0.25)",
|
||||
]);
|
||||
|
||||
// Boton newsletter
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn', [
|
||||
'width' => '100%',
|
||||
'padding' => '0.75rem 1.5rem',
|
||||
'background-color' => $buttonBgColor,
|
||||
'color' => $buttonTextColor,
|
||||
'border' => 'none',
|
||||
'border-radius' => $buttonRadius,
|
||||
'font-weight' => '500',
|
||||
'cursor' => 'pointer',
|
||||
'transition' => "background-color {$transition}",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:hover', [
|
||||
'background-color' => $buttonHoverBg,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:disabled', [
|
||||
'opacity' => '0.7',
|
||||
'cursor' => 'not-allowed',
|
||||
]);
|
||||
|
||||
// Mensaje newsletter
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message', [
|
||||
'margin-top' => '0.75rem',
|
||||
'padding' => '0.5rem',
|
||||
'border-radius' => '4px',
|
||||
'font-size' => '0.875rem',
|
||||
'display' => 'none',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.success', [
|
||||
'background-color' => '#d1e7dd',
|
||||
'color' => '#0f5132',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.error', [
|
||||
'background-color' => '#f8d7da',
|
||||
'color' => '#842029',
|
||||
]);
|
||||
|
||||
// Footer bottom (copyright)
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-bottom', [
|
||||
'border-top' => "1px solid {$borderTopColor}",
|
||||
'padding-top' => $copyrightPy,
|
||||
'margin-top' => '2rem',
|
||||
'text-align' => 'center',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .copyright-text', [
|
||||
'margin' => '0',
|
||||
'opacity' => '0.9',
|
||||
]);
|
||||
|
||||
// Responsive visibility
|
||||
if (!$showDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) { .roi-footer { display: none !important; } }";
|
||||
}
|
||||
if (!$showMobile) {
|
||||
$cssRules[] = "@media (max-width: 991px) { .roi-footer { display: none !important; } }";
|
||||
}
|
||||
|
||||
return '<style>' . implode("\n", $cssRules) . '</style>';
|
||||
}
|
||||
|
||||
private function generateHTML(array $data): string
|
||||
{
|
||||
$widget1 = $data['widget_1'] ?? [];
|
||||
$widget2 = $data['widget_2'] ?? [];
|
||||
$widget3 = $data['widget_3'] ?? [];
|
||||
$newsletter = $data['newsletter'] ?? [];
|
||||
$footerBottom = $data['footer_bottom'] ?? [];
|
||||
|
||||
$widget1Visible = $this->toBool($widget1['widget_1_visible'] ?? true);
|
||||
$widget2Visible = $this->toBool($widget2['widget_2_visible'] ?? true);
|
||||
$widget3Visible = $this->toBool($widget3['widget_3_visible'] ?? true);
|
||||
$newsletterVisible = $this->toBool($newsletter['newsletter_visible'] ?? true);
|
||||
|
||||
$widget1Title = esc_html($widget1['widget_1_title'] ?? 'Recursos');
|
||||
$widget2Title = esc_html($widget2['widget_2_title'] ?? 'Soporte');
|
||||
$widget3Title = esc_html($widget3['widget_3_title'] ?? 'Empresa');
|
||||
|
||||
$newsletterTitle = esc_html($newsletter['newsletter_title'] ?? 'Suscribete al Newsletter');
|
||||
$newsletterDesc = esc_html($newsletter['newsletter_description'] ?? 'Recibe las ultimas actualizaciones.');
|
||||
$newsletterPlaceholder = esc_attr($newsletter['newsletter_placeholder'] ?? 'Email');
|
||||
$newsletterBtnText = esc_html($newsletter['newsletter_button_text'] ?? 'Suscribirse');
|
||||
|
||||
$copyrightText = esc_html($footerBottom['copyright_text'] ?? date('Y') . ' Todos los derechos reservados.');
|
||||
|
||||
$nonce = wp_create_nonce(self::NONCE_ACTION);
|
||||
$ajaxUrl = admin_url('admin-ajax.php');
|
||||
|
||||
$html = '<footer class="roi-footer">';
|
||||
$html .= '<div class="container">';
|
||||
$html .= '<div class="footer-grid">';
|
||||
|
||||
// Widget 1
|
||||
if ($widget1Visible) {
|
||||
$html .= '<div class="footer-widget footer-widget-menu">';
|
||||
$html .= '<h5 class="widget-title">' . $widget1Title . '</h5>';
|
||||
$html .= $this->renderMenu('footer_menu_1');
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Widget 2
|
||||
if ($widget2Visible) {
|
||||
$html .= '<div class="footer-widget footer-widget-menu">';
|
||||
$html .= '<h5 class="widget-title">' . $widget2Title . '</h5>';
|
||||
$html .= $this->renderMenu('footer_menu_2');
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Widget 3
|
||||
if ($widget3Visible) {
|
||||
$html .= '<div class="footer-widget footer-widget-menu">';
|
||||
$html .= '<h5 class="widget-title">' . $widget3Title . '</h5>';
|
||||
$html .= $this->renderMenu('footer_menu_3');
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Widget Newsletter
|
||||
if ($newsletterVisible) {
|
||||
$html .= '<div class="footer-widget footer-widget-newsletter">';
|
||||
$html .= '<h5 class="widget-title">' . $newsletterTitle . '</h5>';
|
||||
$html .= '<p class="newsletter-description">' . $newsletterDesc . '</p>';
|
||||
$html .= '<form id="roi-newsletter-form" class="newsletter-form">';
|
||||
$html .= '<input type="hidden" name="action" value="roi_newsletter_subscribe">';
|
||||
$html .= '<input type="hidden" name="nonce" value="' . esc_attr($nonce) . '">';
|
||||
$html .= '<input type="email" name="email" class="newsletter-input" placeholder="' . $newsletterPlaceholder . '" required>';
|
||||
$html .= '<button type="submit" class="newsletter-btn">' . $newsletterBtnText . '</button>';
|
||||
$html .= '<div class="newsletter-message"></div>';
|
||||
$html .= '</form>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>'; // .footer-grid
|
||||
|
||||
// Footer bottom
|
||||
$html .= '<div class="footer-bottom">';
|
||||
$html .= '<p class="copyright-text">© ' . $copyrightText . '</p>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>'; // .container
|
||||
$html .= '</footer>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function renderMenu(string $menuLocation): string
|
||||
{
|
||||
if (!has_nav_menu($menuLocation)) {
|
||||
return '<p class="text-muted">Menu no asignado</p>';
|
||||
}
|
||||
|
||||
return wp_nav_menu([
|
||||
'theme_location' => $menuLocation,
|
||||
'container' => false,
|
||||
'menu_class' => 'footer-nav',
|
||||
'fallback_cb' => false,
|
||||
'echo' => false,
|
||||
'depth' => 1,
|
||||
]) ?: '';
|
||||
}
|
||||
|
||||
private function generateJS(array $data): string
|
||||
{
|
||||
$newsletter = $data['newsletter'] ?? [];
|
||||
$successMsg = esc_js($newsletter['newsletter_success_message'] ?? 'Gracias por suscribirte!');
|
||||
$errorMsg = esc_js($newsletter['newsletter_error_message'] ?? 'Error al suscribirse. Intenta de nuevo.');
|
||||
|
||||
$ajaxUrl = admin_url('admin-ajax.php');
|
||||
|
||||
$js = <<<JS
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('roi-newsletter-form');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = form.querySelector('.newsletter-btn');
|
||||
const msgDiv = form.querySelector('.newsletter-message');
|
||||
const emailInput = form.querySelector('input[name="email"]');
|
||||
const originalText = btn.textContent;
|
||||
|
||||
// Reset message
|
||||
msgDiv.style.display = 'none';
|
||||
msgDiv.className = 'newsletter-message';
|
||||
|
||||
// Validate email
|
||||
if (!emailInput.value || !emailInput.validity.valid) {
|
||||
msgDiv.textContent = 'Por favor ingresa un email valido';
|
||||
msgDiv.classList.add('error');
|
||||
msgDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Enviando...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
|
||||
const response = await fetch('{$ajaxUrl}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
msgDiv.textContent = '{$successMsg}';
|
||||
msgDiv.classList.add('success');
|
||||
emailInput.value = '';
|
||||
} else {
|
||||
msgDiv.textContent = result.data?.message || '{$errorMsg}';
|
||||
msgDiv.classList.add('error');
|
||||
}
|
||||
} catch (error) {
|
||||
msgDiv.textContent = '{$errorMsg}';
|
||||
msgDiv.classList.add('error');
|
||||
}
|
||||
|
||||
msgDiv.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
JS;
|
||||
|
||||
return $js;
|
||||
}
|
||||
|
||||
private function toBool($value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
278
Public/Hero/Infrastructure/Ui/HeroRenderer.php
Normal file
278
Public/Hero/Infrastructure/Ui/HeroRenderer.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Class HeroRenderer
|
||||
*
|
||||
* Renderizador del componente Hero para el frontend.
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar HTML del hero section con título del post/página
|
||||
* - Mostrar badges de categorías (dinámicos desde WordPress)
|
||||
* - Delegar generación de CSS a CSSGeneratorInterface
|
||||
* - Validar visibilidad (is_enabled, show_on_pages, responsive)
|
||||
* - Manejar visibilidad responsive con clases Bootstrap
|
||||
*
|
||||
* NO responsable de:
|
||||
* - Generar string CSS (delega a CSSGeneratorService)
|
||||
* - Persistir datos
|
||||
* - Lógica de negocio
|
||||
*
|
||||
* @package ROITheme\Public\Hero\Infrastructure\Ui
|
||||
*/
|
||||
final class HeroRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'hero';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'home':
|
||||
return is_front_page() || is_home();
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$gradientStart = $colors['gradient_start'] ?? '#1e3a5f';
|
||||
$gradientEnd = $colors['gradient_end'] ?? '#2c5282';
|
||||
$titleColor = $colors['title_color'] ?? '#FFFFFF';
|
||||
$badgeBgColor = $colors['badge_bg_color'] ?? '#FFFFFF';
|
||||
$badgeTextColor = $colors['badge_text_color'] ?? '#FFFFFF';
|
||||
$badgeIconColor = $colors['badge_icon_color'] ?? '#FFB800';
|
||||
$badgeHoverBg = $colors['badge_hover_bg'] ?? '#FF8600';
|
||||
|
||||
$titleFontSize = $typography['title_font_size'] ?? '2.5rem';
|
||||
$titleFontSizeMobile = $typography['title_font_size_mobile'] ?? '1.75rem';
|
||||
$titleFontWeight = $typography['title_font_weight'] ?? '700';
|
||||
$titleLineHeight = $typography['title_line_height'] ?? '1.4';
|
||||
$badgeFontSize = $typography['badge_font_size'] ?? '0.813rem';
|
||||
|
||||
$paddingVertical = $spacing['padding_vertical'] ?? '3rem';
|
||||
$marginBottom = $spacing['margin_bottom'] ?? '1.5rem';
|
||||
$badgePadding = $spacing['badge_padding'] ?? '0.375rem 0.875rem';
|
||||
$badgeBorderRadius = $spacing['badge_border_radius'] ?? '20px';
|
||||
|
||||
$boxShadow = $effects['box_shadow'] ?? '0 4px 16px rgba(30, 58, 95, 0.25)';
|
||||
$titleTextShadow = $effects['title_text_shadow'] ?? '1px 1px 2px rgba(0, 0, 0, 0.2)';
|
||||
$badgeBackdropBlur = $effects['badge_backdrop_blur'] ?? '10px';
|
||||
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section', [
|
||||
'background' => "linear-gradient(135deg, {$gradientStart} 0%, {$gradientEnd} 100%)",
|
||||
'box-shadow' => $boxShadow,
|
||||
'padding' => "{$paddingVertical} 0",
|
||||
'margin-bottom' => $marginBottom,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
|
||||
'color' => "{$titleColor} !important",
|
||||
'font-weight' => $titleFontWeight,
|
||||
'font-size' => $titleFontSize,
|
||||
'line-height' => $titleLineHeight,
|
||||
'text-shadow' => $titleTextShadow,
|
||||
'margin-bottom' => '0',
|
||||
'text-align' => 'center',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge', [
|
||||
'background' => $this->hexToRgba($badgeBgColor, 0.15),
|
||||
'backdrop-filter' => "blur({$badgeBackdropBlur})",
|
||||
'-webkit-backdrop-filter' => "blur({$badgeBackdropBlur})",
|
||||
'border' => '1px solid ' . $this->hexToRgba($badgeBgColor, 0.2),
|
||||
'color' => $this->hexToRgba($badgeTextColor, 0.95),
|
||||
'padding' => $badgePadding,
|
||||
'border-radius' => $badgeBorderRadius,
|
||||
'font-size' => $badgeFontSize,
|
||||
'font-weight' => '500',
|
||||
'text-decoration' => 'none',
|
||||
'display' => 'inline-block',
|
||||
'transition' => 'all 0.3s ease',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge:hover', [
|
||||
'background' => $this->hexToRgba($badgeHoverBg, 0.2),
|
||||
'border-color' => $this->hexToRgba($badgeHoverBg, 0.4),
|
||||
'color' => '#ffffff',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge i', [
|
||||
'color' => $badgeIconColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = "@media (max-width: 767.98px) {
|
||||
.hero-section__title {
|
||||
font-size: {$titleFontSizeMobile};
|
||||
}
|
||||
}";
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 767.98px) {
|
||||
.hero-section { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 768px) {
|
||||
.hero-section { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$showCategories = $content['show_categories'] ?? true;
|
||||
$showBadgeIcon = $content['show_badge_icon'] ?? true;
|
||||
$badgeIconClass = $content['badge_icon_class'] ?? 'bi-folder-fill';
|
||||
$titleTag = $content['title_tag'] ?? 'h1';
|
||||
|
||||
$allowedTags = ['h1', 'h2', 'div'];
|
||||
if (!in_array($titleTag, $allowedTags, true)) {
|
||||
$titleTag = 'h1';
|
||||
}
|
||||
|
||||
$title = is_singular() ? get_the_title() : '';
|
||||
if (empty($title)) {
|
||||
$title = wp_title('', false);
|
||||
}
|
||||
|
||||
$html = '<div class="container-fluid hero-section">';
|
||||
$html .= '<div class="container">';
|
||||
|
||||
if ($showCategories && is_single()) {
|
||||
$categories = get_the_category();
|
||||
if (!empty($categories)) {
|
||||
$html .= '<div class="mb-3 d-flex justify-content-center">';
|
||||
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$categoryLink = esc_url(get_category_link($category->term_id));
|
||||
$categoryName = esc_html($category->name);
|
||||
$iconHtml = $showBadgeIcon
|
||||
? '<i class="bi ' . esc_attr($badgeIconClass) . ' me-1"></i>'
|
||||
: '';
|
||||
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="hero-section__badge">%s%s</a>',
|
||||
$categoryLink,
|
||||
$iconHtml,
|
||||
$categoryName
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$html .= sprintf(
|
||||
'<%s class="hero-section__title">%s</%s>',
|
||||
$titleTag,
|
||||
esc_html($title),
|
||||
$titleTag
|
||||
);
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function hexToRgba(string $hex, float $alpha): string
|
||||
{
|
||||
$hex = ltrim($hex, '#');
|
||||
|
||||
if (strlen($hex) === 3) {
|
||||
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
||||
}
|
||||
|
||||
$r = hexdec(substr($hex, 0, 2));
|
||||
$g = hexdec(substr($hex, 2, 2));
|
||||
$b = hexdec(substr($hex, 4, 2));
|
||||
|
||||
return "rgba({$r}, {$g}, {$b}, {$alpha})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera clases Bootstrap de visibilidad responsive
|
||||
*
|
||||
* @param bool $desktop Si debe mostrarse en desktop
|
||||
* @param bool $mobile Si debe mostrarse en mobile
|
||||
* @return string|null Clases Bootstrap o null si visible en todos
|
||||
*/
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if ($desktop && $mobile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-md-block';
|
||||
}
|
||||
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-block d-md-none';
|
||||
}
|
||||
|
||||
return 'd-none';
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\HeroSection\Infrastructure\Presentation\Public;
|
||||
namespace ROITheme\Public\herosection\infrastructure\ui;
|
||||
|
||||
use ROITheme\Component\Domain\Component;
|
||||
use ROITheme\Component\Domain\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
|
||||
/**
|
||||
* HeroSectionRenderer - Renderiza la sección hero con badges y título
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML de la sección hero
|
||||
*
|
||||
* CARACTERÍSTICAS:
|
||||
* - Badges de categorías con múltiples fuentes de datos
|
||||
* - Título H1 con gradiente opcional
|
||||
* - Múltiples tipos de fondo (color, gradiente, imagen)
|
||||
* - Lógica condicional de visibilidad por tipo de página
|
||||
*
|
||||
* @package ROITheme\Public\HeroSection\Presentation
|
||||
*/
|
||||
final class HeroSectionRenderer implements RendererInterface
|
||||
{
|
||||
public function render(Component $component): string
|
||||
364
Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
Normal file
364
Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use Walker_Nav_Menu;
|
||||
|
||||
/**
|
||||
* NavbarRenderer - Renderiza el menú de navegación principal
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML del menú de navegación WordPress
|
||||
*
|
||||
* CARACTERÍSTICAS:
|
||||
* - Integración con wp_nav_menu()
|
||||
* - Walker personalizado para Bootstrap 5
|
||||
* - Soporte para submenús desplegables
|
||||
* - Responsive con navbar-toggler
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar navbar)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\Navbar\Infrastructure\Ui
|
||||
*/
|
||||
final class NavbarRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildMenu($data);
|
||||
|
||||
return sprintf(
|
||||
"<style>%s</style>\n%s",
|
||||
$css,
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return isset($data['visibility']['is_enabled']) &&
|
||||
$data['visibility']['is_enabled'] === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnMobile(array $data): bool
|
||||
{
|
||||
return isset($data['visibility']['show_on_mobile']) &&
|
||||
$data['visibility']['show_on_mobile'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar CSS usando CSSGeneratorService
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string CSS generado
|
||||
*/
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$css = '';
|
||||
|
||||
// Obtener valores de configuración
|
||||
$stickyEnabled = $data['visibility']['sticky_enabled'] ?? true;
|
||||
$paddingVertical = $data['layout']['padding_vertical'] ?? '0.75rem 0';
|
||||
$zIndex = $data['layout']['z_index'] ?? '1030';
|
||||
|
||||
$bgColor = $data['colors']['background_color'] ?? '#1e3a5f';
|
||||
$boxShadow = $data['colors']['box_shadow'] ?? '0 4px 12px rgba(30, 58, 95, 0.15)';
|
||||
|
||||
$linkTextColor = $data['links']['text_color'] ?? '#FFFFFF';
|
||||
$linkHoverColor = $data['links']['hover_color'] ?? '#FF8600';
|
||||
$linkActiveColor = $data['links']['active_color'] ?? '#FF8600';
|
||||
$linkFontSize = $data['links']['font_size'] ?? '0.9rem';
|
||||
$linkFontWeight = $data['links']['font_weight'] ?? '500';
|
||||
$linkPadding = $data['links']['padding'] ?? '0.5rem 0.65rem';
|
||||
$linkBorderRadius = $data['links']['border_radius'] ?? '4px';
|
||||
$showUnderlineEffect = $data['links']['show_underline_effect'] ?? true;
|
||||
$underlineColor = $data['links']['underline_color'] ?? '#FF8600';
|
||||
|
||||
// Estilos del navbar container
|
||||
$navbarStyles = [
|
||||
'background-color' => $bgColor . ' !important',
|
||||
'box-shadow' => $boxShadow,
|
||||
'padding' => $paddingVertical,
|
||||
'transition' => 'all 0.3s ease',
|
||||
];
|
||||
|
||||
if ($stickyEnabled) {
|
||||
$navbarStyles['position'] = 'sticky';
|
||||
$navbarStyles['top'] = '0';
|
||||
$navbarStyles['z-index'] = $zIndex;
|
||||
}
|
||||
|
||||
$css .= $this->cssGenerator->generate('.navbar', $navbarStyles);
|
||||
|
||||
// Efecto scrolled del navbar
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar.scrolled', [
|
||||
'box-shadow' => '0 6px 20px rgba(30, 58, 95, 0.25)',
|
||||
]);
|
||||
|
||||
// Estilos de los enlaces del navbar
|
||||
$navLinkStyles = [
|
||||
'color' => 'rgba(255, 255, 255, 0.9) !important',
|
||||
'font-weight' => $linkFontWeight,
|
||||
'position' => 'relative',
|
||||
'padding' => $linkPadding . ' !important',
|
||||
'transition' => 'all 0.3s ease',
|
||||
'font-size' => $linkFontSize,
|
||||
'white-space' => 'nowrap',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link', $navLinkStyles);
|
||||
|
||||
// Efecto de subrayado (::after pseudo-element)
|
||||
if ($showUnderlineEffect) {
|
||||
$css .= "\n.navbar .nav-link::after {";
|
||||
$css .= "\n content: '';";
|
||||
$css .= "\n position: absolute;";
|
||||
$css .= "\n bottom: 0;";
|
||||
$css .= "\n left: 50%;";
|
||||
$css .= "\n transform: translateX(-50%) scaleX(0);";
|
||||
$css .= "\n width: 80%;";
|
||||
$css .= "\n height: 2px;";
|
||||
$css .= "\n background: {$underlineColor};";
|
||||
$css .= "\n transition: transform 0.3s ease;";
|
||||
$css .= "\n}";
|
||||
|
||||
$css .= "\n.navbar .nav-link:hover::after {";
|
||||
$css .= "\n transform: translateX(-50%) scaleX(1);";
|
||||
$css .= "\n}";
|
||||
}
|
||||
|
||||
// Estilos hover y focus de los enlaces
|
||||
$navLinkHoverStyles = [
|
||||
'color' => $linkHoverColor . ' !important',
|
||||
'background-color' => 'rgba(255, 133, 0, 0.1)',
|
||||
'border-radius' => $linkBorderRadius,
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link:hover, .navbar .nav-link:focus', $navLinkHoverStyles);
|
||||
|
||||
// Estilos de enlaces activos
|
||||
$navLinkActiveStyles = [
|
||||
'color' => $linkActiveColor . ' !important',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link.active, .navbar .nav-item.current-menu-item > .nav-link', $navLinkActiveStyles);
|
||||
|
||||
// Estilos del dropdown menu
|
||||
$dropdownMaxHeight = $data['visual_effects']['dropdown_max_height'] ?? '300px';
|
||||
$dropdownStyles = [
|
||||
'background' => $data['visual_effects']['background_color'] ?? '#ffffff',
|
||||
'border' => 'none',
|
||||
'box-shadow' => $data['visual_effects']['shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.12)',
|
||||
'border-radius' => $data['visual_effects']['border_radius'] ?? '8px',
|
||||
'padding' => '0.5rem 0',
|
||||
'max-height' => $dropdownMaxHeight,
|
||||
'overflow-y' => 'auto',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-menu', $dropdownStyles);
|
||||
|
||||
// Hover en desktop para mostrar dropdown (sin necesidad de clic)
|
||||
$css .= "\n@media (min-width: 992px) {";
|
||||
$css .= "\n .navbar .dropdown:hover > .dropdown-menu {";
|
||||
$css .= "\n display: block;";
|
||||
$css .= "\n margin-top: 0;";
|
||||
$css .= "\n }";
|
||||
$css .= "\n .navbar .dropdown > .dropdown-toggle:active {";
|
||||
$css .= "\n pointer-events: none;";
|
||||
$css .= "\n }";
|
||||
$css .= "\n}";
|
||||
|
||||
// Estilos de items del dropdown
|
||||
$dropdownItemStyles = [
|
||||
'color' => $data['visual_effects']['item_color'] ?? '#495057',
|
||||
'padding' => $data['visual_effects']['item_padding'] ?? '0.625rem 1.25rem',
|
||||
'transition' => 'all 0.3s ease',
|
||||
'font-weight' => '500',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item', $dropdownItemStyles);
|
||||
|
||||
// Estilos hover de items del dropdown
|
||||
$dropdownItemHoverStyles = [
|
||||
'background-color' => $data['visual_effects']['item_hover_background'] ?? 'rgba(255, 133, 0, 0.1)',
|
||||
'color' => $linkHoverColor,
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item:hover, .navbar .dropdown-item:focus', $dropdownItemHoverStyles);
|
||||
|
||||
// Estilos del brand (texto)
|
||||
$brandStyles = [
|
||||
'color' => ($data['media']['brand_color'] ?? '#FFFFFF') . ' !important',
|
||||
'font-weight' => '700',
|
||||
'font-size' => $data['media']['brand_font_size'] ?? '1.5rem',
|
||||
'transition' => 'color 0.3s ease',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand, .navbar .roi-navbar-brand', $brandStyles);
|
||||
|
||||
// Estilos hover del brand
|
||||
$brandHoverStyles = [
|
||||
'color' => ($data['media']['brand_hover_color'] ?? '#FF8600') . ' !important',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand:hover, .navbar .roi-navbar-brand:hover', $brandHoverStyles);
|
||||
|
||||
// Estilos del logo (imagen)
|
||||
$logoStyles = [
|
||||
'height' => $data['media']['logo_height'] ?? '40px',
|
||||
'width' => 'auto',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.navbar .roi-navbar-logo', $logoStyles);
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
private function buildMenu(array $data): string
|
||||
{
|
||||
$menuLocation = $data['behavior']['menu_location'] ?? 'primary';
|
||||
$enableDropdowns = $data['behavior']['enable_dropdowns'] ?? true;
|
||||
$mobileBreakpoint = $data['behavior']['mobile_breakpoint'] ?? 'lg';
|
||||
|
||||
$ulClass = 'navbar-nav mb-2 mb-lg-0';
|
||||
|
||||
$args = [
|
||||
'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation,
|
||||
'menu' => $menuLocation === 'custom' ? ($data['behavior']['custom_menu_id'] ?? 0) : '',
|
||||
'container' => false,
|
||||
'menu_class' => $ulClass,
|
||||
'fallback_cb' => '__return_false',
|
||||
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
'depth' => $enableDropdowns ? 2 : 1,
|
||||
'walker' => new ROI_Bootstrap_Nav_Walker()
|
||||
];
|
||||
|
||||
ob_start();
|
||||
wp_nav_menu($args);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
|
||||
*
|
||||
* Implementa tabla de decisión según especificación:
|
||||
* - Desktop Y Mobile = null (visible en ambos)
|
||||
* - Solo Desktop = 'd-none d-lg-block'
|
||||
* - Solo Mobile = 'd-lg-none'
|
||||
* - Ninguno = 'd-none' (oculto)
|
||||
*
|
||||
* @param bool $desktop Mostrar en desktop
|
||||
* @param bool $mobile Mostrar en mobile
|
||||
* @return string|null Clases CSS o null si visible en ambos
|
||||
*/
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if ($desktop && $mobile) {
|
||||
return null; // Sin clases = visible siempre
|
||||
}
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
return 'd-none';
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'navbar';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Walker for Bootstrap 5 Navigation
|
||||
*
|
||||
* RESPONSABILIDAD: Adaptar wp_nav_menu() a Bootstrap 5
|
||||
*
|
||||
* CARACTERÍSTICAS:
|
||||
* - Clases Bootstrap 5 (.nav-item, .nav-link, .dropdown)
|
||||
* - Atributos data-bs-toggle para dropdowns
|
||||
* - Soporte para current-menu-item
|
||||
*/
|
||||
class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu
|
||||
{
|
||||
public function start_lvl(&$output, $depth = 0, $args = null)
|
||||
{
|
||||
$indent = str_repeat("\t", $depth);
|
||||
$output .= "\n$indent<ul class=\"dropdown-menu\">\n";
|
||||
}
|
||||
|
||||
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0)
|
||||
{
|
||||
$indent = ($depth) ? str_repeat("\t", $depth) : '';
|
||||
|
||||
$classes = empty($item->classes) ? [] : (array) $item->classes;
|
||||
$classes[] = 'nav-item';
|
||||
|
||||
if ($args->walker->has_children) {
|
||||
$classes[] = 'dropdown';
|
||||
}
|
||||
|
||||
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
|
||||
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
|
||||
|
||||
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
|
||||
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
|
||||
|
||||
$output .= $indent . '<li' . $id . $class_names . '>';
|
||||
|
||||
$atts = [];
|
||||
$atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
|
||||
$atts['target'] = !empty($item->target) ? $item->target : '';
|
||||
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
|
||||
$atts['href'] = !empty($item->url) ? $item->url : '';
|
||||
|
||||
if ($depth === 0) {
|
||||
$atts['class'] = 'nav-link';
|
||||
if ($args->walker->has_children) {
|
||||
$atts['class'] .= ' dropdown-toggle';
|
||||
$atts['data-bs-toggle'] = 'dropdown';
|
||||
$atts['role'] = 'button';
|
||||
$atts['aria-expanded'] = 'false';
|
||||
}
|
||||
} else {
|
||||
$atts['class'] = 'dropdown-item';
|
||||
}
|
||||
|
||||
if (in_array('current-menu-item', $classes)) {
|
||||
$atts['class'] .= ' active';
|
||||
}
|
||||
|
||||
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
|
||||
|
||||
$attributes = '';
|
||||
foreach ($atts as $attr => $value) {
|
||||
if (!empty($value)) {
|
||||
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
|
||||
$attributes .= ' ' . $attr . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$title = apply_filters('the_title', $item->title, $item->ID);
|
||||
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
|
||||
|
||||
$item_output = $args->before;
|
||||
$item_output .= '<a' . $attributes . '>';
|
||||
$item_output .= $args->link_before . $title . $args->link_after;
|
||||
$item_output .= '</a>';
|
||||
$item_output .= $args->after;
|
||||
|
||||
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
|
||||
}
|
||||
}
|
||||
380
Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
Normal file
380
Public/RelatedPost/Infrastructure/Ui/RelatedPostRenderer.php
Normal file
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* RelatedPostRenderer - Renderiza seccion de posts relacionados
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del componente Related Posts
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Grid responsive de cards
|
||||
* - Query dinamica de posts
|
||||
* - Paginacion Bootstrap
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* @package ROITheme\Public\RelatedPost\Infrastructure\Ui
|
||||
*/
|
||||
final class RelatedPostRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$visibilityClass = $this->getVisibilityClass($data);
|
||||
if ($visibilityClass === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data, $visibilityClass);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'related-post';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
|
||||
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
|
||||
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
|
||||
|
||||
if (!$showDesktop && !$showMobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$showDesktop && $showMobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($showDesktop && !$showMobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Variables de colores del tema (defaults del template)
|
||||
$colorNavyPrimary = $colors['section_title_color'] ?? '#0E2337';
|
||||
$colorOrangePrimary = $colors['card_hover_border_color'] ?? '#FF8600';
|
||||
$colorNeutral50 = '#f9fafb';
|
||||
$colorNeutral100 = '#e5e7eb';
|
||||
$colorNeutral600 = $colors['card_border_color'] ?? '#6b7280';
|
||||
|
||||
// Container - margin 3rem 0
|
||||
$cssRules[] = $this->cssGenerator->generate('.related-posts', [
|
||||
'margin' => '3rem 0',
|
||||
]);
|
||||
|
||||
// Section title - color navy, font-weight 700, margin-bottom 2rem
|
||||
$cssRules[] = $this->cssGenerator->generate('.related-posts h2', [
|
||||
'color' => $colorNavyPrimary,
|
||||
'font-weight' => '700',
|
||||
'margin-bottom' => '2rem',
|
||||
]);
|
||||
|
||||
// Card styles - cursor pointer, border, border-left 4px
|
||||
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
|
||||
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? $colorNeutral50;
|
||||
|
||||
$cssRules[] = ".related-posts .card {
|
||||
cursor: pointer;
|
||||
background: {$cardBgColor} !important;
|
||||
border: 1px solid {$colorNeutral100} !important;
|
||||
border-left: 4px solid {$colorNeutral600} !important;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}";
|
||||
|
||||
// Card hover - background change, shadow, border-left orange
|
||||
$cssRules[] = ".related-posts .card:hover {
|
||||
background: {$cardHoverBgColor} !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
border-left-color: {$colorOrangePrimary} !important;
|
||||
}";
|
||||
|
||||
// Card body - padding 1.5rem
|
||||
$cssRules[] = $this->cssGenerator->generate('.related-posts .card-body', [
|
||||
'padding' => '1.5rem !important',
|
||||
]);
|
||||
|
||||
// Card title - color navy, font-weight 600, font-size 0.95rem
|
||||
$cardTitleColor = $colors['card_title_color'] ?? $colorNavyPrimary;
|
||||
|
||||
$cssRules[] = ".related-posts .card-title {
|
||||
color: {$cardTitleColor} !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}";
|
||||
|
||||
// Link hover - title changes to orange
|
||||
$cssRules[] = ".related-posts a:hover .card-title {
|
||||
color: {$colorOrangePrimary} !important;
|
||||
}";
|
||||
|
||||
// Pagination styles - matching template exactly
|
||||
$cssRules[] = ".related-posts .page-link {
|
||||
color: {$colorNeutral600};
|
||||
border: 1px solid {$colorNeutral100};
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}";
|
||||
|
||||
$cssRules[] = ".related-posts .page-link:hover {
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: {$colorOrangePrimary};
|
||||
color: {$colorOrangePrimary};
|
||||
}";
|
||||
|
||||
$cssRules[] = ".related-posts .page-item.active .page-link {
|
||||
background-color: {$colorOrangePrimary};
|
||||
border-color: {$colorOrangePrimary};
|
||||
color: #ffffff;
|
||||
}";
|
||||
|
||||
// Responsive visibility
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
||||
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 991.98px) {
|
||||
.related-posts { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.related-posts { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data, string $visibilityClass): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$layout = $data['layout'] ?? [];
|
||||
|
||||
$sectionTitle = $content['section_title'] ?? 'Descubre Mas Contenido';
|
||||
$postsPerPage = (int)($content['posts_per_page'] ?? 12);
|
||||
$orderby = $content['orderby'] ?? 'rand';
|
||||
$order = $content['order'] ?? 'DESC';
|
||||
$showPagination = $content['show_pagination'] ?? true;
|
||||
$showPagination = $showPagination === true || $showPagination === '1' || $showPagination === 1;
|
||||
|
||||
// Layout columns (cast to string to handle boolean conversion from DB)
|
||||
$colsDesktop = (string)($layout['columns_desktop'] ?? '3');
|
||||
$colsTablet = (string)($layout['columns_tablet'] ?? '2');
|
||||
$colsMobile = (string)($layout['columns_mobile'] ?? '1');
|
||||
|
||||
// Handle '1' stored as boolean true in DB
|
||||
if ($colsDesktop === '1' || $colsDesktop === '') $colsDesktop = '3';
|
||||
if ($colsTablet === '1' || $colsTablet === '') $colsTablet = '2';
|
||||
if ($colsMobile === '1' || $colsMobile === '') $colsMobile = '1';
|
||||
|
||||
// Bootstrap column classes
|
||||
$colClass = $this->getColumnClass($colsDesktop, $colsTablet, $colsMobile);
|
||||
|
||||
// Query related posts
|
||||
$posts = $this->getRelatedPosts($postsPerPage, $orderby, $order);
|
||||
|
||||
if (empty($posts)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$containerClass = 'my-5 related-posts';
|
||||
if (!empty($visibilityClass)) {
|
||||
$containerClass .= ' ' . $visibilityClass;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
||||
$html .= sprintf(
|
||||
'<h2 class="h3 mb-4">%s</h2>',
|
||||
esc_html($sectionTitle)
|
||||
);
|
||||
$html .= '<div class="row g-4">';
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$html .= $this->buildCardHTML($post, $colClass);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
if ($showPagination) {
|
||||
$html .= $this->buildPaginationHTML($data);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
// Reset post data
|
||||
wp_reset_postdata();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getColumnClass(string $desktop, string $tablet, string $mobile): string
|
||||
{
|
||||
$desktopCols = 12 / (int)$desktop;
|
||||
$tabletCols = 12 / (int)$tablet;
|
||||
$mobileCols = 12 / (int)$mobile;
|
||||
|
||||
// Template original usa col-md-4 (3 columnas desde tablet)
|
||||
// col-{mobile} col-md-{tablet/desktop}
|
||||
return sprintf('col-%d col-md-%d', $mobileCols, $desktopCols);
|
||||
}
|
||||
|
||||
private function getRelatedPosts(int $perPage, string $orderby, string $order): array
|
||||
{
|
||||
$currentPostId = get_the_ID();
|
||||
|
||||
$args = [
|
||||
'post_type' => 'post',
|
||||
'posts_per_page' => $perPage,
|
||||
'post__not_in' => $currentPostId ? [$currentPostId] : [],
|
||||
'orderby' => $orderby,
|
||||
'order' => $order,
|
||||
'no_found_rows' => true,
|
||||
];
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
|
||||
return $query->posts;
|
||||
}
|
||||
|
||||
private function buildCardHTML(\WP_Post $post, string $colClass): string
|
||||
{
|
||||
$permalink = get_permalink($post);
|
||||
$title = get_the_title($post);
|
||||
|
||||
$html = sprintf('<div class="%s">', esc_attr($colClass));
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="text-decoration-none">',
|
||||
esc_url($permalink)
|
||||
);
|
||||
$html .= '<div class="card h-100">';
|
||||
$html .= '<div class="card-body d-flex align-items-center justify-content-center">';
|
||||
$html .= sprintf(
|
||||
'<h5 class="card-title h6 mb-0 text-center">%s</h5>',
|
||||
esc_html($title)
|
||||
);
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</a>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPaginationHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$textFirst = $content['pagination_text_first'] ?? 'Inicio';
|
||||
$textLast = $content['pagination_text_last'] ?? 'Fin';
|
||||
$textMore = $content['pagination_text_more'] ?? 'Ver mas';
|
||||
|
||||
$html = '<nav aria-label="' . esc_attr__('Navegacion de posts relacionados', 'roi-theme') . '" class="mt-5">';
|
||||
$html .= '<ul class="pagination justify-content-center">';
|
||||
|
||||
// First page
|
||||
$html .= '<li class="page-item">';
|
||||
$html .= sprintf(
|
||||
'<a class="page-link" href="#" aria-label="%s">%s</a>',
|
||||
esc_attr($textFirst),
|
||||
esc_html($textFirst)
|
||||
);
|
||||
$html .= '</li>';
|
||||
|
||||
// Page numbers (static for now, can be enhanced with AJAX later)
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$activeClass = $i === 1 ? ' active' : '';
|
||||
$ariaCurrent = $i === 1 ? ' aria-current="page"' : '';
|
||||
$html .= sprintf(
|
||||
'<li class="page-item%s"%s><a class="page-link" href="#">%d</a></li>',
|
||||
$activeClass,
|
||||
$ariaCurrent,
|
||||
$i
|
||||
);
|
||||
}
|
||||
|
||||
// More link
|
||||
$html .= '<li class="page-item">';
|
||||
$html .= sprintf(
|
||||
'<a class="page-link" href="#">%s</a>',
|
||||
esc_html($textMore)
|
||||
);
|
||||
$html .= '</li>';
|
||||
|
||||
// Last page
|
||||
$html .= '<li class="page-item">';
|
||||
$html .= sprintf(
|
||||
'<a class="page-link" href="#" aria-label="%s">%s</a>',
|
||||
esc_attr($textLast),
|
||||
esc_html($textLast)
|
||||
);
|
||||
$html .= '</li>';
|
||||
|
||||
$html .= '</ul>';
|
||||
$html .= '</nav>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
390
Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
Normal file
390
Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
Normal file
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del componente Social Share
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - 6 redes sociales: Facebook, Instagram, LinkedIn, WhatsApp, X, Email
|
||||
* - Colores configurables por red
|
||||
* - Toggle individual por red social
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar social share)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\SocialShare\Infrastructure\Ui
|
||||
*/
|
||||
final class SocialShareRenderer implements RendererInterface
|
||||
{
|
||||
private const NETWORKS = [
|
||||
'facebook' => [
|
||||
'field' => 'show_facebook',
|
||||
'url_field' => 'facebook_url',
|
||||
'icon' => 'bi-facebook',
|
||||
'label' => 'Facebook',
|
||||
'share_pattern' => 'https://www.facebook.com/sharer/sharer.php?u=%s',
|
||||
],
|
||||
'instagram' => [
|
||||
'field' => 'show_instagram',
|
||||
'url_field' => 'instagram_url',
|
||||
'icon' => 'bi-instagram',
|
||||
'label' => 'Instagram',
|
||||
'share_pattern' => '', // Instagram no tiene share directo - requiere URL configurada
|
||||
],
|
||||
'linkedin' => [
|
||||
'field' => 'show_linkedin',
|
||||
'url_field' => 'linkedin_url',
|
||||
'icon' => 'bi-linkedin',
|
||||
'label' => 'LinkedIn',
|
||||
'share_pattern' => 'https://www.linkedin.com/sharing/share-offsite/?url=%s',
|
||||
],
|
||||
'whatsapp' => [
|
||||
'field' => 'show_whatsapp',
|
||||
'url_field' => 'whatsapp_number',
|
||||
'icon' => 'bi-whatsapp',
|
||||
'label' => 'WhatsApp',
|
||||
'share_pattern' => 'https://wa.me/?text=%s', // Compartir via WhatsApp
|
||||
],
|
||||
'twitter' => [
|
||||
'field' => 'show_twitter',
|
||||
'url_field' => 'twitter_url',
|
||||
'icon' => 'bi-twitter-x',
|
||||
'label' => 'X',
|
||||
'share_pattern' => 'https://twitter.com/intent/tweet?url=%s&text=%s',
|
||||
],
|
||||
'email' => [
|
||||
'field' => 'show_email',
|
||||
'url_field' => 'email_address',
|
||||
'icon' => 'bi-envelope',
|
||||
'label' => 'Email',
|
||||
'share_pattern' => 'mailto:?subject=%s&body=%s', // Compartir via Email
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'social-share';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false;
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
||||
$buttonBorderWidth = $effects['button_border_width'] ?? '2px';
|
||||
|
||||
// Container styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.social-share-container', [
|
||||
'margin-top' => $spacing['container_margin_top'] ?? '3rem',
|
||||
'margin-bottom' => $spacing['container_margin_bottom'] ?? '3rem',
|
||||
'padding-top' => $spacing['container_padding_top'] ?? '1.5rem',
|
||||
'padding-bottom' => $spacing['container_padding_bottom'] ?? '1.5rem',
|
||||
'border-top' => sprintf('%s solid %s',
|
||||
$effects['border_top_width'] ?? '1px',
|
||||
$colors['border_top_color'] ?? '#dee2e6'
|
||||
),
|
||||
]);
|
||||
|
||||
// Label styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-label', [
|
||||
'font-size' => $typography['label_font_size'] ?? '1rem',
|
||||
'color' => $colors['label_color'] ?? '#6c757d',
|
||||
'margin-bottom' => $spacing['label_margin_bottom'] ?? '1rem',
|
||||
]);
|
||||
|
||||
// Buttons wrapper
|
||||
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons', [
|
||||
'display' => 'flex',
|
||||
'flex-wrap' => 'wrap',
|
||||
'gap' => $spacing['buttons_gap'] ?? '0.5rem',
|
||||
]);
|
||||
|
||||
// Base button styles
|
||||
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn', [
|
||||
'padding' => $spacing['button_padding'] ?? '0.25rem 0.5rem',
|
||||
'font-size' => $typography['icon_font_size'] ?? '1rem',
|
||||
'border-width' => $buttonBorderWidth,
|
||||
'border-radius' => $effects['button_border_radius'] ?? '0.375rem',
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
'background-color' => $colors['button_background'] ?? '#ffffff',
|
||||
]);
|
||||
|
||||
// Hover effect
|
||||
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn:hover', [
|
||||
'box-shadow' => $effects['hover_box_shadow'] ?? '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
]);
|
||||
|
||||
// Network-specific colors
|
||||
$networkColors = [
|
||||
'facebook' => $colors['facebook_color'] ?? '#0d6efd',
|
||||
'instagram' => $colors['instagram_color'] ?? '#dc3545',
|
||||
'linkedin' => $colors['linkedin_color'] ?? '#0dcaf0',
|
||||
'whatsapp' => $colors['whatsapp_color'] ?? '#198754',
|
||||
'twitter' => $colors['twitter_color'] ?? '#212529',
|
||||
'email' => $colors['email_color'] ?? '#6c757d',
|
||||
];
|
||||
|
||||
foreach ($networkColors as $network => $color) {
|
||||
// Outline style
|
||||
$cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}", [
|
||||
'color' => $color,
|
||||
'border-color' => $color,
|
||||
]);
|
||||
// Hover fills the button
|
||||
$cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}:hover", [
|
||||
'background-color' => $color,
|
||||
'color' => '#ffffff',
|
||||
]);
|
||||
}
|
||||
|
||||
// Responsive visibility (normalizar booleanos desde BD)
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
||||
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 991.98px) {
|
||||
.social-share-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.social-share-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$networks = $data['networks'] ?? [];
|
||||
|
||||
$labelText = $content['label_text'] ?? 'Compartir:';
|
||||
$showLabel = $content['show_label'] ?? true;
|
||||
$showLabel = $showLabel === true || $showLabel === '1' || $showLabel === 1;
|
||||
|
||||
$html = '<div class="social-share-container">';
|
||||
|
||||
// Label
|
||||
if ($showLabel && !empty($labelText)) {
|
||||
$html .= sprintf(
|
||||
'<p class="share-label">%s</p>',
|
||||
esc_html($labelText)
|
||||
);
|
||||
}
|
||||
|
||||
// Buttons wrapper
|
||||
$html .= '<div class="share-buttons">';
|
||||
|
||||
// Get current post data for share URLs
|
||||
$shareUrl = $this->getCurrentUrl();
|
||||
$shareTitle = $this->getCurrentTitle();
|
||||
|
||||
foreach (self::NETWORKS as $networkKey => $networkData) {
|
||||
$fieldKey = $networkData['field'];
|
||||
$isEnabled = $networks[$fieldKey] ?? true;
|
||||
$isEnabled = $isEnabled === true || $isEnabled === '1' || $isEnabled === 1;
|
||||
|
||||
if (!$isEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Obtener URL configurada para esta red
|
||||
$urlFieldKey = $networkData['url_field'];
|
||||
$configuredUrl = $networks[$urlFieldKey] ?? '';
|
||||
|
||||
$shareHref = $this->buildNetworkUrl(
|
||||
$networkKey,
|
||||
$configuredUrl,
|
||||
$networkData['share_pattern'],
|
||||
$shareUrl,
|
||||
$shareTitle
|
||||
);
|
||||
|
||||
// Si no hay URL válida usar "#" como fallback (para mantener el icono visible)
|
||||
if (empty($shareHref)) {
|
||||
$shareHref = '#';
|
||||
}
|
||||
|
||||
$ariaLabel = sprintf('Compartir en %s', $networkData['label']);
|
||||
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="btn btn-share-%s" aria-label="%s" target="_blank" rel="noopener noreferrer">
|
||||
<i class="bi %s"></i>
|
||||
</a>',
|
||||
esc_url($shareHref),
|
||||
esc_attr($networkKey),
|
||||
esc_attr($ariaLabel),
|
||||
esc_attr($networkData['icon'])
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>'; // .share-buttons
|
||||
$html .= '</div>'; // .social-share-container
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getCurrentUrl(): string
|
||||
{
|
||||
if (is_singular()) {
|
||||
return get_permalink() ?: '';
|
||||
}
|
||||
return home_url(add_query_arg([], $GLOBALS['wp']->request ?? ''));
|
||||
}
|
||||
|
||||
private function getCurrentTitle(): string
|
||||
{
|
||||
if (is_singular()) {
|
||||
return get_the_title() ?: '';
|
||||
}
|
||||
return wp_title('', false) ?: get_bloginfo('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye la URL para un botón de red social
|
||||
*
|
||||
* Prioridad:
|
||||
* 1. URL configurada por el usuario → enlace directo al perfil
|
||||
* 2. Sin URL configurada → usar patrón de compartir (si existe)
|
||||
*/
|
||||
private function buildNetworkUrl(
|
||||
string $network,
|
||||
string $configuredUrl,
|
||||
string $sharePattern,
|
||||
string $pageUrl,
|
||||
string $pageTitle
|
||||
): string {
|
||||
// Si hay URL configurada, usarla directamente
|
||||
if (!empty($configuredUrl)) {
|
||||
return $this->formatConfiguredUrl($network, $configuredUrl);
|
||||
}
|
||||
|
||||
// Si no hay URL configurada pero existe patrón de compartir
|
||||
if (!empty($sharePattern)) {
|
||||
return $this->formatShareUrl($network, $sharePattern, $pageUrl, $pageTitle);
|
||||
}
|
||||
|
||||
return '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea URL configurada según el tipo de red
|
||||
*/
|
||||
private function formatConfiguredUrl(string $network, string $url): string
|
||||
{
|
||||
switch ($network) {
|
||||
case 'whatsapp':
|
||||
// Para WhatsApp, el número debe ir sin el +
|
||||
$number = preg_replace('/[^0-9]/', '', $url);
|
||||
return "https://wa.me/{$number}";
|
||||
case 'email':
|
||||
// Para email, agregar mailto: si no lo tiene
|
||||
if (!str_starts_with($url, 'mailto:')) {
|
||||
return "mailto:{$url}";
|
||||
}
|
||||
return $url;
|
||||
default:
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea URL de compartir usando el patrón
|
||||
*/
|
||||
private function formatShareUrl(string $network, string $pattern, string $url, string $title): string
|
||||
{
|
||||
$encodedUrl = rawurlencode($url);
|
||||
$encodedTitle = rawurlencode($title);
|
||||
|
||||
switch ($network) {
|
||||
case 'twitter':
|
||||
return sprintf($pattern, $encodedUrl, $encodedTitle);
|
||||
case 'whatsapp':
|
||||
$text = $title . ' - ' . $url;
|
||||
return sprintf($pattern, rawurlencode($text));
|
||||
case 'email':
|
||||
return sprintf($pattern, $encodedTitle, $encodedUrl);
|
||||
default:
|
||||
return sprintf($pattern, $encodedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if (!$desktop && !$mobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
||||
/**
|
||||
* TableOfContentsRenderer - Renderiza tabla de contenido con navegacion automatica
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS de la tabla de contenido
|
||||
*
|
||||
* CARACTERISTICAS:
|
||||
* - Generacion automatica desde headings del contenido
|
||||
* - ScrollSpy para navegacion activa
|
||||
* - Sticky positioning configurable
|
||||
* - Smooth scroll
|
||||
* - Estilos 100% desde BD via CSSGenerator
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar TOC)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\TableOfContents\Infrastructure\Ui
|
||||
*/
|
||||
final class TableOfContentsRenderer implements RendererInterface
|
||||
{
|
||||
private array $headingCounter = [];
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tocItems = $this->generateTocItems($data);
|
||||
|
||||
if (empty($tocItems)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data, $tocItems);
|
||||
$script = $this->buildScript($data);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s\n%s", $css, $html, $script);
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'table-of-contents';
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
case 'posts':
|
||||
return is_single();
|
||||
case 'pages':
|
||||
return is_page();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
if (!$desktop && !$mobile) {
|
||||
return null;
|
||||
}
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function generateTocItems(array $data): array
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$autoGenerate = $content['auto_generate'] ?? true;
|
||||
|
||||
if (!$autoGenerate) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$headingLevelsStr = $content['heading_levels'] ?? 'h2,h3';
|
||||
$headingLevels = array_map('trim', explode(',', $headingLevelsStr));
|
||||
|
||||
return $this->generateTocFromContent($headingLevels);
|
||||
}
|
||||
|
||||
private function generateTocFromContent(array $headingLevels): array
|
||||
{
|
||||
global $post;
|
||||
|
||||
if (!$post || empty($post->post_content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = apply_filters('the_content', $post->post_content);
|
||||
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $content);
|
||||
libxml_clear_errors();
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
$tocItems = [];
|
||||
|
||||
$xpathQuery = implode(' | ', array_map(function($level) {
|
||||
return '//' . $level;
|
||||
}, $headingLevels));
|
||||
|
||||
$headings = $xpath->query($xpathQuery);
|
||||
|
||||
if ($headings->length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($headings as $heading) {
|
||||
$tagName = strtolower($heading->tagName);
|
||||
$level = intval(substr($tagName, 1));
|
||||
|
||||
$text = trim($heading->textContent);
|
||||
|
||||
if (empty($text)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingId = $heading->getAttribute('id');
|
||||
|
||||
if (empty($existingId)) {
|
||||
$anchor = $this->generateAnchorId($text);
|
||||
$this->addIdToHeading($text, $anchor);
|
||||
} else {
|
||||
$anchor = $existingId;
|
||||
}
|
||||
|
||||
$tocItems[] = [
|
||||
'text' => $text,
|
||||
'anchor' => $anchor,
|
||||
'level' => $level
|
||||
];
|
||||
}
|
||||
|
||||
return $tocItems;
|
||||
}
|
||||
|
||||
private function generateAnchorId(string $text): string
|
||||
{
|
||||
$id = strtolower($text);
|
||||
$id = remove_accents($id);
|
||||
$id = preg_replace('/[^a-z0-9]+/', '-', $id);
|
||||
$id = trim($id, '-');
|
||||
|
||||
$baseId = $id;
|
||||
$count = 1;
|
||||
|
||||
while (isset($this->headingCounter[$id])) {
|
||||
$id = $baseId . '-' . $count;
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->headingCounter[$id] = true;
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function addIdToHeading(string $headingText, string $anchorId): void
|
||||
{
|
||||
add_filter('the_content', function($content) use ($headingText, $anchorId) {
|
||||
$pattern = '/<(h[2-6])([^>]*)>(\s*)' . preg_quote($headingText, '/') . '(\s*)<\/\1>/i';
|
||||
$replacement = '<$1 id="' . esc_attr($anchorId) . '"$2>$3' . $headingText . '$4</$1>';
|
||||
return preg_replace($pattern, $replacement, $content, 1);
|
||||
}, 20);
|
||||
}
|
||||
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$typography = $data['typography'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
$behavior = $data['behavior'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Container styles - Flexbox layout for proper scrolling
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container', [
|
||||
'background-color' => $colors['background_color'] ?? '#ffffff',
|
||||
'border' => ($effects['border_width'] ?? '1px') . ' solid ' . ($colors['border_color'] ?? '#E6E9ED'),
|
||||
'border-radius' => $effects['border_radius'] ?? '8px',
|
||||
'box-shadow' => $effects['box_shadow'] ?? '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
'padding' => $spacing['container_padding'] ?? '12px 16px',
|
||||
'margin-bottom' => $spacing['margin_bottom'] ?? '13px',
|
||||
'max-height' => $behavior['max_height'] ?? 'calc(100vh - 71px - 10px - 250px - 15px - 15px)',
|
||||
'display' => 'flex',
|
||||
'flex-direction' => 'column',
|
||||
'overflow' => 'visible',
|
||||
]);
|
||||
|
||||
// Sticky behavior - aplica al wrapper .sidebar-sticky de single.php
|
||||
// NO al .toc-container individual (ver template líneas 817-835)
|
||||
if (($behavior['is_sticky'] ?? true)) {
|
||||
$cssRules[] = $this->cssGenerator->generate('.sidebar-sticky', [
|
||||
'position' => 'sticky',
|
||||
'top' => '85px',
|
||||
'display' => 'flex',
|
||||
'flex-direction' => 'column',
|
||||
]);
|
||||
}
|
||||
|
||||
// Custom scrollbar
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar', [
|
||||
'width' => $spacing['scrollbar_width'] ?? '6px',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-track', [
|
||||
'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB',
|
||||
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-thumb', [
|
||||
'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280',
|
||||
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
|
||||
]);
|
||||
|
||||
// Title styles - Color #1e3a5f = navy-primary del Design System
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-title', [
|
||||
'font-size' => $typography['title_font_size'] ?? '1rem',
|
||||
'font-weight' => $typography['title_font_weight'] ?? '600',
|
||||
'color' => $colors['title_color'] ?? '#1e3a5f',
|
||||
'padding-bottom' => $spacing['title_padding_bottom'] ?? '8px',
|
||||
'margin-bottom' => $spacing['title_margin_bottom'] ?? '0.75rem',
|
||||
'border-bottom' => '2px solid ' . ($colors['title_border_color'] ?? '#E6E9ED'),
|
||||
'margin-top' => '0',
|
||||
]);
|
||||
|
||||
// List styles - Scrollable area with flex
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list', [
|
||||
'margin' => '0',
|
||||
'padding' => '0',
|
||||
'padding-right' => '0.5rem',
|
||||
'list-style' => 'none',
|
||||
'overflow-y' => 'auto',
|
||||
'flex' => '1',
|
||||
'min-height' => '0',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list li', [
|
||||
'margin-bottom' => $spacing['item_margin_bottom'] ?? '0.15rem',
|
||||
]);
|
||||
|
||||
// Link styles - Color #495057 = neutral-600 del template
|
||||
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link', [
|
||||
'display' => 'block',
|
||||
'font-size' => $typography['link_font_size'] ?? '0.9rem',
|
||||
'line-height' => $typography['link_line_height'] ?? '1.3',
|
||||
'color' => $colors['link_color'] ?? '#495057',
|
||||
'text-decoration' => 'none',
|
||||
'padding' => $spacing['link_padding'] ?? '0.3rem 0.85rem',
|
||||
'border-radius' => $effects['link_border_radius'] ?? '4px',
|
||||
'border-left' => ($effects['active_border_left_width'] ?? '3px') . ' solid transparent',
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
// Link hover - Color #1e3a5f = navy-primary del Design System
|
||||
// Template: background, border-left-color, color
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link:hover', [
|
||||
'color' => $colors['link_hover_color'] ?? '#1e3a5f',
|
||||
'background-color' => $colors['link_hover_background'] ?? '#F9FAFB',
|
||||
'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f',
|
||||
]);
|
||||
|
||||
// Active link - Color #1e3a5f = navy-primary del Design System
|
||||
// Template: font-weight: 600
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link.active', [
|
||||
'color' => $colors['active_text_color'] ?? '#1e3a5f',
|
||||
'background-color' => $colors['active_background_color'] ?? '#F9FAFB',
|
||||
'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f',
|
||||
'font-weight' => '600',
|
||||
]);
|
||||
|
||||
// Level indentation
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-3 .toc-link', [
|
||||
'padding-left' => $spacing['level_three_padding_left'] ?? '1.5rem',
|
||||
'font-size' => $typography['level_three_font_size'] ?? '0.85rem',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-4 .toc-link', [
|
||||
'padding-left' => $spacing['level_four_padding_left'] ?? '2rem',
|
||||
'font-size' => $typography['level_four_font_size'] ?? '0.8rem',
|
||||
]);
|
||||
|
||||
// Scrollbar for toc-list
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar', [
|
||||
'width' => $spacing['scrollbar_width'] ?? '6px',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-track', [
|
||||
'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB',
|
||||
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb', [
|
||||
'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280',
|
||||
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb:hover', [
|
||||
'background' => $colors['active_border_color'] ?? '#1e3a5f',
|
||||
]);
|
||||
|
||||
// Responsive visibility
|
||||
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
||||
$showOnMobile = $visibility['show_on_mobile'] ?? false;
|
||||
|
||||
if (!$showOnMobile) {
|
||||
$cssRules[] = "@media (max-width: 991.98px) {
|
||||
.toc-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
if (!$showOnDesktop) {
|
||||
$cssRules[] = "@media (min-width: 992px) {
|
||||
.toc-container { display: none !important; }
|
||||
}";
|
||||
}
|
||||
|
||||
// Responsive layout adjustments
|
||||
$cssRules[] = "@media (max-width: 991px) {
|
||||
.sidebar-sticky {
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
.toc-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.toc-container .toc-list {
|
||||
max-height: 300px;
|
||||
}
|
||||
}";
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(array $data, array $tocItems): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
|
||||
$title = $content['title'] ?? 'Tabla de Contenido';
|
||||
|
||||
// NOTA: El sticky behavior se maneja en el wrapper .sidebar-sticky de single.php
|
||||
// El TOC no debe tener la clase sidebar-sticky - está dentro del wrapper
|
||||
$html = '<div class="toc-container">';
|
||||
|
||||
$html .= sprintf(
|
||||
'<h4 class="toc-title">%s</h4>',
|
||||
esc_html($title)
|
||||
);
|
||||
|
||||
$html .= '<ol class="list-unstyled toc-list">';
|
||||
|
||||
foreach ($tocItems as $item) {
|
||||
$text = $item['text'] ?? '';
|
||||
$anchor = $item['anchor'] ?? '';
|
||||
$level = $item['level'] ?? 2;
|
||||
|
||||
if (empty($text) || empty($anchor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indentClass = $level > 2 ? 'toc-level-' . $level : '';
|
||||
|
||||
$html .= sprintf(
|
||||
'<li class="%s"><a href="#%s" class="toc-link" data-level="%d">%s</a></li>',
|
||||
esc_attr($indentClass),
|
||||
esc_attr($anchor),
|
||||
intval($level),
|
||||
esc_html($text)
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</ol>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildScript(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$behavior = $data['behavior'] ?? [];
|
||||
|
||||
$smoothScroll = $content['smooth_scroll'] ?? true;
|
||||
$scrollOffset = intval($behavior['scroll_offset'] ?? 100);
|
||||
|
||||
if (!$smoothScroll) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$script = <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var tocLinks = document.querySelectorAll('.toc-link');
|
||||
var offsetTop = {$scrollOffset};
|
||||
|
||||
tocLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var targetId = this.getAttribute('href');
|
||||
var targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
var elementPosition = targetElement.getBoundingClientRect().top;
|
||||
var offsetPosition = elementPosition + window.pageYOffset - offsetTop;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ScrollSpy
|
||||
var sections = [];
|
||||
tocLinks.forEach(function(link) {
|
||||
var id = link.getAttribute('href').substring(1);
|
||||
var section = document.getElementById(id);
|
||||
if (section) {
|
||||
sections.push({ id: id, element: section });
|
||||
}
|
||||
});
|
||||
|
||||
function updateActiveLink() {
|
||||
var scrollPosition = window.pageYOffset + offsetTop + 50;
|
||||
var currentSection = '';
|
||||
|
||||
sections.forEach(function(section) {
|
||||
if (section.element.offsetTop <= scrollPosition) {
|
||||
currentSection = section.id;
|
||||
}
|
||||
});
|
||||
|
||||
tocLinks.forEach(function(link) {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + currentSection) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', updateActiveLink);
|
||||
updateActiveLink();
|
||||
});
|
||||
</script>
|
||||
JS;
|
||||
|
||||
return $script;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Class TopNotificationBarRenderer
|
||||
*
|
||||
* Renderizador del componente Top Notification Bar para el frontend.
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar HTML del componente top-notification-bar
|
||||
* - Delegar generación de CSS a CSSGeneratorInterface
|
||||
* - Validar visibilidad (is_enabled, show_on_pages, hide_on_mobile)
|
||||
* - Manejar visibilidad responsive con clases Bootstrap
|
||||
* - Generar script para funcionalidad dismissible
|
||||
* - Sanitizar todos los outputs
|
||||
*
|
||||
* NO responsable de:
|
||||
* - Generar string CSS (delega a CSSGeneratorService)
|
||||
* - Persistir datos (ya están en Component)
|
||||
* - Lógica de negocio (está en Domain)
|
||||
*
|
||||
* Cumple con:
|
||||
* - DIP: Recibe CSSGeneratorInterface por constructor
|
||||
* - SRP: Una responsabilidad (renderizar este componente)
|
||||
* - Clean Architecture: Infrastructure puede usar WordPress
|
||||
*
|
||||
* @package ROITheme\Public\topnotificationbar\infrastructure\ui
|
||||
*/
|
||||
final class TopNotificationBarRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||
*/
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por página
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generar CSS usando CSSGeneratorService
|
||||
$css = $this->generateCSS($data);
|
||||
|
||||
// Generar HTML
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
// Combinar todo
|
||||
return sprintf(
|
||||
"<style>%s</style>\n%s",
|
||||
$css,
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'top-notification-bar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente está habilitado
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return ($data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si debe mostrarse en la página actual
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
||||
|
||||
return match ($showOn) {
|
||||
'all' => true,
|
||||
'home' => is_front_page(),
|
||||
'posts' => is_single(),
|
||||
'pages' => is_page(),
|
||||
'custom' => $this->isInCustomPages($data),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si está en páginas personalizadas
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isInCustomPages(array $data): bool
|
||||
{
|
||||
$pageIds = $data['visibility']['custom_page_ids'] ?? '';
|
||||
|
||||
if (empty($pageIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowedIds = array_map('trim', explode(',', $pageIds));
|
||||
$currentId = (string) get_the_ID();
|
||||
|
||||
return in_array($currentId, $allowedIds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente fue dismissed por el usuario
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isDismissed(array $data): bool
|
||||
{
|
||||
if (!$this->isDismissible($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cookieName = 'roi_notification_bar_dismissed';
|
||||
return isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente es dismissible
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return bool
|
||||
*/
|
||||
private function isDismissible(array $data): bool
|
||||
{
|
||||
return ($data['behavior']['is_dismissible'] ?? false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar CSS usando CSSGeneratorService
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string CSS generado
|
||||
*/
|
||||
private function generateCSS(array $data): string
|
||||
{
|
||||
$css = '';
|
||||
|
||||
// Estilos base de la barra
|
||||
$baseStyles = [
|
||||
'background_color' => $data['styles']['background_color'] ?? '#0E2337',
|
||||
'color' => $data['styles']['text_color'] ?? '#FFFFFF',
|
||||
'font_size' => $data['styles']['font_size'] ?? '0.9rem',
|
||||
'padding' => $data['styles']['padding'] ?? '0.5rem 0',
|
||||
'width' => '100%',
|
||||
'z_index' => '1050',
|
||||
];
|
||||
$css .= $this->cssGenerator->generate('.top-notification-bar', $baseStyles);
|
||||
|
||||
// Estilos del ícono
|
||||
$iconStyles = [
|
||||
'color' => $data['styles']['icon_color'] ?? '#FF8600',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-icon', $iconStyles);
|
||||
|
||||
// Estilos de la etiqueta (label)
|
||||
$labelStyles = [
|
||||
'color' => $data['styles']['label_color'] ?? '#FF8600',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-label', $labelStyles);
|
||||
|
||||
// Estilos del enlace
|
||||
$linkStyles = [
|
||||
'color' => $data['styles']['link_color'] ?? '#FFFFFF',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link', $linkStyles);
|
||||
|
||||
// Estilos del enlace hover
|
||||
$linkHoverStyles = [
|
||||
'color' => $data['styles']['link_hover_color'] ?? '#FF8600',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link:hover', $linkHoverStyles);
|
||||
|
||||
// Estilos del ícono personalizado
|
||||
$customIconStyles = [
|
||||
'width' => '24px',
|
||||
'height' => '24px',
|
||||
];
|
||||
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .custom-icon', $customIconStyles);
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar HTML del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML generado
|
||||
*/
|
||||
private function buildHTML(array $data): string
|
||||
{
|
||||
$classes = $this->buildClasses($data);
|
||||
$content = $this->buildContent($data);
|
||||
|
||||
return sprintf(
|
||||
'<div class="%s">%s</div>',
|
||||
esc_attr($classes),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir clases CSS del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Clases CSS
|
||||
*/
|
||||
private function buildClasses(array $data): string
|
||||
{
|
||||
return 'top-notification-bar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir atributos data para dismissible
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Atributos HTML
|
||||
*/
|
||||
private function buildDismissAttributes(array $data): string
|
||||
{
|
||||
if (!$this->isDismissible($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
|
||||
return sprintf(' data-dismissible-days="%d"', $days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir contenido del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del contenido
|
||||
*/
|
||||
private function buildContent(array $data): string
|
||||
{
|
||||
$html = '<div class="container">';
|
||||
$html .= '<div class="d-flex align-items-center justify-content-center">';
|
||||
|
||||
// Ícono
|
||||
$html .= $this->buildIcon($data);
|
||||
|
||||
// Texto del anuncio
|
||||
$html .= $this->buildAnnouncementText($data);
|
||||
|
||||
// Enlace
|
||||
$html .= $this->buildLink($data);
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir ícono del componente
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del ícono
|
||||
*/
|
||||
private function buildIcon(array $data): string
|
||||
{
|
||||
// Siempre usar Bootstrap icon desde content.icon_class
|
||||
$iconClass = $data['content']['icon_class'] ?? 'bi-megaphone-fill';
|
||||
|
||||
// Asegurar prefijo 'bi-'
|
||||
if (strpos($iconClass, 'bi-') !== 0) {
|
||||
$iconClass = 'bi-' . $iconClass;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<i class="bi %s notification-icon me-2"></i>',
|
||||
esc_attr($iconClass)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir texto del anuncio
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del texto
|
||||
*/
|
||||
private function buildAnnouncementText(array $data): string
|
||||
{
|
||||
$label = $data['content']['label_text'] ?? '';
|
||||
$text = $data['content']['message_text'] ?? '';
|
||||
|
||||
if (empty($text)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<span>';
|
||||
|
||||
if (!empty($label)) {
|
||||
$html .= sprintf('<strong class="notification-label">%s</strong> ', esc_html($label));
|
||||
}
|
||||
|
||||
$html .= esc_html($text);
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir enlace de acción
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string HTML del enlace
|
||||
*/
|
||||
private function buildLink(array $data): string
|
||||
{
|
||||
$linkText = $data['content']['link_text'] ?? '';
|
||||
$linkUrl = $data['content']['link_url'] ?? '#';
|
||||
|
||||
if (empty($linkText)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" class="notification-link ms-2 text-decoration-underline">%s</a>',
|
||||
esc_url($linkUrl),
|
||||
esc_html($linkText)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir botón de cerrar
|
||||
*
|
||||
* @return string HTML del botón
|
||||
*/
|
||||
private function buildDismissButton(): string
|
||||
{
|
||||
return '<button type="button" class="btn-close btn-close-white ms-3 roi-dismiss-notification" aria-label="Cerrar"></button>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir estilos CSS de animaciones
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string CSS de animaciones
|
||||
*/
|
||||
private function buildAnimationStyles(array $data): string
|
||||
{
|
||||
$animationType = $data['visual_effects']['animation_type'] ?? 'slide-down';
|
||||
|
||||
$animations = [
|
||||
'slide-down' => [
|
||||
'keyframes' => '@keyframes roiSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }',
|
||||
'animation' => 'roiSlideDown 0.5s ease-out',
|
||||
],
|
||||
'fade-in' => [
|
||||
'keyframes' => '@keyframes roiFadeIn { from { opacity: 0; } to { opacity: 1; } }',
|
||||
'animation' => 'roiFadeIn 0.5s ease-out',
|
||||
],
|
||||
];
|
||||
|
||||
$anim = $animations[$animationType] ?? $animations['slide-down'];
|
||||
|
||||
return sprintf(
|
||||
"%s\n.top-notification-bar.roi-animated.roi-%s { animation: %s; }",
|
||||
$anim['keyframes'],
|
||||
$animationType,
|
||||
$anim['animation']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construir script para funcionalidad dismissible
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string JavaScript
|
||||
*/
|
||||
private function buildDismissScript(array $data): string
|
||||
{
|
||||
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
|
||||
|
||||
return sprintf(
|
||||
'<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const dismissBtn = document.querySelector(".roi-dismiss-notification");
|
||||
if (dismissBtn) {
|
||||
dismissBtn.addEventListener("click", function() {
|
||||
const bar = document.querySelector(".top-notification-bar");
|
||||
if (bar) {
|
||||
bar.style.display = "none";
|
||||
}
|
||||
|
||||
const days = %d;
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
const expires = "expires=" + date.toUTCString();
|
||||
document.cookie = "roi_notification_bar_dismissed=1;" + expires + ";path=/";
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>',
|
||||
$days
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
|
||||
*
|
||||
* Implementa tabla de decisión según especificación (10.03):
|
||||
* - Desktop Y Mobile = null (visible en ambos)
|
||||
* - Solo Desktop = 'd-none d-lg-block'
|
||||
* - Solo Mobile = 'd-lg-none'
|
||||
* - Ninguno = 'd-none' (oculto)
|
||||
*
|
||||
* @param bool $desktop Mostrar en desktop
|
||||
* @param bool $mobile Mostrar en mobile
|
||||
* @return string|null Clases CSS o null si visible en ambos
|
||||
*/
|
||||
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
||||
{
|
||||
// Desktop Y Mobile = visible en ambos dispositivos
|
||||
if ($desktop && $mobile) {
|
||||
return null; // Sin clases = visible siempre
|
||||
}
|
||||
|
||||
// Solo Desktop
|
||||
if ($desktop && !$mobile) {
|
||||
return 'd-none d-lg-block';
|
||||
}
|
||||
|
||||
// Solo Mobile
|
||||
if (!$desktop && $mobile) {
|
||||
return 'd-lg-none';
|
||||
}
|
||||
|
||||
// Ninguno = oculto completamente
|
||||
return 'd-none';
|
||||
}
|
||||
}
|
||||
257
admin/README.md
257
admin/README.md
@@ -1,257 +0,0 @@
|
||||
# Contexto Admin - Administración de Componentes
|
||||
|
||||
## Propósito
|
||||
|
||||
El contexto `admin/` contiene **todo el código relacionado con la administración de componentes** en el panel
|
||||
de WordPress. Cada componente tiene su propia carpeta con su Clean Architecture completa.
|
||||
|
||||
## Filosofía: Context-First Architecture
|
||||
|
||||
En lugar de separar por capas (Domain, Application, Infrastructure), separamos por **contextos**
|
||||
(admin, public, shared) y cada contexto tiene sus propias capas internas.
|
||||
|
||||
## Estructura (Fase-00)
|
||||
|
||||
En Fase-00 solo creamos la estructura base. Los componentes se crearán en fases posteriores:
|
||||
|
||||
```
|
||||
admin/
|
||||
├── README.md (este archivo)
|
||||
└── .gitkeep (preserva directorio en Git)
|
||||
```
|
||||
|
||||
## Estructura Futura (Post Fase-00)
|
||||
|
||||
```
|
||||
admin/
|
||||
├── Navbar/ # Componente Navbar
|
||||
│ ├── Domain/
|
||||
│ │ ├── NavbarComponent.php # Entidad
|
||||
│ │ └── NavbarRepositoryInterface.php
|
||||
│ ├── Application/
|
||||
│ │ ├── SaveNavbarUseCase.php # Caso de uso
|
||||
│ │ └── DTO/
|
||||
│ │ └── NavbarSettingsDTO.php
|
||||
│ ├── Infrastructure/
|
||||
│ │ ├── Persistence/
|
||||
│ │ │ └── WordPressNavbarRepository.php
|
||||
│ │ ├── UI/
|
||||
│ │ │ ├── NavbarAdminPage.php # Página de admin
|
||||
│ │ │ ├── NavbarForm.php # Formulario
|
||||
│ │ │ └── views/
|
||||
│ │ │ └── navbar-settings.php
|
||||
│ │ └── API/
|
||||
│ │ └── NavbarAjaxHandler.php # AJAX endpoints
|
||||
│ └── README.md
|
||||
├── Footer/ # Otro componente
|
||||
│ └── (misma estructura)
|
||||
└── (más componentes...)
|
||||
```
|
||||
|
||||
## Principios del Contexto Admin
|
||||
|
||||
1. **Aislamiento**: Cada componente es independiente
|
||||
2. **Clean Architecture**: Cada componente tiene Domain, Application, Infrastructure
|
||||
3. **Sin acoplamiento**: Un componente no depende de otro directamente
|
||||
4. **Código compartido**: Va en `shared/` si es usado por múltiples componentes
|
||||
|
||||
## Responsabilidades
|
||||
|
||||
El contexto `admin/` se encarga de:
|
||||
|
||||
✅ **Formularios de configuración** de componentes
|
||||
✅ **Validación de entrada** del usuario administrador
|
||||
✅ **Guardado de configuraciones** en la base de datos
|
||||
✅ **Páginas de administración** en el panel de WordPress
|
||||
✅ **AJAX endpoints** para operaciones administrativas
|
||||
✅ **Permisos y capabilities** de administrador
|
||||
|
||||
❌ **NO** se encarga de:
|
||||
- Renderizado frontend de componentes (va en `public/`)
|
||||
- Lógica compartida entre contextos (va en `shared/`)
|
||||
- Configuraciones globales del tema
|
||||
|
||||
## Ejemplo de Flujo de Admin
|
||||
|
||||
### 1. Usuario administra el Navbar
|
||||
|
||||
```
|
||||
Usuario Admin (navegador)
|
||||
↓
|
||||
NavbarAdminPage.php (muestra formulario)
|
||||
↓
|
||||
Usuario envía formulario via AJAX
|
||||
↓
|
||||
NavbarAjaxHandler.php (recibe request)
|
||||
↓
|
||||
SaveNavbarUseCase.php (orquesta la lógica)
|
||||
↓
|
||||
WordPressNavbarRepository.php (guarda en DB)
|
||||
↓
|
||||
Respuesta JSON al navegador
|
||||
```
|
||||
|
||||
### 2. Código de ejemplo
|
||||
|
||||
```php
|
||||
// admin/Navbar/Application/SaveNavbarUseCase.php
|
||||
namespace ROITheme\Admin\Navbar\Application;
|
||||
|
||||
use ROITheme\Admin\Navbar\Domain\NavbarRepositoryInterface;
|
||||
use ROITheme\Shared\Application\Contracts\ValidationServiceInterface;
|
||||
|
||||
final class SaveNavbarUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NavbarRepositoryInterface $repository,
|
||||
private readonly ValidationServiceInterface $validator
|
||||
) {}
|
||||
|
||||
public function execute(array $data): void
|
||||
{
|
||||
$validated = $this->validator->validate($data, [
|
||||
'logo_url' => 'url',
|
||||
'menu_items' => 'array',
|
||||
'sticky' => 'bool',
|
||||
]);
|
||||
|
||||
if ($this->validator->fails()) {
|
||||
throw new ValidationException($this->validator->errors());
|
||||
}
|
||||
|
||||
$this->repository->save($validated);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// admin/Navbar/Infrastructure/UI/NavbarAdminPage.php
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\UI;
|
||||
|
||||
final class NavbarAdminPage
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_menu', [$this, 'addMenuPage']);
|
||||
}
|
||||
|
||||
public function addMenuPage(): void
|
||||
{
|
||||
add_menu_page(
|
||||
'Navbar Settings',
|
||||
'Navbar',
|
||||
'manage_options',
|
||||
'roi-navbar-settings',
|
||||
[$this, 'render'],
|
||||
'dashicons-menu',
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
public function render(): void
|
||||
{
|
||||
require __DIR__ . '/views/navbar-settings.php';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Relación con Otros Contextos
|
||||
|
||||
```
|
||||
admin/ → Administra configuraciones
|
||||
↓ guarda
|
||||
Base de Datos → Almacena settings
|
||||
↓ lee
|
||||
public/ → Renderiza componentes en frontend
|
||||
```
|
||||
|
||||
**Clave**: `admin/` y `public/` NO se hablan directamente. Se comunican a través de la base de datos.
|
||||
|
||||
## Reglas de Dependencia
|
||||
|
||||
✅ **PUEDE** depender de:
|
||||
- `shared/Domain/` (Value Objects, Exceptions)
|
||||
- `shared/Application/` (Contracts, Services)
|
||||
- `shared/Infrastructure/` (Implementaciones de servicios)
|
||||
- WordPress admin functions (`add_menu_page`, `add_settings_section`, etc.)
|
||||
|
||||
❌ **NO PUEDE** depender de:
|
||||
- `public/` (contexto independiente)
|
||||
- Código de frontend (JS/CSS va en Infrastructure/UI/)
|
||||
|
||||
## Testing
|
||||
|
||||
### Tests Unitarios
|
||||
```php
|
||||
// tests/Unit/Admin/Navbar/Application/SaveNavbarUseCaseTest.php
|
||||
public function test_saves_valid_navbar_settings()
|
||||
{
|
||||
$repository = $this->createMock(NavbarRepositoryInterface::class);
|
||||
$validator = $this->createMock(ValidationServiceInterface::class);
|
||||
|
||||
$useCase = new SaveNavbarUseCase($repository, $validator);
|
||||
$useCase->execute(['logo_url' => 'http://example.com/logo.png']);
|
||||
|
||||
// Assertions...
|
||||
}
|
||||
```
|
||||
|
||||
### Tests de Integración
|
||||
```php
|
||||
// tests/Integration/Admin/Navbar/Infrastructure/WordPressNavbarRepositoryTest.php
|
||||
public function test_saves_and_retrieves_navbar_settings()
|
||||
{
|
||||
$repository = new WordPressNavbarRepository();
|
||||
$settings = ['logo_url' => 'http://example.com/logo.png'];
|
||||
|
||||
$repository->save($settings);
|
||||
$retrieved = $repository->get();
|
||||
|
||||
$this->assertEquals($settings, $retrieved);
|
||||
}
|
||||
```
|
||||
|
||||
### Tests E2E (Playwright)
|
||||
```php
|
||||
// tests/E2E/Admin/NavbarAdminPageTest.php
|
||||
public function test_admin_can_save_navbar_settings()
|
||||
{
|
||||
$this->loginAsAdmin();
|
||||
$this->visit('/wp-admin/admin.php?page=roi-navbar-settings');
|
||||
$this->fillField('logo_url', 'http://example.com/logo.png');
|
||||
$this->click('Save Settings');
|
||||
$this->see('Settings saved successfully');
|
||||
}
|
||||
```
|
||||
|
||||
## Cuándo Agregar Código Aquí
|
||||
|
||||
Agrega código a `admin/` cuando:
|
||||
- Creas un nuevo componente administrable
|
||||
- Necesitas una página de configuración en el panel de WordPress
|
||||
- Manejas formularios de administrador
|
||||
- Procesas AJAX desde el admin
|
||||
- Validas o guardas configuraciones
|
||||
|
||||
No agregues aquí:
|
||||
- Renderizado frontend (va en `public/`)
|
||||
- Lógica compartida (va en `shared/`)
|
||||
- Configuraciones globales del tema
|
||||
|
||||
## Estado Actual (Fase-00)
|
||||
|
||||
En Fase-00, `admin/` solo tiene la estructura base. Los componentes se crearán en las siguientes fases:
|
||||
|
||||
- **Fase-1**: Estructura base e infraestructura
|
||||
- **Fase-2**: Migración de base de datos
|
||||
- **Fase-3**: Implementación de componentes admin
|
||||
- **Fase-4+**: Componentes adicionales
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Crear primer componente en Fase-3 (ej: Navbar)
|
||||
2. Implementar Domain layer del componente
|
||||
3. Implementar Application layer (casos de uso)
|
||||
4. Implementar Infrastructure layer (WordPress integration)
|
||||
5. Crear tests unitarios e integración
|
||||
6. Repetir para cada componente
|
||||
@@ -1,511 +0,0 @@
|
||||
/**
|
||||
* Admin Panel Styles
|
||||
*
|
||||
* Estilos base para el panel de administración
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Container
|
||||
======================================== */
|
||||
|
||||
.roi-admin-panel {
|
||||
max-width: 1400px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Header
|
||||
======================================== */
|
||||
|
||||
.roi-admin-panel h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.roi-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) {
|
||||
.roi-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%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MEJORAS ESPECÍFICAS PARA ADMIN PANEL
|
||||
============================================ */
|
||||
|
||||
/* Variables CSS */
|
||||
:root {
|
||||
--color-navy-primary: #1E3A5F;
|
||||
--color-navy-light: #2C5282;
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-orange-primary: #FF8600;
|
||||
--color-orange-hover: #FF6B35;
|
||||
--color-neutral-50: #F9FAFB;
|
||||
--color-neutral-100: #F3F4F6;
|
||||
--color-neutral-600: #6B7280;
|
||||
--color-neutral-700: #4B5563;
|
||||
}
|
||||
|
||||
/* Colores de marca como clases de utilidad */
|
||||
.text-navy-primary { color: var(--color-navy-primary) !important; }
|
||||
.text-navy-dark { color: var(--color-navy-dark) !important; }
|
||||
.text-orange-primary { color: var(--color-orange-primary) !important; }
|
||||
.text-neutral-600 { color: var(--color-neutral-600) !important; }
|
||||
.text-neutral-700 { color: var(--color-neutral-700) !important; }
|
||||
|
||||
.bg-neutral-50 { background-color: var(--color-neutral-50) !important; }
|
||||
.bg-neutral-100 { background-color: var(--color-neutral-100) !important; }
|
||||
|
||||
/* Tab Header mejorado */
|
||||
.tab-header {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(30, 58, 95, 0.03) 0%, rgba(255, 134, 0, 0.02) 100%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
border-left: 4px solid var(--color-orange-primary);
|
||||
}
|
||||
|
||||
/* Section Title con icono */
|
||||
.section-title {
|
||||
color: var(--color-navy-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid var(--color-neutral-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section-title .title-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--color-orange-primary), var(--color-orange-hover));
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Form Section Cards mejorados */
|
||||
.form-section.card {
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
/* Toggle Container mejorado */
|
||||
.toggle-container {
|
||||
background: var(--color-neutral-50);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
}
|
||||
|
||||
/* Switch más grande */
|
||||
.form-switch-lg .form-check-input {
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-switch-lg .form-check-input:checked {
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.form-switch-lg .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(255, 134, 0, 0.25);
|
||||
border-color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.form-switch-lg .form-check-label {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Input Group merge (sin borde en el medio) */
|
||||
.input-group-merge .input-group-text {
|
||||
border-right: 0;
|
||||
background-color: var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.input-group-merge .form-control {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.input-group-merge .form-control:focus {
|
||||
border-color: var(--color-orange-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Color Picker mejorado */
|
||||
.color-picker-wrapper .form-control-color {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--color-neutral-100);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.color-picker-wrapper .form-control-color:hover {
|
||||
border-color: var(--color-orange-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(255, 134, 0, 0.15);
|
||||
}
|
||||
|
||||
.color-picker-wrapper .form-control-color::-webkit-color-swatch {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.color-picker-wrapper .color-preview-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.color-picker-wrapper .color-preview-text code {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Alert personalizado */
|
||||
.alert-info-custom {
|
||||
background: linear-gradient(135deg, rgba(255, 134, 0, 0.08), rgba(255, 134, 0, 0.03));
|
||||
border: 1px solid rgba(255, 134, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.alert-info-custom .alert-heading {
|
||||
color: var(--color-navy-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.alert-info-custom p {
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
/* Preview Container */
|
||||
.preview-container {
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.top-bar-preview {
|
||||
animation: fadeInUp 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Botones de marca */
|
||||
.btn-navy-primary {
|
||||
background-color: var(--color-navy-primary);
|
||||
border-color: var(--color-navy-primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-navy-primary:hover {
|
||||
background-color: var(--color-navy-light);
|
||||
border-color: var(--color-navy-light);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(30, 58, 95, 0.2);
|
||||
}
|
||||
|
||||
.btn-orange-primary {
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-orange-primary:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
border-color: var(--color-orange-hover);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(255, 134, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Sticky Footer Actions */
|
||||
.sticky-bottom {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Progress bar para textarea */
|
||||
.progress {
|
||||
background-color: var(--color-neutral-100);
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress .progress-bar {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress .progress-bar.bg-orange-primary {
|
||||
background-color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
/* Badges personalizados */
|
||||
.badge.bg-neutral-100 {
|
||||
background-color: var(--color-neutral-100) !important;
|
||||
}
|
||||
|
||||
.badge.text-neutral-600 {
|
||||
color: var(--color-neutral-600) !important;
|
||||
}
|
||||
|
||||
/* Form control improvements */
|
||||
.form-control-lg {
|
||||
font-size: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.form-select-lg {
|
||||
font-size: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
/* Gap utilities */
|
||||
.g-4 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Border utilities */
|
||||
.border-0 {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.border-neutral-100 {
|
||||
border-color: var(--color-neutral-100) !important;
|
||||
}
|
||||
|
||||
/* Shadow utilities */
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
/* Rounded utilities */
|
||||
.rounded-3 {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.rounded-bottom {
|
||||
border-bottom-left-radius: 0.5rem !important;
|
||||
border-bottom-right-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Card utilities */
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Typography utilities */
|
||||
.fw-bold {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive mejoras */
|
||||
@media (max-width: 768px) {
|
||||
.tab-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tab-header .d-flex {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.color-picker-wrapper .form-control-color {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.form-switch-lg .form-check-input {
|
||||
width: 2.5rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
/**
|
||||
* Theme Options Admin Styles
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* Main Container */
|
||||
.roi-theme-options {
|
||||
margin: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.roi-options-header {
|
||||
background: #fff;
|
||||
border: 1px solid #c3c4c7;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
.roi-options-logo h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #1d2327;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.roi-options-logo .version {
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.roi-options-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.roi-options-actions .button .dashicons {
|
||||
margin-top: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.roi-options-form {
|
||||
background: #fff;
|
||||
border: 1px solid #c3c4c7;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
/* Tabs Container */
|
||||
.roi-options-container {
|
||||
display: flex;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Tabs Navigation */
|
||||
.roi-tabs-nav {
|
||||
width: 200px;
|
||||
background: #f6f7f7;
|
||||
border-right: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.roi-tabs-nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li:first-child {
|
||||
border-top: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a {
|
||||
display: block;
|
||||
padding: 15px 20px;
|
||||
color: #50575e;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a .dashicons {
|
||||
margin-right: 8px;
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a:hover {
|
||||
background: #fff;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a:hover .dashicons {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li.active a {
|
||||
background: #fff;
|
||||
color: #2271b1;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid #2271b1;
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li.active a .dashicons {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
/* Tabs Content */
|
||||
.roi-tabs-content {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.roi-tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roi-tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.roi-tab-pane h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.roi-tab-pane > p.description {
|
||||
margin: 0 0 20px 0;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.roi-tab-pane h3 {
|
||||
margin: 30px 0 0 0;
|
||||
padding: 15px 0 10px 0;
|
||||
border-top: 1px solid #dcdcde;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Form Table */
|
||||
.roi-tab-pane .form-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.roi-tab-pane .form-table th {
|
||||
padding: 20px 10px 20px 0;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.roi-tab-pane .form-table td {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.roi-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.roi-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.roi-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.roi-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .roi-slider {
|
||||
background-color: #2271b1;
|
||||
}
|
||||
|
||||
input:focus + .roi-slider {
|
||||
box-shadow: 0 0 1px #2271b1;
|
||||
}
|
||||
|
||||
input:checked + .roi-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Image Upload */
|
||||
.roi-image-upload {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.roi-image-preview {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #c3c4c7;
|
||||
background: #f6f7f7;
|
||||
padding: 10px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.roi-image-preview:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roi-preview-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.roi-upload-image,
|
||||
.roi-remove-image {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
.roi-options-form .submit {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid #c3c4c7;
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.roi-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.roi-modal-content {
|
||||
background-color: #fff;
|
||||
margin: 10% auto;
|
||||
padding: 30px;
|
||||
border: 1px solid #c3c4c7;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.roi-modal-close {
|
||||
color: #646970;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.roi-modal-close:hover,
|
||||
.roi-modal-close:focus {
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.roi-modal-content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.roi-modal-content textarea {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Notices */
|
||||
.roi-notice {
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
.roi-notice.success {
|
||||
border-left-color: #00a32a;
|
||||
}
|
||||
|
||||
.roi-notice.error {
|
||||
border-left-color: #d63638;
|
||||
}
|
||||
|
||||
.roi-notice.warning {
|
||||
border-left-color: #dba617;
|
||||
}
|
||||
|
||||
.roi-notice.info {
|
||||
border-left-color: #2271b1;
|
||||
}
|
||||
|
||||
/* Code Editor */
|
||||
textarea.code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 782px) {
|
||||
.roi-options-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.roi-tabs-nav {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.roi-tabs-nav ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
border-right: 1px solid #c3c4c7;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a {
|
||||
text-align: center;
|
||||
padding: 12px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.roi-tabs-nav a .dashicons {
|
||||
display: block;
|
||||
margin: 0 auto 5px;
|
||||
}
|
||||
|
||||
.roi-tabs-nav li.active a {
|
||||
border-left: none;
|
||||
border-bottom: 3px solid #2271b1;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.roi-tabs-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.roi-options-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.roi-options-actions {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.roi-options-actions .button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roi-tab-pane .form-table th {
|
||||
width: auto;
|
||||
padding: 15px 10px 5px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.roi-tab-pane .form-table td {
|
||||
display: block;
|
||||
padding: 5px 10px 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.roi-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(0,0,0,.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: #2271b1;
|
||||
animation: roispin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes roispin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Helper Classes */
|
||||
.roi-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.roi-text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roi-mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.roi-mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Color Picker */
|
||||
.wp-picker-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Field Dependencies */
|
||||
.roi-field-dependency {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Success Animation */
|
||||
@keyframes roisaved {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.roi-saved {
|
||||
animation: roisaved 0.3s ease-in-out;
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
/**
|
||||
* Admin Panel Application
|
||||
*
|
||||
* Gestión de configuraciones de componentes del tema
|
||||
*
|
||||
* @package ROI_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
|
||||
const saveBtn = document.getElementById('saveSettings');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => {
|
||||
this.saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Detectar cambios en formularios
|
||||
const enableSaveButton = () => {
|
||||
this.STATE.hasChanges = true;
|
||||
const btn = document.getElementById('saveSettings');
|
||||
if (btn) btn.disabled = false;
|
||||
};
|
||||
|
||||
document.querySelectorAll('input, select, textarea').forEach(input => {
|
||||
// Evento 'input' se dispara mientras se escribe (tiempo real)
|
||||
input.addEventListener('input', enableSaveButton);
|
||||
// Evento 'change' se dispara cuando pierde foco (para select y checkboxes)
|
||||
input.addEventListener('change', enableSaveButton);
|
||||
});
|
||||
|
||||
// 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: roiAdminData.ajaxUrl,
|
||||
data: new URLSearchParams({
|
||||
action: 'roi_get_settings',
|
||||
nonce: roiAdminData.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();
|
||||
|
||||
// Crear FormData para WordPress AJAX
|
||||
const postData = new URLSearchParams();
|
||||
postData.append('action', 'roi_save_settings');
|
||||
postData.append('nonce', roiAdminData.nonce);
|
||||
|
||||
// Agregar components como JSON string
|
||||
postData.append('components', JSON.stringify(formData.components));
|
||||
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: roiAdminData.ajaxUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
data: postData
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.STATE.hasChanges = false;
|
||||
const btn = document.getElementById('saveSettings');
|
||||
if (btn) btn.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í cuando se implemente
|
||||
*/
|
||||
collectFormData() {
|
||||
return {
|
||||
components: {
|
||||
// Los componentes se agregarán aquí cuando se ejecute el algoritmo
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renderizar todos los componentes
|
||||
*/
|
||||
renderAllComponents() {
|
||||
const components = this.STATE.settings.components || {};
|
||||
|
||||
// Los métodos render de componentes se llamarán aquí cuando se implementen
|
||||
},
|
||||
|
||||
/**
|
||||
* 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('.roi-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();
|
||||
});
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* Theme Options Admin JavaScript
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
var ROIThemeOptions = {
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
init: function() {
|
||||
this.tabs();
|
||||
this.imageUpload();
|
||||
this.resetOptions();
|
||||
this.exportOptions();
|
||||
this.importOptions();
|
||||
this.formValidation();
|
||||
this.conditionalFields();
|
||||
},
|
||||
|
||||
/**
|
||||
* Tab Navigation
|
||||
*/
|
||||
tabs: function() {
|
||||
// Tab click handler
|
||||
$('.roi-tabs-nav a').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var tabId = $(this).attr('href');
|
||||
|
||||
// Update active states
|
||||
$('.roi-tabs-nav li').removeClass('active');
|
||||
$(this).parent().addClass('active');
|
||||
|
||||
// Show/hide tab content
|
||||
$('.roi-tab-pane').removeClass('active');
|
||||
$(tabId).addClass('active');
|
||||
|
||||
// Update URL hash without scrolling
|
||||
if (history.pushState) {
|
||||
history.pushState(null, null, tabId);
|
||||
} else {
|
||||
window.location.hash = tabId;
|
||||
}
|
||||
});
|
||||
|
||||
// Load tab from URL hash on page load
|
||||
if (window.location.hash) {
|
||||
var hash = window.location.hash;
|
||||
if ($(hash).length) {
|
||||
$('.roi-tabs-nav a[href="' + hash + '"]').trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
$(window).on('hashchange', function() {
|
||||
if (window.location.hash) {
|
||||
$('.roi-tabs-nav a[href="' + window.location.hash + '"]').trigger('click');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Image Upload
|
||||
*/
|
||||
imageUpload: function() {
|
||||
var self = this;
|
||||
var mediaUploader;
|
||||
|
||||
// Upload button click
|
||||
$(document).on('click', '.roi-upload-image', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var container = button.closest('.roi-image-upload');
|
||||
var preview = container.find('.roi-image-preview');
|
||||
var input = container.find('.roi-image-id');
|
||||
var removeBtn = container.find('.roi-remove-image');
|
||||
|
||||
// If the media uploader already exists, reopen it
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new media uploader
|
||||
mediaUploader = wp.media({
|
||||
title: roiAdminOptions.strings.selectImage,
|
||||
button: {
|
||||
text: roiAdminOptions.strings.useImage
|
||||
},
|
||||
multiple: false
|
||||
});
|
||||
|
||||
// When an image is selected
|
||||
mediaUploader.on('select', function() {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
|
||||
// Set image ID
|
||||
input.val(attachment.id);
|
||||
|
||||
// Show preview
|
||||
var imgUrl = attachment.sizes && attachment.sizes.medium ?
|
||||
attachment.sizes.medium.url : attachment.url;
|
||||
preview.html('<img src="' + imgUrl + '" class="roi-preview-image" />');
|
||||
|
||||
// Show remove button
|
||||
removeBtn.show();
|
||||
});
|
||||
|
||||
// Open the uploader
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
// Remove button click
|
||||
$(document).on('click', '.roi-remove-image', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var container = button.closest('.roi-image-upload');
|
||||
var preview = container.find('.roi-image-preview');
|
||||
var input = container.find('.roi-image-id');
|
||||
|
||||
// Clear values
|
||||
input.val('');
|
||||
preview.empty();
|
||||
button.hide();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset Options
|
||||
*/
|
||||
resetOptions: function() {
|
||||
$('#roi-reset-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm(roiAdminOptions.strings.confirmReset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.ajax({
|
||||
url: roiAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'roi_reset_options',
|
||||
nonce: roiAdminOptions.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
ROIThemeOptions.showNotice('success', response.data.message);
|
||||
|
||||
// Reload page after 1 second
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
ROIThemeOptions.showNotice('error', response.data.message);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Export Options
|
||||
*/
|
||||
exportOptions: function() {
|
||||
$('#roi-export-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.ajax({
|
||||
url: roiAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'roi_export_options',
|
||||
nonce: roiAdminOptions.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Create download link
|
||||
var blob = new Blob([response.data.data], { type: 'application/json' });
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = response.data.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
ROIThemeOptions.showNotice('success', 'Options exported successfully!');
|
||||
} else {
|
||||
ROIThemeOptions.showNotice('error', response.data.message);
|
||||
}
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
},
|
||||
error: function() {
|
||||
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import Options
|
||||
*/
|
||||
importOptions: function() {
|
||||
var modal = $('#roi-import-modal');
|
||||
var importData = $('#roi-import-data');
|
||||
|
||||
// Show modal
|
||||
$('#roi-import-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Close modal
|
||||
$('.roi-modal-close, #roi-import-cancel').on('click', function() {
|
||||
modal.hide();
|
||||
importData.val('');
|
||||
});
|
||||
|
||||
// Close modal on outside click
|
||||
$(window).on('click', function(e) {
|
||||
if ($(e.target).is(modal)) {
|
||||
modal.hide();
|
||||
importData.val('');
|
||||
}
|
||||
});
|
||||
|
||||
// Submit import
|
||||
$('#roi-import-submit').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var data = importData.val().trim();
|
||||
|
||||
if (!data) {
|
||||
alert('Please paste your import data.');
|
||||
return;
|
||||
}
|
||||
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.ajax({
|
||||
url: roiAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'roi_import_options',
|
||||
nonce: roiAdminOptions.nonce,
|
||||
import_data: data
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
ROIThemeOptions.showNotice('success', response.data.message);
|
||||
modal.hide();
|
||||
importData.val('');
|
||||
|
||||
// Reload page after 1 second
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
ROIThemeOptions.showNotice('error', response.data.message);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Form Validation
|
||||
*/
|
||||
formValidation: function() {
|
||||
$('.roi-options-form').on('submit', function(e) {
|
||||
var valid = true;
|
||||
var firstError = null;
|
||||
|
||||
// Validate required fields
|
||||
$(this).find('[required]').each(function() {
|
||||
if (!$(this).val()) {
|
||||
valid = false;
|
||||
$(this).addClass('error');
|
||||
|
||||
if (!firstError) {
|
||||
firstError = $(this);
|
||||
}
|
||||
} else {
|
||||
$(this).removeClass('error');
|
||||
}
|
||||
});
|
||||
|
||||
// Validate number fields
|
||||
$(this).find('input[type="number"]').each(function() {
|
||||
var val = $(this).val();
|
||||
var min = $(this).attr('min');
|
||||
var max = $(this).attr('max');
|
||||
|
||||
if (val && min && parseInt(val) < parseInt(min)) {
|
||||
valid = false;
|
||||
$(this).addClass('error');
|
||||
if (!firstError) {
|
||||
firstError = $(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (val && max && parseInt(val) > parseInt(max)) {
|
||||
valid = false;
|
||||
$(this).addClass('error');
|
||||
if (!firstError) {
|
||||
firstError = $(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Validate URL fields
|
||||
$(this).find('input[type="url"]').each(function() {
|
||||
var val = $(this).val();
|
||||
if (val && !ROIThemeOptions.isValidUrl(val)) {
|
||||
valid = false;
|
||||
$(this).addClass('error');
|
||||
if (!firstError) {
|
||||
firstError = $(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
e.preventDefault();
|
||||
|
||||
if (firstError) {
|
||||
// Scroll to first error
|
||||
$('html, body').animate({
|
||||
scrollTop: firstError.offset().top - 100
|
||||
}, 500);
|
||||
firstError.focus();
|
||||
}
|
||||
|
||||
ROIThemeOptions.showNotice('error', 'Please fix the errors in the form.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add saving animation
|
||||
$(this).find('.submit .button-primary').addClass('updating-message');
|
||||
});
|
||||
|
||||
// Remove error class on input
|
||||
$('.roi-options-form input, .roi-options-form select, .roi-options-form textarea').on('change input', function() {
|
||||
$(this).removeClass('error');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Conditional Fields
|
||||
*/
|
||||
conditionalFields: function() {
|
||||
// Enable/disable related posts options based on checkbox
|
||||
$('#enable_related_posts').on('change', function() {
|
||||
var checked = $(this).is(':checked');
|
||||
var fields = $('#related_posts_count, #related_posts_taxonomy, #related_posts_title, #related_posts_columns');
|
||||
|
||||
fields.closest('tr').toggleClass('roi-field-dependency', !checked);
|
||||
fields.prop('disabled', !checked);
|
||||
}).trigger('change');
|
||||
|
||||
// Enable/disable breadcrumb separator based on breadcrumbs checkbox
|
||||
$('#enable_breadcrumbs').on('change', function() {
|
||||
var checked = $(this).is(':checked');
|
||||
var field = $('#breadcrumb_separator');
|
||||
|
||||
field.closest('tr').toggleClass('roi-field-dependency', !checked);
|
||||
field.prop('disabled', !checked);
|
||||
}).trigger('change');
|
||||
},
|
||||
|
||||
/**
|
||||
* Show Notice
|
||||
*/
|
||||
showNotice: function(type, message) {
|
||||
var notice = $('<div class="notice notice-' + type + ' is-dismissible"><p>' + message + '</p></div>');
|
||||
|
||||
$('.roi-theme-options h1').after(notice);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
notice.fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Scroll to top
|
||||
$('html, body').animate({ scrollTop: 0 }, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate URL
|
||||
*/
|
||||
isValidUrl: function(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(document).ready(function() {
|
||||
ROIThemeOptions.init();
|
||||
});
|
||||
|
||||
// Make it globally accessible
|
||||
window.ROIThemeOptions = ROIThemeOptions;
|
||||
|
||||
})(jQuery);
|
||||
@@ -1,205 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Menu Class
|
||||
*
|
||||
* Registra menú en WordPress admin y carga assets
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ROI_Admin_Menu {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('admin_menu', array($this, 'register_menu'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar página de admin
|
||||
* Crea menú de nivel superior en sidebar (NO dentro de Apariencia)
|
||||
*/
|
||||
public function register_menu() {
|
||||
// Menú principal de nivel superior (sin callback para que sea solo contenedor)
|
||||
add_menu_page(
|
||||
'ROI Theme', // Page title
|
||||
'ROI Theme', // Menu title
|
||||
'manage_options', // Capability
|
||||
'roi-theme-menu', // Menu slug (solo identificador, no página real)
|
||||
'', // Sin callback = solo contenedor
|
||||
'dashicons-admin-generic', // Icon (WordPress Dashicon)
|
||||
61 // Position (61 = después de Appearance que es 60)
|
||||
);
|
||||
|
||||
// Submenú 1: "Theme Options" (formulario viejo de apariencia)
|
||||
add_submenu_page(
|
||||
'roi-theme-menu', // Parent slug
|
||||
'Theme Options', // Page title
|
||||
'Theme Options', // Menu title
|
||||
'manage_options', // Capability
|
||||
'roi-theme-settings', // Menu slug
|
||||
array($this, 'render_admin_page') // Callback
|
||||
);
|
||||
|
||||
// Submenú 2: "Componentes" (nuevo sistema de tabs)
|
||||
add_submenu_page(
|
||||
'roi-theme-menu', // Parent slug
|
||||
'Componentes', // Page title
|
||||
'Componentes', // Menu title
|
||||
'manage_options', // Capability
|
||||
'roi-theme-components', // Menu slug
|
||||
array($this, 'render_components_page') // Callback
|
||||
);
|
||||
|
||||
// Remover el primer submenú duplicado que WordPress crea automáticamente
|
||||
remove_submenu_page('roi-theme-menu', 'roi-theme-menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderizar página de Theme Options (formulario viejo)
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('No tienes permisos para acceder a esta página.'));
|
||||
}
|
||||
|
||||
// Cargar el formulario viejo de theme options
|
||||
require_once get_template_directory() . '/admin/theme-options/options-page-template.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderizar página de Componentes (nuevo sistema de tabs)
|
||||
*/
|
||||
public function render_components_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('No tienes permisos para acceder a esta página.'));
|
||||
}
|
||||
|
||||
// Cargar el nuevo admin panel con tabs de componentes
|
||||
require_once ROI_ADMIN_PANEL_PATH . 'pages/main.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encolar assets (CSS/JS)
|
||||
*/
|
||||
public function enqueue_assets($hook) {
|
||||
// Solo cargar en nuestras páginas de admin
|
||||
$allowed_hooks = array(
|
||||
'roi-theme_page_roi-theme-settings', // Theme Options
|
||||
'roi-theme_page_roi-theme-components' // Componentes
|
||||
);
|
||||
|
||||
if (!in_array($hook, $allowed_hooks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CSS y JS específico para Theme Options (formulario viejo)
|
||||
if ($hook === 'roi-theme_page_roi-theme-settings') {
|
||||
// Enqueue WordPress media uploader
|
||||
wp_enqueue_media();
|
||||
|
||||
// Enqueue admin styles para theme options
|
||||
wp_enqueue_style(
|
||||
'roi-admin-options',
|
||||
get_template_directory_uri() . '/admin/assets/css/theme-options.css',
|
||||
array(),
|
||||
ROI_VERSION
|
||||
);
|
||||
|
||||
// Enqueue admin scripts para theme options
|
||||
wp_enqueue_script(
|
||||
'roi-admin-options',
|
||||
get_template_directory_uri() . '/admin/assets/js/theme-options.js',
|
||||
array('jquery', 'wp-color-picker'),
|
||||
ROI_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize script
|
||||
wp_localize_script('roi-admin-options', 'roiAdminOptions', array(
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('roi_admin_nonce'),
|
||||
'strings' => array(
|
||||
'selectImage' => __('Select Image', 'roi-theme'),
|
||||
'useImage' => __('Use Image', 'roi-theme'),
|
||||
'removeImage' => __('Remove Image', 'roi-theme'),
|
||||
'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'),
|
||||
'saved' => __('Settings saved successfully!', 'roi-theme'),
|
||||
'error' => __('An error occurred while saving settings.', 'roi-theme'),
|
||||
),
|
||||
));
|
||||
|
||||
// No cargar Bootstrap ni otros assets del nuevo panel
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootstrap 5.3.2 CSS (solo para Componentes)
|
||||
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 (Core)
|
||||
wp_enqueue_style(
|
||||
'roi-admin-panel-css',
|
||||
ROI_ADMIN_PANEL_URL . 'assets/css/admin-panel.css',
|
||||
array('bootstrap'),
|
||||
ROI_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 (Core)
|
||||
wp_enqueue_script(
|
||||
'roi-admin-panel-js',
|
||||
ROI_ADMIN_PANEL_URL . 'assets/js/admin-app.js',
|
||||
array('jquery', 'axios'),
|
||||
ROI_ADMIN_PANEL_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar datos a JavaScript
|
||||
wp_localize_script('roi-admin-panel-js', 'roiAdminData', array(
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('roi_admin_nonce')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Instanciar clase
|
||||
new ROI_Admin_Menu();
|
||||
@@ -1,318 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Manager Class
|
||||
*
|
||||
* Gestión de tablas personalizadas del tema
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.2.0
|
||||
* @deprecated 2.0.0 Use ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository instead
|
||||
* @see ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository
|
||||
*
|
||||
* Esta clase será eliminada en la versión 3.0.0
|
||||
* Por favor migre su código a la nueva arquitectura Clean Architecture
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ROI_DB_Manager
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
class ROI_DB_Manager {
|
||||
|
||||
/**
|
||||
* Nombre de la tabla de componentes (sin prefijo)
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
const TABLE_COMPONENTS = 'roi_theme_components';
|
||||
|
||||
/**
|
||||
* Nombre de la tabla de defaults (sin prefijo)
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
const TABLE_DEFAULTS = 'roi_theme_components_defaults';
|
||||
|
||||
/**
|
||||
* Versión de la base de datos
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
const DB_VERSION = '1.0';
|
||||
|
||||
/**
|
||||
* Opción para almacenar la versión de la DB
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
const DB_VERSION_OPTION = 'roi_db_version';
|
||||
|
||||
/**
|
||||
* @var \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter
|
||||
*/
|
||||
private $adapter;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*/
|
||||
public function __construct() {
|
||||
_deprecated_function(
|
||||
__CLASS__ . '::__construct',
|
||||
'2.0.0',
|
||||
'ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository'
|
||||
);
|
||||
|
||||
$this->logDeprecation(__CLASS__, __FUNCTION__);
|
||||
|
||||
// Hook para verificar/actualizar DB en cada carga
|
||||
add_action('admin_init', array($this, 'maybe_create_tables'));
|
||||
|
||||
// Inicializar adapter para mantener compatibilidad
|
||||
$this->adapter = $this->getLegacyAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener nombre completo de tabla con prefijo
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*
|
||||
* @param string $table_type Tipo de tabla: 'components' (personalizaciones) o 'defaults' (valores por defecto)
|
||||
* @return string Nombre completo de la tabla con prefijo
|
||||
*/
|
||||
public function get_table_name($table_type = 'components') {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'Direct database access not recommended - use repositories'
|
||||
);
|
||||
|
||||
global $wpdb;
|
||||
|
||||
if ($table_type === 'defaults') {
|
||||
return $wpdb->prefix . self::TABLE_DEFAULTS;
|
||||
}
|
||||
|
||||
return $wpdb->prefix . self::TABLE_COMPONENTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si las tablas necesitan ser creadas o actualizadas
|
||||
*
|
||||
* @deprecated 2.0.0 Tables are now managed through database migrations
|
||||
*/
|
||||
public function maybe_create_tables() {
|
||||
// Keep for backward compatibility but tables are managed differently now
|
||||
$installed_version = get_option(self::DB_VERSION_OPTION);
|
||||
|
||||
if ($installed_version !== self::DB_VERSION) {
|
||||
$this->create_tables();
|
||||
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear tablas personalizadas
|
||||
*
|
||||
* @deprecated 2.0.0 Use DatabaseMigrator service instead
|
||||
*/
|
||||
public function create_tables() {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'ROITheme\Infrastructure\Services\DatabaseMigrator'
|
||||
);
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
// Table structure (kept for backward compatibility)
|
||||
$table_structure = "(
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
component_name VARCHAR(50) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL,
|
||||
config_value TEXT NOT NULL,
|
||||
data_type ENUM('string', 'boolean', 'integer', 'json') DEFAULT 'string',
|
||||
version VARCHAR(10) DEFAULT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY component_config (component_name, config_key),
|
||||
INDEX idx_component (component_name),
|
||||
INDEX idx_updated (updated_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Create components table
|
||||
$table_components = $this->get_table_name('components');
|
||||
$sql_components = "CREATE TABLE IF NOT EXISTS $table_components $table_structure";
|
||||
dbDelta($sql_components);
|
||||
|
||||
// Create defaults table
|
||||
$table_defaults = $this->get_table_name('defaults');
|
||||
$sql_defaults = "CREATE TABLE IF NOT EXISTS $table_defaults $table_structure";
|
||||
dbDelta($sql_defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si una tabla existe
|
||||
*
|
||||
* @deprecated 2.0.0
|
||||
*
|
||||
* @param string $table_type Tipo de tabla
|
||||
* @return bool True si existe, false si no
|
||||
*/
|
||||
public function table_exists($table_type = 'components') {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name($table_type);
|
||||
$query = $wpdb->prepare('SHOW TABLES LIKE %s', $table_name);
|
||||
return $wpdb->get_var($query) === $table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save component configuration
|
||||
*
|
||||
* @deprecated 2.0.0 Use SaveComponentUseCase::execute() instead
|
||||
*
|
||||
* @param string $component_name Component name (e.g., 'top_notification_bar', 'navbar', 'footer', 'hero_section')
|
||||
* @param string $config_key Configuration key
|
||||
* @param mixed $config_value Configuration value
|
||||
* @param string $data_type Data type (string, boolean, integer, array)
|
||||
* @param string|null $version Schema version
|
||||
* @param string $table_type Table type (components or defaults)
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function save_config($component_name, $config_key, $config_value, $data_type = 'string', $version = null, $table_type = 'components') {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'ROITheme\Application\UseCases\SaveComponent\SaveComponentUseCase::execute()'
|
||||
);
|
||||
|
||||
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
|
||||
|
||||
// Delegar al adapter para mantener funcionalidad
|
||||
return $this->adapter->save_config(
|
||||
$component_name,
|
||||
$config_key,
|
||||
$config_value,
|
||||
$data_type,
|
||||
$version,
|
||||
$table_type
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get component configuration
|
||||
*
|
||||
* @deprecated 2.0.0 Use GetComponentUseCase::execute() instead
|
||||
*
|
||||
* @param string $component_name Component name
|
||||
* @param string|null $config_key Specific configuration key (null for all)
|
||||
* @param string $table_type Table type (components or defaults)
|
||||
* @return mixed Configuration value(s) or null
|
||||
*/
|
||||
public function get_config($component_name, $config_key = null, $table_type = 'components') {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'ROITheme\Application\UseCases\GetComponent\GetComponentUseCase::execute()'
|
||||
);
|
||||
|
||||
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
|
||||
|
||||
return $this->adapter->get_config($component_name, $config_key, $table_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete component configuration
|
||||
*
|
||||
* @deprecated 2.0.0 Use DeleteComponentUseCase::execute() instead
|
||||
*
|
||||
* @param string $component_name Component name
|
||||
* @param string|null $config_key Specific configuration key (null for all component)
|
||||
* @param string $table_type Table type (components or defaults)
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function delete_config($component_name, $config_key = null, $table_type = 'components') {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'ROITheme\Application\UseCases\DeleteComponent\DeleteComponentUseCase::execute()'
|
||||
);
|
||||
|
||||
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
|
||||
|
||||
// If deleting specific key, not supported in new architecture
|
||||
// Delete entire component instead
|
||||
return $this->adapter->delete_config($component_name, $table_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all components
|
||||
*
|
||||
* @deprecated 2.0.0 Use ComponentRepository::findAll() instead
|
||||
*
|
||||
* @param string $table_type Table type
|
||||
* @return array List of components
|
||||
*/
|
||||
public function list_components($table_type = 'components') {
|
||||
_deprecated_function(
|
||||
__FUNCTION__,
|
||||
'2.0.0',
|
||||
'ROITheme\Domain\Contracts\ComponentRepositoryInterface::findAll()'
|
||||
);
|
||||
|
||||
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
|
||||
|
||||
global $wpdb;
|
||||
$table = $this->get_table_name($table_type);
|
||||
|
||||
$query = "SELECT DISTINCT component_name FROM $table ORDER BY component_name ASC";
|
||||
$components = $wpdb->get_col($query);
|
||||
|
||||
return $components ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legacy adapter
|
||||
*
|
||||
* @return \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter
|
||||
*/
|
||||
private function getLegacyAdapter() {
|
||||
if (!class_exists('ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter')) {
|
||||
require_once get_template_directory() . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
return new \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log deprecation usage
|
||||
*
|
||||
* @param string $class Class name
|
||||
* @param string $method Method name
|
||||
* @param array $args Arguments
|
||||
*/
|
||||
private function logDeprecation($class, $method, $args = array()) {
|
||||
if (!class_exists('ROITheme\Infrastructure\Logging\DeprecationLogger')) {
|
||||
require_once get_template_directory() . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
$logger = \ROITheme\Infrastructure\Logging\DeprecationLogger::getInstance();
|
||||
$logger->log(
|
||||
$class,
|
||||
$method,
|
||||
$args,
|
||||
'See documentation for Clean Architecture migration',
|
||||
'2.0.0'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings Manager Class
|
||||
*
|
||||
* CRUD de configuraciones por componentes
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ROI_Settings_Manager {
|
||||
|
||||
const OPTION_NAME = 'roi_theme_settings';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_roi_get_settings', array($this, 'ajax_get_settings'));
|
||||
add_action('wp_ajax_roi_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 ROI_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'] = ROI_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
|
||||
* Lee los defaults desde la tabla wp_roi_theme_components_defaults
|
||||
*/
|
||||
public function get_defaults() {
|
||||
$db_manager = new ROI_DB_Manager();
|
||||
$component_names = $db_manager->list_components('defaults');
|
||||
|
||||
$defaults = array(
|
||||
'version' => ROI_ADMIN_PANEL_VERSION,
|
||||
'components' => array()
|
||||
);
|
||||
|
||||
// Obtener configuraciones de cada componente desde la tabla de defaults
|
||||
foreach ($component_names as $component_name) {
|
||||
$defaults['components'][$component_name] = $db_manager->get_config($component_name, null, 'defaults');
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizar configuraciones
|
||||
* NOTA: Los sanitizers de componentes se ejecutarán aquí cuando se implementen
|
||||
*/
|
||||
public function sanitize_settings($data) {
|
||||
$sanitized = array(
|
||||
'components' => array()
|
||||
);
|
||||
|
||||
// Los componentes se sanitizarán aquí cuando se ejecute el algoritmo
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Obtener configuraciones
|
||||
*/
|
||||
public function ajax_get_settings() {
|
||||
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
|
||||
check_ajax_referer('roi_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() {
|
||||
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
|
||||
check_ajax_referer('roi_admin_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Permisos insuficientes');
|
||||
}
|
||||
|
||||
// Los datos vienen como JSON string en $_POST['components']
|
||||
if (!isset($_POST['components'])) {
|
||||
wp_send_json_error('Datos inválidos - falta components');
|
||||
}
|
||||
|
||||
$components = json_decode(stripslashes($_POST['components']), true);
|
||||
|
||||
if (!is_array($components)) {
|
||||
wp_send_json_error('Datos inválidos - components no es un array válido');
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'components' => $components
|
||||
);
|
||||
|
||||
$result = $this->save_settings($data);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
} else {
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instanciar clase
|
||||
new ROI_Settings_Manager();
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Validator Class
|
||||
*
|
||||
* Validación de datos por componentes
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ROI_Validator {
|
||||
|
||||
/**
|
||||
* Validar todas las configuraciones
|
||||
* Los validators de componentes se ejecutarán aquí cuando se implementen
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
// Los componentes se validarán aquí cuando se ejecute el algoritmo
|
||||
|
||||
return array(
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Sanitizer Helper
|
||||
*
|
||||
* Métodos estáticos reutilizables para sanitización de datos
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class ROI_Sanitizer_Helper
|
||||
*
|
||||
* Proporciona métodos estáticos para sanitización común,
|
||||
* eliminando código duplicado en los sanitizadores de componentes
|
||||
*/
|
||||
class ROI_Sanitizer_Helper {
|
||||
|
||||
/**
|
||||
* Sanitiza un valor booleano
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @return bool Valor booleano sanitizado
|
||||
*/
|
||||
public static function sanitize_boolean($data, $key) {
|
||||
return !empty($data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples valores booleanos
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $keys Array de claves a sanitizar
|
||||
* @return array Array asociativo con valores booleanos sanitizados
|
||||
*/
|
||||
public static function sanitize_booleans($data, $keys) {
|
||||
$result = array();
|
||||
foreach ($keys as $key) {
|
||||
$result[$key] = self::sanitize_boolean($data, $key);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un campo de texto con valor por defecto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param string $default Valor por defecto (default: '')
|
||||
* @return string Texto sanitizado
|
||||
*/
|
||||
public static function sanitize_text($data, $key, $default = '') {
|
||||
return sanitize_text_field($data[$key] ?? $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples campos de texto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $keys Array de claves a sanitizar
|
||||
* @param string $default Valor por defecto para todos (default: '')
|
||||
* @return array Array asociativo con textos sanitizados
|
||||
*/
|
||||
public static function sanitize_texts($data, $keys, $default = '') {
|
||||
$result = array();
|
||||
foreach ($keys as $key) {
|
||||
$result[$key] = self::sanitize_text($data, $key, $default);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un color hexadecimal con valor por defecto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param string $default Valor por defecto (default: '')
|
||||
* @return string Color hexadecimal sanitizado
|
||||
*/
|
||||
public static function sanitize_color($data, $key, $default = '') {
|
||||
return sanitize_hex_color($data[$key] ?? $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples colores hexadecimales
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $keys Array de claves a sanitizar
|
||||
* @param string $default Valor por defecto para todos (default: '')
|
||||
* @return array Array asociativo con colores sanitizados
|
||||
*/
|
||||
public static function sanitize_colors($data, $keys, $default = '') {
|
||||
$result = array();
|
||||
foreach ($keys as $key) {
|
||||
$result[$key] = self::sanitize_color($data, $key, $default);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un valor con validación enum (in_array)
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param array $allowed_values Valores permitidos
|
||||
* @param mixed $default Valor por defecto
|
||||
* @return mixed Valor sanitizado
|
||||
*/
|
||||
public static function sanitize_enum($data, $key, $allowed_values, $default) {
|
||||
return in_array($data[$key] ?? '', $allowed_values, true)
|
||||
? $data[$key]
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples valores enum
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $config Array de configuración [key => ['allowed' => [...], 'default' => ...]]
|
||||
* @return array Array asociativo con valores enum sanitizados
|
||||
*/
|
||||
public static function sanitize_enums($data, $config) {
|
||||
$result = array();
|
||||
foreach ($config as $key => $settings) {
|
||||
$result[$key] = self::sanitize_enum(
|
||||
$data,
|
||||
$key,
|
||||
$settings['allowed'],
|
||||
$settings['default']
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un valor entero con valor por defecto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param int $default Valor por defecto
|
||||
* @return int Entero sanitizado
|
||||
*/
|
||||
public static function sanitize_int($data, $key, $default = 0) {
|
||||
return isset($data[$key]) ? intval($data[$key]) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples valores enteros
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $config Array de configuración [key => default_value]
|
||||
* @return array Array asociativo con enteros sanitizados
|
||||
*/
|
||||
public static function sanitize_ints($data, $config) {
|
||||
$result = array();
|
||||
foreach ($config as $key => $default) {
|
||||
$result[$key] = self::sanitize_int($data, $key, $default);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un valor float con valor por defecto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param float $default Valor por defecto
|
||||
* @return float Float sanitizado
|
||||
*/
|
||||
public static function sanitize_float($data, $key, $default = 0.0) {
|
||||
return isset($data[$key]) ? floatval($data[$key]) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza múltiples valores float
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param array $config Array de configuración [key => default_value]
|
||||
* @return array Array asociativo con floats sanitizados
|
||||
*/
|
||||
public static function sanitize_floats($data, $config) {
|
||||
$result = array();
|
||||
foreach ($config as $key => $default) {
|
||||
$result[$key] = self::sanitize_float($data, $key, $default);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza una URL con valor por defecto
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param string $default Valor por defecto (default: '')
|
||||
* @return string URL sanitizada
|
||||
*/
|
||||
public static function sanitize_url($data, $key, $default = '') {
|
||||
return esc_url_raw($data[$key] ?? $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un array de strings
|
||||
*
|
||||
* @param array $data Array de datos
|
||||
* @param string $key Clave del dato
|
||||
* @param array $default Array por defecto
|
||||
* @return array Array de strings sanitizados
|
||||
*/
|
||||
public static function sanitize_array_of_strings($data, $key, $default = array()) {
|
||||
return isset($data[$key]) && is_array($data[$key])
|
||||
? array_map('sanitize_text_field', $data[$key])
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un grupo de campos anidados (custom_styles, dropdown, etc.)
|
||||
*
|
||||
* @param array $data Array de datos completo
|
||||
* @param string $group_key Clave del grupo (ej: 'custom_styles')
|
||||
* @param array $sanitization_rules Reglas de sanitización por campo
|
||||
* Formato: [
|
||||
* 'campo' => ['type' => 'text|color|int|float|enum|bool', 'default' => valor, 'allowed' => array()]
|
||||
* ]
|
||||
* @return array Array con campos del grupo sanitizados
|
||||
*/
|
||||
public static function sanitize_nested_group($data, $group_key, $sanitization_rules) {
|
||||
$result = array();
|
||||
$group_data = $data[$group_key] ?? array();
|
||||
|
||||
foreach ($sanitization_rules as $field => $rule) {
|
||||
$type = $rule['type'];
|
||||
$default = $rule['default'] ?? null;
|
||||
|
||||
switch ($type) {
|
||||
case 'text':
|
||||
$result[$field] = self::sanitize_text($group_data, $field, $default ?? '');
|
||||
break;
|
||||
case 'color':
|
||||
$result[$field] = self::sanitize_color($group_data, $field, $default ?? '');
|
||||
break;
|
||||
case 'int':
|
||||
$result[$field] = self::sanitize_int($group_data, $field, $default ?? 0);
|
||||
break;
|
||||
case 'float':
|
||||
$result[$field] = self::sanitize_float($group_data, $field, $default ?? 0.0);
|
||||
break;
|
||||
case 'enum':
|
||||
$result[$field] = self::sanitize_enum(
|
||||
$group_data,
|
||||
$field,
|
||||
$rule['allowed'] ?? array(),
|
||||
$default
|
||||
);
|
||||
break;
|
||||
case 'bool':
|
||||
$result[$field] = self::sanitize_boolean($group_data, $field);
|
||||
break;
|
||||
default:
|
||||
$result[$field] = $group_data[$field] ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel Module - Initialization
|
||||
*
|
||||
* Sistema de configuración por componentes
|
||||
* Cada componente del tema es configurable desde el admin panel
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Module constants
|
||||
define('ROI_ADMIN_PANEL_VERSION', '2.1.4');
|
||||
define('ROI_ADMIN_PANEL_PATH', get_template_directory() . '/admin/');
|
||||
define('ROI_ADMIN_PANEL_URL', get_template_directory_uri() . '/admin/');
|
||||
|
||||
// Load classes
|
||||
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-admin-menu.php';
|
||||
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-db-manager.php';
|
||||
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-validator.php';
|
||||
|
||||
// Settings Manager
|
||||
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-settings-manager.php';
|
||||
|
||||
// Initialize Database Manager
|
||||
new ROI_DB_Manager();
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel - Main Page
|
||||
*
|
||||
* Interfaz de administración de componentes del tema
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<!-- Tabs de componentes se generarán aquí cuando se ejecute el algoritmo -->
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<!-- Contenido de tabs de componentes se generará aquí cuando se ejecute el algoritmo -->
|
||||
</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>
|
||||
@@ -1,394 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Options Usage Examples
|
||||
*
|
||||
* This file contains examples of how to use theme options throughout the theme.
|
||||
* DO NOT include this file in functions.php - it's for reference only.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 1: Using options in header.php
|
||||
*/
|
||||
function example_display_logo() {
|
||||
$logo_url = roi_get_logo_url();
|
||||
|
||||
if ($logo_url) {
|
||||
?>
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="custom-logo-link">
|
||||
<img src="<?php echo esc_url($logo_url); ?>" alt="<?php bloginfo('name'); ?>" class="custom-logo" />
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<h1 class="site-title">
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>"><?php bloginfo('name'); ?></a>
|
||||
</h1>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 2: Displaying breadcrumbs
|
||||
*/
|
||||
function example_show_breadcrumbs() {
|
||||
if (roi_show_breadcrumbs() && !is_front_page()) {
|
||||
$separator = roi_get_breadcrumb_separator();
|
||||
|
||||
echo '<nav class="breadcrumbs">';
|
||||
echo '<a href="' . esc_url(home_url('/')) . '">Home</a>';
|
||||
echo ' ' . esc_html($separator) . ' ';
|
||||
|
||||
if (is_single()) {
|
||||
the_category(' ' . esc_html($separator) . ' ');
|
||||
echo ' ' . esc_html($separator) . ' ';
|
||||
the_title();
|
||||
} elseif (is_category()) {
|
||||
single_cat_title();
|
||||
}
|
||||
|
||||
echo '</nav>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 3: Customizing excerpt
|
||||
*/
|
||||
function example_custom_excerpt_length($length) {
|
||||
return roi_get_excerpt_length();
|
||||
}
|
||||
add_filter('excerpt_length', 'example_custom_excerpt_length');
|
||||
|
||||
function example_custom_excerpt_more($more) {
|
||||
return roi_get_excerpt_more();
|
||||
}
|
||||
add_filter('excerpt_more', 'example_custom_excerpt_more');
|
||||
|
||||
/**
|
||||
* EXAMPLE 4: Displaying related posts in single.php
|
||||
*/
|
||||
function example_display_related_posts() {
|
||||
if (roi_show_related_posts() && is_single()) {
|
||||
$count = roi_get_related_posts_count();
|
||||
$taxonomy = roi_get_related_posts_taxonomy();
|
||||
$title = roi_get_related_posts_title();
|
||||
|
||||
// Get related posts
|
||||
$post_id = get_the_ID();
|
||||
$args = array(
|
||||
'posts_per_page' => $count,
|
||||
'post__not_in' => array($post_id),
|
||||
);
|
||||
|
||||
if ($taxonomy === 'category') {
|
||||
$categories = wp_get_post_categories($post_id);
|
||||
if ($categories) {
|
||||
$args['category__in'] = $categories;
|
||||
}
|
||||
} elseif ($taxonomy === 'tag') {
|
||||
$tags = wp_get_post_tags($post_id, array('fields' => 'ids'));
|
||||
if ($tags) {
|
||||
$args['tag__in'] = $tags;
|
||||
}
|
||||
}
|
||||
|
||||
$related = new WP_Query($args);
|
||||
|
||||
if ($related->have_posts()) {
|
||||
?>
|
||||
<div class="related-posts">
|
||||
<h3><?php echo esc_html($title); ?></h3>
|
||||
<div class="related-posts-grid">
|
||||
<?php
|
||||
while ($related->have_posts()) {
|
||||
$related->the_post();
|
||||
?>
|
||||
<article class="related-post-item">
|
||||
<?php if (has_post_thumbnail()) : ?>
|
||||
<a href="<?php the_permalink(); ?>">
|
||||
<?php the_post_thumbnail('roi-thumbnail'); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<h4>
|
||||
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
|
||||
</h4>
|
||||
<div class="post-meta">
|
||||
<time datetime="<?php echo get_the_date('c'); ?>">
|
||||
<?php echo get_the_date(roi_get_date_format()); ?>
|
||||
</time>
|
||||
</div>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
wp_reset_postdata();
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 5: Conditional comments display
|
||||
*/
|
||||
function example_maybe_show_comments() {
|
||||
if (is_single() && roi_comments_enabled_for_posts()) {
|
||||
comments_template();
|
||||
} elseif (is_page() && roi_comments_enabled_for_pages()) {
|
||||
comments_template();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 6: Featured image on single posts
|
||||
*/
|
||||
function example_display_featured_image() {
|
||||
if (is_single() && roi_show_featured_image_single() && has_post_thumbnail()) {
|
||||
?>
|
||||
<div class="post-thumbnail">
|
||||
<?php the_post_thumbnail('roi-featured-large'); ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 7: Author box on single posts
|
||||
*/
|
||||
function example_display_author_box() {
|
||||
if (is_single() && roi_show_author_box()) {
|
||||
$author_id = get_the_author_meta('ID');
|
||||
?>
|
||||
<div class="author-box">
|
||||
<div class="author-avatar">
|
||||
<?php echo get_avatar($author_id, 80); ?>
|
||||
</div>
|
||||
<div class="author-info">
|
||||
<h4 class="author-name"><?php the_author(); ?></h4>
|
||||
<p class="author-bio"><?php the_author_meta('description'); ?></p>
|
||||
<a href="<?php echo get_author_posts_url($author_id); ?>" class="author-link">
|
||||
<?php _e('View all posts', 'roi-theme'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 8: Social media links in footer
|
||||
*/
|
||||
function example_display_social_links() {
|
||||
$social_links = roi_get_social_links();
|
||||
|
||||
// Filter out empty links
|
||||
$social_links = array_filter($social_links);
|
||||
|
||||
if (!empty($social_links)) {
|
||||
?>
|
||||
<div class="social-links">
|
||||
<?php foreach ($social_links as $network => $url) : ?>
|
||||
<a href="<?php echo esc_url($url); ?>"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="social-link social-<?php echo esc_attr($network); ?>">
|
||||
<span class="screen-reader-text"><?php echo ucfirst($network); ?></span>
|
||||
<i class="icon-<?php echo esc_attr($network); ?>"></i>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 9: Copyright text in footer
|
||||
*/
|
||||
function example_display_copyright() {
|
||||
$copyright = roi_get_copyright_text();
|
||||
|
||||
if ($copyright) {
|
||||
echo '<div class="copyright">' . wp_kses_post($copyright) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 10: Custom CSS in header
|
||||
*/
|
||||
function example_add_custom_css() {
|
||||
$custom_css = roi_get_custom_css();
|
||||
|
||||
if ($custom_css) {
|
||||
echo '<style type="text/css">' . "\n";
|
||||
echo strip_tags($custom_css);
|
||||
echo "\n</style>\n";
|
||||
}
|
||||
}
|
||||
add_action('wp_head', 'example_add_custom_css', 100);
|
||||
|
||||
/**
|
||||
* EXAMPLE 11: Custom JS in header
|
||||
*/
|
||||
function example_add_custom_js_header() {
|
||||
$custom_js = roi_get_custom_js_header();
|
||||
|
||||
if ($custom_js) {
|
||||
echo '<script type="text/javascript">' . "\n";
|
||||
echo $custom_js;
|
||||
echo "\n</script>\n";
|
||||
}
|
||||
}
|
||||
add_action('wp_head', 'example_add_custom_js_header', 100);
|
||||
|
||||
/**
|
||||
* EXAMPLE 12: Custom JS in footer
|
||||
*/
|
||||
function example_add_custom_js_footer() {
|
||||
$custom_js = roi_get_custom_js_footer();
|
||||
|
||||
if ($custom_js) {
|
||||
echo '<script type="text/javascript">' . "\n";
|
||||
echo $custom_js;
|
||||
echo "\n</script>\n";
|
||||
}
|
||||
}
|
||||
add_action('wp_footer', 'example_add_custom_js_footer', 100);
|
||||
|
||||
/**
|
||||
* EXAMPLE 13: Posts per page for archives
|
||||
*/
|
||||
function example_set_archive_posts_per_page($query) {
|
||||
if ($query->is_archive() && !is_admin() && $query->is_main_query()) {
|
||||
$posts_per_page = roi_get_archive_posts_per_page();
|
||||
$query->set('posts_per_page', $posts_per_page);
|
||||
}
|
||||
}
|
||||
add_action('pre_get_posts', 'example_set_archive_posts_per_page');
|
||||
|
||||
/**
|
||||
* EXAMPLE 14: Performance optimizations
|
||||
*/
|
||||
function example_apply_performance_settings() {
|
||||
// Remove emoji scripts
|
||||
if (roi_is_performance_enabled('remove_emoji')) {
|
||||
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
||||
remove_action('wp_print_styles', 'print_emoji_styles');
|
||||
}
|
||||
|
||||
// Remove embeds
|
||||
if (roi_is_performance_enabled('remove_embeds')) {
|
||||
wp_deregister_script('wp-embed');
|
||||
}
|
||||
|
||||
// Remove Dashicons for non-logged users
|
||||
if (roi_is_performance_enabled('remove_dashicons') && !is_user_logged_in()) {
|
||||
wp_deregister_style('dashicons');
|
||||
}
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'example_apply_performance_settings', 100);
|
||||
|
||||
/**
|
||||
* EXAMPLE 15: Lazy loading images
|
||||
*/
|
||||
function example_add_lazy_loading($attr, $attachment, $size) {
|
||||
if (roi_is_lazy_loading_enabled()) {
|
||||
$attr['loading'] = 'lazy';
|
||||
}
|
||||
return $attr;
|
||||
}
|
||||
add_filter('wp_get_attachment_image_attributes', 'example_add_lazy_loading', 10, 3);
|
||||
|
||||
/**
|
||||
* EXAMPLE 16: Layout classes based on settings
|
||||
*/
|
||||
function example_get_layout_class() {
|
||||
$layout = 'right-sidebar'; // default
|
||||
|
||||
if (is_single()) {
|
||||
$layout = roi_get_default_post_layout();
|
||||
} elseif (is_page()) {
|
||||
$layout = roi_get_default_page_layout();
|
||||
}
|
||||
|
||||
return 'layout-' . $layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 17: Display post meta conditionally
|
||||
*/
|
||||
function example_display_post_meta() {
|
||||
if (!roi_get_option('show_post_meta', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="post-meta">
|
||||
<span class="post-date">
|
||||
<time datetime="<?php echo get_the_date('c'); ?>">
|
||||
<?php echo get_the_date(roi_get_date_format()); ?>
|
||||
</time>
|
||||
</span>
|
||||
<span class="post-author">
|
||||
<?php the_author(); ?>
|
||||
</span>
|
||||
<?php if (roi_get_option('show_post_categories', true)) : ?>
|
||||
<span class="post-categories">
|
||||
<?php the_category(', '); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 18: Display post tags conditionally
|
||||
*/
|
||||
function example_display_post_tags() {
|
||||
if (is_single() && roi_get_option('show_post_tags', true)) {
|
||||
the_tags('<div class="post-tags">', ', ', '</div>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXAMPLE 19: Get all options (for debugging)
|
||||
*/
|
||||
function example_debug_all_options() {
|
||||
if (current_user_can('manage_options') && isset($_GET['debug_options'])) {
|
||||
$all_options = roi_get_all_options();
|
||||
echo '<pre>';
|
||||
print_r($all_options);
|
||||
echo '</pre>';
|
||||
}
|
||||
}
|
||||
add_action('wp_footer', 'example_debug_all_options');
|
||||
|
||||
/**
|
||||
* EXAMPLE 20: Check if specific feature is enabled
|
||||
*/
|
||||
function example_check_feature() {
|
||||
// Multiple ways to check boolean options
|
||||
|
||||
// Method 1: Using helper function
|
||||
if (roi_is_option_enabled('enable_breadcrumbs')) {
|
||||
// Breadcrumbs are enabled
|
||||
}
|
||||
|
||||
// Method 2: Using get_option with default
|
||||
if (roi_get_option('enable_related_posts', true)) {
|
||||
// Related posts are enabled
|
||||
}
|
||||
|
||||
// Method 3: Direct check
|
||||
$options = roi_get_all_options();
|
||||
if (isset($options['enable_lazy_loading']) && $options['enable_lazy_loading']) {
|
||||
// Lazy loading is enabled
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Options Settings API
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all theme settings
|
||||
*/
|
||||
function roi_register_settings() {
|
||||
// Register main options group
|
||||
register_setting(
|
||||
'roi_theme_options_group',
|
||||
'roi_theme_options',
|
||||
array(
|
||||
'sanitize_callback' => 'roi_sanitize_options',
|
||||
'default' => roi_get_default_options(),
|
||||
)
|
||||
);
|
||||
|
||||
// General Settings Section
|
||||
add_settings_section(
|
||||
'roi_general_section',
|
||||
__('General Settings', 'roi-theme'),
|
||||
'roi_general_section_callback',
|
||||
'roitheme-options'
|
||||
);
|
||||
|
||||
// Content Settings Section
|
||||
add_settings_section(
|
||||
'roi_content_section',
|
||||
__('Content Settings', 'roi-theme'),
|
||||
'roi_content_section_callback',
|
||||
'roitheme-options'
|
||||
);
|
||||
|
||||
// Performance Settings Section
|
||||
add_settings_section(
|
||||
'roi_performance_section',
|
||||
__('Performance Settings', 'roi-theme'),
|
||||
'roi_performance_section_callback',
|
||||
'roitheme-options'
|
||||
);
|
||||
|
||||
// Related Posts Settings Section
|
||||
add_settings_section(
|
||||
'roi_related_posts_section',
|
||||
__('Related Posts Settings', 'roi-theme'),
|
||||
'roi_related_posts_section_callback',
|
||||
'roitheme-options'
|
||||
);
|
||||
|
||||
// Social Share Settings Section
|
||||
add_settings_section(
|
||||
'roi_social_share_section',
|
||||
__('Social Share Buttons', 'roi-theme'),
|
||||
'roi_social_share_section_callback',
|
||||
'roitheme-options'
|
||||
);
|
||||
}
|
||||
add_action('admin_init', 'roi_register_settings');
|
||||
|
||||
/**
|
||||
* Get default options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function roi_get_default_options() {
|
||||
return array(
|
||||
// General
|
||||
'site_logo' => 0,
|
||||
'site_favicon' => 0,
|
||||
'enable_breadcrumbs' => true,
|
||||
'breadcrumb_separator' => '>',
|
||||
'date_format' => 'd/m/Y',
|
||||
'time_format' => 'H:i',
|
||||
'copyright_text' => sprintf(__('© %s %s. All rights reserved.', 'roi-theme'), date('Y'), get_bloginfo('name')),
|
||||
'social_facebook' => '',
|
||||
'social_twitter' => '',
|
||||
'social_instagram' => '',
|
||||
'social_linkedin' => '',
|
||||
'social_youtube' => '',
|
||||
|
||||
// Content
|
||||
'excerpt_length' => 55,
|
||||
'excerpt_more' => '...',
|
||||
'default_post_layout' => 'right-sidebar',
|
||||
'default_page_layout' => 'right-sidebar',
|
||||
'archive_posts_per_page' => 10,
|
||||
'show_featured_image_single' => true,
|
||||
'show_author_box' => true,
|
||||
'enable_comments_posts' => true,
|
||||
'enable_comments_pages' => false,
|
||||
'show_post_meta' => true,
|
||||
'show_post_tags' => true,
|
||||
'show_post_categories' => true,
|
||||
|
||||
// Performance
|
||||
'enable_lazy_loading' => true,
|
||||
'performance_remove_emoji' => true,
|
||||
'performance_remove_embeds' => false,
|
||||
'performance_remove_dashicons' => true,
|
||||
'performance_defer_js' => false,
|
||||
'performance_minify_html' => false,
|
||||
'performance_disable_gutenberg' => false,
|
||||
|
||||
// Related Posts
|
||||
'enable_related_posts' => true,
|
||||
'related_posts_count' => 3,
|
||||
'related_posts_taxonomy' => 'category',
|
||||
'related_posts_title' => __('Related Posts', 'roi-theme'),
|
||||
'related_posts_columns' => 3,
|
||||
|
||||
// Social Share Buttons
|
||||
'roi_enable_share_buttons' => '1',
|
||||
'roi_share_text' => __('Compartir:', 'roi-theme'),
|
||||
|
||||
// Advanced
|
||||
'custom_css' => '',
|
||||
'custom_js_header' => '',
|
||||
'custom_js_footer' => '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section Callbacks
|
||||
*/
|
||||
function roi_general_section_callback() {
|
||||
echo '<p>' . __('Configure general theme settings including logo, branding, and social media.', 'roi-theme') . '</p>';
|
||||
}
|
||||
|
||||
function roi_content_section_callback() {
|
||||
echo '<p>' . __('Configure content display settings for posts, pages, and archives.', 'roi-theme') . '</p>';
|
||||
}
|
||||
|
||||
function roi_performance_section_callback() {
|
||||
echo '<p>' . __('Optimize your site performance with these settings.', 'roi-theme') . '</p>';
|
||||
}
|
||||
|
||||
function roi_related_posts_section_callback() {
|
||||
echo '<p>' . __('Configure related posts display on single post pages.', 'roi-theme') . '</p>';
|
||||
}
|
||||
|
||||
function roi_social_share_section_callback() {
|
||||
echo '<p>' . __('Configure social share buttons display on single post pages.', 'roi-theme') . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize all options
|
||||
*
|
||||
* @param array $input The input array
|
||||
* @return array The sanitized array
|
||||
*/
|
||||
function roi_sanitize_options($input) {
|
||||
$sanitized = array();
|
||||
|
||||
if (!is_array($input)) {
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
// General Settings
|
||||
$sanitized['site_logo'] = isset($input['site_logo']) ? absint($input['site_logo']) : 0;
|
||||
$sanitized['site_favicon'] = isset($input['site_favicon']) ? absint($input['site_favicon']) : 0;
|
||||
$sanitized['enable_breadcrumbs'] = isset($input['enable_breadcrumbs']) ? (bool) $input['enable_breadcrumbs'] : false;
|
||||
$sanitized['breadcrumb_separator'] = isset($input['breadcrumb_separator']) ? sanitize_text_field($input['breadcrumb_separator']) : '>';
|
||||
$sanitized['date_format'] = isset($input['date_format']) ? sanitize_text_field($input['date_format']) : 'd/m/Y';
|
||||
$sanitized['time_format'] = isset($input['time_format']) ? sanitize_text_field($input['time_format']) : 'H:i';
|
||||
$sanitized['copyright_text'] = isset($input['copyright_text']) ? wp_kses_post($input['copyright_text']) : '';
|
||||
|
||||
// Social Media
|
||||
$social_fields = array('facebook', 'twitter', 'instagram', 'linkedin', 'youtube');
|
||||
foreach ($social_fields as $social) {
|
||||
$key = 'social_' . $social;
|
||||
$sanitized[$key] = isset($input[$key]) ? esc_url_raw($input[$key]) : '';
|
||||
}
|
||||
|
||||
// Content Settings
|
||||
$sanitized['excerpt_length'] = isset($input['excerpt_length']) ? absint($input['excerpt_length']) : 55;
|
||||
$sanitized['excerpt_more'] = isset($input['excerpt_more']) ? sanitize_text_field($input['excerpt_more']) : '...';
|
||||
$sanitized['default_post_layout'] = isset($input['default_post_layout']) ? sanitize_text_field($input['default_post_layout']) : 'right-sidebar';
|
||||
$sanitized['default_page_layout'] = isset($input['default_page_layout']) ? sanitize_text_field($input['default_page_layout']) : 'right-sidebar';
|
||||
$sanitized['archive_posts_per_page'] = isset($input['archive_posts_per_page']) ? absint($input['archive_posts_per_page']) : 10;
|
||||
$sanitized['show_featured_image_single'] = isset($input['show_featured_image_single']) ? (bool) $input['show_featured_image_single'] : false;
|
||||
$sanitized['show_author_box'] = isset($input['show_author_box']) ? (bool) $input['show_author_box'] : false;
|
||||
$sanitized['enable_comments_posts'] = isset($input['enable_comments_posts']) ? (bool) $input['enable_comments_posts'] : false;
|
||||
$sanitized['enable_comments_pages'] = isset($input['enable_comments_pages']) ? (bool) $input['enable_comments_pages'] : false;
|
||||
$sanitized['show_post_meta'] = isset($input['show_post_meta']) ? (bool) $input['show_post_meta'] : false;
|
||||
$sanitized['show_post_tags'] = isset($input['show_post_tags']) ? (bool) $input['show_post_tags'] : false;
|
||||
$sanitized['show_post_categories'] = isset($input['show_post_categories']) ? (bool) $input['show_post_categories'] : false;
|
||||
|
||||
// Performance Settings
|
||||
$sanitized['enable_lazy_loading'] = isset($input['enable_lazy_loading']) ? (bool) $input['enable_lazy_loading'] : false;
|
||||
$sanitized['performance_remove_emoji'] = isset($input['performance_remove_emoji']) ? (bool) $input['performance_remove_emoji'] : false;
|
||||
$sanitized['performance_remove_embeds'] = isset($input['performance_remove_embeds']) ? (bool) $input['performance_remove_embeds'] : false;
|
||||
$sanitized['performance_remove_dashicons'] = isset($input['performance_remove_dashicons']) ? (bool) $input['performance_remove_dashicons'] : false;
|
||||
$sanitized['performance_defer_js'] = isset($input['performance_defer_js']) ? (bool) $input['performance_defer_js'] : false;
|
||||
$sanitized['performance_minify_html'] = isset($input['performance_minify_html']) ? (bool) $input['performance_minify_html'] : false;
|
||||
$sanitized['performance_disable_gutenberg'] = isset($input['performance_disable_gutenberg']) ? (bool) $input['performance_disable_gutenberg'] : false;
|
||||
|
||||
// Related Posts
|
||||
$sanitized['enable_related_posts'] = isset($input['enable_related_posts']) ? (bool) $input['enable_related_posts'] : false;
|
||||
$sanitized['related_posts_count'] = isset($input['related_posts_count']) ? absint($input['related_posts_count']) : 3;
|
||||
$sanitized['related_posts_taxonomy'] = isset($input['related_posts_taxonomy']) ? sanitize_text_field($input['related_posts_taxonomy']) : 'category';
|
||||
$sanitized['related_posts_title'] = isset($input['related_posts_title']) ? sanitize_text_field($input['related_posts_title']) : __('Related Posts', 'roi-theme');
|
||||
$sanitized['related_posts_columns'] = isset($input['related_posts_columns']) ? absint($input['related_posts_columns']) : 3;
|
||||
|
||||
// Social Share Buttons
|
||||
$sanitized['roi_enable_share_buttons'] = isset($input['roi_enable_share_buttons']) ? sanitize_text_field($input['roi_enable_share_buttons']) : '1';
|
||||
$sanitized['roi_share_text'] = isset($input['roi_share_text']) ? sanitize_text_field($input['roi_share_text']) : __('Compartir:', 'roi-theme');
|
||||
|
||||
// Advanced Settings
|
||||
$sanitized['custom_css'] = isset($input['custom_css']) ? roi_sanitize_css($input['custom_css']) : '';
|
||||
$sanitized['custom_js_header'] = isset($input['custom_js_header']) ? roi_sanitize_js($input['custom_js_header']) : '';
|
||||
$sanitized['custom_js_footer'] = isset($input['custom_js_footer']) ? roi_sanitize_js($input['custom_js_footer']) : '';
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: All sanitization functions have been moved to inc/sanitize-functions.php
|
||||
* to avoid function redeclaration errors. This includes:
|
||||
* - roi_sanitize_css()
|
||||
* - roi_sanitize_js()
|
||||
* - roi_sanitize_integer()
|
||||
* - roi_sanitize_text()
|
||||
* - roi_sanitize_url()
|
||||
* - roi_sanitize_html()
|
||||
* - roi_sanitize_checkbox()
|
||||
* - roi_sanitize_select()
|
||||
*/
|
||||
@@ -1,659 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Options Page Template
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get current options
|
||||
$options = get_option('roi_theme_options', roi_get_default_options());
|
||||
?>
|
||||
|
||||
<div class="wrap roi-theme-options">
|
||||
<div class="roi-options-header">
|
||||
<div class="roi-options-logo">
|
||||
<h2><?php _e('ROI Theme', 'roi-theme'); ?></h2>
|
||||
<span class="version"><?php echo 'v' . ROI_VERSION; ?></span>
|
||||
</div>
|
||||
<div class="roi-options-actions">
|
||||
<button type="button" class="button button-secondary" id="roi-export-options">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
<?php _e('Export Options', 'roi-theme'); ?>
|
||||
</button>
|
||||
<button type="button" class="button button-secondary" id="roi-import-options">
|
||||
<span class="dashicons dashicons-upload"></span>
|
||||
<?php _e('Import Options', 'roi-theme'); ?>
|
||||
</button>
|
||||
<button type="button" class="button button-secondary" id="roi-reset-options">
|
||||
<span class="dashicons dashicons-image-rotate"></span>
|
||||
<?php _e('Reset to Defaults', 'roi-theme'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="options.php" class="roi-options-form">
|
||||
<?php
|
||||
settings_fields('roi_theme_options_group');
|
||||
?>
|
||||
|
||||
<div class="roi-options-container">
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="roi-tabs-nav">
|
||||
<ul>
|
||||
<li class="active">
|
||||
<a href="#general" data-tab="general">
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
<?php _e('General', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#content" data-tab="content">
|
||||
<span class="dashicons dashicons-edit-page"></span>
|
||||
<?php _e('Content', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#performance" data-tab="performance">
|
||||
<span class="dashicons dashicons-performance"></span>
|
||||
<?php _e('Performance', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#related-posts" data-tab="related-posts">
|
||||
<span class="dashicons dashicons-admin-links"></span>
|
||||
<?php _e('Related Posts', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#advanced" data-tab="advanced">
|
||||
<span class="dashicons dashicons-admin-tools"></span>
|
||||
<?php _e('Advanced', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Content -->
|
||||
<div class="roi-tabs-content">
|
||||
|
||||
<!-- General Tab -->
|
||||
<div id="general" class="roi-tab-pane active">
|
||||
<h2><?php _e('General Settings', 'roi-theme'); ?></h2>
|
||||
<p class="description"><?php _e('Configure general theme settings including logo, branding, and social media.', 'roi-theme'); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<!-- Site Logo -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="site_logo"><?php _e('Site Logo', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<div class="roi-image-upload">
|
||||
<input type="hidden" name="roi_theme_options[site_logo]" id="site_logo" value="<?php echo esc_attr($options['site_logo'] ?? 0); ?>" class="roi-image-id" />
|
||||
<div class="roi-image-preview">
|
||||
<?php
|
||||
$logo_id = $options['site_logo'] ?? 0;
|
||||
if ($logo_id) {
|
||||
echo wp_get_attachment_image($logo_id, 'medium', false, array('class' => 'roi-preview-image'));
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<button type="button" class="button roi-upload-image"><?php _e('Upload Logo', 'roi-theme'); ?></button>
|
||||
<button type="button" class="button roi-remove-image" <?php echo (!$logo_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Logo', 'roi-theme'); ?></button>
|
||||
<p class="description"><?php _e('Upload your site logo. Recommended size: 200x60px', 'roi-theme'); ?></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Site Favicon -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="site_favicon"><?php _e('Site Favicon', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<div class="roi-image-upload">
|
||||
<input type="hidden" name="roi_theme_options[site_favicon]" id="site_favicon" value="<?php echo esc_attr($options['site_favicon'] ?? 0); ?>" class="roi-image-id" />
|
||||
<div class="roi-image-preview">
|
||||
<?php
|
||||
$favicon_id = $options['site_favicon'] ?? 0;
|
||||
if ($favicon_id) {
|
||||
echo wp_get_attachment_image($favicon_id, 'thumbnail', false, array('class' => 'roi-preview-image'));
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<button type="button" class="button roi-upload-image"><?php _e('Upload Favicon', 'roi-theme'); ?></button>
|
||||
<button type="button" class="button roi-remove-image" <?php echo (!$favicon_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Favicon', 'roi-theme'); ?></button>
|
||||
<p class="description"><?php _e('Upload your site favicon. Recommended size: 32x32px or 64x64px', 'roi-theme'); ?></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Enable Breadcrumbs -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="enable_breadcrumbs"><?php _e('Enable Breadcrumbs', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[enable_breadcrumbs]" id="enable_breadcrumbs" value="1" <?php checked(isset($options['enable_breadcrumbs']) ? $options['enable_breadcrumbs'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Show breadcrumbs navigation on pages and posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Breadcrumb Separator -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="breadcrumb_separator"><?php _e('Breadcrumb Separator', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="roi_theme_options[breadcrumb_separator]" id="breadcrumb_separator" value="<?php echo esc_attr($options['breadcrumb_separator'] ?? '>'); ?>" class="regular-text" />
|
||||
<p class="description"><?php _e('Character or symbol to separate breadcrumb items (e.g., >, /, »)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Date Format -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="date_format"><?php _e('Date Format', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="roi_theme_options[date_format]" id="date_format" value="<?php echo esc_attr($options['date_format'] ?? 'd/m/Y'); ?>" class="regular-text" />
|
||||
<p class="description"><?php _e('PHP date format (e.g., d/m/Y, m/d/Y, Y-m-d)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Time Format -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="time_format"><?php _e('Time Format', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="roi_theme_options[time_format]" id="time_format" value="<?php echo esc_attr($options['time_format'] ?? 'H:i'); ?>" class="regular-text" />
|
||||
<p class="description"><?php _e('PHP time format (e.g., H:i, g:i A)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Copyright Text -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="copyright_text"><?php _e('Copyright Text', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="roi_theme_options[copyright_text]" id="copyright_text" rows="3" class="large-text"><?php echo esc_textarea($options['copyright_text'] ?? sprintf(__('© %s %s. All rights reserved.', 'roi-theme'), date('Y'), get_bloginfo('name'))); ?></textarea>
|
||||
<p class="description"><?php _e('Footer copyright text. HTML allowed.', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3><?php _e('Social Media Links', 'roi-theme'); ?></h3>
|
||||
<table class="form-table">
|
||||
<!-- Facebook -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="social_facebook"><?php _e('Facebook URL', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="roi_theme_options[social_facebook]" id="social_facebook" value="<?php echo esc_url($options['social_facebook'] ?? ''); ?>" class="regular-text" placeholder="https://facebook.com/yourpage" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Twitter -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="social_twitter"><?php _e('Twitter URL', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="roi_theme_options[social_twitter]" id="social_twitter" value="<?php echo esc_url($options['social_twitter'] ?? ''); ?>" class="regular-text" placeholder="https://twitter.com/youraccount" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Instagram -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="social_instagram"><?php _e('Instagram URL', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="roi_theme_options[social_instagram]" id="social_instagram" value="<?php echo esc_url($options['social_instagram'] ?? ''); ?>" class="regular-text" placeholder="https://instagram.com/youraccount" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- LinkedIn -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="social_linkedin"><?php _e('LinkedIn URL', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="roi_theme_options[social_linkedin]" id="social_linkedin" value="<?php echo esc_url($options['social_linkedin'] ?? ''); ?>" class="regular-text" placeholder="https://linkedin.com/company/yourcompany" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- YouTube -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="social_youtube"><?php _e('YouTube URL', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="roi_theme_options[social_youtube]" id="social_youtube" value="<?php echo esc_url($options['social_youtube'] ?? ''); ?>" class="regular-text" placeholder="https://youtube.com/yourchannel" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Content Tab -->
|
||||
<div id="content" class="roi-tab-pane">
|
||||
<h2><?php _e('Content Settings', 'roi-theme'); ?></h2>
|
||||
<p class="description"><?php _e('Configure content display settings for posts, pages, and archives.', 'roi-theme'); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<!-- Excerpt Length -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="excerpt_length"><?php _e('Excerpt Length', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="roi_theme_options[excerpt_length]" id="excerpt_length" value="<?php echo esc_attr($options['excerpt_length'] ?? 55); ?>" class="small-text" min="10" max="500" />
|
||||
<p class="description"><?php _e('Number of words to show in excerpt', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Excerpt More -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="excerpt_more"><?php _e('Excerpt More Text', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="roi_theme_options[excerpt_more]" id="excerpt_more" value="<?php echo esc_attr($options['excerpt_more'] ?? '...'); ?>" class="regular-text" />
|
||||
<p class="description"><?php _e('Text to append at the end of excerpts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Default Post Layout -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="default_post_layout"><?php _e('Default Post Layout', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="roi_theme_options[default_post_layout]" id="default_post_layout">
|
||||
<option value="right-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'roi-theme'); ?></option>
|
||||
<option value="left-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'roi-theme'); ?></option>
|
||||
<option value="no-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'roi-theme'); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php _e('Default layout for single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Default Page Layout -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="default_page_layout"><?php _e('Default Page Layout', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="roi_theme_options[default_page_layout]" id="default_page_layout">
|
||||
<option value="right-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'roi-theme'); ?></option>
|
||||
<option value="left-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'roi-theme'); ?></option>
|
||||
<option value="no-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'roi-theme'); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php _e('Default layout for pages', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Archive Posts Per Page -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="archive_posts_per_page"><?php _e('Archive Posts Per Page', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="roi_theme_options[archive_posts_per_page]" id="archive_posts_per_page" value="<?php echo esc_attr($options['archive_posts_per_page'] ?? 10); ?>" class="small-text" min="1" max="100" />
|
||||
<p class="description"><?php _e('Number of posts to show on archive pages. Set to 0 to use WordPress default.', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Show Featured Image on Single Posts -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="show_featured_image_single"><?php _e('Show Featured Image', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[show_featured_image_single]" id="show_featured_image_single" value="1" <?php checked(isset($options['show_featured_image_single']) ? $options['show_featured_image_single'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Display featured image at the top of single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Show Author Box -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="show_author_box"><?php _e('Show Author Box', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[show_author_box]" id="show_author_box" value="1" <?php checked(isset($options['show_author_box']) ? $options['show_author_box'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Display author information box on single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Enable Comments on Posts -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="enable_comments_posts"><?php _e('Enable Comments on Posts', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[enable_comments_posts]" id="enable_comments_posts" value="1" <?php checked(isset($options['enable_comments_posts']) ? $options['enable_comments_posts'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Allow comments on blog posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Enable Comments on Pages -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="enable_comments_pages"><?php _e('Enable Comments on Pages', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[enable_comments_pages]" id="enable_comments_pages" value="1" <?php checked(isset($options['enable_comments_pages']) ? $options['enable_comments_pages'] : false, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Allow comments on pages', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Show Post Meta -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="show_post_meta"><?php _e('Show Post Meta', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[show_post_meta]" id="show_post_meta" value="1" <?php checked(isset($options['show_post_meta']) ? $options['show_post_meta'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Display post meta information (date, author, etc.)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Show Post Tags -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="show_post_tags"><?php _e('Show Post Tags', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[show_post_tags]" id="show_post_tags" value="1" <?php checked(isset($options['show_post_tags']) ? $options['show_post_tags'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Display tags on single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Show Post Categories -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="show_post_categories"><?php _e('Show Post Categories', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[show_post_categories]" id="show_post_categories" value="1" <?php checked(isset($options['show_post_categories']) ? $options['show_post_categories'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Display categories on single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Performance Tab -->
|
||||
<div id="performance" class="roi-tab-pane">
|
||||
<h2><?php _e('Performance Settings', 'roi-theme'); ?></h2>
|
||||
<p class="description"><?php _e('Optimize your site performance with these settings. Be careful when enabling these options.', 'roi-theme'); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<!-- Enable Lazy Loading -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="enable_lazy_loading"><?php _e('Enable Lazy Loading', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[enable_lazy_loading]" id="enable_lazy_loading" value="1" <?php checked(isset($options['enable_lazy_loading']) ? $options['enable_lazy_loading'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Enable lazy loading for images to improve page load times', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Remove Emoji Scripts -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_remove_emoji"><?php _e('Remove Emoji Scripts', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_remove_emoji]" id="performance_remove_emoji" value="1" <?php checked(isset($options['performance_remove_emoji']) ? $options['performance_remove_emoji'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Remove WordPress emoji scripts and styles (reduces HTTP requests)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Remove Embeds -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_remove_embeds"><?php _e('Remove Embeds', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_remove_embeds]" id="performance_remove_embeds" value="1" <?php checked(isset($options['performance_remove_embeds']) ? $options['performance_remove_embeds'] : false, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Remove WordPress embed scripts if you don\'t use oEmbed', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Remove Dashicons on Frontend -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_remove_dashicons"><?php _e('Remove Dashicons', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_remove_dashicons]" id="performance_remove_dashicons" value="1" <?php checked(isset($options['performance_remove_dashicons']) ? $options['performance_remove_dashicons'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Remove Dashicons from frontend for non-logged in users', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Defer JavaScript -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_defer_js"><?php _e('Defer JavaScript', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_defer_js]" id="performance_defer_js" value="1" <?php checked(isset($options['performance_defer_js']) ? $options['performance_defer_js'] : false, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Add defer attribute to JavaScript files (may break some scripts)', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Minify HTML -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_minify_html"><?php _e('Minify HTML', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_minify_html]" id="performance_minify_html" value="1" <?php checked(isset($options['performance_minify_html']) ? $options['performance_minify_html'] : false, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Minify HTML output to reduce page size', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Disable Gutenberg -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="performance_disable_gutenberg"><?php _e('Disable Gutenberg', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[performance_disable_gutenberg]" id="performance_disable_gutenberg" value="1" <?php checked(isset($options['performance_disable_gutenberg']) ? $options['performance_disable_gutenberg'] : false, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Disable Gutenberg editor and revert to classic editor', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Related Posts Tab -->
|
||||
<div id="related-posts" class="roi-tab-pane">
|
||||
<h2><?php _e('Related Posts Settings', 'roi-theme'); ?></h2>
|
||||
<p class="description"><?php _e('Configure related posts display on single post pages.', 'roi-theme'); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<!-- Enable Related Posts -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="enable_related_posts"><?php _e('Enable Related Posts', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label class="roi-switch">
|
||||
<input type="checkbox" name="roi_theme_options[enable_related_posts]" id="enable_related_posts" value="1" <?php checked(isset($options['enable_related_posts']) ? $options['enable_related_posts'] : true, true); ?> />
|
||||
<span class="roi-slider"></span>
|
||||
</label>
|
||||
<p class="description"><?php _e('Show related posts at the end of single posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Related Posts Count -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="related_posts_count"><?php _e('Number of Related Posts', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="roi_theme_options[related_posts_count]" id="related_posts_count" value="<?php echo esc_attr($options['related_posts_count'] ?? 3); ?>" class="small-text" min="1" max="12" />
|
||||
<p class="description"><?php _e('How many related posts to display', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Related Posts Taxonomy -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="related_posts_taxonomy"><?php _e('Relate Posts By', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="roi_theme_options[related_posts_taxonomy]" id="related_posts_taxonomy">
|
||||
<option value="category" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'category'); ?>><?php _e('Category', 'roi-theme'); ?></option>
|
||||
<option value="tag" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'tag'); ?>><?php _e('Tag', 'roi-theme'); ?></option>
|
||||
<option value="both" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'both'); ?>><?php _e('Category and Tag', 'roi-theme'); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php _e('How to determine related posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Related Posts Title -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="related_posts_title"><?php _e('Related Posts Title', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="roi_theme_options[related_posts_title]" id="related_posts_title" value="<?php echo esc_attr($options['related_posts_title'] ?? __('Related Posts', 'roi-theme')); ?>" class="regular-text" />
|
||||
<p class="description"><?php _e('Title to display above related posts section', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Related Posts Columns -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="related_posts_columns"><?php _e('Columns', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="roi_theme_options[related_posts_columns]" id="related_posts_columns">
|
||||
<option value="2" <?php selected($options['related_posts_columns'] ?? 3, 2); ?>><?php _e('2 Columns', 'roi-theme'); ?></option>
|
||||
<option value="3" <?php selected($options['related_posts_columns'] ?? 3, 3); ?>><?php _e('3 Columns', 'roi-theme'); ?></option>
|
||||
<option value="4" <?php selected($options['related_posts_columns'] ?? 3, 4); ?>><?php _e('4 Columns', 'roi-theme'); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php _e('Number of columns to display related posts', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Tab -->
|
||||
<div id="advanced" class="roi-tab-pane">
|
||||
<h2><?php _e('Advanced Settings', 'roi-theme'); ?></h2>
|
||||
<p class="description"><?php _e('Advanced customization options. Use with caution.', 'roi-theme'); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<!-- Custom CSS -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="custom_css"><?php _e('Custom CSS', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="roi_theme_options[custom_css]" id="custom_css" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_css'] ?? ''); ?></textarea>
|
||||
<p class="description"><?php _e('Add custom CSS code. This will be added to the <head> section.', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Custom JS Header -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="custom_js_header"><?php _e('Custom JavaScript (Header)', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="roi_theme_options[custom_js_header]" id="custom_js_header" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_header'] ?? ''); ?></textarea>
|
||||
<p class="description"><?php _e('Add custom JavaScript code. This will be added to the <head> section. Do not include <script> tags.', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Custom JS Footer -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="custom_js_footer"><?php _e('Custom JavaScript (Footer)', 'roi-theme'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="roi_theme_options[custom_js_footer]" id="custom_js_footer" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_footer'] ?? ''); ?></textarea>
|
||||
<p class="description"><?php _e('Add custom JavaScript code. This will be added before the closing </body> tag. Do not include <script> tags.', 'roi-theme'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php submit_button(__('Save All Settings', 'roi-theme'), 'primary large', 'submit', true); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Import Modal -->
|
||||
<div id="roi-import-modal" class="roi-modal" style="display:none;">
|
||||
<div class="roi-modal-content">
|
||||
<span class="roi-modal-close">×</span>
|
||||
<h2><?php _e('Import Options', 'roi-theme'); ?></h2>
|
||||
<p><?php _e('Paste your exported options JSON here:', 'roi-theme'); ?></p>
|
||||
<textarea id="roi-import-data" rows="10" class="large-text code"></textarea>
|
||||
<p>
|
||||
<button type="button" class="button button-primary" id="roi-import-submit"><?php _e('Import', 'roi-theme'); ?></button>
|
||||
<button type="button" class="button" id="roi-import-cancel"><?php _e('Cancel', 'roi-theme'); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,272 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Related Posts Configuration Options
|
||||
*
|
||||
* This file provides helper functions and documentation for configuring
|
||||
* related posts functionality via WordPress options.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all related posts options with their current values
|
||||
*
|
||||
* @return array Array of options with their values
|
||||
*/
|
||||
function roi_get_related_posts_options() {
|
||||
return array(
|
||||
'enabled' => array(
|
||||
'key' => 'roi_related_posts_enabled',
|
||||
'value' => get_option('roi_related_posts_enabled', true),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'label' => __('Enable Related Posts', 'roi-theme'),
|
||||
'description' => __('Show related posts section at the end of single posts', 'roi-theme'),
|
||||
),
|
||||
'title' => array(
|
||||
'key' => 'roi_related_posts_title',
|
||||
'value' => get_option('roi_related_posts_title', __('Related Posts', 'roi-theme')),
|
||||
'type' => 'text',
|
||||
'default' => __('Related Posts', 'roi-theme'),
|
||||
'label' => __('Section Title', 'roi-theme'),
|
||||
'description' => __('Title displayed above related posts', 'roi-theme'),
|
||||
),
|
||||
'count' => array(
|
||||
'key' => 'roi_related_posts_count',
|
||||
'value' => get_option('roi_related_posts_count', 3),
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
'min' => 1,
|
||||
'max' => 12,
|
||||
'label' => __('Number of Posts', 'roi-theme'),
|
||||
'description' => __('Maximum number of related posts to display', 'roi-theme'),
|
||||
),
|
||||
'columns' => array(
|
||||
'key' => 'roi_related_posts_columns',
|
||||
'value' => get_option('roi_related_posts_columns', 3),
|
||||
'type' => 'select',
|
||||
'default' => 3,
|
||||
'options' => array(
|
||||
1 => __('1 Column', 'roi-theme'),
|
||||
2 => __('2 Columns', 'roi-theme'),
|
||||
3 => __('3 Columns', 'roi-theme'),
|
||||
4 => __('4 Columns', 'roi-theme'),
|
||||
),
|
||||
'label' => __('Grid Columns', 'roi-theme'),
|
||||
'description' => __('Number of columns in the grid layout (responsive)', 'roi-theme'),
|
||||
),
|
||||
'show_excerpt' => array(
|
||||
'key' => 'roi_related_posts_show_excerpt',
|
||||
'value' => get_option('roi_related_posts_show_excerpt', true),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'label' => __('Show Excerpt', 'roi-theme'),
|
||||
'description' => __('Display post excerpt in related posts cards', 'roi-theme'),
|
||||
),
|
||||
'excerpt_length' => array(
|
||||
'key' => 'roi_related_posts_excerpt_length',
|
||||
'value' => get_option('roi_related_posts_excerpt_length', 20),
|
||||
'type' => 'number',
|
||||
'default' => 20,
|
||||
'min' => 5,
|
||||
'max' => 100,
|
||||
'label' => __('Excerpt Length', 'roi-theme'),
|
||||
'description' => __('Number of words in the excerpt', 'roi-theme'),
|
||||
),
|
||||
'show_date' => array(
|
||||
'key' => 'roi_related_posts_show_date',
|
||||
'value' => get_option('roi_related_posts_show_date', true),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'label' => __('Show Date', 'roi-theme'),
|
||||
'description' => __('Display publication date in related posts', 'roi-theme'),
|
||||
),
|
||||
'show_category' => array(
|
||||
'key' => 'roi_related_posts_show_category',
|
||||
'value' => get_option('roi_related_posts_show_category', true),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'label' => __('Show Category', 'roi-theme'),
|
||||
'description' => __('Display category badge on related posts', 'roi-theme'),
|
||||
),
|
||||
'bg_colors' => array(
|
||||
'key' => 'roi_related_posts_bg_colors',
|
||||
'value' => get_option('roi_related_posts_bg_colors', array(
|
||||
'#1a73e8', '#e91e63', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4',
|
||||
)),
|
||||
'type' => 'color_array',
|
||||
'default' => array(
|
||||
'#1a73e8', // Blue
|
||||
'#e91e63', // Pink
|
||||
'#4caf50', // Green
|
||||
'#ff9800', // Orange
|
||||
'#9c27b0', // Purple
|
||||
'#00bcd4', // Cyan
|
||||
),
|
||||
'label' => __('Background Colors', 'roi-theme'),
|
||||
'description' => __('Colors used for posts without featured images', 'roi-theme'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a related posts option
|
||||
*
|
||||
* @param string $option_key The option key (without 'roi_related_posts_' prefix)
|
||||
* @param mixed $value The new value
|
||||
* @return bool True if updated successfully
|
||||
*/
|
||||
function roi_update_related_posts_option($option_key, $value) {
|
||||
$full_key = 'roi_related_posts_' . $option_key;
|
||||
return update_option($full_key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset related posts options to defaults
|
||||
*
|
||||
* @return bool True if reset successfully
|
||||
*/
|
||||
function roi_reset_related_posts_options() {
|
||||
$options = roi_get_related_posts_options();
|
||||
$success = true;
|
||||
|
||||
foreach ($options as $option) {
|
||||
if (!update_option($option['key'], $option['default'])) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Programmatically configure related posts
|
||||
*
|
||||
* This function shows how to configure related posts options programmatically.
|
||||
* You can call this from your functions.php or a plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function roi_example_configure_related_posts() {
|
||||
// Example usage - uncomment to use:
|
||||
|
||||
// Enable related posts
|
||||
// update_option('roi_related_posts_enabled', true);
|
||||
|
||||
// Set custom title
|
||||
// update_option('roi_related_posts_title', __('You Might Also Like', 'roi-theme'));
|
||||
|
||||
// Show 4 related posts
|
||||
// update_option('roi_related_posts_count', 4);
|
||||
|
||||
// Use 2 columns layout
|
||||
// update_option('roi_related_posts_columns', 2);
|
||||
|
||||
// Show excerpt with 30 words
|
||||
// update_option('roi_related_posts_show_excerpt', true);
|
||||
// update_option('roi_related_posts_excerpt_length', 30);
|
||||
|
||||
// Show date and category
|
||||
// update_option('roi_related_posts_show_date', true);
|
||||
// update_option('roi_related_posts_show_category', true);
|
||||
|
||||
// Custom background colors for posts without images
|
||||
// update_option('roi_related_posts_bg_colors', array(
|
||||
// '#FF6B6B', // Red
|
||||
// '#4ECDC4', // Teal
|
||||
// '#45B7D1', // Blue
|
||||
// '#FFA07A', // Coral
|
||||
// '#98D8C8', // Mint
|
||||
// '#F7DC6F', // Yellow
|
||||
// ));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter hook example: Modify related posts query
|
||||
*
|
||||
* This example shows how to customize the related posts query.
|
||||
* Add this to your functions.php or child theme.
|
||||
*/
|
||||
function roi_example_modify_related_posts_query($args, $post_id) {
|
||||
// Example: Order by date instead of random
|
||||
// $args['orderby'] = 'date';
|
||||
// $args['order'] = 'DESC';
|
||||
|
||||
// Example: Only show posts from the last 6 months
|
||||
// $args['date_query'] = array(
|
||||
// array(
|
||||
// 'after' => '6 months ago',
|
||||
// ),
|
||||
// );
|
||||
|
||||
// Example: Exclude specific category
|
||||
// $args['category__not_in'] = array(5); // Replace 5 with category ID
|
||||
|
||||
return $args;
|
||||
}
|
||||
// add_filter('roi_related_posts_args', 'roi_example_modify_related_posts_query', 10, 2);
|
||||
|
||||
/**
|
||||
* Get documentation for related posts configuration
|
||||
*
|
||||
* @return array Documentation array
|
||||
*/
|
||||
function roi_get_related_posts_documentation() {
|
||||
return array(
|
||||
'overview' => array(
|
||||
'title' => __('Related Posts Overview', 'roi-theme'),
|
||||
'content' => __(
|
||||
'The related posts feature automatically displays relevant posts at the end of each blog post. ' .
|
||||
'Posts are related based on shared categories and displayed in a responsive Bootstrap grid.',
|
||||
'roi-theme'
|
||||
),
|
||||
),
|
||||
'features' => array(
|
||||
'title' => __('Key Features', 'roi-theme'),
|
||||
'items' => array(
|
||||
__('Automatic category-based matching', 'roi-theme'),
|
||||
__('Responsive Bootstrap 5 grid layout', 'roi-theme'),
|
||||
__('Configurable number of posts and columns', 'roi-theme'),
|
||||
__('Support for posts with and without featured images', 'roi-theme'),
|
||||
__('Beautiful color backgrounds for posts without images', 'roi-theme'),
|
||||
__('Customizable excerpt length', 'roi-theme'),
|
||||
__('Optional display of dates and categories', 'roi-theme'),
|
||||
__('Smooth hover animations', 'roi-theme'),
|
||||
__('Print-friendly styles', 'roi-theme'),
|
||||
__('Dark mode support', 'roi-theme'),
|
||||
),
|
||||
),
|
||||
'configuration' => array(
|
||||
'title' => __('How to Configure', 'roi-theme'),
|
||||
'methods' => array(
|
||||
'database' => array(
|
||||
'title' => __('Via WordPress Options API', 'roi-theme'),
|
||||
'code' => "update_option('roi_related_posts_enabled', true);\nupdate_option('roi_related_posts_count', 4);",
|
||||
),
|
||||
'filter' => array(
|
||||
'title' => __('Via Filter Hook', 'roi-theme'),
|
||||
'code' => "add_filter('roi_related_posts_args', function(\$args, \$post_id) {\n \$args['posts_per_page'] = 6;\n return \$args;\n}, 10, 2);",
|
||||
),
|
||||
),
|
||||
),
|
||||
'customization' => array(
|
||||
'title' => __('Customization Examples', 'roi-theme'),
|
||||
'examples' => array(
|
||||
array(
|
||||
'title' => __('Change title and layout', 'roi-theme'),
|
||||
'code' => "update_option('roi_related_posts_title', 'También te puede interesar');\nupdate_option('roi_related_posts_columns', 4);",
|
||||
),
|
||||
array(
|
||||
'title' => __('Customize colors', 'roi-theme'),
|
||||
'code' => "update_option('roi_related_posts_bg_colors', array(\n '#FF6B6B',\n '#4ECDC4',\n '#45B7D1'\n));",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Options Admin Page
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu
|
||||
* DESACTIVADO: Ahora se usa el nuevo Admin Panel en admin/includes/class-admin-menu.php
|
||||
*/
|
||||
/*
|
||||
function roi_add_admin_menu() {
|
||||
add_theme_page(
|
||||
__('ROI Theme Options', 'roi-theme'), // Page title
|
||||
__('Theme Options', 'roi-theme'), // Menu title
|
||||
'manage_options', // Capability
|
||||
'roi-theme-options', // Menu slug
|
||||
'roi_render_options_page', // Callback function
|
||||
30 // Position
|
||||
);
|
||||
}
|
||||
add_action('admin_menu', 'roi_add_admin_menu');
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render the options page
|
||||
*/
|
||||
function roi_render_options_page() {
|
||||
// Check user capabilities
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.', 'roi-theme'));
|
||||
}
|
||||
|
||||
// Load the template
|
||||
include get_template_directory() . '/admin/theme-options/options-page-template.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts and styles
|
||||
*/
|
||||
function roi_enqueue_admin_scripts($hook) {
|
||||
// Only load on our theme options page
|
||||
if ($hook !== 'appearance_page_roi-theme-options') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue WordPress media uploader
|
||||
wp_enqueue_media();
|
||||
|
||||
// Enqueue admin styles
|
||||
wp_enqueue_style(
|
||||
'roiadmin-options',
|
||||
get_template_directory_uri() . '/admin/assets/css/theme-options.css',
|
||||
array(),
|
||||
ROI_VERSION
|
||||
);
|
||||
|
||||
// Enqueue admin scripts
|
||||
wp_enqueue_script(
|
||||
'roiadmin-options',
|
||||
get_template_directory_uri() . '/admin/assets/js/theme-options.js',
|
||||
array('jquery', 'wp-color-picker'),
|
||||
ROI_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize script
|
||||
wp_localize_script('roiadmin-options', 'rroiminOptions', array(
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('roi_admin_nonce'),
|
||||
'strings' => array(
|
||||
'selectImage' => __('Select Image', 'roi-theme'),
|
||||
'useImage' => __('Use Image', 'roi-theme'),
|
||||
'removeImage' => __('Remove Image', 'roi-theme'),
|
||||
'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'),
|
||||
'saved' => __('Settings saved successfully!', 'roi-theme'),
|
||||
'error' => __('An error occurred while saving settings.', 'roi-theme'),
|
||||
),
|
||||
));
|
||||
}
|
||||
add_action('admin_enqueue_scripts', 'roi_enqueue_admin_scripts');
|
||||
|
||||
/**
|
||||
* Add settings link to theme actions
|
||||
*/
|
||||
function roi_add_settings_link($links) {
|
||||
$settings_link = '<a href="' . admin_url('themes.php?page=roi-theme-options') . '">' . __('Settings', 'roi-theme') . '</a>';
|
||||
array_unshift($links, $settings_link);
|
||||
return $links;
|
||||
}
|
||||
add_filter('theme_action_links_' . get_template(), 'roi_add_settings_link');
|
||||
|
||||
/**
|
||||
* AJAX handler for resetting options
|
||||
*/
|
||||
function roi_reset_options_ajax() {
|
||||
check_ajax_referer('roi_admin_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
|
||||
}
|
||||
|
||||
// Delete options to reset to defaults
|
||||
delete_option('roi_theme_options');
|
||||
|
||||
wp_send_json_success(array('message' => __('Options reset to defaults successfully.', 'roi-theme')));
|
||||
}
|
||||
add_action('wp_ajax_roi_reset_options', 'roi_reset_options_ajax');
|
||||
|
||||
/**
|
||||
* AJAX handler for exporting options
|
||||
*/
|
||||
function roi_export_options_ajax() {
|
||||
check_ajax_referer('roi_admin_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
|
||||
}
|
||||
|
||||
$options = get_option('roi_theme_options', array());
|
||||
|
||||
wp_send_json_success(array(
|
||||
'data' => json_encode($options, JSON_PRETTY_PRINT),
|
||||
'filename' => 'roi-theme-options-' . date('Y-m-d') . '.json'
|
||||
));
|
||||
}
|
||||
add_action('wp_ajax_roi_export_options', 'roi_export_options_ajax');
|
||||
|
||||
/**
|
||||
* AJAX handler for importing options
|
||||
*/
|
||||
function roi_import_options_ajax() {
|
||||
check_ajax_referer('roi_admin_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
|
||||
}
|
||||
|
||||
if (!isset($_POST['import_data'])) {
|
||||
wp_send_json_error(array('message' => __('No import data provided.', 'roi-theme')));
|
||||
}
|
||||
|
||||
$import_data = json_decode(stripslashes($_POST['import_data']), true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
wp_send_json_error(array('message' => __('Invalid JSON data.', 'roi-theme')));
|
||||
}
|
||||
|
||||
// Sanitize imported data
|
||||
$sanitized_data = roi_sanitize_options($import_data);
|
||||
|
||||
// Update options
|
||||
update_option('roi_theme_options', $sanitized_data);
|
||||
|
||||
wp_send_json_success(array('message' => __('Options imported successfully.', 'roi-theme')));
|
||||
}
|
||||
add_action('wp_ajax_roi_import_options', 'roi_import_options_ajax');
|
||||
|
||||
/**
|
||||
* Add admin notices
|
||||
*/
|
||||
function roi_admin_notices() {
|
||||
$screen = get_current_screen();
|
||||
if ($screen->id !== 'appearance_page_roi-theme-options') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if settings were updated
|
||||
if (isset($_GET['settings-updated']) && $_GET['settings-updated'] === 'true') {
|
||||
?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
<p><?php _e('Settings saved successfully!', 'roi-theme'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
add_action('admin_notices', 'roi_admin_notices');
|
||||
|
||||
/**
|
||||
* Register theme options in Customizer as well (for preview)
|
||||
*/
|
||||
function roi_customize_register($wp_customize) {
|
||||
// Add a panel for theme options
|
||||
$wp_customize->add_panel('roi_theme_options', array(
|
||||
'title' => __('ROI Theme Options', 'roi-theme'),
|
||||
'description' => __('Configure theme options (Also available in Theme Options page)', 'roi-theme'),
|
||||
'priority' => 10,
|
||||
));
|
||||
|
||||
// General Section
|
||||
$wp_customize->add_section('roi_general', array(
|
||||
'title' => __('General Settings', 'roi-theme'),
|
||||
'panel' => 'roi_theme_options',
|
||||
'priority' => 10,
|
||||
));
|
||||
|
||||
// Enable breadcrumbs
|
||||
$wp_customize->add_setting('roi_theme_options[enable_breadcrumbs]', array(
|
||||
'default' => true,
|
||||
'type' => 'option',
|
||||
'sanitize_callback' => 'roi_sanitize_checkbox',
|
||||
));
|
||||
|
||||
$wp_customize->add_control('roi_theme_options[enable_breadcrumbs]', array(
|
||||
'label' => __('Enable Breadcrumbs', 'roi-theme'),
|
||||
'section' => 'roi_general',
|
||||
'type' => 'checkbox',
|
||||
));
|
||||
}
|
||||
add_action('customize_register', 'roi_customize_register');
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Buttons Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos de botones personalizados del tema
|
||||
* - Botón Let's Talk (navbar)
|
||||
* - Otros botones custom del tema
|
||||
*
|
||||
* REACTIVADO: Issue #121 - Arquitectura de separación de componentes
|
||||
* El CSS NO debe estar en style.css sino en archivos individuales
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.7
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Botón Let's Talk (Navbar)
|
||||
======================================== */
|
||||
|
||||
.btn-lets-talk {
|
||||
background-color: var(--color-orange-primary) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-lets-talk:hover {
|
||||
background-color: var(--color-orange-hover) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.btn-lets-talk i {
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* CTA A/B Testing Styles
|
||||
*
|
||||
* CSS EXACTO copiado del template style.css (líneas 835-865)
|
||||
* Sin extras, sin !important innecesario, sin media queries complicadas
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.2
|
||||
*/
|
||||
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, var(--color-orange-primary) 0%, var(--color-orange-light) 100%);
|
||||
box-shadow: 0 8px 24px rgba(255, 133, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.cta-section h3 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.cta-section p {
|
||||
color: rgba(255, 255, 255, 0.95) !important;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
background-color: var(--color-orange-primary);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 2rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Responsive Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.cta-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* CTA Box Sidebar Styles
|
||||
*
|
||||
* Styles for the CTA box component that appears in the sidebar
|
||||
* below the Table of Contents on single posts.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
CTA Box Container
|
||||
======================================== */
|
||||
|
||||
.cta-box-sidebar {
|
||||
background: var(--color-orange-primary);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
height: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
CTA Box Content
|
||||
======================================== */
|
||||
|
||||
.cta-box-title {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta-box-text {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
CTA Button
|
||||
======================================== */
|
||||
|
||||
.btn-cta-box {
|
||||
background-color: #ffffff;
|
||||
color: var(--color-orange-primary);
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-cta-box:hover {
|
||||
background-color: var(--color-navy-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Icon Spacing
|
||||
======================================== */
|
||||
|
||||
.btn-cta-box i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Responsive Design
|
||||
======================================== */
|
||||
|
||||
/* Hide on tablets and mobile */
|
||||
@media (max-width: 991px) {
|
||||
.cta-box-sidebar {
|
||||
display: none; /* Ocultar en móviles */
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Print Styles
|
||||
======================================== */
|
||||
|
||||
@media print {
|
||||
.cta-box-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Footer Contact Form Styles
|
||||
*
|
||||
* Styles for the footer section including the contact form
|
||||
* and contact information.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Contact Form Styles
|
||||
======================================== */
|
||||
|
||||
.form-control {
|
||||
border: 2px solid var(--color-neutral-100);
|
||||
border-radius: 6px;
|
||||
padding: 0.625rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--color-orange-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-contact-submit {
|
||||
background-color: var(--color-orange-primary);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 2rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-contact-submit:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-submit-form {
|
||||
background-color: var(--color-orange-primary);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-submit-form:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Contact Info Styles
|
||||
======================================== */
|
||||
|
||||
.contact-info i {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Contact Section - Text Colors Override
|
||||
======================================== */
|
||||
|
||||
/*
|
||||
* Fix Issue #128: Textos demasiado oscuros en sección de contacto
|
||||
* Template usa #495057 (gris medio) en lugar de #212529 (gris oscuro)
|
||||
* Aplicar solo a sección bg-secondary (sección de contacto arriba del footer)
|
||||
*/
|
||||
|
||||
section.bg-secondary h2,
|
||||
section.bg-secondary h3,
|
||||
section.bg-secondary p {
|
||||
color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */
|
||||
}
|
||||
|
||||
section.bg-secondary h6 {
|
||||
color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */
|
||||
}
|
||||
|
||||
/* NOTA: NO sobrescribir estilos Bootstrap h6 - Template usa defaults */
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Footer Principal Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos del footer principal del sitio
|
||||
* - Background navy dark
|
||||
* - Títulos y enlaces
|
||||
* - Botón de newsletter
|
||||
* - Hover states
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.19
|
||||
* @source roi-theme-template/css/style.css (líneas 987-1021)
|
||||
* @reference CSS-ESPECIFICO.md
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
FOOTER
|
||||
======================================== */
|
||||
|
||||
footer {
|
||||
background-color: var(--color-navy-dark);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
footer h5 {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
footer .btn-primary {
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
footer .btn-primary:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
border-color: var(--color-orange-hover);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Hero Section Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos del componente Hero Section
|
||||
* - Contenedor principal con gradiente navy
|
||||
* - Título H1 del post
|
||||
* - Category badges con efecto glassmorphism
|
||||
*
|
||||
* CORRECCIÓN Issue #121: Este archivo estaba vacío (solo comentarios)
|
||||
* diciendo que los estilos estaban en style.css
|
||||
*
|
||||
* Ahora contiene el CSS correcto del template según:
|
||||
* _planeacion/_desarrollo-tema-roi/theme-documentation/08-componente-hero-section/CSS-ESPECIFICO.md
|
||||
*
|
||||
* ELIMINADO: hero-section.css (duplicado con CSS incorrecto)
|
||||
* - hero-section.css usaba clases .hero-section y .hero-category-badge
|
||||
* - El HTML real usa .hero-title y .category-badge
|
||||
* - Se consolidó todo en este archivo con las clases correctas
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.9
|
||||
* @source roi-theme-template/css/style.css líneas 186-222
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Hero Section
|
||||
======================================== */
|
||||
|
||||
.hero-title {
|
||||
background: linear-gradient(135deg, var(--color-navy-primary) 0%, var(--color-navy-light) 100%);
|
||||
box-shadow: 0 4px 16px rgba(30, 58, 95, 0.25);
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.hero-title h1 {
|
||||
color: #ffffff !important;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Category Badges (Glassmorphism Effect)
|
||||
======================================== */
|
||||
|
||||
.category-badge {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
padding: 0.375rem 0.875rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.813rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-badge:hover {
|
||||
background: rgba(255, 133, 0, 0.2);
|
||||
border-color: rgba(255, 133, 0, 0.4);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.category-badge i {
|
||||
color: var(--color-orange-light);
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
/**
|
||||
* Modal de Contacto - Estilos
|
||||
*
|
||||
* Estilos para el modal de contacto con webhook
|
||||
* Compatible con Bootstrap 5.3.2
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
1. ESTRUCTURA DEL MODAL
|
||||
========================================================================== */
|
||||
|
||||
.modal-content {
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-close:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. FORMULARIO
|
||||
========================================================================== */
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-label .text-danger {
|
||||
font-weight: 700;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.65rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control:hover {
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #FF8600;
|
||||
box-shadow: 0 0 0 0.2rem rgba(255, 133, 0, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: #dc3545;
|
||||
padding-right: calc(1.5em + 0.75rem);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
}
|
||||
|
||||
.form-control.is-invalid:focus {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.form-control.is-valid {
|
||||
border-color: #28a745;
|
||||
padding-right: calc(1.5em + 0.75rem);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
}
|
||||
|
||||
.form-control.is-valid:focus {
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.form-control.is-invalid ~ .invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. BOTÓN DE ENVÍO
|
||||
========================================================================== */
|
||||
|
||||
.btn-submit-form {
|
||||
background: linear-gradient(135deg, #FF5722 0%, #FF6B35 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(255, 87, 34, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-submit-form::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.btn-submit-form:hover {
|
||||
background: linear-gradient(135deg, #E64A19 0%, #FF5722 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 87, 34, 0.4);
|
||||
}
|
||||
|
||||
.btn-submit-form:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-submit-form:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(255, 87, 34, 0.3);
|
||||
}
|
||||
|
||||
.btn-submit-form:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.5), 0 4px 12px rgba(255, 87, 34, 0.3);
|
||||
}
|
||||
|
||||
.btn-submit-form:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Spinner en botón */
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 0.15em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. MENSAJES DE FEEDBACK
|
||||
========================================================================== */
|
||||
|
||||
#formMessage {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
5. ANIMACIONES DEL MODAL
|
||||
========================================================================== */
|
||||
|
||||
.modal.fade .modal-dialog {
|
||||
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
|
||||
transform: translate(0, -50px);
|
||||
}
|
||||
|
||||
.modal.show .modal-dialog {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Backdrop personalizado */
|
||||
.modal-backdrop.show {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
6. RESPONSIVE
|
||||
========================================================================== */
|
||||
|
||||
/* Tablets y dispositivos pequeños */
|
||||
@media (max-width: 768px) {
|
||||
.modal-dialog {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0.75rem 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
font-size: 16px; /* Previene zoom en iOS */
|
||||
}
|
||||
}
|
||||
|
||||
/* Móviles pequeños */
|
||||
@media (max-width: 576px) {
|
||||
.modal-dialog {
|
||||
margin: 0.5rem;
|
||||
max-width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0.5rem 0.75rem 0.75rem 0.75rem;
|
||||
}
|
||||
|
||||
.btn-submit-form {
|
||||
padding: 0.65rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
7. ACCESIBILIDAD
|
||||
========================================================================== */
|
||||
|
||||
/* Indicador de foco visible para navegación por teclado */
|
||||
.modal-content *:focus-visible {
|
||||
outline: 2px solid #FF8600;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Mejora de contraste para lectores de pantalla */
|
||||
.screen-reader-text {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.form-control {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.btn-submit-form {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal.fade .modal-dialog,
|
||||
.btn-submit-form,
|
||||
.form-control,
|
||||
.btn-close {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-submit-form::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#formMessage {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
8. DARK MODE (OPCIONAL)
|
||||
========================================================================== */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.modal-content {
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: #34495e;
|
||||
border-color: #4a5f7f;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: #34495e;
|
||||
border-color: #FF8600;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
9. PRINT STYLES
|
||||
========================================================================== */
|
||||
|
||||
@media print {
|
||||
.modal,
|
||||
.modal-backdrop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Navbar Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos del componente de navegación principal
|
||||
* - Navbar sticky
|
||||
* - Navbar brand
|
||||
* - Nav links y efectos hover
|
||||
* - Dropdown menu
|
||||
* - Dropdown items
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.7
|
||||
* @source roi-theme-template/css/style.css
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Navbar Principal
|
||||
======================================== */
|
||||
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1030;
|
||||
background-color: var(--color-navy-primary) !important;
|
||||
box-shadow: 0 4px 12px rgba(30, 58, 95, 0.15);
|
||||
padding: 0.75rem 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
box-shadow: 0 6px 20px rgba(30, 58, 95, 0.25);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Navbar Brand
|
||||
======================================== */
|
||||
|
||||
.navbar-brand {
|
||||
color: #ffffff !important;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand:hover {
|
||||
color: var(--color-orange-primary) !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Nav Links
|
||||
======================================== */
|
||||
|
||||
.nav-link {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
padding: 0.5rem 0.65rem !important;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scaleX(0);
|
||||
width: 80%;
|
||||
height: 2px;
|
||||
background: var(--color-orange-primary);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-orange-primary) !important;
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Dropdown Menu
|
||||
======================================== */
|
||||
|
||||
.dropdown-menu {
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-gray-400) transparent;
|
||||
}
|
||||
|
||||
/* Webkit browsers (Chrome, Safari, Edge) scrollbar */
|
||||
.dropdown-menu::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dropdown-menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-gray-400);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dropdown-menu::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Dropdown Items
|
||||
======================================== */
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--color-neutral-600);
|
||||
padding: 0.5rem 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Dropdown Hover (Desktop Only)
|
||||
======================================== */
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.nav-item:hover > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Post Content Component
|
||||
*
|
||||
* Estilos para el contenedor y contenido de posts
|
||||
* Source: roi-theme-template/css/style.css líneas 245-298
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
POST CONTENT CONTAINER
|
||||
============================================ */
|
||||
|
||||
.post-content {
|
||||
background: #ffffff;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
POST CONTENT TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
.post-content h2 {
|
||||
color: var(--color-navy-primary);
|
||||
font-weight: 700;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 3px solid var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
color: var(--color-navy-light);
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post-content h4 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
color: var(--color-neutral-600);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.post-content ul,
|
||||
.post-content ol {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.post-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
.post-content strong {
|
||||
color: var(--color-navy-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.post-content a {
|
||||
color: var(--color-orange-primary);
|
||||
text-decoration: underline;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.post-content a:hover {
|
||||
color: var(--color-orange-hover);
|
||||
}
|
||||
|
||||
.post-content blockquote {
|
||||
border-left: 4px solid #0d6efd;
|
||||
padding-left: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.post-content code {
|
||||
background: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
.post-content pre {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.post-content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.post-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.post-content h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.post-content p,
|
||||
.post-content li {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Related Posts Component
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos para las cards de posts relacionados
|
||||
* - Cards con borde izquierdo navy de 4px
|
||||
* - Hover effect con borde naranja
|
||||
* - Cursor pointer
|
||||
* - Integración con Bootstrap 5 cards
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.17
|
||||
* @source roi-theme-template/css/style.css (líneas del template)
|
||||
* @reference CSS-ESPECIFICO.md líneas 62-132
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Related Posts Section
|
||||
======================================== */
|
||||
|
||||
.related-posts {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.related-posts h2 {
|
||||
color: var(--color-navy-primary);
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Related Posts Cards
|
||||
======================================== */
|
||||
|
||||
.related-posts .card {
|
||||
cursor: pointer;
|
||||
background: #ffffff !important;
|
||||
border: 1px solid var(--color-neutral-100) !important;
|
||||
border-left: 4px solid var(--color-neutral-600) !important;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.related-posts .card:hover {
|
||||
background: var(--color-neutral-50) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
border-left-color: var(--color-orange-primary) !important;
|
||||
}
|
||||
|
||||
.related-posts .card-body {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.related-posts .card-title {
|
||||
color: var(--color-navy-primary) !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.related-posts a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.related-posts a:hover .card-title {
|
||||
color: var(--color-orange-primary) !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Pagination (si existe)
|
||||
======================================== */
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--color-neutral-600);
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: var(--color-orange-primary);
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Social Share Buttons - Estilos Mínimos
|
||||
*
|
||||
* Según CSS-ESPECIFICO.md de la documentación:
|
||||
* Solo 2 reglas CSS simples. Bootstrap maneja el resto.
|
||||
*
|
||||
* Fuente: roi-theme-template/css/style.css líneas 795-806
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* === SHARE BUTTONS === */
|
||||
|
||||
.share-buttons .btn {
|
||||
transition: all 0.3s ease;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.share-buttons .btn:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Sidebar TOC (Table of Contents) Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos del componente TOC Sidebar
|
||||
* - Contenedor sticky (.sidebar-sticky)
|
||||
* - Contenedor TOC (.toc-container)
|
||||
* - Título del TOC (.toc-container h4)
|
||||
* - Lista de enlaces (.toc-list)
|
||||
* - Items y enlaces del TOC
|
||||
* - Scrollbar personalizado
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.5
|
||||
* @source roi-theme-template/css/style.css líneas 663-746
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Contenedor Sticky del Sidebar
|
||||
======================================== */
|
||||
|
||||
.sidebar-sticky {
|
||||
position: sticky;
|
||||
top: 85px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Contenedor del TOC
|
||||
======================================== */
|
||||
|
||||
.toc-container {
|
||||
margin-bottom: 13px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: 12px 16px;
|
||||
max-height: calc(100vh - 71px - 10px - 250px - 15px - 15px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Título del TOC
|
||||
======================================== */
|
||||
|
||||
.toc-container h4 {
|
||||
color: var(--color-navy-primary);
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--color-neutral-100);
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Lista de Enlaces del TOC
|
||||
======================================== */
|
||||
|
||||
.toc-list {
|
||||
overflow-y: auto;
|
||||
padding-right: 0.5rem;
|
||||
list-style: none;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.toc-container li {
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Enlaces del TOC
|
||||
======================================== */
|
||||
|
||||
.toc-container a {
|
||||
display: block;
|
||||
padding: 0.3rem 0.85rem;
|
||||
color: var(--color-neutral-600);
|
||||
text-decoration: none;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.toc-container a:hover {
|
||||
background: var(--color-neutral-50);
|
||||
border-left-color: var(--color-navy-primary);
|
||||
color: var(--color-navy-primary);
|
||||
}
|
||||
|
||||
.toc-container a.active {
|
||||
background: var(--color-neutral-50);
|
||||
border-left-color: var(--color-navy-primary);
|
||||
color: var(--color-navy-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Scrollbar Personalizado (Webkit)
|
||||
======================================== */
|
||||
|
||||
.toc-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.toc-list::-webkit-scrollbar-track {
|
||||
background: var(--color-neutral-50);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.toc-list::-webkit-scrollbar-thumb {
|
||||
background: var(--color-neutral-600);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.toc-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-neutral-700);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Top Notification Bar Styles
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos de la barra de notificación superior
|
||||
* - Contenedor principal
|
||||
* - Palabras destacadas (strong)
|
||||
* - Iconos
|
||||
* - Enlaces
|
||||
*
|
||||
* CORRECCIÓN Issue #121: Este archivo contenía CSS avanzado (position fixed, botón cerrar)
|
||||
* que NO coincidía con el HTML simple del template en header.php
|
||||
*
|
||||
* Ahora contiene el CSS correcto del template según:
|
||||
* _planeacion/_desarrollo-tema-roi/theme-documentation/05-componente-top-bar/CSS-ESPECIFICO.md
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.8
|
||||
* @source roi-theme-template/css/style.css líneas 57-80
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Top Notification Bar
|
||||
======================================== */
|
||||
|
||||
.top-notification-bar {
|
||||
background-color: var(--color-navy-dark);
|
||||
color: #ffffff;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.top-notification-bar strong {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.top-notification-bar i {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.top-notification-bar a {
|
||||
color: #ffffff;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.top-notification-bar a:hover {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
@@ -37,9 +37,7 @@
|
||||
color: var(--color-orange-primary);
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: var(--color-orange-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 133, 0, 0.15);
|
||||
z-index: 2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-link:focus {
|
||||
@@ -53,17 +51,8 @@
|
||||
/* Active page */
|
||||
.page-item.active .page-link {
|
||||
color: #ffffff;
|
||||
background: var(--color-orange-primary);
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.3);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.page-item.active .page-link:hover {
|
||||
background: var(--color-orange-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 133, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
|
||||
90
footer.php
90
footer.php
@@ -2,93 +2,19 @@
|
||||
/**
|
||||
* Footer Template
|
||||
*
|
||||
* Replica EXACTAMENTE la estructura del template (líneas 1093-1149)
|
||||
* Footer con 3 columnas de navegación + newsletter simple (solo email).
|
||||
* Renderiza el footer usando el componente dinámico desde BD.
|
||||
* Los menús se gestionan desde Apariencia > Menús.
|
||||
* La configuración se gestiona desde ROI Theme > Footer.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Renderizar footer dinámico
|
||||
echo roi_render_component('footer');
|
||||
|
||||
wp_footer();
|
||||
?>
|
||||
|
||||
<!-- Footer (Template líneas 1093-1149) -->
|
||||
<footer class="py-5 mt-0 bg-dark text-white">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Sección 1: Navegación -->
|
||||
<div class="col-6 col-md-2 mb-3">
|
||||
<h5><?php esc_html_e('Recursos', 'roi-theme'); ?></h5>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Inicio', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/blog'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Blog', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/catalogo'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Catálogo', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/precios'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Precios', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/nosotros'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Nosotros', 'roi-theme'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Sección 2: Navegación -->
|
||||
<div class="col-6 col-md-2 mb-3">
|
||||
<h5><?php esc_html_e('Soporte', 'roi-theme'); ?></h5>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/faq'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Preguntas Frecuentes', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/ayuda'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Centro de Ayuda', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/contacto'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Contacto', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/politicas'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Políticas', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/terminos'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Términos', 'roi-theme'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Sección 3: Navegación -->
|
||||
<div class="col-6 col-md-2 mb-3">
|
||||
<h5><?php esc_html_e('Empresa', 'roi-theme'); ?></h5>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/nosotros'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Acerca de', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/equipo'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Equipo', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/trabajos'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Trabaja con Nosotros', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/prensa'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Prensa', 'roi-theme'); ?></a></li>
|
||||
<li class="nav-item mb-2"><a href="<?php echo home_url('/partners'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Partners', 'roi-theme'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Newsletter Simple (solo email) -->
|
||||
<div class="col-md-5 offset-md-1 mb-3">
|
||||
<form>
|
||||
<h5><?php esc_html_e('Suscríbete al Newsletter', 'roi-theme'); ?></h5>
|
||||
<p><?php esc_html_e('Recibe las últimas actualizaciones de APUs.', 'roi-theme'); ?></p>
|
||||
<div class="d-flex flex-column flex-sm-row w-100 gap-2">
|
||||
<label for="newsletter1" class="visually-hidden"><?php esc_html_e('Email', 'roi-theme'); ?></label>
|
||||
<input id="newsletter1" type="email" class="form-control" placeholder="<?php esc_attr_e('Email', 'roi-theme'); ?>">
|
||||
<button class="btn btn-primary" type="button"><?php esc_html_e('Suscribirse', 'roi-theme'); ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copyright y Redes Sociales -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between py-4 my-4 border-top">
|
||||
<p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. <?php esc_html_e('Todos los derechos reservados.', 'roi-theme'); ?></p>
|
||||
<ul class="list-unstyled d-flex">
|
||||
<li class="ms-3">
|
||||
<a class="link-light" href="https://twitter.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Twitter', 'roi-theme'); ?>">
|
||||
<i class="bi bi-twitter"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="ms-3">
|
||||
<a class="link-light" href="https://instagram.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Instagram', 'roi-theme'); ?>">
|
||||
<i class="bi bi-instagram"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="ms-3">
|
||||
<a class="link-light" href="https://facebook.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Facebook', 'roi-theme'); ?>">
|
||||
<i class="bi bi-facebook"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php wp_footer(); ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
232
functions-addon.php
Normal file
232
functions-addon.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
// =============================================================================
|
||||
// AUTOLOADER PARA COMPONENTES
|
||||
// =============================================================================
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
// Mapeo de namespaces a directorios
|
||||
$prefixes = [
|
||||
'ROITheme\\Shared\\' => get_template_directory() . '/Shared/',
|
||||
'ROITheme\\Public\\' => get_template_directory() . '/Public/',
|
||||
'ROITheme\\Admin\\' => get_template_directory() . '/Admin/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $base_dir) {
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) === 0) {
|
||||
$relative_class = substr($class, $len);
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTION: roi_get_navbar_setting()
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Obtiene un valor de configuración del navbar desde la BD
|
||||
*
|
||||
* @param string $group Nombre del grupo (ej: 'media', 'visibility')
|
||||
* @param string $attribute Nombre del atributo (ej: 'show_brand', 'logo_url')
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed Valor del atributo
|
||||
*/
|
||||
function roi_get_navbar_setting(string $group, string $attribute, $default = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'roi_theme_component_settings';
|
||||
$value = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT attribute_value FROM {$table}
|
||||
WHERE component_name = 'navbar'
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s",
|
||||
$group,
|
||||
$attribute
|
||||
));
|
||||
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// Convertir booleanos
|
||||
if ($value === '1') return true;
|
||||
if ($value === '0') return false;
|
||||
|
||||
// Intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTION: roi_render_component()
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Renderiza un componente por su nombre
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @return string HTML del componente renderizado
|
||||
*/
|
||||
function roi_render_component(string $componentName): string {
|
||||
global $wpdb;
|
||||
|
||||
// DEBUG: Trace component rendering
|
||||
error_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}");
|
||||
|
||||
try {
|
||||
// Obtener datos del componente desde BD normalizada
|
||||
$table = $wpdb->prefix . 'roi_theme_component_settings';
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT group_name, attribute_name, attribute_value
|
||||
FROM {$table}
|
||||
WHERE component_name = %s
|
||||
ORDER BY group_name, attribute_name",
|
||||
$componentName
|
||||
));
|
||||
|
||||
if (empty($rows)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Reconstruir estructura de datos agrupada
|
||||
$data = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!isset($data[$row->group_name])) {
|
||||
$data[$row->group_name] = [];
|
||||
}
|
||||
// Decodificar valor
|
||||
$value = $row->attribute_value;
|
||||
|
||||
// Convertir booleanos almacenados como '1' o '0'
|
||||
if ($value === '1' || $value === '0') {
|
||||
$value = ($value === '1');
|
||||
} else {
|
||||
// Intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$value = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$data[$row->group_name][$row->attribute_name] = $value;
|
||||
}
|
||||
|
||||
// Crear Value Objects requeridos
|
||||
$name = new \ROITheme\Shared\Domain\ValueObjects\ComponentName($componentName);
|
||||
$configuration = new \ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration($data);
|
||||
$visibility = \ROITheme\Shared\Domain\ValueObjects\ComponentVisibility::allDevices(); // Default: visible en todas partes
|
||||
|
||||
// Crear instancia del componente
|
||||
$component = new \ROITheme\Shared\Domain\Entities\Component(
|
||||
$name,
|
||||
$configuration,
|
||||
$visibility
|
||||
);
|
||||
|
||||
// Obtener renderer específico para el componente
|
||||
$renderer = null;
|
||||
|
||||
// Crear instancia del CSSGeneratorService (reutilizable para todos los renderers que lo necesiten)
|
||||
$cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService();
|
||||
|
||||
switch ($componentName) {
|
||||
// Componentes nuevos (namespace PascalCase correcto)
|
||||
case 'top-notification-bar':
|
||||
$renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator);
|
||||
break;
|
||||
case 'navbar':
|
||||
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator);
|
||||
break;
|
||||
case 'cta-lets-talk':
|
||||
$renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator);
|
||||
break;
|
||||
case 'hero':
|
||||
error_log("ROI Theme DEBUG: Creating HeroRenderer");
|
||||
$renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator);
|
||||
error_log("ROI Theme DEBUG: HeroRenderer created successfully");
|
||||
break;
|
||||
// Componentes legacy (namespace minúsculas - pendiente migración)
|
||||
case 'hero-section':
|
||||
$renderer = new \ROITheme\Public\herosection\infrastructure\ui\HeroSectionRenderer();
|
||||
break;
|
||||
case 'featured-image':
|
||||
$renderer = new \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer($cssGenerator);
|
||||
break;
|
||||
case 'table-of-contents':
|
||||
$renderer = new \ROITheme\Public\TableOfContents\Infrastructure\Ui\TableOfContentsRenderer($cssGenerator);
|
||||
break;
|
||||
case 'cta-box-sidebar':
|
||||
$renderer = new \ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui\CtaBoxSidebarRenderer($cssGenerator);
|
||||
break;
|
||||
case 'social-share':
|
||||
$renderer = new \ROITheme\Public\SocialShare\Infrastructure\Ui\SocialShareRenderer($cssGenerator);
|
||||
break;
|
||||
case 'cta-post':
|
||||
$renderer = new \ROITheme\Public\CtaPost\Infrastructure\Ui\CtaPostRenderer($cssGenerator);
|
||||
break;
|
||||
case 'related-post':
|
||||
$renderer = new \ROITheme\Public\RelatedPost\Infrastructure\Ui\RelatedPostRenderer($cssGenerator);
|
||||
break;
|
||||
case 'contact-form':
|
||||
$renderer = new \ROITheme\Public\ContactForm\Infrastructure\Ui\ContactFormRenderer($cssGenerator);
|
||||
break;
|
||||
case 'footer':
|
||||
$renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$renderer) {
|
||||
error_log("ROI Theme DEBUG: No renderer for {$componentName}");
|
||||
return '';
|
||||
}
|
||||
|
||||
error_log("ROI Theme DEBUG: Calling render() for {$componentName}");
|
||||
$output = $renderer->render($component);
|
||||
error_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}");
|
||||
return $output;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Always log errors for debugging
|
||||
error_log('ROI Theme ERROR: Exception rendering component ' . $componentName . ': ' . $e->getMessage());
|
||||
error_log('ROI Theme ERROR: Stack trace: ' . $e->getTraceAsString());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CARGAR ARCHIVOS DE INC/
|
||||
// =============================================================================
|
||||
|
||||
// CTA A/B Testing System
|
||||
$cta_ab_testing = get_template_directory() . '/Inc/cta-ab-testing.php';
|
||||
if (file_exists($cta_ab_testing)) {
|
||||
require_once $cta_ab_testing;
|
||||
}
|
||||
|
||||
// CTA Customizer Settings
|
||||
$cta_customizer = get_template_directory() . '/Inc/customizer-cta.php';
|
||||
if (file_exists($cta_customizer)) {
|
||||
require_once $cta_customizer;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ESTILOS BASE PARA TOP NOTIFICATION BAR
|
||||
// =============================================================================
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// NOTA: Los estilos de TOC y CTA Box Sidebar se generan dinámicamente
|
||||
// desde la base de datos a través de sus respectivos Renderers.
|
||||
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
|
||||
// =============================================================================
|
||||
551
functions.php
551
functions.php
@@ -1,66 +1,240 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* ROI Theme Functions and Definitions
|
||||
* ROI Theme - Clean Architecture Bootstrap
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
* Este archivo es el punto de entrada del tema.
|
||||
* Solo contiene bootstrap (carga del autoloader e inicialización del DI Container).
|
||||
* TODO el código de lógica de negocio está en admin/, public/, shared/
|
||||
*
|
||||
* @package ROITheme
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
// Prevenir acceso directo
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* BOOTSTRAP CLEAN ARCHITECTURE (Fase 1)
|
||||
* ========================================================================
|
||||
*
|
||||
* Carga el autoloader de Composer y el DI Container.
|
||||
* Esta sección inicializa la arquitectura limpia del tema.
|
||||
*/
|
||||
// Definir constante de versión del tema
|
||||
define('ROI_VERSION', '1.0.19');
|
||||
|
||||
// Load Composer autoloader
|
||||
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
// =============================================================================
|
||||
// 1. CARGAR AUTOLOADER MANUAL
|
||||
// =============================================================================
|
||||
|
||||
// IMPORTANTE: Cargar functions-addon.php PRIMERO para registrar el autoloader
|
||||
// que permite cargar clases de ROITheme automáticamente
|
||||
require_once __DIR__ . '/functions-addon.php';
|
||||
|
||||
// =============================================================================
|
||||
// 2. CARGAR ARCHIVOS DE INC/ (ANTES DE LA CONFIGURACIÓN)
|
||||
// =============================================================================
|
||||
|
||||
// IMPORTANTE: Cargar archivos inc/ ANTES del DI Container
|
||||
// para que los estilos y scripts se registren correctamente
|
||||
|
||||
require_once get_template_directory() . '/Inc/sanitize-functions.php';
|
||||
require_once get_template_directory() . '/Inc/theme-options-helpers.php';
|
||||
require_once get_template_directory() . '/Inc/nav-walker.php';
|
||||
require_once get_template_directory() . '/Inc/enqueue-scripts.php';
|
||||
require_once get_template_directory() . '/Inc/customizer-fonts.php';
|
||||
require_once get_template_directory() . '/Inc/seo.php';
|
||||
require_once get_template_directory() . '/Inc/performance.php';
|
||||
require_once get_template_directory() . '/Inc/critical-css.php';
|
||||
require_once get_template_directory() . '/Inc/image-optimization.php';
|
||||
require_once get_template_directory() . '/Inc/template-functions.php';
|
||||
require_once get_template_directory() . '/Inc/template-tags.php';
|
||||
require_once get_template_directory() . '/Inc/featured-image.php';
|
||||
require_once get_template_directory() . '/Inc/category-badge.php';
|
||||
require_once get_template_directory() . '/Inc/adsense-delay.php';
|
||||
require_once get_template_directory() . '/Inc/related-posts.php';
|
||||
require_once get_template_directory() . '/Inc/toc.php';
|
||||
require_once get_template_directory() . '/Inc/apu-tables.php';
|
||||
require_once get_template_directory() . '/Inc/search-disable.php';
|
||||
require_once get_template_directory() . '/Inc/comments-disable.php';
|
||||
require_once get_template_directory() . '/Inc/social-share.php';
|
||||
require_once get_template_directory() . '/Inc/cta-ab-testing.php';
|
||||
require_once get_template_directory() . '/Inc/customizer-cta.php';
|
||||
|
||||
// =============================================================================
|
||||
// 3. INICIALIZAR DI CONTAINER (Clean Architecture)
|
||||
// =============================================================================
|
||||
|
||||
use ROITheme\Shared\Infrastructure\DI\DIContainer;
|
||||
|
||||
try {
|
||||
global $wpdb;
|
||||
|
||||
// Path a los schemas
|
||||
$schemas_path = get_template_directory() . '/schemas';
|
||||
|
||||
// Crear instancia del DI Container con dependencias
|
||||
$container = new DIContainer($wpdb, $schemas_path);
|
||||
|
||||
// TODO: Inicializar controladores cuando estén implementados (Fase 5+)
|
||||
// $ajaxController = $container->getAjaxController();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Manejar errores de inicialización (no crítico, solo log)
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme: Failed to initialize DI Container: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
}
|
||||
// NO hacer return - permitir que el tema continúe funcionando
|
||||
$container = null;
|
||||
}
|
||||
|
||||
// Initialize DI Container
|
||||
use ROITheme\Infrastructure\DI\DIContainer;
|
||||
// =============================================================================
|
||||
// 3.1. INICIALIZAR PANEL DE ADMINISTRACIÓN (Clean Architecture)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get DI Container instance
|
||||
*
|
||||
* Helper function to access the DI Container throughout the theme.
|
||||
*
|
||||
* @return DIContainer
|
||||
*/
|
||||
function roi_container(): DIContainer {
|
||||
return DIContainer::getInstance();
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Infrastructure\API\WordPress\AdminMenuRegistrar;
|
||||
use ROITheme\Admin\Infrastructure\Services\AdminAssetEnqueuer;
|
||||
|
||||
try {
|
||||
// Obtener Use Case para cargar configuraciones
|
||||
$getComponentSettingsUseCase = $container?->getGetComponentSettingsUseCase();
|
||||
|
||||
// Crear MenuItem con configuración del panel
|
||||
$menuItem = new MenuItem(
|
||||
pageTitle: 'ROI Theme - Panel de Administración',
|
||||
menuTitle: 'ROI Theme',
|
||||
capability: 'manage_options',
|
||||
menuSlug: 'roi-theme-admin',
|
||||
icon: 'dashicons-admin-settings',
|
||||
position: 60
|
||||
);
|
||||
|
||||
// Crear renderer del dashboard con inyección del Use Case
|
||||
$dashboardRenderer = new AdminDashboardRenderer($getComponentSettingsUseCase);
|
||||
|
||||
// Crear caso de uso para renderizar
|
||||
$renderDashboardUseCase = new RenderDashboardUseCase($dashboardRenderer);
|
||||
|
||||
// Crear y registrar el menú de administración
|
||||
$adminMenuRegistrar = new AdminMenuRegistrar($menuItem, $renderDashboardUseCase);
|
||||
$adminMenuRegistrar->register();
|
||||
|
||||
// Crear y registrar el enqueuer de assets
|
||||
$adminAssetEnqueuer = new AdminAssetEnqueuer(get_template_directory_uri());
|
||||
$adminAssetEnqueuer->register();
|
||||
|
||||
// Obtener Use Case para guardar configuraciones
|
||||
$saveComponentSettingsUseCase = $container?->getSaveComponentSettingsUseCase();
|
||||
|
||||
// Crear y registrar el handler AJAX con inyección del Use Case
|
||||
$adminAjaxHandler = new \ROITheme\Admin\Infrastructure\API\WordPress\AdminAjaxHandler($saveComponentSettingsUseCase);
|
||||
$adminAjaxHandler->register();
|
||||
|
||||
// Crear y registrar el handler AJAX para el Contact Form (público)
|
||||
$contactFormAjaxHandler = new \ROITheme\Public\ContactForm\Infrastructure\Api\WordPress\ContactFormAjaxHandler(
|
||||
$container->getComponentSettingsRepository()
|
||||
);
|
||||
$contactFormAjaxHandler->register();
|
||||
|
||||
// Crear y registrar el handler AJAX para Newsletter (público)
|
||||
$newsletterAjaxHandler = new \ROITheme\Public\Footer\Infrastructure\Api\WordPress\NewsletterAjaxHandler(
|
||||
$container->getComponentSettingsRepository()
|
||||
);
|
||||
$newsletterAjaxHandler->register();
|
||||
|
||||
// Log en modo debug
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme: Admin Panel initialized successfully');
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Manejar errores de inicialización del panel
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('ROI Theme: Failed to initialize Admin Panel: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* END BOOTSTRAP
|
||||
* ========================================================================
|
||||
*/
|
||||
// =============================================================================
|
||||
// 4. CONFIGURACIÓN DEL TEMA
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* THEME DATABASE TABLES SETUP
|
||||
* ========================================================================
|
||||
* Setup del tema
|
||||
*
|
||||
* Crea las tablas del tema cuando se activa.
|
||||
* Esto asegura que el tema sea portable y funcione en cualquier instalación WordPress.
|
||||
* Configuraciones básicas de WordPress theme support
|
||||
*/
|
||||
add_action('after_setup_theme', function() {
|
||||
// Soporte para título del documento
|
||||
add_theme_support('title-tag');
|
||||
|
||||
// Soporte para imágenes destacadas
|
||||
add_theme_support('post-thumbnails');
|
||||
|
||||
// Soporte para HTML5
|
||||
add_theme_support('html5', [
|
||||
'search-form',
|
||||
'comment-form',
|
||||
'comment-list',
|
||||
'gallery',
|
||||
'caption',
|
||||
'style',
|
||||
'script'
|
||||
]);
|
||||
|
||||
// Soporte para feeds automáticos
|
||||
add_theme_support('automatic-feed-links');
|
||||
|
||||
// Registro de ubicaciones de menús
|
||||
register_nav_menus([
|
||||
'primary' => __('Primary Menu', 'roi-theme'),
|
||||
'footer' => __('Footer Menu', 'roi-theme'),
|
||||
'footer_menu_1' => __('Footer Menu 1 (Widget 1)', 'roi-theme'),
|
||||
'footer_menu_2' => __('Footer Menu 2 (Widget 2)', 'roi-theme'),
|
||||
'footer_menu_3' => __('Footer Menu 3 (Widget 3)', 'roi-theme'),
|
||||
]);
|
||||
|
||||
// TODO: Agregar más configuraciones según sea necesario
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 5. HOOKS DE INICIALIZACIÓN (Para fases posteriores)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Hook para sincronización de schemas
|
||||
* TODO: Implementar en Fase 6
|
||||
*/
|
||||
// add_action('admin_init', function() use ($container) {
|
||||
// $syncService = $container->getSchemaSyncService();
|
||||
// // Verificar si hay schemas desactualizados
|
||||
// });
|
||||
|
||||
/**
|
||||
* Hook para detección de schemas desactualizados
|
||||
* TODO: Implementar en Fase 6
|
||||
*/
|
||||
// add_action('admin_notices', function() use ($container) {
|
||||
// // Mostrar aviso si hay schemas desactualizados
|
||||
// });
|
||||
|
||||
// =============================================================================
|
||||
// 5. INFORMACIÓN DE DEBUG (Solo en desarrollo)
|
||||
// =============================================================================
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
// Registrar que el tema se inicializó correctamente
|
||||
error_log('ROI Theme: Bootstrap completed successfully');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 6. INSTALACIÓN DE TABLAS DEL TEMA
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Crear tablas del tema en la activación
|
||||
*
|
||||
* Este hook se ejecuta cuando el tema se activa en WordPress.
|
||||
* Crea las tablas necesarias si no existen.
|
||||
*
|
||||
* @since 1.0.19
|
||||
*/
|
||||
add_action('after_switch_theme', function() {
|
||||
global $wpdb;
|
||||
@@ -68,39 +242,25 @@ add_action('after_switch_theme', function() {
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Tabla de components
|
||||
$table_components = $wpdb->prefix . 'roi_theme_components';
|
||||
$sql_components = "CREATE TABLE {$table_components} (
|
||||
// Tabla de configuración de componentes (normalizada)
|
||||
$table_settings = $wpdb->prefix . 'roi_theme_component_settings';
|
||||
$sql_settings = "CREATE TABLE {$table_settings} (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
component_name VARCHAR(50) NOT NULL,
|
||||
configuration LONGTEXT NOT NULL,
|
||||
content LONGTEXT,
|
||||
visibility TEXT NOT NULL,
|
||||
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
schema_version VARCHAR(20) NOT NULL,
|
||||
component_name VARCHAR(100) NOT NULL,
|
||||
group_name VARCHAR(100) NOT NULL,
|
||||
attribute_name VARCHAR(100) NOT NULL,
|
||||
attribute_value LONGTEXT NOT NULL,
|
||||
is_editable TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY component_name (component_name),
|
||||
INDEX idx_enabled (is_enabled),
|
||||
INDEX idx_schema_version (schema_version)
|
||||
UNIQUE KEY unique_setting (component_name, group_name, attribute_name),
|
||||
INDEX idx_component (component_name),
|
||||
INDEX idx_editable (is_editable)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Tabla de defaults/schemas
|
||||
$table_defaults = $wpdb->prefix . 'roi_theme_defaults';
|
||||
$sql_defaults = "CREATE TABLE {$table_defaults} (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
component_name VARCHAR(50) NOT NULL,
|
||||
default_schema LONGTEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY component_name (component_name)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Crear/actualizar tablas
|
||||
dbDelta($sql_components);
|
||||
dbDelta($sql_defaults);
|
||||
// Crear/actualizar tabla
|
||||
dbDelta($sql_settings);
|
||||
|
||||
// Log en modo debug
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
@@ -108,268 +268,11 @@ add_action('after_switch_theme', function() {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Theme Version
|
||||
*/
|
||||
define('ROI_VERSION', '1.0.19');
|
||||
|
||||
/**
|
||||
* Theme Setup
|
||||
*/
|
||||
function roi_theme_setup() {
|
||||
// Make theme available for translation
|
||||
load_theme_textdomain('roi-theme', get_template_directory() . '/languages');
|
||||
|
||||
// Let WordPress manage the document title
|
||||
add_theme_support('title-tag');
|
||||
|
||||
// Enable support for Post Thumbnails
|
||||
add_theme_support('post-thumbnails');
|
||||
|
||||
// Add image sizes
|
||||
add_image_size('roi-thumbnail', 400, 300, true);
|
||||
add_image_size('roi-medium', 800, 600, true);
|
||||
add_image_size('roi-large', 1200, 900, true);
|
||||
add_image_size('roi-featured-large', 1200, 600, true);
|
||||
add_image_size('roi-featured-medium', 800, 400, true);
|
||||
|
||||
// Switch default core markup to output valid HTML5
|
||||
add_theme_support('html5', array(
|
||||
'gallery',
|
||||
'caption',
|
||||
'style',
|
||||
'script',
|
||||
));
|
||||
|
||||
// Set content width
|
||||
if (!isset($content_width)) {
|
||||
$content_width = 1200;
|
||||
}
|
||||
|
||||
// Register navigation menus
|
||||
register_nav_menus(array(
|
||||
'primary' => __('Primary Menu', 'roi-theme'),
|
||||
'footer' => __('Footer Menu', 'roi-theme'),
|
||||
));
|
||||
}
|
||||
add_action('after_setup_theme', 'roi_theme_setup');
|
||||
|
||||
/**
|
||||
* Set the content width in pixels
|
||||
*/
|
||||
function roi_content_width() {
|
||||
$GLOBALS['content_width'] = apply_filters('roi_content_width', 1200);
|
||||
}
|
||||
add_action('after_setup_theme', 'roi_content_width', 0);
|
||||
|
||||
/**
|
||||
* ELIMINADO: roi_enqueue_scripts()
|
||||
*
|
||||
* Esta función estaba duplicando la carga de CSS.
|
||||
* El sistema modular en inc/enqueue-scripts.php ya carga style.css como 'roi-main-style' (prioridad 5).
|
||||
* Esta función duplicada lo cargaba otra vez como 'roi-theme-style' (prioridad 10).
|
||||
*
|
||||
* Fecha eliminación: 2025-01-08
|
||||
* Issue: #128 - Footer Contact Form
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register Widget Areas
|
||||
*/
|
||||
function roi_register_widget_areas() {
|
||||
// Primary Sidebar
|
||||
register_sidebar(array(
|
||||
'name' => __('Primary Sidebar', 'roi-theme'),
|
||||
'id' => 'sidebar-1',
|
||||
'description' => __('Main sidebar widget area', 'roi-theme'),
|
||||
'before_widget' => '<section id="%1$s" class="widget %2$s">',
|
||||
'after_widget' => '</section>',
|
||||
'before_title' => '<h2 class="widget-title">',
|
||||
'after_title' => '</h2>',
|
||||
));
|
||||
|
||||
// Footer Contact Form (Issue #37) - ARRIBA de los 4 widgets
|
||||
register_sidebar(array(
|
||||
'name' => __('Footer Contact Form', 'roi-theme'),
|
||||
'id' => 'footer-contact',
|
||||
'description' => __('Área de contacto arriba de los 4 widgets del footer', 'roi-theme'),
|
||||
'before_widget' => '<section id="%1$s" class="footer-contact-widget %2$s">',
|
||||
'after_widget' => '</section>',
|
||||
'before_title' => '<h3 class="widget-title">',
|
||||
'after_title' => '</h3>',
|
||||
));
|
||||
|
||||
// Footer Widget Areas
|
||||
for ($i = 1; $i <= 4; $i++) {
|
||||
register_sidebar(array(
|
||||
'name' => sprintf(__('Footer Column %d', 'roi-theme'), $i),
|
||||
'id' => 'footer-' . $i,
|
||||
'description' => sprintf(__('Footer widget area %d', 'roi-theme'), $i),
|
||||
'before_widget' => '<div id="%1$s" class="widget %2$s">',
|
||||
'after_widget' => '</div>',
|
||||
'before_title' => '<h3 class="widget-title">',
|
||||
'after_title' => '</h3>',
|
||||
));
|
||||
}
|
||||
}
|
||||
add_action('widgets_init', 'roi_register_widget_areas');
|
||||
|
||||
/**
|
||||
* Configure locale and date format
|
||||
*/
|
||||
function roi_configure_locale() {
|
||||
// Set locale to es_MX
|
||||
add_filter('locale', function($locale) {
|
||||
return 'es_MX';
|
||||
});
|
||||
}
|
||||
add_action('after_setup_theme', 'roi_configure_locale');
|
||||
|
||||
/**
|
||||
* Custom date format
|
||||
*/
|
||||
function roi_custom_date_format($format) {
|
||||
return 'd/m/Y'; // Format: day/month/year
|
||||
}
|
||||
add_filter('date_format', 'roi_custom_date_format');
|
||||
|
||||
/**
|
||||
* Include modular files
|
||||
*/
|
||||
// Sanitize Functions (load first to avoid redeclaration errors)
|
||||
if (file_exists(get_template_directory() . '/inc/sanitize-functions.php')) {
|
||||
require_once get_template_directory() . '/inc/sanitize-functions.php';
|
||||
}
|
||||
|
||||
// Theme Options Helpers (load first as other files may depend on it)
|
||||
if (file_exists(get_template_directory() . '/inc/theme-options-helpers.php')) {
|
||||
require_once get_template_directory() . '/inc/theme-options-helpers.php';
|
||||
}
|
||||
|
||||
// Admin Options API (Theme Options)
|
||||
// Cargar solo options-api.php para funciones auxiliares como roi_get_default_options()
|
||||
// theme-options.php está desactivado porque el menú se registra en admin/includes/class-admin-menu.php
|
||||
if (is_admin()) {
|
||||
if (file_exists(get_template_directory() . '/admin/theme-options/options-api.php')) {
|
||||
require_once get_template_directory() . '/admin/theme-options/options-api.php';
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap Nav Walker
|
||||
if (file_exists(get_template_directory() . '/inc/nav-walker.php')) {
|
||||
require_once get_template_directory() . '/inc/nav-walker.php';
|
||||
}
|
||||
|
||||
// Bootstrap and Script Enqueuing
|
||||
if (file_exists(get_template_directory() . '/inc/enqueue-scripts.php')) {
|
||||
require_once get_template_directory() . '/inc/enqueue-scripts.php';
|
||||
}
|
||||
|
||||
// Font customizer options
|
||||
if (file_exists(get_template_directory() . '/inc/customizer-fonts.php')) {
|
||||
require_once get_template_directory() . '/inc/customizer-fonts.php';
|
||||
}
|
||||
|
||||
// SEO optimizations and Rank Math compatibility
|
||||
if (file_exists(get_template_directory() . '/inc/seo.php')) {
|
||||
require_once get_template_directory() . '/inc/seo.php';
|
||||
}
|
||||
|
||||
// Performance optimizations
|
||||
if (file_exists(get_template_directory() . '/inc/performance.php')) {
|
||||
require_once get_template_directory() . '/inc/performance.php';
|
||||
}
|
||||
|
||||
// Critical CSS (optional, disabled by default)
|
||||
if (file_exists(get_template_directory() . '/inc/critical-css.php')) {
|
||||
require_once get_template_directory() . '/inc/critical-css.php';
|
||||
}
|
||||
|
||||
// Image optimization
|
||||
if (file_exists(get_template_directory() . '/inc/image-optimization.php')) {
|
||||
require_once get_template_directory() . '/inc/image-optimization.php';
|
||||
}
|
||||
|
||||
// Template functions
|
||||
if (file_exists(get_template_directory() . '/inc/template-functions.php')) {
|
||||
require_once get_template_directory() . '/inc/template-functions.php';
|
||||
}
|
||||
|
||||
// Template tags
|
||||
if (file_exists(get_template_directory() . '/inc/template-tags.php')) {
|
||||
require_once get_template_directory() . '/inc/template-tags.php';
|
||||
}
|
||||
|
||||
// Featured image functions
|
||||
if (file_exists(get_template_directory() . '/inc/featured-image.php')) {
|
||||
require_once get_template_directory() . '/inc/featured-image.php';
|
||||
}
|
||||
|
||||
// Category badge functions
|
||||
if (file_exists(get_template_directory() . '/inc/category-badge.php')) {
|
||||
require_once get_template_directory() . '/inc/category-badge.php';
|
||||
}
|
||||
|
||||
// AdSense delay loading
|
||||
if (file_exists(get_template_directory() . '/inc/adsense-delay.php')) {
|
||||
require_once get_template_directory() . '/inc/adsense-delay.php';
|
||||
}
|
||||
|
||||
// Related posts functionality
|
||||
if (file_exists(get_template_directory() . '/inc/related-posts.php')) {
|
||||
require_once get_template_directory() . '/inc/related-posts.php';
|
||||
}
|
||||
|
||||
// Related posts configuration options (admin helpers)
|
||||
if (file_exists(get_template_directory() . '/admin/theme-options/related-posts-options.php')) {
|
||||
require_once get_template_directory() . '/admin/theme-options/related-posts-options.php';
|
||||
}
|
||||
|
||||
// Table of Contents
|
||||
if (file_exists(get_template_directory() . '/inc/toc.php')) {
|
||||
require_once get_template_directory() . '/inc/toc.php';
|
||||
}
|
||||
|
||||
// APU Tables - Funciones para tablas de Análisis de Precios Unitarios (Issue #30)
|
||||
if (file_exists(get_template_directory() . '/inc/apu-tables.php')) {
|
||||
require_once get_template_directory() . '/inc/apu-tables.php';
|
||||
}
|
||||
|
||||
// Desactivar búsqueda nativa (Issue #3)
|
||||
if (file_exists(get_template_directory() . '/inc/search-disable.php')) {
|
||||
require_once get_template_directory() . '/inc/search-disable.php';
|
||||
}
|
||||
|
||||
// Desactivar comentarios (Issue #4)
|
||||
if (file_exists(get_template_directory() . '/inc/comments-disable.php')) {
|
||||
require_once get_template_directory() . '/inc/comments-disable.php';
|
||||
}
|
||||
|
||||
// Social share buttons (Issue #31)
|
||||
if (file_exists(get_template_directory() . '/inc/social-share.php')) {
|
||||
require_once get_template_directory() . '/inc/social-share.php';
|
||||
}
|
||||
|
||||
// CTA A/B Testing system (Issue #32)
|
||||
if (file_exists(get_template_directory() . '/inc/cta-ab-testing.php')) {
|
||||
require_once get_template_directory() . '/inc/cta-ab-testing.php';
|
||||
}
|
||||
|
||||
// CTA Customizer options (Issue #32)
|
||||
if (file_exists(get_template_directory() . '/inc/customizer-cta.php')) {
|
||||
require_once get_template_directory() . '/inc/customizer-cta.php';
|
||||
}
|
||||
|
||||
// Admin Panel Module (Phase 1-2: Base Structure)
|
||||
if (file_exists(get_template_directory() . '/admin/init.php')) {
|
||||
require_once get_template_directory() . '/admin/init.php';
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// REGISTRO DE COMANDOS WP-CLI
|
||||
// =============================================================================
|
||||
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
require_once get_template_directory() . '/src/Infrastructure/API/WordPress/MigrationCommand.php';
|
||||
require_once get_template_directory() . '/Shared/Infrastructure/Api/WordPress/MigrationCommand.php';
|
||||
}
|
||||
|
||||
|
||||
73
header.php
73
header.php
@@ -20,13 +20,44 @@
|
||||
<body <?php body_class(); ?> data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100">
|
||||
<?php wp_body_open(); ?>
|
||||
|
||||
<?php get_template_part('template-parts/top-notification-bar'); ?>
|
||||
<?php
|
||||
if (function_exists('roi_render_component')) {
|
||||
echo roi_render_component('top-notification-bar');
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- 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', 'roi-theme'); ?>">
|
||||
<div class="container">
|
||||
|
||||
<!-- Hamburger Toggle Button - PRIMERO según template línea 286 -->
|
||||
<?php
|
||||
// Brand/Logo - Leer configuración desde BD
|
||||
$show_brand = roi_get_navbar_setting('media', 'show_brand', false);
|
||||
if ($show_brand) :
|
||||
$use_logo = roi_get_navbar_setting('media', 'use_logo', false);
|
||||
$brand_text = roi_get_navbar_setting('media', 'brand_text', get_bloginfo('name'));
|
||||
$home_url = home_url('/');
|
||||
|
||||
if ($use_logo) :
|
||||
$logo_url = roi_get_navbar_setting('media', 'logo_url', '');
|
||||
if (!empty($logo_url)) :
|
||||
?>
|
||||
<a class="navbar-brand" href="<?php echo esc_url($home_url); ?>">
|
||||
<img src="<?php echo esc_url($logo_url); ?>" alt="<?php echo esc_attr($brand_text); ?>" class="roi-navbar-logo">
|
||||
</a>
|
||||
<?php
|
||||
endif;
|
||||
else :
|
||||
?>
|
||||
<a class="navbar-brand roi-navbar-brand" href="<?php echo esc_url($home_url); ?>">
|
||||
<?php echo esc_html($brand_text); ?>
|
||||
</a>
|
||||
<?php
|
||||
endif;
|
||||
endif;
|
||||
?>
|
||||
|
||||
<!-- Hamburger Toggle Button -->
|
||||
<button class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
@@ -40,38 +71,18 @@
|
||||
<!-- Collapsible Menu -->
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<?php
|
||||
if (has_nav_menu('primary')) {
|
||||
wp_nav_menu(array(
|
||||
'theme_location' => 'primary',
|
||||
'container' => false,
|
||||
'menu_class' => 'navbar-nav mb-2 mb-lg-0',
|
||||
'fallback_cb' => false,
|
||||
'depth' => 2,
|
||||
'walker' => new WP_Bootstrap_Navwalker(),
|
||||
));
|
||||
} else {
|
||||
// Fallback si no hay menú asignado
|
||||
?>
|
||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/')); ?>">
|
||||
<?php esc_html_e('Home', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(get_post_type_archive_link('post')); ?>">
|
||||
<?php esc_html_e('Blog', 'roi-theme'); ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php
|
||||
// Navbar Component - Menu de navegación
|
||||
if (function_exists('roi_render_component')) {
|
||||
echo roi_render_component('navbar');
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Let's Talk Button (Template líneas 315-317) -->
|
||||
<button class="btn btn-lets-talk ms-lg-3" type="button" data-bs-toggle="modal" data-bs-target="#contactModal">
|
||||
<i class="bi bi-lightning-charge-fill me-2"></i><?php esc_html_e('Let\'s Talk', 'roi-theme'); ?>
|
||||
</button>
|
||||
<?php
|
||||
// CTA "Let's Talk" Button Component
|
||||
if (function_exists('roi_render_component')) {
|
||||
echo roi_render_component('cta-lets-talk');
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
</div><!-- .container -->
|
||||
|
||||
@@ -114,8 +114,8 @@ function roi_preload_custom_fonts() {
|
||||
// Example preload links - uncomment and modify when you have custom font files
|
||||
/*
|
||||
?>
|
||||
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Regular.woff2'); ?>" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Bold.woff2'); ?>" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/Assets/fonts/CustomSans-Regular.woff2'); ?>" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/Assets/fonts/CustomSans-Bold.woff2'); ?>" as="font" type="font/woff2" crossorigin>
|
||||
<?php
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ function roi_enqueue_fonts() {
|
||||
// Fonts CSS local
|
||||
wp_enqueue_style(
|
||||
'roi-fonts',
|
||||
get_template_directory_uri() . '/assets/css/css-global-fonts.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-fonts.css',
|
||||
array('google-fonts-poppins'),
|
||||
'1.0.0',
|
||||
'all'
|
||||
@@ -43,7 +43,7 @@ function roi_enqueue_bootstrap() {
|
||||
// Bootstrap CSS - with high priority
|
||||
wp_enqueue_style(
|
||||
'roi-bootstrap',
|
||||
get_template_directory_uri() . '/assets/vendor/bootstrap/css/bootstrap.min.css',
|
||||
get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css',
|
||||
array('roi-fonts'),
|
||||
'5.3.2',
|
||||
'all'
|
||||
@@ -52,7 +52,7 @@ function roi_enqueue_bootstrap() {
|
||||
// Bootstrap Icons CSS - LOCAL (Issue #135: CORS bloqueaba CDN)
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
get_template_directory_uri() . '/assets/vendor/bootstrap-icons.min.css',
|
||||
get_template_directory_uri() . '/Assets/vendor/bootstrap-icons.min.css',
|
||||
array('roi-bootstrap'),
|
||||
'1.11.3',
|
||||
'all'
|
||||
@@ -61,7 +61,7 @@ function roi_enqueue_bootstrap() {
|
||||
// Variables CSS del Template RDash (NIVEL 1 - BLOQUEANTE - Issue #48)
|
||||
wp_enqueue_style(
|
||||
'roi-variables',
|
||||
get_template_directory_uri() . '/assets/css/css-global-variables.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-variables.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -70,7 +70,7 @@ function roi_enqueue_bootstrap() {
|
||||
// Bootstrap JS Bundle - in footer with defer
|
||||
wp_enqueue_script(
|
||||
'roi-bootstrap-js',
|
||||
get_template_directory_uri() . '/assets/vendor/bootstrap/js/bootstrap.bundle.min.js',
|
||||
get_template_directory_uri() . '/Assets/vendor/bootstrap/js/bootstrap.bundle.min.js',
|
||||
array(),
|
||||
'5.3.2',
|
||||
array(
|
||||
@@ -93,7 +93,7 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_bootstrap', 5);
|
||||
function roi_enqueue_main_stylesheet() {
|
||||
wp_enqueue_style(
|
||||
'roi-main-style',
|
||||
get_template_directory_uri() . '/assets/css/style.css',
|
||||
get_template_directory_uri() . '/Assets/css/style.css',
|
||||
array('roi-variables'),
|
||||
'1.0.5', // Arquitectura: Separación de responsabilidades CSS
|
||||
'all'
|
||||
@@ -106,54 +106,34 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_main_stylesheet', 5);
|
||||
* Enqueue FASE 2 CSS - Template RDash Component Styles (Issues #58-64)
|
||||
*
|
||||
* Estilos que replican componentes del template RDash
|
||||
*
|
||||
* NOTA: Hero Section, Post Content y Related Posts ahora usan
|
||||
* estilos generados dinámicamente desde sus Renderers.
|
||||
*/
|
||||
function roi_enqueue_fase2_styles() {
|
||||
// Hero Section CSS - Gradiente azul (Issue #59)
|
||||
wp_enqueue_style(
|
||||
'roi-hero',
|
||||
get_template_directory_uri() . '/assets/css/componente-hero-section.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-hero-section.css'),
|
||||
'all'
|
||||
);
|
||||
// Hero Section CSS - DESHABILITADO: estilos generados por HeroRenderer
|
||||
// @see Public/Hero/Infrastructure/Ui/HeroRenderer.php
|
||||
|
||||
// Category Badges CSS - Clase genérica (Issue #62)
|
||||
wp_enqueue_style(
|
||||
'roi-badges',
|
||||
get_template_directory_uri() . '/assets/css/css-global-badges.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-badges.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/css-global-badges.css'),
|
||||
filemtime(get_template_directory() . '/Assets/css/css-global-badges.css'),
|
||||
'all'
|
||||
);
|
||||
|
||||
// Pagination CSS - Estilos personalizados (Issue #64)
|
||||
wp_enqueue_style(
|
||||
'roi-pagination',
|
||||
get_template_directory_uri() . '/assets/css/css-global-pagination.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-pagination.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/css-global-pagination.css'),
|
||||
filemtime(get_template_directory() . '/Assets/css/css-global-pagination.css'),
|
||||
'all'
|
||||
);
|
||||
|
||||
// Post Content Typography - Solo en posts individuales (Issue #63)
|
||||
if (is_single()) {
|
||||
wp_enqueue_style(
|
||||
'roi-post-content',
|
||||
get_template_directory_uri() . '/assets/css/componente-post-content.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-post-content.css'),
|
||||
'all'
|
||||
);
|
||||
|
||||
// Related Posts CSS - Background gris (Issue #60)
|
||||
wp_enqueue_style(
|
||||
'roi-related-posts',
|
||||
get_template_directory_uri() . '/assets/css/componente-related-posts.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-related-posts.css'),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
// Post Content Typography y Related Posts - DESHABILITADOS
|
||||
// Los estilos ahora están integrados en style.css o generados dinámicamente
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6);
|
||||
@@ -167,32 +147,48 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6);
|
||||
* @since 1.0.7
|
||||
*/
|
||||
function roi_enqueue_global_components() {
|
||||
// Notification Bar CSS - Barra superior (Issue #39)
|
||||
// Notification Bar CSS - DESHABILITADO: Los estilos de la barra de notificación ahora se generan
|
||||
// dinámicamente desde el TopNotificationBarRenderer basado en los valores de la BD.
|
||||
// @see Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
|
||||
/*
|
||||
wp_enqueue_style(
|
||||
'roi-notification-bar',
|
||||
get_template_directory_uri() . '/assets/css/componente-top-bar.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-top-bar.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-top-bar.css'),
|
||||
filemtime(get_template_directory() . '/Assets/css/componente-top-bar.css'),
|
||||
'all'
|
||||
);
|
||||
*/
|
||||
|
||||
// Navbar CSS - Navegación principal
|
||||
// Navbar CSS - DESHABILITADO: Los estilos del navbar ahora se generan
|
||||
// dinámicamente desde el NavbarRenderer basado en los valores de la BD.
|
||||
// El archivo componente-navbar.css tenía !important que sobrescribía
|
||||
// los estilos configurados por el usuario en el Admin Panel.
|
||||
// @see Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
|
||||
/*
|
||||
wp_enqueue_style(
|
||||
'roi-navbar',
|
||||
get_template_directory_uri() . '/assets/css/componente-navbar.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-navbar.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-navbar.css'),
|
||||
filemtime(get_template_directory() . '/Assets/css/componente-navbar.css'),
|
||||
'all'
|
||||
);
|
||||
*/
|
||||
|
||||
// Buttons CSS - Botones personalizados (Let's Talk, etc.)
|
||||
// Buttons CSS - DESHABILITADO: Los estilos del botón Let's Talk ahora se generan
|
||||
// dinámicamente desde el CtaLetsTalkRenderer basado en los valores de la BD.
|
||||
// El archivo componente-boton-lets-talk.css tenía !important que sobrescribía
|
||||
// los estilos configurados por el usuario en el Admin Panel.
|
||||
// @see Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
|
||||
/*
|
||||
wp_enqueue_style(
|
||||
'roi-buttons',
|
||||
get_template_directory_uri() . '/assets/css/componente-boton-lets-talk.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-boton-lets-talk.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-boton-lets-talk.css'),
|
||||
filemtime(get_template_directory() . '/Assets/css/componente-boton-lets-talk.css'),
|
||||
'all'
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_global_components', 7);
|
||||
@@ -204,7 +200,7 @@ function roi_enqueue_header() {
|
||||
// Header CSS
|
||||
wp_enqueue_style(
|
||||
'roi-header',
|
||||
get_template_directory_uri() . '/assets/css/componente-footer-principal.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-footer-principal.css',
|
||||
array('roi-fonts'),
|
||||
'1.0.0',
|
||||
'all'
|
||||
@@ -213,7 +209,7 @@ function roi_enqueue_header() {
|
||||
// Header JS - with defer strategy
|
||||
wp_enqueue_script(
|
||||
'roi-header-js',
|
||||
get_template_directory_uri() . '/assets/js/header.js',
|
||||
get_template_directory_uri() . '/Assets/js/header.js',
|
||||
array(),
|
||||
'1.0.0',
|
||||
array(
|
||||
@@ -240,7 +236,7 @@ function roi_enqueue_generic_tables() {
|
||||
// Generic Tables CSS
|
||||
wp_enqueue_style(
|
||||
'roi-generic-tables',
|
||||
get_template_directory_uri() . '/assets/css/css-global-generic-tables.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-generic-tables.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -264,7 +260,7 @@ function roi_enqueue_video_styles() {
|
||||
// Video CSS
|
||||
wp_enqueue_style(
|
||||
'roi-video',
|
||||
get_template_directory_uri() . '/assets/css/css-global-video.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-video.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -284,7 +280,7 @@ function roi_enqueue_main_javascript() {
|
||||
// Main JavaScript - navbar scroll effects and interactions
|
||||
wp_enqueue_script(
|
||||
'roi-main-js',
|
||||
get_template_directory_uri() . '/assets/js/main.js',
|
||||
get_template_directory_uri() . '/Assets/js/main.js',
|
||||
array('roi-bootstrap-js'),
|
||||
'1.0.3', // Cache bust: force remove defer with filter
|
||||
true // Load in footer
|
||||
@@ -331,7 +327,7 @@ function roi_enqueue_accessibility() {
|
||||
// Accessibility CSS
|
||||
wp_enqueue_style(
|
||||
'roi-accessibility',
|
||||
get_template_directory_uri() . '/assets/css/css-global-accessibility.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-accessibility.css',
|
||||
array('roi-theme-style'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -340,7 +336,7 @@ function roi_enqueue_accessibility() {
|
||||
// Accessibility JavaScript
|
||||
wp_enqueue_script(
|
||||
'roi-accessibility-js',
|
||||
get_template_directory_uri() . '/assets/js/accessibility.js',
|
||||
get_template_directory_uri() . '/Assets/js/accessibility.js',
|
||||
array('roi-bootstrap-js'),
|
||||
ROI_VERSION,
|
||||
array(
|
||||
@@ -375,7 +371,7 @@ function roi_enqueue_adsense_loader() {
|
||||
// Enqueue del script de carga de AdSense
|
||||
wp_enqueue_script(
|
||||
'roi-adsense-loader',
|
||||
get_template_directory_uri() . '/assets/js/adsense-loader.js',
|
||||
get_template_directory_uri() . '/Assets/js/adsense-loader.js',
|
||||
array(),
|
||||
ROI_VERSION,
|
||||
array(
|
||||
@@ -399,7 +395,7 @@ function roi_enqueue_theme_styles() {
|
||||
// Theme Core Styles - ELIMINADO theme.css
|
||||
// wp_enqueue_style(
|
||||
// 'roi-theme',
|
||||
// get_template_directory_uri() . '/assets/css/theme.css',
|
||||
// get_template_directory_uri() . '/Assets/css/theme.css',
|
||||
// array('roi-bootstrap'),
|
||||
// '1.0.0',
|
||||
// 'all'
|
||||
@@ -408,7 +404,7 @@ function roi_enqueue_theme_styles() {
|
||||
// Theme Animations
|
||||
wp_enqueue_style(
|
||||
'roi-animations',
|
||||
get_template_directory_uri() . '/assets/css/css-global-animations.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-animations.css',
|
||||
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
|
||||
'1.0.0',
|
||||
'all'
|
||||
@@ -417,7 +413,7 @@ function roi_enqueue_theme_styles() {
|
||||
// Theme Responsive Styles
|
||||
wp_enqueue_style(
|
||||
'roi-responsive',
|
||||
get_template_directory_uri() . '/assets/css/css-global-responsive.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-responsive.css',
|
||||
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
|
||||
'1.0.0',
|
||||
'all'
|
||||
@@ -426,7 +422,7 @@ function roi_enqueue_theme_styles() {
|
||||
// Theme Utilities
|
||||
wp_enqueue_style(
|
||||
'roi-utilities',
|
||||
get_template_directory_uri() . '/assets/css/css-global-utilities.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-utilities.css',
|
||||
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
|
||||
'1.0.0',
|
||||
'all'
|
||||
@@ -435,7 +431,7 @@ function roi_enqueue_theme_styles() {
|
||||
// Print Styles
|
||||
wp_enqueue_style(
|
||||
'roi-print',
|
||||
get_template_directory_uri() . '/assets/css/css-global-print.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-global-print.css',
|
||||
array(),
|
||||
'1.0.0',
|
||||
'print'
|
||||
@@ -449,7 +445,7 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_theme_styles', 13);
|
||||
*
|
||||
* HABILITADO: CSS de share buttons debe estar en su propio archivo
|
||||
* Arquitectura correcta: cada componente tiene su archivo CSS individual
|
||||
* Ver: wp-content/themes/roi-theme/assets/css/componente-share-buttons.css
|
||||
* Ver: wp-content/themes/roi-theme/Assets/css/componente-share-buttons.css
|
||||
*/
|
||||
function roi_enqueue_social_share_styles() {
|
||||
// Only enqueue on single posts
|
||||
@@ -460,7 +456,7 @@ function roi_enqueue_social_share_styles() {
|
||||
// Social Share CSS
|
||||
wp_enqueue_style(
|
||||
'roi-social-share',
|
||||
get_template_directory_uri() . '/assets/css/componente-share-buttons.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-share-buttons.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -476,7 +472,7 @@ function roi_enqueue_apu_tables_styles() {
|
||||
// APU Tables CSS
|
||||
wp_enqueue_style(
|
||||
'roi-tables-apu',
|
||||
get_template_directory_uri() . '/assets/css/css-tablas-apu.css',
|
||||
get_template_directory_uri() . '/Assets/css/css-tablas-apu.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -497,7 +493,7 @@ function roi_enqueue_apu_tables_autoclass_script() {
|
||||
// APU Tables Auto-Class JS
|
||||
wp_enqueue_script(
|
||||
'roi-apu-tables-autoclass',
|
||||
get_template_directory_uri() . '/assets/js/apu-tables-auto-class.js',
|
||||
get_template_directory_uri() . '/Assets/js/apu-tables-auto-class.js',
|
||||
array(),
|
||||
ROI_VERSION,
|
||||
array(
|
||||
@@ -527,7 +523,7 @@ function roi_enqueue_cta_assets() {
|
||||
// CTA CSS
|
||||
wp_enqueue_style(
|
||||
'roi-cta-style',
|
||||
get_template_directory_uri() . '/assets/css/componente-cta-ab-testing.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-cta-ab-testing.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
@@ -536,7 +532,7 @@ function roi_enqueue_cta_assets() {
|
||||
// CTA Tracking JS
|
||||
wp_enqueue_script(
|
||||
'roi-cta-tracking',
|
||||
get_template_directory_uri() . '/assets/js/cta-tracking.js',
|
||||
get_template_directory_uri() . '/Assets/js/cta-tracking.js',
|
||||
array(),
|
||||
ROI_VERSION,
|
||||
array(
|
||||
@@ -550,63 +546,36 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_cta_assets', 16);
|
||||
|
||||
/**
|
||||
* Enqueue CTA Box Sidebar styles (Issue #36)
|
||||
*
|
||||
* DESHABILITADO: Los estilos del CTA Box Sidebar ahora se generan
|
||||
* dinámicamente desde CtaBoxSidebarRenderer basado en valores de BD.
|
||||
* @see Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
|
||||
*/
|
||||
function roi_enqueue_cta_box_sidebar_assets() {
|
||||
// Solo enqueue en posts individuales
|
||||
if (!is_single()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CTA Box Sidebar CSS
|
||||
wp_enqueue_style(
|
||||
'roi-cta-box-sidebar',
|
||||
get_template_directory_uri() . '/assets/css/componente-cta-box-sidebar.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-cta-box-sidebar.css'),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_cta_box_sidebar_assets', 17);
|
||||
// function roi_enqueue_cta_box_sidebar_assets() - REMOVED
|
||||
|
||||
/**
|
||||
* Enqueue TOC Sidebar styles (only on single posts)
|
||||
*
|
||||
* ARQUITECTURA: Cada componente debe tener su propio archivo CSS
|
||||
* Issue #121 - Separación de responsabilidades CSS
|
||||
* DESHABILITADO: Los estilos del TOC ahora se generan
|
||||
* dinámicamente desde TableOfContentsRenderer basado en valores de BD.
|
||||
* @see Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
|
||||
*
|
||||
* @since 1.0.5
|
||||
*/
|
||||
function roi_enqueue_toc_sidebar_assets() {
|
||||
// Only load on single posts
|
||||
if (!is_single()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TOC Sidebar CSS
|
||||
wp_enqueue_style(
|
||||
'roi-toc-sidebar',
|
||||
get_template_directory_uri() . '/assets/css/componente-sidebar-toc.css',
|
||||
array('roi-bootstrap'),
|
||||
filemtime(get_template_directory() . '/assets/css/componente-sidebar-toc.css'),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', 'roi_enqueue_toc_sidebar_assets', 18);
|
||||
// function roi_enqueue_toc_sidebar_assets() - REMOVED
|
||||
|
||||
/**
|
||||
* Enqueue Footer Contact Form styles
|
||||
*
|
||||
* ARQUITECTURA CORRECTA: Cada componente debe tener su propio archivo CSS
|
||||
* Footer Contact Form CSS ahora está en su archivo individual
|
||||
* Ver: wp-content/themes/roi-theme/assets/css/componente-footer-contact-form.css
|
||||
* Ver: wp-content/themes/roi-theme/Assets/css/componente-footer-contact-form.css
|
||||
*/
|
||||
function roi_enqueue_footer_contact_assets() {
|
||||
// Footer Contact CSS
|
||||
wp_enqueue_style(
|
||||
'roi-footer-contact',
|
||||
get_template_directory_uri() . '/assets/css/componente-footer-contact-form.css',
|
||||
get_template_directory_uri() . '/Assets/css/componente-footer-contact-form.css',
|
||||
array('roi-bootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
|
||||
@@ -336,7 +336,7 @@ function roi_preload_critical_resources() {
|
||||
);
|
||||
|
||||
foreach ( $fonts as $font ) {
|
||||
$font_url = get_template_directory_uri() . '/assets/fonts/' . $font;
|
||||
$font_url = get_template_directory_uri() . '/Assets/fonts/' . $font;
|
||||
printf(
|
||||
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin="anonymous">' . "\n",
|
||||
esc_url( $font_url )
|
||||
@@ -344,14 +344,14 @@ function roi_preload_critical_resources() {
|
||||
}
|
||||
|
||||
// Preload del CSS de Bootstrap (crítico para el layout)
|
||||
$bootstrap_css = get_template_directory_uri() . '/assets/vendor/bootstrap/css/bootstrap.min.css';
|
||||
$bootstrap_css = get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css';
|
||||
printf(
|
||||
'<link rel="preload" href="%s" as="style">' . "\n",
|
||||
esc_url( $bootstrap_css )
|
||||
);
|
||||
|
||||
// Preload del CSS de fuentes (crítico para evitar FOIT/FOUT)
|
||||
$fonts_css = get_template_directory_uri() . '/assets/css/fonts.css';
|
||||
$fonts_css = get_template_directory_uri() . '/Assets/css/fonts.css';
|
||||
printf(
|
||||
'<link rel="preload" href="%s" as="style">' . "\n",
|
||||
esc_url( $fonts_css )
|
||||
|
||||
@@ -250,7 +250,7 @@ function roi_enqueue_related_posts_styles() {
|
||||
if ($enabled) {
|
||||
wp_enqueue_style(
|
||||
'roirelated-posts',
|
||||
get_template_directory_uri() . '/assets/css/related-posts.css',
|
||||
get_template_directory_uri() . '/Assets/css/related-posts.css',
|
||||
array('roibootstrap'),
|
||||
ROI_VERSION,
|
||||
'all'
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<!-- Contact Modal -->
|
||||
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title fw-bold" id="contactModalLabel">¿Listo para comenzar?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar modal de contacto"></button>
|
||||
</div>
|
||||
<div class="modal-body px-4 pb-4">
|
||||
<p class="text-muted mb-4">Completa el formulario y nos pondremos en contacto contigo lo antes posible.</p>
|
||||
<form id="contactForm" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="fullName" class="form-label">
|
||||
Nombre completo <span class="text-danger" aria-label="Campo obligatorio">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-describedby="fullNameHelp"
|
||||
autocomplete="name"
|
||||
>
|
||||
<div class="invalid-feedback" id="fullNameHelp">
|
||||
Por favor ingresa tu nombre completo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="company" class="form-label">Empresa</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="company"
|
||||
name="company"
|
||||
aria-describedby="companyHelp"
|
||||
autocomplete="organization"
|
||||
>
|
||||
<small id="companyHelp" class="form-text text-muted">Opcional</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="whatsapp" class="form-label">
|
||||
WhatsApp <span class="text-danger" aria-label="Campo obligatorio">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
class="form-control"
|
||||
id="whatsapp"
|
||||
name="whatsapp"
|
||||
placeholder="+52 ___ ___ ____"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-describedby="whatsappHelp"
|
||||
autocomplete="tel"
|
||||
>
|
||||
<div class="invalid-feedback" id="whatsappHelp">
|
||||
Por favor ingresa un número de WhatsApp válido (10-15 dígitos)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
Correo electrónico <span class="text-danger" aria-label="Campo obligatorio">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-describedby="emailHelp"
|
||||
autocomplete="email"
|
||||
>
|
||||
<div class="invalid-feedback" id="emailHelp">
|
||||
Por favor ingresa un correo electrónico válido
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="comments" class="form-label">Comentarios</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="comments"
|
||||
name="comments"
|
||||
rows="3"
|
||||
aria-describedby="commentsHelp"
|
||||
></textarea>
|
||||
<small id="commentsHelp" class="form-text text-muted">Opcional - Cuéntanos más sobre tu proyecto</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-submit-form btn-lg" aria-label="Enviar formulario de contacto">
|
||||
Enviar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="formMessage" class="mt-3 alert" style="display: none;" role="alert" aria-live="polite"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
393
public/README.md
393
public/README.md
@@ -1,393 +0,0 @@
|
||||
# Contexto Public - Renderizado Frontend de Componentes
|
||||
|
||||
## Propósito
|
||||
|
||||
El contexto `public/` contiene **todo el código relacionado con el renderizado de componentes en el frontend**.
|
||||
Cada componente tiene su propia carpeta con su Clean Architecture completa.
|
||||
|
||||
## Filosofía: Context-First Architecture
|
||||
|
||||
Similar a `admin/`, el contexto `public/` agrupa todo lo relacionado con la visualización pública de componentes.
|
||||
Cada componente es independiente y tiene sus propias capas de Clean Architecture.
|
||||
|
||||
## Estructura (Fase-00)
|
||||
|
||||
En Fase-00 solo creamos la estructura base. Los componentes se crearán en fases posteriores:
|
||||
|
||||
```
|
||||
public/
|
||||
├── README.md (este archivo)
|
||||
└── .gitkeep (preserva directorio en Git)
|
||||
```
|
||||
|
||||
## Estructura Futura (Post Fase-00)
|
||||
|
||||
```
|
||||
public/
|
||||
├── Navbar/ # Componente Navbar
|
||||
│ ├── Domain/
|
||||
│ │ ├── NavbarData.php # Entidad de lectura
|
||||
│ │ └── NavbarDataProviderInterface.php
|
||||
│ ├── Application/
|
||||
│ │ ├── GetNavbarDataUseCase.php # Caso de uso
|
||||
│ │ └── DTO/
|
||||
│ │ └── NavbarDataDTO.php
|
||||
│ ├── Infrastructure/
|
||||
│ │ ├── Persistence/
|
||||
│ │ │ └── WordPressNavbarDataProvider.php
|
||||
│ │ ├── UI/
|
||||
│ │ │ ├── NavbarRenderer.php # Renderizador
|
||||
│ │ │ ├── views/
|
||||
│ │ │ │ └── navbar.php # Template
|
||||
│ │ │ └── assets/
|
||||
│ │ │ ├── css/
|
||||
│ │ │ │ └── navbar.css
|
||||
│ │ │ └── js/
|
||||
│ │ │ └── navbar.js
|
||||
│ │ └── Hooks/
|
||||
│ │ └── NavbarHooks.php # WordPress hooks
|
||||
│ └── README.md
|
||||
├── Footer/ # Otro componente
|
||||
│ └── (misma estructura)
|
||||
└── (más componentes...)
|
||||
```
|
||||
|
||||
## Principios del Contexto Public
|
||||
|
||||
1. **Solo lectura**: El frontend solo lee configuraciones, no las modifica
|
||||
2. **Performance**: Optimizado para carga rápida y caché
|
||||
3. **Presentación**: Se enfoca en renderizar HTML, CSS, JS
|
||||
4. **Independencia**: Cada componente es autónomo
|
||||
|
||||
## Responsabilidades
|
||||
|
||||
El contexto `public/` se encarga de:
|
||||
|
||||
✅ **Renderizado de componentes** en el frontend
|
||||
✅ **Lectura de configuraciones** de la base de datos
|
||||
✅ **Generación de HTML** según templates
|
||||
✅ **Carga de assets** (CSS, JS) del componente
|
||||
✅ **Enqueue de scripts y estilos**
|
||||
✅ **Hooks de WordPress** para frontend (`wp_head`, `wp_footer`, etc.)
|
||||
|
||||
❌ **NO** se encarga de:
|
||||
- Administración de componentes (va en `admin/`)
|
||||
- Guardado de configuraciones
|
||||
- Lógica compartida (va en `shared/`)
|
||||
|
||||
## Ejemplo de Flujo de Public
|
||||
|
||||
### 1. Usuario visita página web
|
||||
|
||||
```
|
||||
Usuario (navegador)
|
||||
↓
|
||||
WordPress carga tema
|
||||
↓
|
||||
NavbarHooks.php (registra hook 'wp_body_open')
|
||||
↓
|
||||
Hook ejecutado
|
||||
↓
|
||||
NavbarRenderer.php (obtiene datos y renderiza)
|
||||
↓
|
||||
GetNavbarDataUseCase.php (obtiene configuración)
|
||||
↓
|
||||
WordPressNavbarDataProvider.php (lee de DB)
|
||||
↓
|
||||
navbar.php (template con HTML)
|
||||
↓
|
||||
HTML enviado al navegador
|
||||
```
|
||||
|
||||
### 2. Código de ejemplo
|
||||
|
||||
```php
|
||||
// public/Navbar/Application/GetNavbarDataUseCase.php
|
||||
namespace ROITheme\Public\Navbar\Application;
|
||||
|
||||
use ROITheme\Public\Navbar\Domain\NavbarDataProviderInterface;
|
||||
|
||||
final class GetNavbarDataUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NavbarDataProviderInterface $dataProvider
|
||||
) {}
|
||||
|
||||
public function execute(): array
|
||||
{
|
||||
$data = $this->dataProvider->get();
|
||||
|
||||
// Transformar datos si es necesario
|
||||
return [
|
||||
'logo_url' => $data['logo_url'] ?? '',
|
||||
'menu_items' => $data['menu_items'] ?? [],
|
||||
'sticky' => $data['sticky'] ?? false,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// public/Navbar/Infrastructure/UI/NavbarRenderer.php
|
||||
namespace ROITheme\Public\Navbar\Infrastructure\UI;
|
||||
|
||||
use ROITheme\Public\Navbar\Application\GetNavbarDataUseCase;
|
||||
|
||||
final class NavbarRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetNavbarDataUseCase $getNavbarData
|
||||
) {}
|
||||
|
||||
public function render(): void
|
||||
{
|
||||
$data = $this->getNavbarData->execute();
|
||||
|
||||
// Encolar assets
|
||||
wp_enqueue_style(
|
||||
'roi-navbar',
|
||||
get_template_directory_uri() . '/public/Navbar/Infrastructure/UI/assets/css/navbar.css',
|
||||
[],
|
||||
'1.0.0'
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'roi-navbar',
|
||||
get_template_directory_uri() . '/public/Navbar/Infrastructure/UI/assets/js/navbar.js',
|
||||
['jquery'],
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
|
||||
// Renderizar template
|
||||
require __DIR__ . '/views/navbar.php';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// public/Navbar/Infrastructure/Hooks/NavbarHooks.php
|
||||
namespace ROITheme\Public\Navbar\Infrastructure\Hooks;
|
||||
|
||||
use ROITheme\Public\Navbar\Infrastructure\UI\NavbarRenderer;
|
||||
|
||||
final class NavbarHooks
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NavbarRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_body_open', [$this->renderer, 'render']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// public/Navbar/Infrastructure/UI/views/navbar.php
|
||||
<?php
|
||||
/**
|
||||
* Template del Navbar
|
||||
*
|
||||
* @var array $data Datos del navbar
|
||||
*/
|
||||
?>
|
||||
<nav class="roi-navbar <?= $data['sticky'] ? 'sticky' : '' ?>">
|
||||
<div class="container">
|
||||
<a href="<?= home_url() ?>">
|
||||
<img src="<?= esc_url($data['logo_url']) ?>" alt="Logo">
|
||||
</a>
|
||||
<ul class="menu">
|
||||
<?php foreach ($data['menu_items'] as $item): ?>
|
||||
<li>
|
||||
<a href="<?= esc_url($item['url']) ?>">
|
||||
<?= esc_html($item['label']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
```
|
||||
|
||||
## Relación con Otros Contextos
|
||||
|
||||
```
|
||||
admin/ → Guarda configuraciones
|
||||
↓
|
||||
Base de Datos → Almacena settings
|
||||
↓ lee
|
||||
public/ → Renderiza componentes
|
||||
↓
|
||||
Navegador del Usuario
|
||||
```
|
||||
|
||||
**Clave**: `public/` solo LECTURA, nunca escritura.
|
||||
|
||||
## Optimización y Caché
|
||||
|
||||
### 1. Usar Transients de WordPress
|
||||
|
||||
```php
|
||||
public function get(): array
|
||||
{
|
||||
$cached = get_transient('roi_navbar_data');
|
||||
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$data = $this->fetchFromDatabase();
|
||||
set_transient('roi_navbar_data', $data, HOUR_IN_SECONDS);
|
||||
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Conditional Asset Loading
|
||||
|
||||
```php
|
||||
if ($data['sticky']) {
|
||||
wp_enqueue_script('roi-navbar-sticky');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Lazy Loading de Componentes
|
||||
|
||||
```php
|
||||
add_action('wp_body_open', function() {
|
||||
if (is_front_page()) {
|
||||
// Solo cargar en homepage
|
||||
$renderer->render();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Reglas de Dependencia
|
||||
|
||||
✅ **PUEDE** depender de:
|
||||
- `shared/Domain/` (Value Objects, Exceptions)
|
||||
- `shared/Application/` (Contracts, Services)
|
||||
- `shared/Infrastructure/` (Implementaciones de servicios)
|
||||
- WordPress frontend functions (`wp_enqueue_style`, `wp_head`, etc.)
|
||||
|
||||
❌ **NO PUEDE** depender de:
|
||||
- `admin/` (contexto independiente)
|
||||
- Funciones de administración de WordPress
|
||||
|
||||
## Testing
|
||||
|
||||
### Tests Unitarios
|
||||
```php
|
||||
// tests/Unit/Public/Navbar/Application/GetNavbarDataUseCaseTest.php
|
||||
public function test_returns_navbar_data()
|
||||
{
|
||||
$dataProvider = $this->createMock(NavbarDataProviderInterface::class);
|
||||
$dataProvider->method('get')->willReturn(['logo_url' => 'test.png']);
|
||||
|
||||
$useCase = new GetNavbarDataUseCase($dataProvider);
|
||||
$data = $useCase->execute();
|
||||
|
||||
$this->assertEquals('test.png', $data['logo_url']);
|
||||
}
|
||||
```
|
||||
|
||||
### Tests de Integración
|
||||
```php
|
||||
// tests/Integration/Public/Navbar/Infrastructure/WordPressNavbarDataProviderTest.php
|
||||
public function test_retrieves_navbar_settings_from_db()
|
||||
{
|
||||
update_option('roi_navbar_settings', ['logo_url' => 'test.png']);
|
||||
|
||||
$provider = new WordPressNavbarDataProvider();
|
||||
$data = $provider->get();
|
||||
|
||||
$this->assertEquals('test.png', $data['logo_url']);
|
||||
}
|
||||
```
|
||||
|
||||
### Tests E2E (Playwright)
|
||||
```php
|
||||
// tests/E2E/Public/NavbarRenderingTest.php
|
||||
public function test_navbar_renders_on_homepage()
|
||||
{
|
||||
$this->visit('/');
|
||||
$this->see('.roi-navbar');
|
||||
$this->see('Logo');
|
||||
}
|
||||
```
|
||||
|
||||
### Tests de Performance
|
||||
```php
|
||||
// tests/Performance/Public/NavbarPerformanceTest.php
|
||||
public function test_navbar_renders_in_less_than_100ms()
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$renderer = new NavbarRenderer($this->getNavbarDataUseCase);
|
||||
ob_start();
|
||||
$renderer->render();
|
||||
ob_end_clean();
|
||||
|
||||
$duration = (microtime(true) - $start) * 1000;
|
||||
|
||||
$this->assertLessThan(100, $duration);
|
||||
}
|
||||
```
|
||||
|
||||
## Cuándo Agregar Código Aquí
|
||||
|
||||
Agrega código a `public/` cuando:
|
||||
- Renderizas un componente en el frontend
|
||||
- Necesitas mostrar datos al usuario final
|
||||
- Cargas assets (CSS, JS) para el frontend
|
||||
- Registras hooks de frontend (`wp_head`, `wp_footer`, etc.)
|
||||
- Optimizas performance de renderizado
|
||||
|
||||
No agregues aquí:
|
||||
- Formularios de admin (van en `admin/`)
|
||||
- Guardado de configuraciones
|
||||
- Lógica compartida (va en `shared/`)
|
||||
|
||||
## Assets y Performance
|
||||
|
||||
### Organización de Assets
|
||||
```
|
||||
public/Navbar/Infrastructure/UI/assets/
|
||||
├── css/
|
||||
│ ├── navbar.css # Estilos del componente
|
||||
│ └── navbar.min.css # Versión minificada
|
||||
├── js/
|
||||
│ ├── navbar.js # JavaScript del componente
|
||||
│ └── navbar.min.js # Versión minificada
|
||||
└── images/
|
||||
└── default-logo.png # Imágenes del componente
|
||||
```
|
||||
|
||||
### Minificación y Concatenación
|
||||
En producción, usar versiones minificadas:
|
||||
```php
|
||||
$suffix = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '' : '.min';
|
||||
wp_enqueue_style('roi-navbar', "...navbar{$suffix}.css");
|
||||
```
|
||||
|
||||
## Estado Actual (Fase-00)
|
||||
|
||||
En Fase-00, `public/` solo tiene la estructura base. Los componentes se crearán en las siguientes fases:
|
||||
|
||||
- **Fase-1**: Estructura base e infraestructura
|
||||
- **Fase-2**: Migración de base de datos
|
||||
- **Fase-3**: Implementación de componentes públicos
|
||||
- **Fase-4+**: Componentes adicionales y optimización
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Crear primer componente en Fase-3 (ej: Navbar)
|
||||
2. Implementar Domain layer (entidades de lectura)
|
||||
3. Implementar Application layer (casos de uso de lectura)
|
||||
4. Implementar Infrastructure layer (renderizado, hooks)
|
||||
5. Crear templates y assets
|
||||
6. Optimizar con caché
|
||||
7. Crear tests de performance
|
||||
8. Repetir para cada componente
|
||||
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"component_name": "contact-form-section",
|
||||
"version": "1.0.0",
|
||||
"description": "Sección de contacto con información y formulario funcional mediante AJAX",
|
||||
"groups": {
|
||||
"section": {
|
||||
"label": "Configuración de la Sección",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"show_section": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar sección",
|
||||
"default": true,
|
||||
"description": "Activar o desactivar la sección completa"
|
||||
},
|
||||
"section_title": {
|
||||
"type": "text",
|
||||
"label": "Título de la sección",
|
||||
"default": "¿Tienes alguna pregunta?",
|
||||
"required": true,
|
||||
"description": "Título principal de la sección de contacto"
|
||||
},
|
||||
"section_subtitle": {
|
||||
"type": "textarea",
|
||||
"label": "Subtítulo",
|
||||
"default": "Completa el formulario y nuestro equipo te responderá en menos de 24 horas.",
|
||||
"description": "Descripción o subtítulo de la sección"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact_info": {
|
||||
"label": "Información de Contacto",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"phone_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar teléfono",
|
||||
"default": true
|
||||
},
|
||||
"phone_label": {
|
||||
"type": "text",
|
||||
"label": "Etiqueta de teléfono",
|
||||
"default": "Teléfono"
|
||||
},
|
||||
"phone_value": {
|
||||
"type": "text",
|
||||
"label": "Número de teléfono",
|
||||
"default": "+52 55 1234 5678"
|
||||
},
|
||||
"email_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar email",
|
||||
"default": true
|
||||
},
|
||||
"email_label": {
|
||||
"type": "text",
|
||||
"label": "Etiqueta de email",
|
||||
"default": "Email"
|
||||
},
|
||||
"email_value": {
|
||||
"type": "email",
|
||||
"label": "Dirección de email",
|
||||
"default": "contacto@example.com"
|
||||
},
|
||||
"location_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar ubicación",
|
||||
"default": true
|
||||
},
|
||||
"location_label": {
|
||||
"type": "text",
|
||||
"label": "Etiqueta de ubicación",
|
||||
"default": "Ubicación"
|
||||
},
|
||||
"location_value": {
|
||||
"type": "text",
|
||||
"label": "Ubicación",
|
||||
"default": "Ciudad de México, México"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"label": "Configuración del Formulario",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"submit_button_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Enviar Mensaje",
|
||||
"required": true,
|
||||
"description": "Texto del botón de envío"
|
||||
},
|
||||
"submit_button_icon": {
|
||||
"type": "text",
|
||||
"label": "Ícono del botón",
|
||||
"default": "bi-send-fill",
|
||||
"description": "Clase de Bootstrap Icons"
|
||||
},
|
||||
"success_message": {
|
||||
"type": "textarea",
|
||||
"label": "Mensaje de éxito",
|
||||
"default": "¡Gracias! Tu mensaje ha sido enviado correctamente. Te responderemos pronto.",
|
||||
"description": "Mensaje al enviar exitosamente"
|
||||
},
|
||||
"error_message": {
|
||||
"type": "textarea",
|
||||
"label": "Mensaje de error",
|
||||
"default": "Hubo un error al enviar el mensaje. Por favor, intenta de nuevo.",
|
||||
"description": "Mensaje al fallar el envío"
|
||||
},
|
||||
"to_email": {
|
||||
"type": "email",
|
||||
"label": "Email de destino",
|
||||
"default": "",
|
||||
"description": "Email donde se recibirán los mensajes (deja vacío para usar el admin email)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"background_color": {
|
||||
"type": "text",
|
||||
"label": "Clase de fondo",
|
||||
"default": "bg-secondary bg-opacity-25",
|
||||
"description": "Clase de Bootstrap para el fondo"
|
||||
},
|
||||
"icon_color": {
|
||||
"type": "color",
|
||||
"label": "Color de íconos",
|
||||
"default": "#FF8600",
|
||||
"description": "Color de los íconos de contacto"
|
||||
},
|
||||
"button_bg_color": {
|
||||
"type": "color",
|
||||
"label": "Color del botón",
|
||||
"default": "#FF8600",
|
||||
"description": "Color de fondo del botón"
|
||||
},
|
||||
"button_hover_bg": {
|
||||
"type": "color",
|
||||
"label": "Color del botón (hover)",
|
||||
"default": "#FF6B00",
|
||||
"description": "Color de fondo del botón al hover"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"component_name": "contact-modal",
|
||||
"version": "1.0.0",
|
||||
"description": "Modal de contacto Bootstrap 5 con formulario AJAX para consultas de clientes",
|
||||
"groups": {
|
||||
"general": {
|
||||
"label": "Configuración General",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"modal_title": {
|
||||
"type": "text",
|
||||
"label": "Título del modal",
|
||||
"default": "¿Tienes alguna pregunta?",
|
||||
"required": true,
|
||||
"description": "Título que aparece en el encabezado del modal"
|
||||
},
|
||||
"modal_description": {
|
||||
"type": "textarea",
|
||||
"label": "Descripción",
|
||||
"default": "Completa el formulario y nuestro equipo te responderá en menos de 24 horas.",
|
||||
"description": "Texto descriptivo debajo del título"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
"label": "Campos del Formulario",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"fullName": {
|
||||
"type": "object",
|
||||
"label": "Campo Nombre Completo",
|
||||
"default": {
|
||||
"label": "Nombre completo",
|
||||
"placeholder": "",
|
||||
"required": true
|
||||
},
|
||||
"fields": {
|
||||
"label": {"type": "text", "label": "Etiqueta"},
|
||||
"placeholder": {"type": "text", "label": "Placeholder"},
|
||||
"required": {"type": "boolean", "label": "Requerido"}
|
||||
}
|
||||
},
|
||||
"company": {
|
||||
"type": "object",
|
||||
"label": "Campo Empresa",
|
||||
"default": {
|
||||
"label": "Empresa",
|
||||
"placeholder": "",
|
||||
"required": false
|
||||
},
|
||||
"fields": {
|
||||
"label": {"type": "text", "label": "Etiqueta"},
|
||||
"placeholder": {"type": "text", "label": "Placeholder"},
|
||||
"required": {"type": "boolean", "label": "Requerido"}
|
||||
}
|
||||
},
|
||||
"whatsapp": {
|
||||
"type": "object",
|
||||
"label": "Campo WhatsApp",
|
||||
"default": {
|
||||
"label": "WhatsApp",
|
||||
"placeholder": "",
|
||||
"required": true
|
||||
},
|
||||
"fields": {
|
||||
"label": {"type": "text", "label": "Etiqueta"},
|
||||
"placeholder": {"type": "text", "label": "Placeholder"},
|
||||
"required": {"type": "boolean", "label": "Requerido"}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "object",
|
||||
"label": "Campo Email",
|
||||
"default": {
|
||||
"label": "Correo electrónico",
|
||||
"placeholder": "",
|
||||
"required": true
|
||||
},
|
||||
"fields": {
|
||||
"label": {"type": "text", "label": "Etiqueta"},
|
||||
"placeholder": {"type": "text", "label": "Placeholder"},
|
||||
"required": {"type": "boolean", "label": "Requerido"}
|
||||
}
|
||||
},
|
||||
"comments": {
|
||||
"type": "object",
|
||||
"label": "Campo Comentarios",
|
||||
"default": {
|
||||
"label": "¿En qué podemos ayudarte?",
|
||||
"placeholder": "",
|
||||
"required": false,
|
||||
"rows": 4
|
||||
},
|
||||
"fields": {
|
||||
"label": {"type": "text", "label": "Etiqueta"},
|
||||
"placeholder": {"type": "text", "label": "Placeholder"},
|
||||
"required": {"type": "boolean", "label": "Requerido"},
|
||||
"rows": {"type": "number", "label": "Filas"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit_button": {
|
||||
"label": "Botón de Envío",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Enviar Mensaje",
|
||||
"required": true,
|
||||
"description": "Texto que aparece en el botón de envío"
|
||||
},
|
||||
"icon": {
|
||||
"type": "text",
|
||||
"label": "Ícono del botón",
|
||||
"default": "bi-send-fill",
|
||||
"description": "Clase de Bootstrap Icons (ej: bi-send-fill)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"label": "Mensajes del Sistema",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"success": {
|
||||
"type": "textarea",
|
||||
"label": "Mensaje de éxito",
|
||||
"default": "Mensaje enviado exitosamente. Te responderemos pronto.",
|
||||
"description": "Mensaje cuando el formulario se envía correctamente"
|
||||
},
|
||||
"error": {
|
||||
"type": "textarea",
|
||||
"label": "Mensaje de error",
|
||||
"default": "Error al enviar el mensaje. Por favor intenta nuevamente.",
|
||||
"description": "Mensaje cuando ocurre un error"
|
||||
},
|
||||
"validation_error": {
|
||||
"type": "textarea",
|
||||
"label": "Mensaje de validación",
|
||||
"default": "Por favor completa todos los campos requeridos.",
|
||||
"description": "Mensaje cuando faltan campos obligatorios"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"label": "Configuración Avanzada",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"modal_id": {
|
||||
"type": "text",
|
||||
"label": "ID del modal",
|
||||
"default": "contactModal",
|
||||
"readonly": true,
|
||||
"description": "ID HTML del modal (no modificar)"
|
||||
},
|
||||
"form_id": {
|
||||
"type": "text",
|
||||
"label": "ID del formulario",
|
||||
"default": "modalContactForm",
|
||||
"readonly": true,
|
||||
"description": "ID HTML del formulario (no modificar)"
|
||||
},
|
||||
"ajax_action": {
|
||||
"type": "text",
|
||||
"label": "Acción AJAX",
|
||||
"default": "roi_contact_modal_submit",
|
||||
"readonly": true,
|
||||
"description": "Nombre de la acción AJAX (no modificar)"
|
||||
},
|
||||
"email_to": {
|
||||
"type": "email",
|
||||
"label": "Email de destino",
|
||||
"default": "",
|
||||
"description": "Email donde se recibirán los mensajes (vacío = admin email)"
|
||||
},
|
||||
"email_subject": {
|
||||
"type": "text",
|
||||
"label": "Asunto del email",
|
||||
"default": "Nuevo mensaje de contacto desde el sitio web",
|
||||
"description": "Asunto del email que se enviará"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
{
|
||||
"component_name": "cta-below-content",
|
||||
"version": "1.0.0",
|
||||
"description": "Call to Action que se muestra debajo del contenido del post",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar CTA",
|
||||
"default": true,
|
||||
"required": true,
|
||||
"description": "Activa o desactiva el componente de Call to Action"
|
||||
},
|
||||
"layout": {
|
||||
"type": "select",
|
||||
"label": "Layout del CTA",
|
||||
"default": "two-column",
|
||||
"options": {
|
||||
"two-column": "Dos columnas (texto izquierda, botón derecha)",
|
||||
"centered": "Centrado",
|
||||
"stacked": "Apilado"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Distribución del contenido en el CTA"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"label": "Contenido",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título del CTA",
|
||||
"default": "Accede a 200,000+ Análisis de Precios Unitarios",
|
||||
"maxlength": 200,
|
||||
"required": true,
|
||||
"description": "Título principal que aparece en el CTA"
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "textarea",
|
||||
"label": "Subtítulo del CTA",
|
||||
"default": "Consulta estructuras completas, insumos y dosificaciones de los APUs más utilizados en construcción en México.",
|
||||
"maxlength": 500,
|
||||
"rows": 3,
|
||||
"required": true,
|
||||
"description": "Texto descriptivo que complementa el título"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"label": "Configuración del Botón",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"button_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Ver Catálogo Completo",
|
||||
"maxlength": 100,
|
||||
"required": true,
|
||||
"description": "Texto que aparece en el botón de acción"
|
||||
},
|
||||
"button_url": {
|
||||
"type": "url",
|
||||
"label": "URL del botón",
|
||||
"default": "#",
|
||||
"required": true,
|
||||
"description": "URL de destino al hacer clic en el botón"
|
||||
},
|
||||
"button_target": {
|
||||
"type": "select",
|
||||
"label": "Abrir enlace en",
|
||||
"default": "_self",
|
||||
"options": {
|
||||
"_self": "Misma ventana",
|
||||
"_blank": "Nueva ventana"
|
||||
},
|
||||
"description": "Atributo target del enlace del botón"
|
||||
},
|
||||
"button_color": {
|
||||
"type": "select",
|
||||
"label": "Color del botón",
|
||||
"default": "light",
|
||||
"options": {
|
||||
"light": "Blanco",
|
||||
"dark": "Negro",
|
||||
"primary": "Azul",
|
||||
"success": "Verde",
|
||||
"danger": "Rojo",
|
||||
"warning": "Amarillo"
|
||||
},
|
||||
"description": "Color de fondo del botón"
|
||||
},
|
||||
"button_size": {
|
||||
"type": "select",
|
||||
"label": "Tamaño del botón",
|
||||
"default": "lg",
|
||||
"options": {
|
||||
"sm": "Pequeño",
|
||||
"md": "Mediano",
|
||||
"lg": "Grande"
|
||||
},
|
||||
"description": "Tamaño del botón de acción"
|
||||
},
|
||||
"show_icon": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar icono en botón",
|
||||
"default": true,
|
||||
"description": "Muestra u oculta el icono de flecha en el botón"
|
||||
},
|
||||
"icon_class": {
|
||||
"type": "text",
|
||||
"label": "Clase del icono",
|
||||
"default": "bi bi-arrow-right",
|
||||
"conditional_logic": {
|
||||
"field": "show_icon",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Clase de Bootstrap Icons para el icono del botón"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos y Colores",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"gradient_start_color": {
|
||||
"type": "color",
|
||||
"label": "Color de inicio del gradiente",
|
||||
"default": "#FF8600",
|
||||
"description": "Color hexadecimal de inicio del gradiente"
|
||||
},
|
||||
"gradient_end_color": {
|
||||
"type": "color",
|
||||
"label": "Color de fin del gradiente",
|
||||
"default": "#FFB800",
|
||||
"description": "Color hexadecimal de fin del gradiente"
|
||||
},
|
||||
"gradient_angle": {
|
||||
"type": "number",
|
||||
"label": "Ángulo del gradiente",
|
||||
"default": 135,
|
||||
"min": 0,
|
||||
"max": 360,
|
||||
"description": "Ángulo en grados del gradiente (0-360)"
|
||||
},
|
||||
"text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto",
|
||||
"default": "#FFFFFF",
|
||||
"description": "Color del texto del título y subtítulo"
|
||||
},
|
||||
"container_classes": {
|
||||
"type": "text",
|
||||
"label": "Clases CSS del contenedor",
|
||||
"default": "my-5 p-4 rounded cta-section",
|
||||
"description": "Clases CSS adicionales para el contenedor principal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"label": "Configuración Avanzada",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"animation_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar animación de entrada",
|
||||
"default": false,
|
||||
"description": "Activa animaciones de entrada para hacer el CTA más llamativo"
|
||||
},
|
||||
"animation_type": {
|
||||
"type": "select",
|
||||
"label": "Tipo de animación",
|
||||
"default": "fade-in",
|
||||
"options": {
|
||||
"fade-in": "Fade In",
|
||||
"slide-up": "Slide Up",
|
||||
"scale": "Scale"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "animation_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Tipo de animación de entrada"
|
||||
},
|
||||
"animation_duration": {
|
||||
"type": "number",
|
||||
"label": "Duración de animación (ms)",
|
||||
"default": 500,
|
||||
"min": 100,
|
||||
"max": 3000,
|
||||
"step": 50,
|
||||
"conditional_logic": {
|
||||
"field": "animation_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Duración de la animación en milisegundos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
{
|
||||
"component_name": "cta-box-sidebar",
|
||||
"version": "1.0.0",
|
||||
"description": "CTA Box para sidebar con llamado a la acción destacado",
|
||||
"groups": {
|
||||
"content": {
|
||||
"label": "Contenido del CTA",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título",
|
||||
"default": "¿Listo para potenciar tus proyectos?",
|
||||
"maxlength": 100,
|
||||
"required": true,
|
||||
"description": "Título principal del CTA box"
|
||||
},
|
||||
"description": {
|
||||
"type": "textarea",
|
||||
"label": "Descripción",
|
||||
"default": "Accede a nuestra biblioteca completa de APUs y herramientas profesionales.",
|
||||
"maxlength": 200,
|
||||
"required": true,
|
||||
"description": "Texto descriptivo del CTA"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"label": "Configuración del Botón",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"button_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Solicitar Demo",
|
||||
"maxlength": 50,
|
||||
"required": true,
|
||||
"description": "Texto que aparece en el botón CTA"
|
||||
},
|
||||
"button_icon": {
|
||||
"type": "text",
|
||||
"label": "Ícono del botón",
|
||||
"default": "bi-calendar-check",
|
||||
"description": "Clase de Bootstrap Icons (ej: bi-calendar-check)"
|
||||
},
|
||||
"button_action": {
|
||||
"type": "select",
|
||||
"label": "Acción del botón",
|
||||
"default": "modal",
|
||||
"options": {
|
||||
"modal": "Abrir Modal",
|
||||
"link": "Ir a URL",
|
||||
"custom": "JavaScript Personalizado"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Tipo de acción al hacer clic"
|
||||
},
|
||||
"modal_target": {
|
||||
"type": "text",
|
||||
"label": "ID del modal",
|
||||
"default": "#contactModal",
|
||||
"description": "ID del modal a abrir (si button_action es 'modal')"
|
||||
},
|
||||
"link_url": {
|
||||
"type": "url",
|
||||
"label": "URL de destino",
|
||||
"default": "",
|
||||
"description": "URL del enlace (si button_action es 'link')"
|
||||
},
|
||||
"link_target": {
|
||||
"type": "select",
|
||||
"label": "Abrir enlace en",
|
||||
"default": "_self",
|
||||
"options": {
|
||||
"_self": "Misma pestaña",
|
||||
"_blank": "Nueva pestaña"
|
||||
},
|
||||
"description": "Target del enlace"
|
||||
},
|
||||
"custom_onclick": {
|
||||
"type": "textarea",
|
||||
"label": "JavaScript personalizado",
|
||||
"default": "",
|
||||
"description": "Código JavaScript para onclick (si button_action es 'custom')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"label": "Configuración General",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"height": {
|
||||
"type": "text",
|
||||
"label": "Altura del CTA box",
|
||||
"default": "250px",
|
||||
"description": "Altura del CTA box (CSS válido)"
|
||||
},
|
||||
"show_on_mobile": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar en móviles",
|
||||
"default": true,
|
||||
"description": "Mostrar en dispositivos móviles"
|
||||
},
|
||||
"custom_css_class": {
|
||||
"type": "text",
|
||||
"label": "Clase CSS personalizada",
|
||||
"default": "",
|
||||
"description": "Clase CSS adicional para el contenedor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos de Color",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"background_gradient": {
|
||||
"type": "boolean",
|
||||
"label": "Usar gradiente",
|
||||
"default": false,
|
||||
"description": "Usar gradiente en vez de color sólido"
|
||||
},
|
||||
"background_color": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo",
|
||||
"default": "#FF8600",
|
||||
"description": "Color de fondo del CTA box"
|
||||
},
|
||||
"gradient_start": {
|
||||
"type": "color",
|
||||
"label": "Color inicial del gradiente",
|
||||
"default": "#FF8600",
|
||||
"description": "Color inicial del gradiente"
|
||||
},
|
||||
"gradient_end": {
|
||||
"type": "color",
|
||||
"label": "Color final del gradiente",
|
||||
"default": "#FF6B00",
|
||||
"description": "Color final del gradiente"
|
||||
},
|
||||
"title_color": {
|
||||
"type": "color",
|
||||
"label": "Color del título",
|
||||
"default": "#ffffff",
|
||||
"description": "Color del texto del título"
|
||||
},
|
||||
"description_color": {
|
||||
"type": "text",
|
||||
"label": "Color de la descripción",
|
||||
"default": "rgba(255, 255, 255, 0.95)",
|
||||
"description": "Color del texto de la descripción"
|
||||
},
|
||||
"button_bg_color": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo del botón",
|
||||
"default": "#ffffff",
|
||||
"description": "Color de fondo del botón"
|
||||
},
|
||||
"button_text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto del botón",
|
||||
"default": "#FF8600",
|
||||
"description": "Color del texto del botón"
|
||||
},
|
||||
"button_hover_bg": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo del botón (hover)",
|
||||
"default": "#0E2337",
|
||||
"description": "Color de fondo del botón al hover"
|
||||
},
|
||||
"button_hover_text": {
|
||||
"type": "color",
|
||||
"label": "Color del texto del botón (hover)",
|
||||
"default": "#ffffff",
|
||||
"description": "Color del texto del botón al hover"
|
||||
},
|
||||
"shadow_color": {
|
||||
"type": "text",
|
||||
"label": "Color de sombra",
|
||||
"default": "rgba(255, 133, 0, 0.2)",
|
||||
"description": "Color de la sombra"
|
||||
},
|
||||
"border_radius": {
|
||||
"type": "text",
|
||||
"label": "Radio del borde",
|
||||
"default": "8px",
|
||||
"description": "Radio del borde (CSS válido)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"component_name": "footer",
|
||||
"version": "1.0.0",
|
||||
"description": "Footer completo del sitio con 3 widgets, newsletter, copyright y redes sociales",
|
||||
"groups": {
|
||||
"widget_1": {
|
||||
"label": "Widget 1",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar widget 1",
|
||||
"default": true,
|
||||
"description": "Mostrar u ocultar este widget"
|
||||
},
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título del widget",
|
||||
"default": "Recursos",
|
||||
"description": "Título de la columna del widget"
|
||||
},
|
||||
"links": {
|
||||
"type": "repeater",
|
||||
"label": "Enlaces",
|
||||
"description": "Lista de enlaces del widget",
|
||||
"default": [
|
||||
{"text": "Home", "url": "/"},
|
||||
{"text": "Features", "url": "#features"},
|
||||
{"text": "Pricing", "url": "#pricing"},
|
||||
{"text": "FAQs", "url": "#faqs"},
|
||||
{"text": "About", "url": "#about"}
|
||||
],
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"label": "Texto del enlace",
|
||||
"required": true
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"label": "URL",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget_2": {
|
||||
"label": "Widget 2",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar widget 2",
|
||||
"default": true
|
||||
},
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título del widget",
|
||||
"default": "Servicios"
|
||||
},
|
||||
"links": {
|
||||
"type": "repeater",
|
||||
"label": "Enlaces",
|
||||
"default": [
|
||||
{"text": "Análisis", "url": "#analisis"},
|
||||
{"text": "Presupuestos", "url": "#presupuestos"},
|
||||
{"text": "Cotizaciones", "url": "#cotizaciones"},
|
||||
{"text": "Proyectos", "url": "#proyectos"}
|
||||
],
|
||||
"fields": {
|
||||
"text": {"type": "text", "label": "Texto", "required": true},
|
||||
"url": {"type": "text", "label": "URL", "required": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget_3": {
|
||||
"label": "Widget 3",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar widget 3",
|
||||
"default": true
|
||||
},
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título del widget",
|
||||
"default": "Empresa"
|
||||
},
|
||||
"links": {
|
||||
"type": "repeater",
|
||||
"label": "Enlaces",
|
||||
"default": [
|
||||
{"text": "Acerca de", "url": "#acerca"},
|
||||
{"text": "Blog", "url": "/blog"},
|
||||
{"text": "Contacto", "url": "#contacto"},
|
||||
{"text": "Política de Privacidad", "url": "/privacidad"}
|
||||
],
|
||||
"fields": {
|
||||
"text": {"type": "text", "label": "Texto", "required": true},
|
||||
"url": {"type": "text", "label": "URL", "required": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"newsletter": {
|
||||
"label": "Newsletter",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar newsletter",
|
||||
"default": true,
|
||||
"description": "Mostrar u ocultar sección de newsletter"
|
||||
},
|
||||
"title": {
|
||||
"type": "text",
|
||||
"label": "Título",
|
||||
"default": "Suscríbete a nuestro newsletter",
|
||||
"required": true,
|
||||
"description": "Título de la sección de newsletter"
|
||||
},
|
||||
"description": {
|
||||
"type": "textarea",
|
||||
"label": "Descripción",
|
||||
"default": "Recibe actualizaciones mensuales sobre nuestros productos y servicios.",
|
||||
"description": "Texto descriptivo debajo del título"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "text",
|
||||
"label": "Placeholder del email",
|
||||
"default": "Correo electrónico",
|
||||
"description": "Texto placeholder del campo de email"
|
||||
},
|
||||
"button_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Suscribirse",
|
||||
"required": true,
|
||||
"description": "Texto del botón de suscripción"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyright": {
|
||||
"label": "Copyright",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"label": "Texto de copyright",
|
||||
"default": "ROI Theme. Todos los derechos reservados.",
|
||||
"required": true,
|
||||
"description": "Texto que aparece después del año"
|
||||
},
|
||||
"year_auto": {
|
||||
"type": "boolean",
|
||||
"label": "Año automático",
|
||||
"default": true,
|
||||
"description": "Mostrar el año actual automáticamente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"social_links": {
|
||||
"label": "Redes Sociales",
|
||||
"priority": 60,
|
||||
"fields": {
|
||||
"twitter": {
|
||||
"type": "text",
|
||||
"label": "Twitter URL",
|
||||
"default": "",
|
||||
"description": "URL completa de perfil de Twitter (deja vacío para ocultar)"
|
||||
},
|
||||
"instagram": {
|
||||
"type": "text",
|
||||
"label": "Instagram URL",
|
||||
"default": "",
|
||||
"description": "URL completa de perfil de Instagram"
|
||||
},
|
||||
"facebook": {
|
||||
"type": "text",
|
||||
"label": "Facebook URL",
|
||||
"default": "",
|
||||
"description": "URL completa de página de Facebook"
|
||||
},
|
||||
"linkedin": {
|
||||
"type": "text",
|
||||
"label": "LinkedIn URL",
|
||||
"default": "",
|
||||
"description": "URL completa de perfil o página de LinkedIn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos",
|
||||
"priority": 70,
|
||||
"fields": {
|
||||
"background_color": {
|
||||
"type": "text",
|
||||
"label": "Clase de fondo",
|
||||
"default": "bg-dark",
|
||||
"description": "Clase de Bootstrap para el fondo (ej: bg-dark, bg-secondary)"
|
||||
},
|
||||
"text_color": {
|
||||
"type": "text",
|
||||
"label": "Clase de color de texto",
|
||||
"default": "text-white",
|
||||
"description": "Clase de Bootstrap para el color de texto"
|
||||
},
|
||||
"link_hover_color": {
|
||||
"type": "color",
|
||||
"label": "Color de enlaces al hover",
|
||||
"default": "#FF8600",
|
||||
"description": "Color de los enlaces cuando se pasa el mouse sobre ellos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
{
|
||||
"component_name": "hero-section",
|
||||
"version": "1.0.0",
|
||||
"description": "Sección hero con badges de categorías y título H1 con gradiente",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar hero section",
|
||||
"default": true,
|
||||
"required": true,
|
||||
"description": "Activa o desactiva la sección hero"
|
||||
},
|
||||
"show_on_pages": {
|
||||
"type": "select",
|
||||
"label": "Mostrar en",
|
||||
"default": "posts",
|
||||
"options": {
|
||||
"all": "Todas las páginas",
|
||||
"home": "Solo página de inicio",
|
||||
"posts": "Solo posts individuales",
|
||||
"pages": "Solo páginas",
|
||||
"custom": "Tipos de post específicos"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Define en qué páginas se mostrará la hero section"
|
||||
},
|
||||
"custom_post_types": {
|
||||
"type": "text",
|
||||
"label": "Tipos de post personalizados",
|
||||
"default": "",
|
||||
"placeholder": "Ej: post,page,producto",
|
||||
"conditional_logic": {
|
||||
"field": "show_on_pages",
|
||||
"operator": "==",
|
||||
"value": "custom"
|
||||
},
|
||||
"description": "Slugs de tipos de post separados por comas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"label": "Badges de Categorías",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"show_categories": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar badges de categorías",
|
||||
"default": true,
|
||||
"description": "Muestra badges con las categorías del post"
|
||||
},
|
||||
"categories_source": {
|
||||
"type": "select",
|
||||
"label": "Fuente de categorías",
|
||||
"default": "post_categories",
|
||||
"options": {
|
||||
"post_categories": "Categorías del post",
|
||||
"post_tags": "Etiquetas del post",
|
||||
"custom_taxonomy": "Taxonomía personalizada",
|
||||
"custom_list": "Lista personalizada"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "show_categories",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"required": true,
|
||||
"description": "Define de dónde obtener las categorías"
|
||||
},
|
||||
"custom_taxonomy_name": {
|
||||
"type": "text",
|
||||
"label": "Nombre de taxonomía personalizada",
|
||||
"default": "",
|
||||
"placeholder": "Ej: project_category",
|
||||
"conditional_logic": {
|
||||
"field": "categories_source",
|
||||
"operator": "==",
|
||||
"value": "custom_taxonomy"
|
||||
},
|
||||
"description": "Slug de la taxonomía personalizada"
|
||||
},
|
||||
"custom_categories_list": {
|
||||
"type": "textarea",
|
||||
"label": "Lista personalizada de categorías",
|
||||
"default": "",
|
||||
"placeholder": "Análisis de Precios|#\nConstrucción|#\nMateriales|#",
|
||||
"rows": 5,
|
||||
"conditional_logic": {
|
||||
"field": "categories_source",
|
||||
"operator": "==",
|
||||
"value": "custom_list"
|
||||
},
|
||||
"description": "Una categoría por línea en formato: Nombre|URL"
|
||||
},
|
||||
"max_categories": {
|
||||
"type": "number",
|
||||
"label": "Máximo de categorías a mostrar",
|
||||
"default": 5,
|
||||
"min": 1,
|
||||
"max": 20,
|
||||
"conditional_logic": {
|
||||
"field": "show_categories",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Número máximo de badges a mostrar"
|
||||
},
|
||||
"category_icon": {
|
||||
"type": "text",
|
||||
"label": "Ícono de categoría",
|
||||
"default": "bi-folder-fill",
|
||||
"placeholder": "Ej: bi-folder-fill",
|
||||
"conditional_logic": {
|
||||
"field": "show_categories",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Clase del ícono Bootstrap para los badges"
|
||||
},
|
||||
"categories_alignment": {
|
||||
"type": "select",
|
||||
"label": "Alineación de categorías",
|
||||
"default": "center",
|
||||
"options": {
|
||||
"left": "Izquierda",
|
||||
"center": "Centro",
|
||||
"right": "Derecha"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "show_categories",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Alineación de los badges de categorías"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"label": "Título Principal",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"title_source": {
|
||||
"type": "select",
|
||||
"label": "Fuente del título",
|
||||
"default": "post_title",
|
||||
"options": {
|
||||
"post_title": "Título del post",
|
||||
"custom_field": "Campo personalizado",
|
||||
"custom_text": "Texto personalizado"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Define de dónde obtener el texto del título"
|
||||
},
|
||||
"custom_field_name": {
|
||||
"type": "text",
|
||||
"label": "Nombre del campo personalizado",
|
||||
"default": "",
|
||||
"placeholder": "Ej: hero_title",
|
||||
"conditional_logic": {
|
||||
"field": "title_source",
|
||||
"operator": "==",
|
||||
"value": "custom_field"
|
||||
},
|
||||
"description": "Nombre del custom field de WordPress"
|
||||
},
|
||||
"custom_text": {
|
||||
"type": "textarea",
|
||||
"label": "Texto personalizado",
|
||||
"default": "",
|
||||
"rows": 3,
|
||||
"maxlength": 500,
|
||||
"conditional_logic": {
|
||||
"field": "title_source",
|
||||
"operator": "==",
|
||||
"value": "custom_text"
|
||||
},
|
||||
"description": "Texto personalizado para el título"
|
||||
},
|
||||
"title_tag": {
|
||||
"type": "select",
|
||||
"label": "Etiqueta HTML del título",
|
||||
"default": "h1",
|
||||
"options": {
|
||||
"h1": "H1",
|
||||
"h2": "H2",
|
||||
"h3": "H3",
|
||||
"div": "DIV"
|
||||
},
|
||||
"description": "Etiqueta HTML para el título"
|
||||
},
|
||||
"title_classes": {
|
||||
"type": "text",
|
||||
"label": "Clases CSS adicionales",
|
||||
"default": "display-5 fw-bold",
|
||||
"placeholder": "Ej: display-5 fw-bold",
|
||||
"description": "Clases CSS adicionales para el título"
|
||||
},
|
||||
"title_alignment": {
|
||||
"type": "select",
|
||||
"label": "Alineación del título",
|
||||
"default": "center",
|
||||
"options": {
|
||||
"left": "Izquierda",
|
||||
"center": "Centro",
|
||||
"right": "Derecha"
|
||||
},
|
||||
"description": "Alineación del título"
|
||||
},
|
||||
"enable_gradient": {
|
||||
"type": "boolean",
|
||||
"label": "Activar gradiente en el texto",
|
||||
"default": false,
|
||||
"description": "Aplica efecto de gradiente al texto del título"
|
||||
},
|
||||
"gradient_color_start": {
|
||||
"type": "color",
|
||||
"label": "Color inicial del gradiente",
|
||||
"default": "#1e3a5f",
|
||||
"conditional_logic": {
|
||||
"field": "enable_gradient",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Color de inicio del gradiente"
|
||||
},
|
||||
"gradient_color_end": {
|
||||
"type": "color",
|
||||
"label": "Color final del gradiente",
|
||||
"default": "#FF8600",
|
||||
"conditional_logic": {
|
||||
"field": "enable_gradient",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Color final del gradiente"
|
||||
},
|
||||
"gradient_direction": {
|
||||
"type": "select",
|
||||
"label": "Dirección del gradiente",
|
||||
"default": "to-right",
|
||||
"options": {
|
||||
"to-right": "Izquierda a derecha",
|
||||
"to-left": "Derecha a izquierda",
|
||||
"to-bottom": "Arriba a abajo",
|
||||
"to-top": "Abajo a arriba",
|
||||
"diagonal": "Diagonal"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "enable_gradient",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Dirección del gradiente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"background_type": {
|
||||
"type": "select",
|
||||
"label": "Tipo de fondo",
|
||||
"default": "gradient",
|
||||
"options": {
|
||||
"color": "Color sólido",
|
||||
"gradient": "Gradiente",
|
||||
"image": "Imagen",
|
||||
"none": "Sin fondo"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Tipo de fondo para la hero section"
|
||||
},
|
||||
"background_color": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo",
|
||||
"default": "#1e3a5f",
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "color"
|
||||
},
|
||||
"description": "Color sólido de fondo"
|
||||
},
|
||||
"gradient_start_color": {
|
||||
"type": "color",
|
||||
"label": "Color inicial del gradiente",
|
||||
"default": "#1e3a5f",
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "gradient"
|
||||
},
|
||||
"description": "Color de inicio del gradiente de fondo"
|
||||
},
|
||||
"gradient_end_color": {
|
||||
"type": "color",
|
||||
"label": "Color final del gradiente",
|
||||
"default": "#2c5282",
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "gradient"
|
||||
},
|
||||
"description": "Color final del gradiente de fondo"
|
||||
},
|
||||
"gradient_angle": {
|
||||
"type": "number",
|
||||
"label": "Ángulo del gradiente (grados)",
|
||||
"default": 135,
|
||||
"min": 0,
|
||||
"max": 360,
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "gradient"
|
||||
},
|
||||
"description": "Ángulo del gradiente en grados (0-360)"
|
||||
},
|
||||
"background_image_url": {
|
||||
"type": "media",
|
||||
"label": "Imagen de fondo",
|
||||
"default": "",
|
||||
"media_type": "image",
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "image"
|
||||
},
|
||||
"description": "Imagen de fondo para la hero section"
|
||||
},
|
||||
"background_overlay": {
|
||||
"type": "boolean",
|
||||
"label": "Overlay oscuro sobre imagen",
|
||||
"default": true,
|
||||
"conditional_logic": {
|
||||
"field": "background_type",
|
||||
"operator": "==",
|
||||
"value": "image"
|
||||
},
|
||||
"description": "Agrega capa oscura sobre la imagen de fondo"
|
||||
},
|
||||
"overlay_opacity": {
|
||||
"type": "number",
|
||||
"label": "Opacidad del overlay (%)",
|
||||
"default": 60,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"conditional_logic": {
|
||||
"field": "background_overlay",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Opacidad de la capa oscura (0-100)"
|
||||
},
|
||||
"text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto",
|
||||
"default": "#FFFFFF",
|
||||
"description": "Color del texto del título y elementos"
|
||||
},
|
||||
"padding_vertical": {
|
||||
"type": "select",
|
||||
"label": "Padding vertical",
|
||||
"default": "normal",
|
||||
"options": {
|
||||
"compact": "Compacto (2rem)",
|
||||
"normal": "Normal (3rem)",
|
||||
"spacious": "Espacioso (4rem)",
|
||||
"extra-spacious": "Extra espacioso (5rem)"
|
||||
},
|
||||
"description": "Espaciado vertical de la sección"
|
||||
},
|
||||
"margin_bottom": {
|
||||
"type": "select",
|
||||
"label": "Margen inferior",
|
||||
"default": "normal",
|
||||
"options": {
|
||||
"none": "Sin margen",
|
||||
"small": "Pequeño (1rem)",
|
||||
"normal": "Normal (1.5rem)",
|
||||
"large": "Grande (2rem)"
|
||||
},
|
||||
"description": "Margen inferior de la sección"
|
||||
},
|
||||
"category_badge_background": {
|
||||
"type": "color",
|
||||
"label": "Fondo de badges",
|
||||
"default": "rgba(255, 255, 255, 0.2)",
|
||||
"description": "Color de fondo de los badges de categorías"
|
||||
},
|
||||
"category_badge_text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto de badges",
|
||||
"default": "#FFFFFF",
|
||||
"description": "Color del texto en los badges de categorías"
|
||||
},
|
||||
"category_badge_blur": {
|
||||
"type": "boolean",
|
||||
"label": "Efecto blur en badges",
|
||||
"default": true,
|
||||
"description": "Aplica efecto de desenfoque (backdrop-filter) a los badges"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
{
|
||||
"component_name": "navbar",
|
||||
"version": "1.0.0",
|
||||
"description": "Barra de navegación principal con menú Bootstrap, logo y botón CTA",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar navbar",
|
||||
"default": true,
|
||||
"required": true,
|
||||
"description": "Activa o desactiva la barra de navegación"
|
||||
},
|
||||
"is_sticky": {
|
||||
"type": "boolean",
|
||||
"label": "Navbar fijo (sticky)",
|
||||
"default": true,
|
||||
"description": "Mantiene el navbar fijo al hacer scroll"
|
||||
},
|
||||
"hide_on_scroll": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar al hacer scroll hacia abajo",
|
||||
"default": false,
|
||||
"description": "Oculta el navbar cuando el usuario hace scroll hacia abajo"
|
||||
},
|
||||
"show_on_mobile": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar en dispositivos móviles",
|
||||
"default": true,
|
||||
"description": "Muestra el navbar en pantallas pequeñas con menú hamburguesa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logo": {
|
||||
"label": "Logo",
|
||||
"priority": 20,
|
||||
"fields": {
|
||||
"logo_type": {
|
||||
"type": "select",
|
||||
"label": "Tipo de logo",
|
||||
"default": "image",
|
||||
"options": {
|
||||
"image": "Imagen",
|
||||
"text": "Texto",
|
||||
"none": "Sin logo"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Define el tipo de logo a mostrar"
|
||||
},
|
||||
"logo_image_url": {
|
||||
"type": "media",
|
||||
"label": "Imagen del logo",
|
||||
"default": "",
|
||||
"media_type": "image",
|
||||
"conditional_logic": {
|
||||
"field": "logo_type",
|
||||
"operator": "==",
|
||||
"value": "image"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Sube la imagen del logo (recomendado: PNG transparente)"
|
||||
},
|
||||
"logo_image_width": {
|
||||
"type": "number",
|
||||
"label": "Ancho del logo (px)",
|
||||
"default": 150,
|
||||
"min": 50,
|
||||
"max": 400,
|
||||
"conditional_logic": {
|
||||
"field": "logo_type",
|
||||
"operator": "==",
|
||||
"value": "image"
|
||||
},
|
||||
"description": "Ancho del logo en píxeles"
|
||||
},
|
||||
"logo_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del logo",
|
||||
"default": "",
|
||||
"maxlength": 50,
|
||||
"conditional_logic": {
|
||||
"field": "logo_type",
|
||||
"operator": "==",
|
||||
"value": "text"
|
||||
},
|
||||
"description": "Texto a mostrar como logo"
|
||||
},
|
||||
"logo_link": {
|
||||
"type": "url",
|
||||
"label": "Enlace del logo",
|
||||
"default": "",
|
||||
"placeholder": "Dejar vacío para usar la URL del home",
|
||||
"description": "URL de destino al hacer clic en el logo"
|
||||
},
|
||||
"logo_position": {
|
||||
"type": "select",
|
||||
"label": "Posición del logo",
|
||||
"default": "left",
|
||||
"options": {
|
||||
"left": "Izquierda",
|
||||
"center": "Centro",
|
||||
"right": "Derecha"
|
||||
},
|
||||
"description": "Posición del logo en el navbar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"label": "Menú de Navegación",
|
||||
"priority": 30,
|
||||
"fields": {
|
||||
"menu_location": {
|
||||
"type": "select",
|
||||
"label": "Ubicación del menú",
|
||||
"default": "primary",
|
||||
"options": {
|
||||
"primary": "Menú Principal",
|
||||
"secondary": "Menú Secundario",
|
||||
"custom": "Menú personalizado"
|
||||
},
|
||||
"required": true,
|
||||
"description": "Selecciona qué menú de WordPress mostrar"
|
||||
},
|
||||
"custom_menu_id": {
|
||||
"type": "number",
|
||||
"label": "ID del menú personalizado",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"conditional_logic": {
|
||||
"field": "menu_location",
|
||||
"operator": "==",
|
||||
"value": "custom"
|
||||
},
|
||||
"description": "ID del menú personalizado de WordPress"
|
||||
},
|
||||
"menu_alignment": {
|
||||
"type": "select",
|
||||
"label": "Alineación del menú",
|
||||
"default": "left",
|
||||
"options": {
|
||||
"left": "Izquierda",
|
||||
"center": "Centro",
|
||||
"right": "Derecha"
|
||||
},
|
||||
"description": "Alineación de los items del menú"
|
||||
},
|
||||
"enable_dropdowns": {
|
||||
"type": "boolean",
|
||||
"label": "Habilitar menús desplegables",
|
||||
"default": true,
|
||||
"description": "Permite submenús desplegables"
|
||||
},
|
||||
"dropdown_animation": {
|
||||
"type": "select",
|
||||
"label": "Animación de dropdowns",
|
||||
"default": "fade",
|
||||
"options": {
|
||||
"none": "Sin animación",
|
||||
"fade": "Aparecer gradualmente",
|
||||
"slide": "Deslizar"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "enable_dropdowns",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Tipo de animación para los submenús"
|
||||
},
|
||||
"mobile_breakpoint": {
|
||||
"type": "select",
|
||||
"label": "Breakpoint para menú móvil",
|
||||
"default": "lg",
|
||||
"options": {
|
||||
"sm": "Small (576px)",
|
||||
"md": "Medium (768px)",
|
||||
"lg": "Large (992px)",
|
||||
"xl": "Extra Large (1200px)"
|
||||
},
|
||||
"description": "Punto de quiebre para mostrar menú hamburguesa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta_button": {
|
||||
"label": "Botón CTA (Call to Action)",
|
||||
"priority": 40,
|
||||
"fields": {
|
||||
"button_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Mostrar botón CTA",
|
||||
"default": true,
|
||||
"description": "Activa o desactiva el botón de llamada a la acción"
|
||||
},
|
||||
"button_text": {
|
||||
"type": "text",
|
||||
"label": "Texto del botón",
|
||||
"default": "Let's Talk",
|
||||
"maxlength": 30,
|
||||
"conditional_logic": {
|
||||
"field": "button_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"required": true,
|
||||
"description": "Texto que aparece en el botón"
|
||||
},
|
||||
"button_icon": {
|
||||
"type": "text",
|
||||
"label": "Ícono del botón",
|
||||
"default": "bi-lightning-charge-fill",
|
||||
"placeholder": "Ej: bi-lightning-charge-fill",
|
||||
"conditional_logic": {
|
||||
"field": "button_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Clase del ícono Bootstrap (dejar vacío para sin ícono)"
|
||||
},
|
||||
"button_action_type": {
|
||||
"type": "select",
|
||||
"label": "Tipo de acción del botón",
|
||||
"default": "modal",
|
||||
"options": {
|
||||
"modal": "Abrir modal",
|
||||
"link": "Ir a URL",
|
||||
"scroll": "Scroll a sección"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "button_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"required": true,
|
||||
"description": "Acción que se ejecuta al hacer clic"
|
||||
},
|
||||
"button_modal_target": {
|
||||
"type": "text",
|
||||
"label": "ID del modal",
|
||||
"default": "#contactModal",
|
||||
"placeholder": "#contactModal",
|
||||
"conditional_logic": {
|
||||
"field": "button_action_type",
|
||||
"operator": "==",
|
||||
"value": "modal"
|
||||
},
|
||||
"required": true,
|
||||
"description": "ID del modal de Bootstrap a abrir (incluir #)"
|
||||
},
|
||||
"button_link_url": {
|
||||
"type": "url",
|
||||
"label": "URL del enlace",
|
||||
"default": "",
|
||||
"placeholder": "https://",
|
||||
"conditional_logic": {
|
||||
"field": "button_action_type",
|
||||
"operator": "==",
|
||||
"value": "link"
|
||||
},
|
||||
"required": true,
|
||||
"description": "URL de destino del botón"
|
||||
},
|
||||
"button_link_target": {
|
||||
"type": "select",
|
||||
"label": "Abrir enlace en",
|
||||
"default": "_self",
|
||||
"options": {
|
||||
"_self": "Misma ventana",
|
||||
"_blank": "Nueva ventana"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "button_action_type",
|
||||
"operator": "==",
|
||||
"value": "link"
|
||||
},
|
||||
"description": "Destino del enlace"
|
||||
},
|
||||
"button_scroll_target": {
|
||||
"type": "text",
|
||||
"label": "ID de la sección",
|
||||
"default": "",
|
||||
"placeholder": "#contact",
|
||||
"conditional_logic": {
|
||||
"field": "button_action_type",
|
||||
"operator": "==",
|
||||
"value": "scroll"
|
||||
},
|
||||
"required": true,
|
||||
"description": "ID de la sección a la que hacer scroll (incluir #)"
|
||||
},
|
||||
"button_position": {
|
||||
"type": "select",
|
||||
"label": "Posición del botón",
|
||||
"default": "right",
|
||||
"options": {
|
||||
"left": "Antes del menú",
|
||||
"right": "Después del menú"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "button_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Ubicación del botón en el navbar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"label": "Estilos",
|
||||
"priority": 50,
|
||||
"fields": {
|
||||
"background_color": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo",
|
||||
"default": "#1e3a5f",
|
||||
"description": "Color de fondo del navbar (por defecto: navy primary)"
|
||||
},
|
||||
"text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto",
|
||||
"default": "#FFFFFF",
|
||||
"description": "Color del texto de los links del menú"
|
||||
},
|
||||
"hover_color": {
|
||||
"type": "color",
|
||||
"label": "Color hover",
|
||||
"default": "#FF8600",
|
||||
"description": "Color al pasar el mouse sobre los links (por defecto: orange primary)"
|
||||
},
|
||||
"active_color": {
|
||||
"type": "color",
|
||||
"label": "Color del item activo",
|
||||
"default": "#FF8600",
|
||||
"description": "Color del item de menú activo/actual"
|
||||
},
|
||||
"button_background": {
|
||||
"type": "color",
|
||||
"label": "Color de fondo del botón",
|
||||
"default": "#FF8600",
|
||||
"description": "Color de fondo del botón CTA"
|
||||
},
|
||||
"button_text_color": {
|
||||
"type": "color",
|
||||
"label": "Color del texto del botón",
|
||||
"default": "#FFFFFF",
|
||||
"description": "Color del texto del botón CTA"
|
||||
},
|
||||
"button_hover_background": {
|
||||
"type": "color",
|
||||
"label": "Color hover del botón",
|
||||
"default": "#FF6B35",
|
||||
"description": "Color de fondo del botón al hacer hover"
|
||||
},
|
||||
"padding_vertical": {
|
||||
"type": "select",
|
||||
"label": "Padding vertical",
|
||||
"default": "normal",
|
||||
"options": {
|
||||
"compact": "Compacto (0.5rem)",
|
||||
"normal": "Normal (1rem)",
|
||||
"spacious": "Espacioso (1.5rem)"
|
||||
},
|
||||
"description": "Espaciado vertical del navbar"
|
||||
},
|
||||
"shadow_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Activar sombra",
|
||||
"default": true,
|
||||
"description": "Agrega sombra debajo del navbar"
|
||||
},
|
||||
"shadow_intensity": {
|
||||
"type": "select",
|
||||
"label": "Intensidad de la sombra",
|
||||
"default": "medium",
|
||||
"options": {
|
||||
"light": "Ligera",
|
||||
"medium": "Media",
|
||||
"strong": "Fuerte"
|
||||
},
|
||||
"conditional_logic": {
|
||||
"field": "shadow_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Intensidad de la sombra del navbar"
|
||||
},
|
||||
"border_bottom_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Borde inferior",
|
||||
"default": false,
|
||||
"description": "Agrega un borde en la parte inferior del navbar"
|
||||
},
|
||||
"border_bottom_color": {
|
||||
"type": "color",
|
||||
"label": "Color del borde inferior",
|
||||
"default": "#FF8600",
|
||||
"conditional_logic": {
|
||||
"field": "border_bottom_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Color del borde inferior"
|
||||
},
|
||||
"border_bottom_width": {
|
||||
"type": "number",
|
||||
"label": "Grosor del borde (px)",
|
||||
"default": 3,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"conditional_logic": {
|
||||
"field": "border_bottom_enabled",
|
||||
"operator": "==",
|
||||
"value": true
|
||||
},
|
||||
"description": "Grosor del borde inferior en píxeles"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user